Skip to content

297 finish backend typescript migration and command pattern#300

Merged
InfinityBowman merged 24 commits into
mainfrom
297-finish-backend-typescript-migration-and-command-pattern
Jan 17, 2026
Merged

297 finish backend typescript migration and command pattern#300
InfinityBowman merged 24 commits into
mainfrom
297-finish-backend-typescript-migration-and-command-pattern

Conversation

@InfinityBowman
Copy link
Copy Markdown
Owner

@InfinityBowman InfinityBowman commented Jan 14, 2026

Summary by CodeRabbit

  • New Features

    • Frontend version detection with an update banner (refresh/dismiss) and build-time versioning.
    • Improved billing checkout flow using a new command-based checkout path.
  • Refactoring

    • Backend routes and commands migrated to TypeScript with stronger typing.
    • Centralized request validation via a shared validation hook.
    • Billing and authorization logic reorganized into command/policy surfaces.
  • Documentation

    • Added many guides, plans, audits, and a system STATUS overview.

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

@InfinityBowman InfinityBowman linked an issue Jan 14, 2026 that may be closed by this pull request
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 14, 2026

📝 Walkthrough

Walkthrough

This PR migrates many backend modules to TypeScript, centralizes validation via a shared hook, introduces a command-pattern for billing (checkout/session processing), tightens typings across policies, commands, routes, webhook handlers, and updates documentation and package manifests. Many barrel exports were reorganized.

Changes

Cohort / File(s) Summary
Monorepo manifests & tooling
\.prettierignore, package.json, packages/*/package.json, packages/workers/wrangler.jsonc, scripts/loc-report.mjs
Added ignore patterns, reordered/added package.json metadata and scripts, switched workers entry to src/index.ts, replaced cloc script with tokei-based LOC reporter.
Docs & Guides
packages/docs/**, AGENTS.md, .claude/**, .github/**
Added many plans, audits, status and agent docs; introduced guidance templates and instruction files; removed/merged legacy UI rules.
Validation hook
packages/workers/src/lib/honoValidationHook.ts
New centralized validationHook for OpenAPIHono/Zod validation error handling.
Command pattern (billing)
packages/workers/src/commands/billing/*, packages/workers/src/commands/index.ts
New typed billing commands: createSingleProjectCheckout, processCheckoutSession; commands index re-exports billing surface.
Commands (projects, members, lib)
packages/workers/src/commands/{projects,members,lib}/*
Converted JS -> TS command modules, added typed Actor/Params/Result interfaces, updated/doSync exports and barrels.
Policies & roles
packages/workers/src/policies/*, packages/workers/src/policies/lib/roles.ts
Added typed billing policies; migrated/rewrote role utilities to TS; expanded org/project policy typings and guards.
Billing routes & webhook handling
packages/workers/src/routes/billing/*, packages/workers/src/routes/billing/handlers/*
Routes now use OpenAPIHono<{ Bindings: Env }>, defaultHook → validationHook, tightened error schema keys, checkout route delegates to createSingleProjectCheckout; many webhook handlers typed and refactored to use processCheckoutSession.
Other routes (orgs, admin, projects, storage, users, invitations, google-drive, account-merge, avatars, database, etc.)
packages/workers/src/routes/**
Extensive TS typing, OpenAPIHono generic binding, centralized validationHook adoption, schema detail fields normalized to z.record(z.string(), z.unknown()), ContentfulStatusCode casts, enhanced error handling and context guards across admin/orgs/project routes.
Helpers & libs
packages/workers/src/lib/project-sync.ts, packages/workers/src/routes/billing/helpers/*
Adjusted ProjectMeta shape, added typed orgContext helpers, moved ownerGate logic into policies.
Tests & misc
packages/workers/src/*/__tests__/*, .cursor/*
Minor test adjustments and docs/rule changes; added flakiness note for a WebSocket test.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Route as Checkout Route
    participant Cmd as createSingleProjectCheckout
    participant Stripe as Stripe API
    participant DB as Database

    Client->>Route: POST /checkout
    Route->>Route: validate request & auth
    Route->>Cmd: createSingleProjectCheckout(env, actor, params)
    Cmd->>Cmd: validate config & build idempotency key
    Cmd->>Stripe: create Checkout Session (idempotent)
    Stripe-->>Cmd: session (url, id)
    Cmd-->>Route: { url, sessionId }
    Route-->>Client: 200 { url, sessionId }

    Client->>Route: POST /webhooks/checkout.session.completed
    Route->>Route: verify signature
    Route->>Cmd: processCheckoutSession(session, { db, logger })
    activate Cmd
    Cmd->>DB: read/write grants/subscriptions (idempotent)
    DB-->>Cmd: existing records
    Cmd-->>Route: { handled, result, ledgerContext }
    Route-->>Client: 200 { success }
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • corates#292: Implements command-pattern refactor and reworked commands/index exports—closely aligned with this PR's command extraction and re-exports.
  • corates#288: Migrates Hono routes to OpenAPIHono/Zod validation and defaultHook changes—mirrors the validationHook and OpenAPIHono generics adopted here.
  • corates#296: Large backend TypeScript migration touching commands, policies, and routes—overlaps significantly with this PR's TS conversions.
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: completing the backend TypeScript migration and implementing the command pattern, which aligns with the extensive file modifications shown in the raw summary.
Docstring Coverage ✅ Passed Docstring coverage is 86.11% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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 Jan 14, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

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

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
corates 1ec101e Commit Preview URL Jan 17 2026, 12:19 AM

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

Caution

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

⚠️ Outside diff range comments (2)
packages/workers/src/commands/lib/doSync.ts (1)

17-29: Missing response validation - silent failures possible.

Unlike syncMemberToDO and syncProjectToDO in project-sync.ts, this function does not check response.ok. If the Durable Object returns an error status, it will be silently ignored.

Proposed fix to add response validation
 export async function disconnectAllFromProject(env: Env, projectId: string): Promise<void> {
   const projectDoc = getProjectDocStub(env, projectId);
 
-  await projectDoc.fetch(
+  const response = await projectDoc.fetch(
     new Request('https://internal/disconnect-all', {
       method: 'POST',
       headers: {
         'Content-Type': 'application/json',
         'X-Internal-Request': 'true',
       },
     }),
   );
+
+  if (!response.ok) {
+    const text = await response.text();
+    throw new Error(
+      `[ProjectSync] disconnect-all failed for project ${projectId}: ${response.status} ${text}`,
+    );
+  }
 }
packages/workers/src/commands/members/updateMemberRole.ts (1)

42-45: Wrap database update in try-catch with domain error.

Per coding guidelines, database operations should be wrapped in try-catch blocks and return domain errors using createDomainError with SYSTEM_ERRORS.DB_ERROR. The deleteProject.ts command in this PR demonstrates this pattern.

♻️ Proposed fix
-  await db
-    .update(projectMembers)
-    .set({ role })
-    .where(and(eq(projectMembers.projectId, projectId), eq(projectMembers.userId, userId)));
+  try {
+    await db
+      .update(projectMembers)
+      .set({ role })
+      .where(and(eq(projectMembers.projectId, projectId), eq(projectMembers.userId, userId)));
+  } catch (err) {
+    throw createDomainError(
+      SYSTEM_ERRORS.DB_ERROR,
+      { operation: 'update_member_role', projectId, userId, originalError: (err as Error).message },
+      'Failed to update member role',
+    );
+  }

You'll also need to add the import:

+import { createDomainError, SYSTEM_ERRORS } from '@corates/shared';
🤖 Fix all issues with AI agents
In `@package.json`:
- Around line 45-46: The package.json scripts "fmt" and "fmt:check" reference
oxfmt but oxfmt is missing from devDependencies; add an entry for "oxfmt" under
devDependencies (matching the project's versioning style used for tools like
"prettier" and "eslint") so running the npm scripts works for all
developers—either add the dependency manually in package.json or run the
project’s preferred add-dev-dep command to install it as a devDependency.

In `@packages/docs/audits/billing-orgs-command-pattern.md`:
- Around line 145-160: Update the documented function signature for
processCheckoutSession to match the implementation by removing the first Env
parameter; change the declaration from processCheckoutSession(env, session, ctx)
to processCheckoutSession(session, ctx) and keep the existing
CheckoutSessionResult and ctx type (ctx: { db: Database; logger: Logger }) so
the docs reflect the actual function signature used in code.

In `@packages/docs/plans/shared-zod-api-types.md`:
- Around line 146-153: ProjectMemberSchema's role enum (in ProjectMemberSchema)
omits the 'owner' value used by the existing createProject.ts command; update
the z.enum in ProjectMemberSchema to include 'owner' (i.e.,
['owner','lead','reviewer','viewer']) to match current runtime values, or
alternatively add a clear migration note in the docs referencing
createProject.ts and the planned switch from 'owner' to 'lead' so consumers know
existing records contain 'owner'.

In `@packages/workers/src/commands/projects/updateProject.ts`:
- Line 45: Wrap the database update call in a try-catch inside updateProject
(the await db.update(projects).set(updateData).where(eq(projects.id, projectId))
line); on catch, convert the raw error into a domain error by calling
createDomainError with SYSTEM_ERRORS.DB_ERROR (include the caught error as
context) and rethrow that domain error so the function honors the documented
`@throws` DomainError DB_ERROR contract.

In `@packages/workers/src/policies/lib/roles.ts`:
- Around line 1-16: The ProjectRole union and PROJECT_ROLES array are missing
actual project roles used by the members endpoint, causing type mismatches in
project role checks (e.g., hasProjectRole); update the ProjectRole type to
include 'member' | 'collaborator' | 'viewer' | 'owner' and replace PROJECT_ROLES
with a readonly array containing those values in ascending order (member,
collaborator, viewer, owner) so runtime checks and TypeScript types align with
the members endpoint.

In `@packages/workers/src/routes/billing/checkout.ts`:
- Around line 247-249: The code uses an unnecessary double type assertion to
access the coupon: replace the cast "(promo as unknown as { coupon:
Stripe.Coupon }).coupon" with a direct property access on the PromotionCode
object; update the line that assigns coupon (currently using promo and
promoCodes) to simply use promo.coupon (or promoCodes.data[0].coupon) and remove
the extra type coercion to rely on Stripe's PromotionCode type.
🧹 Nitpick comments (22)
packages/workers/src/commands/lib/notifications.ts (2)

29-47: Consider adding the X-Internal-Request header for consistency.

Other internal DO requests in this codebase (e.g., syncMemberToDO, disconnectAllFromProject) include the X-Internal-Request: true header. This endpoint should follow the same pattern for consistency and to enable the DO to distinguish internal from external requests.

Proposed fix
   await userSession.fetch(
     new Request('https://internal/notify', {
       method: 'POST',
-      headers: { 'Content-Type': 'application/json' },
+      headers: {
+        'Content-Type': 'application/json',
+        'X-Internal-Request': 'true',
+      },
       body: JSON.stringify({
         ...notification,
         timestamp: Date.now(),
       }),
     }),
   );

54-76: Sequential notification processing - consider parallelization for scale.

The current implementation awaits each notification sequentially in the loop. For large user lists, this could be slow. However, the per-user error handling is valuable.

Consider using Promise.allSettled to parallelize while still handling individual failures:

Optional parallel implementation
 export async function notifyUsers(
   env: Env,
   userIds: string[],
   notification: Notification,
   excludeUserId: string | null = null,
 ): Promise<number> {
-  let notifiedCount = 0;
-
-  for (const userId of userIds) {
-    if (excludeUserId && userId === excludeUserId) {
-      continue;
-    }
-
-    try {
-      await notifyUser(env, userId, notification);
-      notifiedCount++;
-    } catch (err) {
-      console.error(`Failed to send notification to user ${userId}:`, err);
-    }
-  }
-
-  return notifiedCount;
+  const filteredUserIds = userIds.filter(userId => userId !== excludeUserId);
+  
+  const results = await Promise.allSettled(
+    filteredUserIds.map(userId => notifyUser(env, userId, notification))
+  );
+
+  let notifiedCount = 0;
+  results.forEach((result, index) => {
+    if (result.status === 'fulfilled') {
+      notifiedCount++;
+    } else {
+      console.error(`Failed to send notification to user ${filteredUserIds[index]}:`, result.reason);
+    }
+  });
+
+  return notifiedCount;
 }
packages/workers/src/commands/billing/processCheckoutSession.ts (4)

26-31: Export ProcessCheckoutSessionContext for external consumers.

The interface is defined but not exported, which may limit reusability if callers need to construct the context object with proper typing.

♻️ Suggested fix
-interface ProcessCheckoutSessionContext {
+export interface ProcessCheckoutSessionContext {
   db: Database;
   logger: {
     stripe: (_event: string, _context: Record<string, unknown>) => void;
   };
 }

128-139: Consider simplifying the timestamp handling.

The type coercion for expiresAt is defensive but adds complexity. Since getGrantByOrgIdAndType returns OrgAccessGrant which should have a consistent type for expiresAt, this could indicate a schema type issue.

#!/bin/bash
# Check the OrgAccessGrant type definition to understand expiresAt type
ast-grep --pattern 'interface OrgAccessGrant {
  $$$
}'

# Also check the schema definition
rg -n "expiresAt" packages/workers/src/db/schema

166-178: Wrap database operation in try-catch for consistent error handling.

Per coding guidelines, database operations should be wrapped in try-catch blocks and return domain errors using createDomainError with SYSTEM_ERRORS.DB_ERROR. The createGrant call could fail and should be handled consistently.

♻️ Suggested approach
+import { createDomainError, SYSTEM_ERRORS } from '@corates/shared';

// In the createGrant section:
+  try {
     const grantId = crypto.randomUUID();
     await createGrant(db, {
       id: grantId,
       orgId,
       type: grantType as 'single_project' | 'trial',
       startsAt: now,
       expiresAt,
       stripeCheckoutSessionId: session.id,
       metadata: {
         purchaserUserId,
         stripeCheckoutSessionId: session.id,
         stripePaymentIntentId: session.payment_intent as string | undefined,
       },
     });
+  } catch (err) {
+    logger.stripe('checkout_grant_creation_failed', {
+      sessionId: session.id,
+      orgId,
+      error: err instanceof Error ? err.message : String(err),
+    });
+    return {
+      handled: false,
+      result: 'grant_creation_failed',
+      error: 'database_error',
+      ledgerContext: { stripeCheckoutSessionId: session.id, orgId },
+    };
+  }

Based on coding guidelines, database operations should use try-catch with domain errors.


139-139: Wrap updateGrantExpiresAt in try-catch for consistency.

Similar to the grant creation, the update operation should also be wrapped for consistent error handling.

packages/workers/src/commands/projects/updateProject.ts (1)

28-31: Clarify the purpose of _actor parameter.

The _actor parameter is unused. If it's intended for future authorization checks, consider adding a TODO comment. If authorization is expected to be performed by middleware before this command is called, document this assumption.

 export async function updateProject(
   env: Env,
-  _actor: UpdateProjectActor,
+  _actor: UpdateProjectActor, // TODO: Use for audit logging or authorization if needed
   { projectId, name, description }: UpdateProjectParams,
 ): Promise<UpdateProjectResult> {
packages/workers/src/commands/billing/createSingleProjectCheckout.ts (1)

66-70: Idempotency window may be too short for slow network conditions.

The 1-minute granularity (Math.floor(Date.now() / 60000)) prevents duplicate sessions from rapid clicks, but could allow duplicates if a user retries after exactly 1 minute. Consider if a 5-minute window would better suit your use case, or document this limitation.

packages/workers/src/commands/projects/deleteProject.ts (1)

37-47: Consider using db.batch() for the two database reads.

Per coding guidelines, use db.batch() for multiple related database operations. While these are read operations, batching them improves atomicity and performance.

♻️ Suggested refactor
-  const project = await db
-    .select({ name: projects.name })
-    .from(projects)
-    .where(eq(projects.id, projectId))
-    .get();
-
-  const members = await db
-    .select({ userId: projectMembers.userId })
-    .from(projectMembers)
-    .where(eq(projectMembers.projectId, projectId))
-    .all();
+  const [project, members] = await db.batch([
+    db
+      .select({ name: projects.name })
+      .from(projects)
+      .where(eq(projects.id, projectId)),
+    db
+      .select({ userId: projectMembers.userId })
+      .from(projectMembers)
+      .where(eq(projectMembers.projectId, projectId)),
+  ]);
packages/workers/src/commands/members/updateMemberRole.ts (1)

32-36: Consider documenting why _actor is unused.

The _actor parameter is accepted but not used. If this is intentional (e.g., for audit logging in the future or API consistency), a brief comment would clarify intent.

packages/workers/src/policies/lib/roles.ts (1)

53-55: Edge case: empty hierarchy array.

If hierarchy is an empty array, hierarchy.length - 1 equals -1, and indexOf would return -1 for any role, making the comparison truthy. Consider adding a guard or documenting that hierarchies must be non-empty.

♻️ Defensive check
 export function isTopRole<T extends string>(hierarchy: readonly T[], role: T | string): boolean {
+  if (hierarchy.length === 0) return false;
   return hierarchy.indexOf(role as T) === hierarchy.length - 1;
 }
packages/workers/src/commands/members/addMember.ts (1)

89-116: Consider stronger typing for batch operations.

The unknown[] type with double assertion loses type safety. You could leverage Drizzle's batch type directly.

♻️ Suggested improvement
-  const insertOperations: unknown[] = [];
+  const insertOperations: Parameters<typeof db.batch>[0] = [];

   // ... push operations ...

-  await db.batch(insertOperations as unknown as Parameters<typeof db.batch>[0]);
+  await db.batch(insertOperations);

This preserves type checking for the operations being pushed to the array.

packages/docs/plans/frontend-version-check.md (2)

25-36: Add fallback for non-git environments.

The version generation script assumes git is available. Consider adding a fallback for environments where git rev-parse might fail (e.g., builds from source tarballs, CI without git).

// Suggested enhancement
const version = {
  hash: (() => {
    try {
      return execSync('git rev-parse --short HEAD').toString().trim();
    } catch {
      return process.env.BUILD_HASH || 'unknown';
    }
  })(),
  timestamp: Date.now(),
};

219-237: Consider brief notification before force reload.

The force update feature immediately reloads without user feedback. Consider showing a brief toast or message before reloading to avoid confusion (e.g., user might think the app crashed).

// Brief notification before reload
if (forceUpdate === 'true') {
  // Show brief notification before reload
  alert('A critical update is being applied...');
  window.location.reload();
  return;
}
packages/workers/src/policies/projects.ts (1)

119-171: Consider extracting shared owner-counting logic.

canChangeRole and canRemoveWithoutOrphaning share nearly identical implementation for checking if the target is the last owner. Consider extracting a helper like countProjectOwners(db, projectId) to reduce duplication. Not blocking, as the current code is clear and correct.

♻️ Optional refactor to reduce duplication
async function countProjectOwners(db: Database, projectId: string): Promise<number> {
  const result = await db
    .select({ count: count() })
    .from(projectMembers)
    .where(and(eq(projectMembers.projectId, projectId), eq(projectMembers.role, 'owner')))
    .get();
  return result?.count || 0;
}

Then both functions can use:

return (await countProjectOwners(db, projectId)) > 1;
packages/workers/src/routes/billing/index.js (1)

35-41: Inconsistent import style within the file.

Only line 39 (./checkout) uses an extensionless import while lines 35-38 and 40-41 retain .js extensions. For consistency during the TypeScript migration, consider updating all imports in this file to the same style.

♻️ Proposed fix for consistency
-import { billingSubscriptionRoutes } from './subscription.js';
-import { billingValidationRoutes } from './validation.js';
-import { billingPortalRoutes } from './portal.js';
-import { billingGrantRoutes } from './grants.js';
+import { billingSubscriptionRoutes } from './subscription';
+import { billingValidationRoutes } from './validation';
+import { billingPortalRoutes } from './portal';
+import { billingGrantRoutes } from './grants';
 import { billingCheckoutRoutes } from './checkout';
-import { billingWebhookRoutes } from './webhooks.js';
-import { billingInvoicesRoutes } from './invoices.js';
+import { billingWebhookRoutes } from './webhooks';
+import { billingInvoicesRoutes } from './invoices';
packages/docs/audits/billing-orgs-command-pattern.md (1)

99-122: Add language specifiers to fenced code blocks.

The code blocks listing file paths should specify a language (e.g., text or plaintext) for consistency and to satisfy linting rules.

📝 Suggested fix
-```
+```text
 commands/billing/
   index.ts
   createSingleProjectCheckout.ts
   processCheckoutSession.ts
 
 policies/
   billing.ts (new)

Modified Files

- +text
routes/billing/checkout.js -> checkout.ts
routes/billing/handlers/checkoutHandlers.js (import from commands)
policies/index.js -> index.ts (add billing exports)
commands/index.js -> index.ts (add billing exports)


### Deleted Files

-```
+```text
routes/billing/helpers/ownerGate.js (moved to policies)
</details>

</blockquote></details>
<details>
<summary>packages/workers/src/policies/billing.ts (1)</summary><blockquote>

`67-85`: **Use `isOrgOwner` helper for consistency.**

The `isOrgOwner` helper is already imported (line 14) and used in `canManageBilling` (line 43), but `requireOrgOwner` uses a direct string comparison. This creates an inconsistency and potential maintenance issue if the owner role value ever changes.


<details>
<summary>Proposed fix</summary>

```diff
 export function requireOrgOwner({
   orgId,
   role,
 }: {
   orgId: string | null;
   role: string | null;
 }): void {
   if (!orgId) {
     throw createDomainError(AUTH_ERRORS.FORBIDDEN, {
       reason: 'no_org_found',
     });
   }
 
-  if (role !== 'owner') {
+  if (!role || !isOrgOwner(role)) {
     throw createDomainError(AUTH_ERRORS.FORBIDDEN, {
       reason: 'org_owner_required',
     });
   }
 }
packages/workers/src/routes/billing/checkout.ts (3)

35-40: Simplify Zod issue code detection.

The current approach of checking 'code' in firstIssue and casting is overly complex. Zod 4 issues always have a code property on the issue object.

Proposed simplification
       let message = firstIssue?.message || 'Validation failed';
-      // Check for missing values using code and message inspection
-      const issueCode = 'code' in firstIssue ? firstIssue.code : '';
       const isMissing =
-        issueCode === 'invalid_type' ||
+        firstIssue?.code === 'invalid_type' ||
         message.includes('received undefined') ||
         message.includes('Required');

291-295: Session type casting loses type safety.

Casting session to Record<string, unknown> defeats TypeScript's type checking. Consider defining a proper type for the session object or using the session's actual type from Better-Auth.

This pattern appears twice (lines 291-295 and 395-399). If the resolveOrgIdWithRole function expects a specific session shape, consider:

  1. Updating resolveOrgIdWithRole to accept the actual session type from Better-Auth
  2. Or extracting only the needed properties explicitly
     const { orgId, role } = await resolveOrgIdWithRole({
       db,
-      session: session as unknown as Record<string, unknown>,
+      session: { activeOrganizationId: session?.activeOrganizationId },
       userId: user!.id,
     });

339-351: Fragile type assertion for Better Auth API.

The cast (auth.api as Record<string, CallableFunction>).upgradeSubscription bypasses type safety. If the Better Auth plugin API changes, this will fail silently at compile time.

Consider creating a typed wrapper or interface for the Better Auth Stripe plugin methods to maintain type safety:

// In a separate types file or auth config
interface BetterAuthStripeApi {
  upgradeSubscription: (params: {
    headers: Headers;
    body: {
      plan: string;
      annual: boolean;
      referenceId: string | null;
      successUrl: string;
      cancelUrl: string;
      returnUrl: string;
    };
  }) => Promise<{ url: string }>;
}
packages/workers/src/commands/members/removeMember.ts (1)

56-62: Consider structured logging for consistency.

The command uses console.error while route handlers use the structured createLogger. For better observability and consistency across the codebase, consider passing a logger instance or creating one in the command.

Example pattern
// Option 1: Pass logger as parameter
export async function removeMember(
  env: Env,
  actor: RemoveMemberActor,
  params: RemoveMemberParams,
  logger?: Logger,
): Promise<RemoveMemberResult> {
  // ...
  } catch (err) {
    logger?.error('member_do_sync_failed', { projectId, userId, error: err });
  }
}

// Option 2: Create simple logger in command
import { createLogger } from '@/lib/observability/logger';
// Note: would need to adapt for non-request context

This is optional since error logging for side-effects (DO sync, notifications) is non-critical.

Comment thread package.json Outdated
Comment on lines +45 to +46
"fmt": "oxfmt",
"fmt:check": "oxfmt --check"
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 | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if oxfmt is listed in any package.json in the repo
rg -n '"oxfmt"' --type json

# Check if oxfmt is in devDependencies specifically
echo "=== Checking package.json devDependencies ==="
cat package.json | jq '.devDependencies' 2>/dev/null | head -50

# Check lock files for oxfmt
echo "=== Checking lock files for oxfmt ==="
if [ -f "package-lock.json" ]; then
  rg -n '"oxfmt"' package-lock.json || echo "Not found in package-lock.json"
fi
if [ -f "yarn.lock" ]; then
  rg -n 'oxfmt' yarn.lock || echo "Not found in yarn.lock"
fi
if [ -f "pnpm-lock.yaml" ]; then
  rg -n 'oxfmt' pnpm-lock.yaml || echo "Not found in pnpm-lock.yaml"
fi

Repository: InfinityBowman/corates

Length of output: 649


Add oxfmt to devDependencies.

The fmt and fmt:check scripts reference oxfmt, but it's not listed in devDependencies. This will cause npm script failures for developers unless oxfmt is globally installed. Following the pattern of other dev tools like prettier and eslint in the project, oxfmt should be added as a dev dependency.

🤖 Prompt for AI Agents
In `@package.json` around lines 45 - 46, The package.json scripts "fmt" and
"fmt:check" reference oxfmt but oxfmt is missing from devDependencies; add an
entry for "oxfmt" under devDependencies (matching the project's versioning style
used for tools like "prettier" and "eslint") so running the npm scripts works
for all developers—either add the dependency manually in package.json or run the
project’s preferred add-dev-dep command to install it as a devDependency.

Comment on lines +145 to +160
### processCheckoutSession

```typescript
interface CheckoutSessionResult {
handled: boolean;
result: string;
ledgerContext?: Record<string, unknown>;
error?: string;
}

export async function processCheckoutSession(
env: Env,
session: Stripe.Checkout.Session,
ctx: { db: Database; logger: Logger },
): Promise<CheckoutSessionResult>;
```
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

**Documentation signature doesn't match implementation.**The documented signature shows processCheckoutSession(env, session, ctx) with three parameters, but based on the relevant code snippet, the actual implementation is processCheckoutSession(session, ctx) with two parameters (no env). Update the documentation to match the implementation.

📝 Suggested fix
 ### processCheckoutSession
 
 ```typescript
 interface CheckoutSessionResult {
   handled: boolean;
   result: string;
   ledgerContext?: Record<string, unknown>;
   error?: string;
 }
 
 export async function processCheckoutSession(
-  env: Env,
   session: Stripe.Checkout.Session,
   ctx: { db: Database; logger: Logger },
 ): Promise<CheckoutSessionResult>;
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

In @packages/docs/audits/billing-orgs-command-pattern.md around lines 145 - 160,
Update the documented function signature for processCheckoutSession to match the
implementation by removing the first Env parameter; change the declaration from
processCheckoutSession(env, session, ctx) to processCheckoutSession(session,
ctx) and keep the existing CheckoutSessionResult and ctx type (ctx: { db:
Database; logger: Logger }) so the docs reflect the actual function signature
used in code.


</details>

<!-- fingerprinting:phantom:poseidon:ocelot -->

<!-- This is an auto-generated comment by CodeRabbit -->

Comment thread packages/docs/plans/shared-zod-api-types.md
Comment thread packages/workers/src/commands/projects/updateProject.ts Outdated
Comment thread packages/workers/src/policies/billing.ts Outdated
Comment on lines +1 to +16
/**
* Role hierarchy definitions and comparison utilities
*
* Centralizes role definitions and provides utilities for comparing roles
* across the application. Used by authorization policies to check permissions.
*/

// Role type definitions
export type OrgRole = 'member' | 'admin' | 'owner';
export type ProjectRole = 'member' | 'owner';

// Organization role hierarchy (lowest to highest)
export const ORG_ROLES: readonly OrgRole[] = ['member', 'admin', 'owner'];

// Project role hierarchy (lowest to highest)
export const PROJECT_ROLES: readonly ProjectRole[] = ['member', 'owner'];
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:

#!/bin/bash
# Search for collaborator and viewer role references in the codebase
echo "=== Searching for collaborator role references ==="
rg "collaborator" --type ts --type js -g '!node_modules/**' -C 2

echo ""
echo "=== Searching for viewer role references ==="
rg "viewer" --type ts --type js -g '!node_modules/**' -C 2

Repository: InfinityBowman/corates

Length of output: 50379


🏁 Script executed:

#!/bin/bash
# Check members.ts file context to understand which roles it's managing
echo "=== Members route context ==="
grep -B5 -A10 "AddMemberRequest\|UpdateRoleRequest" packages/workers/src/routes/members.ts | head -50

echo ""
echo "=== Check if members.ts is for org members ==="
grep -B10 "owners/members\|routes/members" packages/workers/src/routes/members.ts | head -20

echo ""
echo "=== Check where ProjectRole is used ==="
rg "ProjectRole" --type ts -A3 -B3

Repository: InfinityBowman/corates

Length of output: 12070


🏁 Script executed:

#!/bin/bash
# Verify members.ts is indeed for project members (not org)
echo "=== Check memberRoutes context ==="
head -20 packages/workers/src/routes/members.ts

echo ""
echo "=== Confirm projectMembershipMiddleware is for projects ==="
grep -r "projectMembershipMiddleware" packages/workers/src/middleware --type ts -B2

Repository: InfinityBowman/corates

Length of output: 922


Fix PROJECT_ROLES to match actual project member roles in members endpoint.

The members.ts endpoint uses ['owner', 'collaborator', 'viewer'] for project roles, but roles.ts only defines ProjectRole = 'member' | 'owner', missing collaborator and viewer. This creates a type mismatch where hasProjectRole() won't properly validate roles accepted by the API.

Update the ProjectRole type and PROJECT_ROLES array to include all roles used in practice:

export type ProjectRole = 'member' | 'collaborator' | 'viewer' | 'owner';
export const PROJECT_ROLES: readonly ProjectRole[] = ['member', 'collaborator', 'viewer', 'owner'];
🤖 Prompt for AI Agents
In `@packages/workers/src/policies/lib/roles.ts` around lines 1 - 16, The
ProjectRole union and PROJECT_ROLES array are missing actual project roles used
by the members endpoint, causing type mismatches in project role checks (e.g.,
hasProjectRole); update the ProjectRole type to include 'member' |
'collaborator' | 'viewer' | 'owner' and replace PROJECT_ROLES with a readonly
array containing those values in ascending order (member, collaborator, viewer,
owner) so runtime checks and TypeScript types align with the members endpoint.

Comment on lines 247 to +249
const promo = promoCodes.data[0];
const coupon = promo.coupon;
// Access coupon through the expand or direct access
const coupon = (promo as unknown as { coupon: Stripe.Coupon }).coupon;
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

🌐 Web query:

Stripe API promotionCodes.list coupon property TypeScript types

💡 Result:

Short answer: promotionCodes.list returns PromotionCode objects whose coupon property is a Coupon object (Stripe.Coupon) in the official TypeScript types — not a plain ID string. See the Stripe API docs for the Promotion Code object and the TypeScript declaration in stripe-node. [1][2]

Sources:

  • Stripe Promotion Code object (shows coupon embedded). [1]
  • @stripe/stripe-node TypeScript types (PromotionCode.coupon: Stripe.Coupon). [2]

🏁 Script executed:

cd packages/workers/src/routes/billing && head -260 checkout.ts | tail -30

Repository: InfinityBowman/corates

Length of output: 1139


Remove unnecessary double type assertion; access coupon directly.

The Stripe PromotionCode type directly exposes coupon as Stripe.Coupon, so the double assertion is unnecessary. Simplify to const coupon = promo.coupon;

🤖 Prompt for AI Agents
In `@packages/workers/src/routes/billing/checkout.ts` around lines 247 - 249, The
code uses an unnecessary double type assertion to access the coupon: replace the
cast "(promo as unknown as { coupon: Stripe.Coupon }).coupon" with a direct
property access on the PromotionCode object; update the line that assigns coupon
(currently using promo and promoCodes) to simply use promo.coupon (or
promoCodes.data[0].coupon) and remove the extra type coercion to rely on
Stripe's PromotionCode type.

@InfinityBowman
Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 16, 2026

✅ Actions performed

Review triggered.

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

@InfinityBowman
Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 16, 2026

✅ Actions performed

Full review triggered.

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: 19

Caution

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

⚠️ Outside diff range comments (16)
packages/workers/src/routes/google-drive.ts (2)

148-158: Potential null dereference when returning access token.

The condition at line 152 only checks token expiry but doesn't verify tokens.accessToken is truthy. If accessToken is null but accessTokenExpiresAt is set, the non-null assertion at line 153 will incorrectly return null.

Proposed fix
-  if (expiresAt && new Date(expiresAt).getTime() - now.getTime() > bufferTime) {
+  if (tokens.accessToken && expiresAt && new Date(expiresAt).getTime() - now.getTime() > bufferTime) {
     return tokens.accessToken!;
   }
 
-  if (!expiresAt || Number.isNaN(new Date(expiresAt).getTime())) {
-    if (tokens.accessToken) return tokens.accessToken;
-  }
+  // If expiry is unknown but we have an access token, try using it
+  if (tokens.accessToken && (!expiresAt || Number.isNaN(new Date(expiresAt).getTime()))) {
+    return tokens.accessToken;
+  }

498-528: Silent failure on database insert creates orphaned R2 objects.

If the mediaFiles insert fails, the error is logged but the route still returns success. This leaves an orphaned file in R2 with no database record, and the client believes the import succeeded.

Proposed fix — propagate DB error or clean up R2 object
     // Insert into mediaFiles table
     const mediaFileId = crypto.randomUUID();
-    try {
-      await db.insert(mediaFiles).values({
-        id: mediaFileId,
-        filename: uniqueFileName,
-        originalName: originalFileName,
-        fileType: 'application/pdf',
-        fileSize: fileSize,
-        uploadedBy: user!.id,
-        bucketKey: r2Key,
-        orgId: project.orgId,
-        projectId,
-        studyId,
-        createdAt: new Date(),
-      });
-    } catch (dbError) {
-      console.error('Failed to insert mediaFiles record after Google Drive import:', dbError);
-    }
+    await db.insert(mediaFiles).values({
+      id: mediaFileId,
+      filename: uniqueFileName,
+      originalName: originalFileName,
+      fileType: 'application/pdf',
+      fileSize: fileSize,
+      uploadedBy: user!.id,
+      bucketKey: r2Key,
+      orgId: project.orgId,
+      projectId,
+      studyId,
+      createdAt: new Date(),
+    });

The outer catch block (lines 529-540) will handle the error and return a proper error response. Alternatively, if you want to attempt cleanup, delete the R2 object in the catch block before re-throwing.

packages/workers/src/routes/billing/helpers/orgContext.ts (1)

26-44: Wrap DB lookups in domain-error handling.

Both helpers call the database without try/catch, so a DB failure bubbles as a raw error instead of a domain error. Please wrap the lookup blocks and surface SYSTEM_ERRORS.DB_ERROR via createDomainError.

💡 Suggested fix
 import type { DrizzleD1Database } from 'drizzle-orm/d1';
 import type * as schema from '@/db/schema.js';
+import { createDomainError, SYSTEM_ERRORS } from '@corates/shared';

 export async function resolveOrgId({
   db,
   session,
   userId,
 }: ResolveOrgParams): Promise<string | null> {
   let orgId = session?.activeOrganizationId;

   // If no active org in session, get user's first org
   if (!orgId) {
-    const { member } = await import('@/db/schema.js');
-    const { eq } = await import('drizzle-orm');
-    const firstMembership = await db
-      .select({ organizationId: member.organizationId })
-      .from(member)
-      .where(eq(member.userId, userId))
-      .limit(1)
-      .get();
-    orgId = firstMembership?.organizationId;
+    try {
+      const { member } = await import('@/db/schema.js');
+      const { eq } = await import('drizzle-orm');
+      const firstMembership = await db
+        .select({ organizationId: member.organizationId })
+        .from(member)
+        .where(eq(member.userId, userId))
+        .limit(1)
+        .get();
+      orgId = firstMembership?.organizationId;
+    } catch {
+      throw createDomainError(SYSTEM_ERRORS.DB_ERROR);
+    }
   }

   return orgId || null;
 }

 export async function resolveOrgIdWithRole({
   db,
   session,
   userId,
 }: ResolveOrgParams): Promise<OrgIdWithRole> {
   let orgId = session?.activeOrganizationId;
   let role: string | null = null;

-  if (!orgId) {
-    const { member } = await import('@/db/schema.js');
-    const { eq } = await import('drizzle-orm');
-    const firstMembership = await db
-      .select({ organizationId: member.organizationId, role: member.role })
-      .from(member)
-      .where(eq(member.userId, userId))
-      .limit(1)
-      .get();
-    orgId = firstMembership?.organizationId;
-    role = firstMembership?.role || null;
-  } else {
-    // Get role for active org
-    const { member } = await import('@/db/schema.js');
-    const { eq, and } = await import('drizzle-orm');
-    const membership = await db
-      .select({ role: member.role })
-      .from(member)
-      .where(and(eq(member.organizationId, orgId), eq(member.userId, userId)))
-      .get();
-    role = membership?.role || null;
-  }
+  try {
+    if (!orgId) {
+      const { member } = await import('@/db/schema.js');
+      const { eq } = await import('drizzle-orm');
+      const firstMembership = await db
+        .select({ organizationId: member.organizationId, role: member.role })
+        .from(member)
+        .where(eq(member.userId, userId))
+        .limit(1)
+        .get();
+      orgId = firstMembership?.organizationId;
+      role = firstMembership?.role || null;
+    } else {
+      // Get role for active org
+      const { member } = await import('@/db/schema.js');
+      const { eq, and } = await import('drizzle-orm');
+      const membership = await db
+        .select({ role: member.role })
+        .from(member)
+        .where(and(eq(member.organizationId, orgId), eq(member.userId, userId)))
+        .get();
+      role = membership?.role || null;
+    }
+  } catch {
+    throw createDomainError(SYSTEM_ERRORS.DB_ERROR);
+  }

   return { orgId: orgId || null, role };
 }

As per coding guidelines, wrap DB operations and return domain errors.

Also applies to: 52-80

packages/workers/src/routes/orgs/pdfs.ts (4)

372-432: Protect against concurrent filename collisions during upload.
Two parallel uploads can still select the same “available” name; with the current R2-first flow, a later DB insert failure can delete or overwrite another upload’s object. Consider reserving the filename in the DB first (retry on conflict) and only uploading to R2 after a successful insert, or store objects under unique IDs and keep filenames as metadata.


520-527: Fix middleware order to match the required auth chain.
requireProjectAccess should run before org-level write/entitlement checks to keep authorization flow consistent.

🔧 Proposed fix
@@
-  const writeAccessResponse = await runMiddleware(requireOrgWriteAccess(), c);
-  if (writeAccessResponse) return writeAccessResponse;
-
-  const projectAccessResponse = await runMiddleware(requireProjectAccess(), c);
-  if (projectAccessResponse) return projectAccessResponse;
+  const projectAccessResponse = await runMiddleware(requireProjectAccess(), c);
+  if (projectAccessResponse) return projectAccessResponse;
+
+  const writeAccessResponse = await runMiddleware(requireOrgWriteAccess(), c);
+  if (writeAccessResponse) return writeAccessResponse;
@@
-  const writeAccessResponse = await runMiddleware(requireOrgWriteAccess(), c);
-  if (writeAccessResponse) return writeAccessResponse;
-
-  const projectAccessResponse = await runMiddleware(requireProjectAccess(), c);
-  if (projectAccessResponse) return projectAccessResponse;
+  const projectAccessResponse = await runMiddleware(requireProjectAccess(), c);
+  if (projectAccessResponse) return projectAccessResponse;
+
+  const writeAccessResponse = await runMiddleware(requireOrgWriteAccess(), c);
+  if (writeAccessResponse) return writeAccessResponse;

As per coding guidelines, keep the chain in requireAuth → requireOrgMembership → requireProjectAccess → requireEntitlement → validateRequest order.

Also applies to: 793-798


557-609: Accept valid PDF content types with parameters.
Strict equality will reject common variants like application/pdf; charset=binary.

🔧 Proposed fix
-  const contentType = c.req.header('Content-Type') || '';
+  const contentType = (c.req.header('Content-Type') || '').toLowerCase();
@@
-    } else if (contentType === 'application/pdf') {
+    } else if (contentType.startsWith('application/pdf')) {

732-742: Guard against decodeURIComponent throwing on malformed filenames.
Bad percent-encoding will currently throw and surface as a 500. Return a validation error instead.

🔧 Proposed fix
@@
-  const { fileName: rawFileName } = c.req.valid('param');
-  const fileName = decodeURIComponent(rawFileName);
+  const { fileName: rawFileName } = c.req.valid('param');
+  let fileName: string;
+  try {
+    fileName = decodeURIComponent(rawFileName);
+  } catch {
+    const error = createDomainError(
+      VALIDATION_ERRORS.FIELD_INVALID_FORMAT,
+      { field: 'fileName', value: rawFileName },
+      'Invalid file name encoding',
+    );
+    return c.json(error, error.statusCode as ContentfulStatusCode);
+  }
@@
-  const { fileName: rawFileName } = c.req.valid('param');
-  const fileName = decodeURIComponent(rawFileName);
+  const { fileName: rawFileName } = c.req.valid('param');
+  let fileName: string;
+  try {
+    fileName = decodeURIComponent(rawFileName);
+  } catch {
+    const error = createDomainError(
+      VALIDATION_ERRORS.FIELD_INVALID_FORMAT,
+      { field: 'fileName', value: rawFileName },
+      'Invalid file name encoding',
+    );
+    return c.json(error, error.statusCode as ContentfulStatusCode);
+  }

Also applies to: 813-823

packages/workers/src/routes/orgs/members.ts (1)

612-669: Wrap pre-check failures with structured error handling.

requireMemberRemoval calls getProjectMembership, which performs a raw database query without error wrapping. If the database operation fails, it throws an unhandled error. The first try-catch rethrows this, bypassing the structured error contract and surfacing a 500. Wrap unexpected errors into SYSTEM_ERRORS.DB_ERROR to match the pattern used in the second try-catch block for removeMember.

Proposed fix
  try {
    await requireMemberRemoval(db, authUser.id, projectId, memberId);
  } catch (err) {
    if (isDomainError(err)) {
      return c.json(err, err.statusCode as ContentfulStatusCode);
    }
    const error = err as Error;
    console.error('Error validating member removal:', error);
    const dbError = createDomainError(SYSTEM_ERRORS.DB_ERROR, {
      operation: 'remove_project_member_precheck',
      originalError: error.message,
    });
    return c.json(dbError, dbError.statusCode as ContentfulStatusCode);
  }
packages/workers/src/routes/admin/projects.ts (1)

339-346: Guard pagination parsing against non-numeric and invalid inputs.

parseInt can yield NaN (from invalid strings like "abc") or 0 (from "0"), both of which propagate into invalid offset, limit, and totalPages calculations. The current code only handles undefined via the || fallback, leaving these edge cases unguarded.

🛠️ Suggested guard for pagination parsing
-    const page = parseInt(query.page || '1', 10);
-    const limit = Math.min(parseInt(query.limit || '20', 10), 100);
+    const pageRaw = Number.parseInt(query.page ?? '1', 10);
+    const limitRaw = Number.parseInt(query.limit ?? '20', 10);
+    const page = Number.isFinite(pageRaw) && pageRaw > 0 ? pageRaw : 1;
+    const limit = Math.min(
+      Number.isFinite(limitRaw) && limitRaw > 0 ? limitRaw : 20,
+      100,
+    );
packages/workers/src/routes/billing/handlers/customerHandlers.ts (1)

75-89: Wrap database operations in try-catch blocks and return structured domain errors.

Database operations in handleCustomerUpdated and handleCustomerDeleted lack try-catch blocks, violating the coding guideline to wrap database operations and return domain errors using createDomainError. If any database query fails, the exception propagates without structured error handling. Additionally, the db.query.user.findFirst calls should also be protected.

♻️ Suggested error handling pattern
+ import { createDomainError, SYSTEM_ERRORS } from '@corates/shared';
...
export async function handleCustomerUpdated(
  customer: Stripe.Customer,
  ctx: WebhookContext,
): Promise<WebhookResult> {
  const db = getDb(ctx);
  const { logger } = ctx;
  const stripeCustomerId = customer.id;

+ try {
    const existingUser = await db.query.user.findFirst({
      where: (user, { eq }) => eq(user.stripeCustomerId, stripeCustomerId),
    });
+ } catch (error) {
+   return {
+     handled: true,
+     result: 'db_error',
+     error: createDomainError(SYSTEM_ERRORS.DB_ERROR).message,
+   };
+ }

  if (hasChanges) {
+   try {
      await db
        .update(user)
        .set({
          ...updates,
          updatedAt: new Date(),
        })
        .where(eq(user.id, existingUser.id));
+   } catch (error) {
+     return {
+       handled: true,
+       result: 'db_error',
+       error: createDomainError(SYSTEM_ERRORS.DB_ERROR).message,
+     };
+   }
    ...
  }
}
packages/workers/src/routes/admin/stats.ts (1)

562-592: Use autoPagingToArray to fetch all subscription statuses and invoices, not just the first 100.

Lines 570–575 and 616–622 limit results to 100 items per request. If there are more than 100 subscriptions in any status or 100 paid invoices, the counts and revenue totals will be significantly understated. Use autoPagingToArray({ limit: 10000 }) to fetch all results across pages.

Suggested fix
const statusCounts = await Promise.all([
-  stripe.subscriptions.search({ query: 'status:"active"', limit: 100 }),
-  stripe.subscriptions.search({ query: 'status:"trialing"', limit: 100 }),
-  stripe.subscriptions.search({ query: 'status:"past_due"', limit: 100 }),
-  stripe.subscriptions.search({ query: 'status:"canceled"', limit: 100 }),
+  stripe.subscriptions.search({ query: 'status:"active"', limit: 100 }).autoPagingToArray({ limit: 10000 }),
+  stripe.subscriptions.search({ query: 'status:"trialing"', limit: 100 }).autoPagingToArray({ limit: 10000 }),
+  stripe.subscriptions.search({ query: 'status:"past_due"', limit: 100 }).autoPagingToArray({ limit: 10000 }),
+  stripe.subscriptions.search({ query: 'status:"canceled"', limit: 100 }).autoPagingToArray({ limit: 10000 }),
]);

// Similar fix for invoices at line 616:
-const invoices = await stripe.invoices.list({
+const invoices = await stripe.invoices.list({
   status: 'paid',
   created: {
     gte: Math.floor(startDate.getTime() / 1000),
   },
   limit: 100,
-});
+}).autoPagingToArray({ limit: 10000 });
packages/workers/src/commands/projects/deleteProject.ts (1)

35-71: Batch related DB operations and wrap all DB errors.

Project/members reads are outside the DB-error mapping and the related DB ops aren’t batched, which can leak raw errors and break the “all succeed or all fail” guideline.

🔧 Proposed refactor
-  const project = await db
-    .select({ name: projects.name })
-    .from(projects)
-    .where(eq(projects.id, projectId))
-    .get();
-
-  const members = await db
-    .select({ userId: projectMembers.userId })
-    .from(projectMembers)
-    .where(eq(projectMembers.projectId, projectId))
-    .all();
+  let project: { name: string } | undefined;
+  let members: { userId: string }[];
+
+  try {
+    const [projectRows, memberRows, _deleteResult] = await db.batch([
+      db.select({ name: projects.name }).from(projects).where(eq(projects.id, projectId)),
+      db
+        .select({ userId: projectMembers.userId })
+        .from(projectMembers)
+        .where(eq(projectMembers.projectId, projectId)),
+      db.delete(projects).where(eq(projects.id, projectId)),
+    ]);
+    project = (projectRows as { name: string }[])[0];
+    members = memberRows as { userId: string }[];
+  } catch (err) {
+    throw createDomainError(
+      SYSTEM_ERRORS.DB_ERROR,
+      { operation: 'delete_project', projectId, originalError: (err as Error).message },
+      'Failed to delete project',
+    );
+  }
-
-  try {
-    await db.delete(projects).where(eq(projects.id, projectId));
-  } catch (err) {
-    throw createDomainError(
-      SYSTEM_ERRORS.DB_ERROR,
-      { operation: 'delete_project', projectId, originalError: (err as Error).message },
-      'Failed to delete project',
-    );
-  }
As per coding guidelines, related DB operations should be batched and wrapped with domain errors.
packages/workers/src/routes/admin/stripe-tools.ts (1)

734-805: Use null instead of 0 for missing Stripe billing period timestamps.

The Stripe API's current_period_start and current_period_end fields can be missing or null for subscriptions in certain states (incomplete, immediately canceled, or not yet invoiced). Defaulting to 0 misrepresents this as the Unix epoch instead of indicating missing data. Update the schema to use .nullable() and change the fallback to null.

Proposed fix
-          currentPeriodStart: subWithPeriods.current_period_start ?? 0,
-          currentPeriodEnd: subWithPeriods.current_period_end ?? 0,
+          currentPeriodStart: subWithPeriods.current_period_start ?? null,
+          currentPeriodEnd: subWithPeriods.current_period_end ?? null,

Update the response schema to allow null values for these fields.

packages/workers/src/routes/orgs/invitations.ts (1)

915-923: Default invitation roles to 'member' to handle nullable schema columns.

The role column in projectInvitations is nullable (no .notNull() constraint), allowing legacy invites to have null roles. This nullable value is then inserted directly into projectMembers at line 920 without safeguarding, potentially storing null instead of a valid role. Apply a consistent default across all role usages:

🛠️ Suggested fix
+    const memberRole = invitation.role ?? 'member';
+    const orgMemberRole = invitation.orgRole ?? 'member';
     // Always add user to project (projects are invite-only)
     batchOps.push(
       db.insert(projectMembers).values({
         id: crypto.randomUUID(),
         projectId: invitation.projectId,
         userId: authUser.id,
-        role: invitation.role,
+        role: memberRole,
         joinedAt: nowDate,
       }),
     );

Also apply to line 996 (syncMemberToDO) and line 1012 (JSON response) for consistency.

Also applies to: 992-996, 1006-1013

packages/workers/src/index.ts (1)

316-321: Point legacy ProjectDoc clients to the new endpoint.

Line 320 currently directs callers to /api/orgs/:orgId/project-doc/:projectId, which this file also returns 410 for. That sends clients to a dead end; it should point to /api/project-doc/:projectId.

🔧 Suggested fix
-      message: 'This endpoint has been moved. Use /api/orgs/:orgId/project-doc/:projectId instead.',
+      message: 'This endpoint has been moved. Use /api/project-doc/:projectId instead.',
packages/workers/src/routes/billing/subscription.ts (1)

232-257: Coerce projectCount to a number for schema consistency.

Line 256 returns projectCountResult?.count directly without coercion, violating the z.number() response schema (line 41). The same file demonstrates the correct pattern in the usageRoute handler (lines 188-189) where identical count queries are wrapped with Number(). Apply the same pattern here for consistency:

-      projectCount: projectCountResult?.count || 0,
+      projectCount: Number(projectCountResult?.count ?? 0),
🤖 Fix all issues with AI agents
In @.cursor/rules/corates.mdc:
- Line 63: Update the rule text that currently references `@ark-ui/solid` to
instead reference the centralized wrapper package `@corates/ui`; specifically
replace the entry that reads something like "**Ark UI**: UI components
(`@ark-ui/solid`)" so it reads "**Ark UI**: UI components (from `@corates/ui`
package)" to reflect that the codebase imports Ark UI via the `@corates/ui`
wrapper rather than directly from `@ark-ui/solid`.

In @.github/copilot-instructions.md:
- Line 59: Update the Libraries entry for "Ark UI" in the critical rules section
so it no longer instructs developers to import from `@ark-ui/solid`; change the
description to indicate imports should come from the wrapper package `@corates/ui`
(e.g., replace the text "`@ark-ui/solid`" with "`import from `@corates/ui``" or
add a parenthetical note that `@ark-ui/solid` is the underlying technology but all
imports must use `@corates/ui`). Ensure the entry referencing "**Ark UI**" is the
only place modified.

In @.github/instructions/api-routes.instructions.md:
- Around line 56-64: Update the example to use the project's standardized domain
errors instead of freeform strings: replace notFound/forbidden/badRequest usage
with createDomainError and the relevant error constant enums (PROJECT_ERRORS,
AUTH_ERRORS, VALIDATION_ERRORS), and pass contextual metadata (e.g., { projectId
}, { orgId }, { field }) so client handling remains consistent with the rest of
the codebase and createDomainError contract.

In `@packages/docs/audits/ai-agent-pipeline-comparison-solidtype-vs-corates.md`:
- Line 28: Several fenced code blocks in
packages/docs/audits/ai-agent-pipeline-comparison-solidtype-vs-corates.md are
missing language specifiers; update the three-tick fences that wrap the
directory/listing snippets (specifically the blocks containing "AGENTS.md (Entry
Point)", ".github/copilot-instructions.md (GitHub Copilot - Always Applied)",
"plan/", "packages/docs/plans/", and "refs/") to include a language token such
as text or plaintext (e.g., change ``` to ```text) so linting and syntax
highlighting work correctly.

In `@packages/docs/plans/backend-typescript.md`:
- Around line 55-61: Update the Phase 1 list in the backend TypeScript plan to
mark the "shared validation hook (honoValidationHook)" item as completed: locate
the Phase 1 section listing "Extract shared validation hook to
/lib/honoValidationHook.ts" and change its status to "Done" (or add a
check/strike-through and a brief note referencing the implemented
honoValidationHook), so readers won't attempt to re-implement it; ensure the
other Phase 1 items (migrate index.js and docs.js to TypeScript) remain
unchanged.

In `@packages/docs/plans/shared-zod-api-types.md`:
- Around line 23-36: The fenced ASCII diagram in shared-zod-api-types.md is
missing a language specifier; update the opening fence for the diagram block
from ``` to ```text (or ```plaintext) so the diagram is treated as plain
text—locate the ASCII block showing the `@corates/shared` tree (the
triple-backtick section that begins with "@corates/shared" and change its fence
accordingly.

In `@packages/docs/STATUS.md`:
- Line 3: The STATUS.md document contains an outdated date string "**Last
Updated:** 2025-01-15"; update that literal to the correct date (e.g., "**Last
Updated:** 2026-01-15") in the STATUS.md file so the last-updated header
reflects the PR context; locate and replace the date line in
packages/docs/STATUS.md where the "**Last Updated:**" heading appears.

In `@packages/workers/src/commands/billing/processCheckoutSession.ts`:
- Around line 169-177: The metadata object currently casts grantType too broadly
and over-asserts payment_intent; change the grantType assertion in the object to
only 'single_project' (since earlier logic returns unless grantType ===
'single_project') and tighten stripePaymentIntentId to a plain string (or string
| null if you prefer) instead of 'string | undefined' because session.mode ===
'payment' guarantees an ID string; update the object where grantType and
stripePaymentIntentId are set (the metadata block in processCheckoutSession.ts
referencing grantType and session.payment_intent).

In `@packages/workers/src/commands/members/updateMemberRole.ts`:
- Around line 7-15: The DB write that updates the member role should be wrapped
in a try/catch that maps any unexpected DB failure to a DomainError with
SYSTEM_ERRORS.DB_ERROR while preserving existing DomainError throws for
LAST_OWNER; locate the update call using createDb() and projectMembers (the
write likely inside the function that calls requireSafeRoleChange and then
performs the DB update and subsequent sync/notify). Surround the db
update/execute call with try { ... } catch (err) { throw new
DomainError(SYSTEM_ERRORS.DB_ERROR, { cause: err }); } so existing checks that
throw the LAST_OWNER DomainError remain unchanged and only genuine DB exceptions
are remapped. Ensure the same pattern is applied to the other DB write in the
file that the reviewer flagged (the similar update block near the
syncMemberToDO/notifyUser flow).

In `@packages/workers/src/index.ts`:
- Around line 74-88: The OpenAPI exposure is currently unconditional but the
comment says "development only"; wrap the app.doc31('/openapi.json', ...)
registration in an environment check using c.env.ENVIRONMENT (e.g. only register
when ENVIRONMENT !== 'production') so the handler is enabled in
development/local but not in production; locate the app.doc31 call in
packages/workers/src/index.ts and guard it with the ENVIRONMENT check (or
alternatively update the comment to remove "development only" if you prefer
keeping it always-on).

In `@packages/workers/src/lib/honoValidationHook.ts`:
- Around line 26-47: The validationHook currently always constructs the error
with VALIDATION_ERRORS.FIELD_REQUIRED; update validationHook to choose the error
code based on the detected failure type: use FIELD_REQUIRED when isMissing is
true, map firstIssue.code (e.g. 'too_big' -> VALIDATION_ERRORS.FIELD_TOO_LONG,
'too_small' -> VALIDATION_ERRORS.FIELD_TOO_SHORT,
'invalid_string'/'invalid_format'/'invalid_type' ->
VALIDATION_ERRORS.FIELD_INVALID_FORMAT or VALIDATION_ERRORS.INVALID_INPUT) and
fall back to VALIDATION_ERRORS.INVALID_INPUT for unknown cases; then call
createValidationError(String(field), selectedErrorCode, null) and keep the
computed message (message or customized messages) before returning the error —
reference validationHook, firstIssue, issueReceived, isMissing,
createValidationError, and VALIDATION_ERRORS to locate and implement the logic.

In `@packages/workers/src/policies/billing.ts`:
- Around line 49-67: requireOrgOwner currently checks role with a raw string
comparison; change it to use the shared helper isOrgOwner so role-checking logic
remains consistent with canManageBilling (which uses
isOrgOwner(membership.role)). Update the requireOrgOwner implementation to call
isOrgOwner(role) and throw the same FORBIDDEN domain error when it returns
false, leaving the no-org check unchanged; reference requireOrgOwner,
canManageBilling, and isOrgOwner to locate and modify the function.

In `@packages/workers/src/routes/admin/users.js`:
- Around line 33-37: Update the ValidationResult interface used by
validationHook to include the missing target property so it matches the Hook
type from `@hono/zod-openapi`; change the interface referenced by validationHook
to include target (e.g., target: string or target: keyof ValidationTargets)
alongside success and optional error, leaving the validationHook return type
(Response | undefined) unchanged to maintain compatibility with the Hook
generic.

In `@packages/workers/src/routes/billing/handlers/dunning.ts`:
- Around line 192-200: The email HTML interpolates userName directly in the
template in dunning.ts; add a small HTML-escaping helper (e.g., escapeHtml(str:
string | null): string) that replaces &, <, > and " with their entities and use
it when rendering the greeting so the template uses escapeHtml(userName) ||
'there' instead of userName directly; update any other interpolations in this
template (like invoiceUrl or amount) if they can be user-controlled to use the
same helper or strict validation.

In `@packages/workers/src/routes/billing/handlers/invoiceHandlers.ts`:
- Around line 170-175: The code builds failureReason using
invoice.last_finalization_error?.message or
invoice.status_transitions?.finalized_at (a Unix timestamp) which yields
misleading numeric strings; change the fallback to a meaningful value by
removing the timestamp fallback and instead use something like invoice.status
or, if a date is desired, format finalized_at into an ISO string via new
Date(finalized_at * 1000).toISOString(); update the failureReason assignment
(variable name failureReason in invoiceHandlers.ts) to use
invoice.last_finalization_error?.message || invoice.status || 'unknown' (or the
formatted date approach) so the displayed reason is human-readable.

In `@packages/workers/src/routes/billing/webhookRouter.ts`:
- Around line 97-100: The router currently casts data.object to Stripe.Customer
for the 'customer.deleted' event; change the cast to Stripe.DeletedCustomer and
update the handler signature for handleCustomerDeleted to accept a
Stripe.DeletedCustomer (instead of Stripe.Customer). Locate
handleCustomerDeleted and its router invocation and change the parameter type to
Stripe.DeletedCustomer, and update any internal code in handleCustomerDeleted to
use only fields present on DeletedCustomer (id, object, deleted) to maintain
type correctness.

In `@packages/workers/src/routes/google-drive.ts`:
- Around line 360-377: The route currently only checks Google connection; add a
project access check by invoking requireProjectAccess before proceeding with the
import logic. After you parse { fileId, projectId, studyId } from
c.req.valid('json') (inside googleDriveRoutes.openapi(importRoute, async c => {
... })), call await requireProjectAccess(c, projectId) (or the equivalent
project-scoped middleware) and return its error if any, then continue with
getGoogleTokens/getValidAccessToken and the rest of the handler.

In `@README.md`:
- Line 56: The phrase "CoRATES specific" in the README line for "CoRATES MCP"
should be hyphenated as a compound adjective; update the text "- **CoRATES
MCP**: Custom MCP server for CoRATES specific AI tooling" to read "Custom MCP
server for CoRATES-specific AI tooling" so the compound modifier is
grammatically correct.

In `@scripts/loc-report.mjs`:
- Around line 38-42: The checkCommand function currently uses the Unix-only
'which' and a macOS-specific brew hint; make it cross-platform by selecting the
locator based on process.platform (use 'where' on Windows, 'which' or 'command
-v' on POSIX) or by using a Node package like 'which' to resolve executables,
and update the spawnSync call (refer to checkCommand and the existing spawnSync
invocation) to use the chosen locator; change the error text emitted via
console.error(c('yellow', ...)) to a neutral install hint (e.g., "<command> not
found. Install it via your system package manager or from
https://<command>.org") rather than referencing brew so Windows users aren't
misled.
♻️ Duplicate comments (2)
packages/workers/src/policies/lib/roles.ts (1)

8-16: Project role hierarchy is incomplete.

ProjectRole/PROJECT_ROLES omit roles used elsewhere (e.g., collaborator/viewer), which makes hasProjectRole reject valid roles.

🔧 Proposed fix
-export type ProjectRole = 'member' | 'owner';
+export type ProjectRole = 'viewer' | 'member' | 'collaborator' | 'owner';

 // Project role hierarchy (lowest to highest)
-export const PROJECT_ROLES: readonly ProjectRole[] = ['member', 'owner'];
+export const PROJECT_ROLES: readonly ProjectRole[] = [
+  'viewer',
+  'member',
+  'collaborator',
+  'owner',
+];
Based on learnings, project role hierarchy should include viewer and collaborator roles.
packages/workers/src/routes/billing/checkout.ts (1)

217-219: Unnecessary double type assertion on coupon property.

The Stripe PromotionCode type directly exposes coupon as Stripe.Coupon. The double assertion (promo as unknown as { coupon: Stripe.Coupon }).coupon is unnecessary.

♻️ Simplify coupon access
     const promo = promoCodes.data[0];
-    // Access coupon through the expand or direct access
-    const coupon = (promo as unknown as { coupon: Stripe.Coupon }).coupon;
+    const coupon = promo.coupon;

Comment thread .cursor/rules/corates.mdc
- **Drizzle ORM**: ALL database interactions and migrations
- **Better-Auth**: Authentication and user management
- **Ark UI**: UI components from `@corates/ui` package
- **Ark UI**: UI components (`@ark-ui/solid`)
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:

#!/bin/bash

echo "=== Checking for `@corates/ui` references in source code ==="
rg -n '@corates/ui' packages/ --type ts --type tsx --type js --type jsx 2>/dev/null | head -20

echo ""
echo "=== Checking for `@ark-ui/solid` references in source code ==="
rg -n '@ark-ui/solid' packages/ --type ts --type tsx --type js --type jsx 2>/dev/null | head -20

echo ""
echo "=== Checking corates/ui imports ==="
rg "from ['\"]corates/ui" packages/ --type ts --type tsx --type js --type jsx 2>/dev/null | head -20

echo ""
echo "=== Checking package.json for ui package dependencies ==="
find packages -name 'package.json' -type f -exec sh -c 'echo "=== {} ===" && grep -E "@ark-ui|@corates/ui|corates/ui" "$1" || echo "No Ark UI references found"' _ {} \; 2>/dev/null

echo ""
echo "=== Checking if corates/ui package exists ==="
ls -la packages/ui 2>/dev/null || echo "No packages/ui directory found"

Repository: InfinityBowman/corates

Length of output: 1301


🏁 Script executed:

#!/bin/bash

echo "=== Content of .cursor/rules/corates.mdc around line 63 ==="
cat -n .cursor/rules/corates.mdc | sed -n '55,75p'

echo ""
echo "=== Checking other rule files for Ark UI references ==="
rg -n 'Ark UI|@ark-ui|@corates/ui' .cursor/rules/ .github/

echo ""
echo "=== Checking git history or any comments about this change ==="
cat -n .cursor/rules/corates.mdc | head -75

Repository: InfinityBowman/corates

Length of output: 5337


Update line 63 to reference the @corates/ui package wrapper, not @ark-ui/solid directly.

The rule file incorrectly directs developers to use @ark-ui/solid directly. However, the actual codebase structure uses @corates/ui as a centralized wrapper package that re-exports Ark UI components. All imports in the web package use @corates/ui, not @ark-ui/solid.

Line 63 should read: - **Ark UI**: UI components (from @corates/ui package)

This aligns with:

  • The actual dependency structure (web imports @corates/ui, which internally depends on @ark-ui/solid)
  • The established learnings that require using @corates/ui package
  • Current codebase usage patterns
🤖 Prompt for AI Agents
In @.cursor/rules/corates.mdc at line 63, Update the rule text that currently
references `@ark-ui/solid` to instead reference the centralized wrapper package
`@corates/ui`; specifically replace the entry that reads something like "**Ark
UI**: UI components (`@ark-ui/solid`)" so it reads "**Ark UI**: UI components
(from `@corates/ui` package)" to reflect that the codebase imports Ark UI via
the `@corates/ui` wrapper rather than directly from `@ark-ui/solid`.

- **Drizzle ORM**: ALL database interactions and migrations
- **Better-Auth**: Authentication and user management
- **Ark UI**: UI components from `@corates/ui` package
- **Ark UI**: UI components (`@ark-ui/solid`)
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 `@corates/ui` package exists
echo "=== Checking for corates/ui package ==="
fd -t f "package.json" packages/ui

# Check the contents of the ui package
if [ -d "packages/ui" ]; then
  echo -e "\n=== corates/ui package.json ==="
  cat packages/ui/package.json | head -20
fi

# Search for imports from corates/ui across the codebase
echo -e "\n=== Imports from corates/ui ==="
rg -n "from ['\"]corates/ui['\"]" --type ts --type tsx --type js --type jsx -C1 | head -50

# Search for imports from `@ark-ui/solid`
echo -e "\n=== Imports from `@ark-ui/solid` ==="
rg -n "from ['\"]@ark-ui/solid['\"]" --type ts --type tsx --type js --type jsx -C1 | head -50

# Check if there are any references to corates/ui in import paths
echo -e "\n=== Other corates/ui references ==="
rg "corates/ui" --type ts --type tsx --type js --type jsx --type json | head -30

Repository: InfinityBowman/corates

Length of output: 876


🏁 Script executed:

#!/bin/bash

# Search for imports from corates/ui with broader search
echo "=== All imports from corates/ui ==="
rg "from ['\"]corates/ui['\"]" -C1

# Search for imports from `@ark-ui/solid`
echo -e "\n=== All imports from `@ark-ui/solid` ==="
rg "from ['\"]@ark-ui/solid['\"]" -C1

# Check what components the corates/ui package exports
echo -e "\n=== corates/ui index.ts ==="
cat packages/ui/src/index.ts

# Check the copilot instructions file context around line 59
echo -e "\n=== Full copilot-instructions.md context ==="
cat .github/copilot-instructions.md | head -80

Repository: InfinityBowman/corates

Length of output: 4174


🏁 Script executed:

#!/bin/bash

# Check the exact content around line 59 to see if there's more context
echo "=== copilot-instructions.md lines 50-70 ==="
sed -n '50,70p' .github/copilot-instructions.md

# Verify what the corates/ui package actually exports
echo -e "\n=== corates/ui components structure ==="
find packages/ui/src/components -type f -name "*.ts" -o -name "*.tsx" | head -20

# Check if there are any example imports in docs
echo -e "\n=== Looking for import examples in documentation ==="
rg "import.*from.*corates" packages/docs/

Repository: InfinityBowman/corates

Length of output: 3981


Fix misleading Ark UI library reference in critical rules section.

Line 59 lists the underlying library (@ark-ui/solid), but developers should import from the @corates/ui wrapper package instead. The project's actual documentation guides and established learnings consistently specify importing from @corates/ui.

Update the Libraries section to clarify the import source:

  • Ark UI: UI components (import from @corates/ui)

Or add a note that @ark-ui/solid is the underlying technology but the wrapper package should be used for all imports.

🤖 Prompt for AI Agents
In @.github/copilot-instructions.md at line 59, Update the Libraries entry for
"Ark UI" in the critical rules section so it no longer instructs developers to
import from `@ark-ui/solid`; change the description to indicate imports should
come from the wrapper package `@corates/ui` (e.g., replace the text
"`@ark-ui/solid`" with "`import from `@corates/ui``" or add a parenthetical note
that `@ark-ui/solid` is the underlying technology but all imports must use
`@corates/ui`). Ensure the entry referencing "**Ark UI**" is the only place
modified.

Comment on lines +56 to +64
Use `@corates/shared` error helpers:

```js
import { notFound, forbidden, badRequest } from '@corates/shared';

if (!project) throw notFound('Project not found');
if (!hasAccess) throw forbidden('Access denied');
if (!isValid) throw badRequest('Invalid input');
```
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

Align error-handling guidance with createDomainError + constants.

This section currently demonstrates notFound/forbidden/badRequest with freeform strings; that can conflict with the project’s standardized error codes. Consider updating the example (or adding a note) to emphasize createDomainError + error constants to keep client handling consistent. As per coding guidelines, ...

📝 Suggested doc alignment
-Use `@corates/shared` error helpers:
+Use `@corates/shared` error helpers with standard error constants:

 ```js
-import { notFound, forbidden, badRequest } from '@corates/shared';
+import { createDomainError, PROJECT_ERRORS, AUTH_ERRORS, VALIDATION_ERRORS } from '@corates/shared';

-if (!project) throw notFound('Project not found');
-if (!hasAccess) throw forbidden('Access denied');
-if (!isValid) throw badRequest('Invalid input');
+if (!project) throw createDomainError(PROJECT_ERRORS.NOT_FOUND, { projectId });
+if (!hasAccess) throw createDomainError(AUTH_ERRORS.FORBIDDEN, { orgId });
+if (!isValid) throw createDomainError(VALIDATION_ERRORS.INVALID_INPUT, { field: '...' });

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

In @.github/instructions/api-routes.instructions.md around lines 56 - 64, Update
the example to use the project's standardized domain errors instead of freeform
strings: replace notFound/forbidden/badRequest usage with createDomainError and
the relevant error constant enums (PROJECT_ERRORS, AUTH_ERRORS,
VALIDATION_ERRORS), and pass contextual metadata (e.g., { projectId }, { orgId
}, { field }) so client handling remains consistent with the rest of the
codebase and createDomainError contract.


</details>

<!-- fingerprinting:phantom:poseidon:eagle -->

<!-- This is an auto-generated comment by CodeRabbit -->


SolidType uses a **hierarchical documentation pyramid** with clear reading order:

```
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

Add language specifiers to fenced code blocks.

Multiple fenced code blocks are missing language specifiers, which affects markdown linting and may impact syntax highlighting. For directory structure representations, consider using text or plaintext as the language identifier.

📝 Proposed fixes for fenced code blocks

Line 28 - Add language specifier:

-```
+```text
 AGENTS.md (Entry Point)

Line 61 - Add language specifier:

-```
+```text
 .github/copilot-instructions.md (GitHub Copilot - Always Applied)

Line 101 - Add language specifier:

-```
+```text
 plan/

Line 157 - Add language specifier:

-```
+```text
 packages/docs/plans/

Line 328 - Add language specifier:

-```
+```text
 refs/

Also applies to: 61-61, 101-101, 157-157, 328-328

🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

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

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
In `@packages/docs/audits/ai-agent-pipeline-comparison-solidtype-vs-corates.md` at
line 28, Several fenced code blocks in
packages/docs/audits/ai-agent-pipeline-comparison-solidtype-vs-corates.md are
missing language specifiers; update the three-tick fences that wrap the
directory/listing snippets (specifically the blocks containing "AGENTS.md (Entry
Point)", ".github/copilot-instructions.md (GitHub Copilot - Always Applied)",
"plan/", "packages/docs/plans/", and "refs/") to include a language token such
as text or plaintext (e.g., change ``` to ```text) so linting and syntax
highlighting work correctly.

Comment on lines +55 to +61
Recommended Priority Order

Phase 1: Foundation (First)

1. Extract shared validation hook to /lib/honoValidationHook.ts
2. Migrate index.js to TypeScript
3. Migrate docs.js to TypeScript
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

Mark completed Phase 1 item to avoid confusion.

The shared validation hook appears implemented in this PR; consider annotating it as done in the plan so readers don’t re-scope it.

📝 Suggested doc tweak
-1. Extract shared validation hook to /lib/honoValidationHook.ts
+1. Extract shared validation hook to /lib/honoValidationHook.ts (completed in this PR)
📝 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
Recommended Priority Order
Phase 1: Foundation (First)
1. Extract shared validation hook to /lib/honoValidationHook.ts
2. Migrate index.js to TypeScript
3. Migrate docs.js to TypeScript
Recommended Priority Order
Phase 1: Foundation (First)
1. Extract shared validation hook to /lib/honoValidationHook.ts (completed in this PR)
2. Migrate index.js to TypeScript
3. Migrate docs.js to TypeScript
🤖 Prompt for AI Agents
In `@packages/docs/plans/backend-typescript.md` around lines 55 - 61, Update the
Phase 1 list in the backend TypeScript plan to mark the "shared validation hook
(honoValidationHook)" item as completed: locate the Phase 1 section listing
"Extract shared validation hook to /lib/honoValidationHook.ts" and change its
status to "Done" (or add a check/strike-through and a brief note referencing the
implemented honoValidationHook), so readers won't attempt to re-implement it;
ensure the other Phase 1 items (migrate index.js and docs.js to TypeScript)
remain unchanged.

Comment on lines 170 to 175
const failureReason =
invoice.last_finalization_error?.message ||
invoice.status_transitions?.finalized_at ||
(invoice.status_transitions?.finalized_at ?
String(invoice.status_transitions.finalized_at)
: null) ||
'unknown';
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

Questionable fallback for failureReason.

The fallback chain uses invoice.status_transitions?.finalized_at (a Unix timestamp) as an error reason string. This produces misleading failure reasons like "1705420800" instead of a meaningful message.

Consider using a more appropriate fallback or removing the finalized_at fallback entirely:

Suggested fix
  const failureReason =
    invoice.last_finalization_error?.message ||
-   (invoice.status_transitions?.finalized_at ?
-     String(invoice.status_transitions.finalized_at)
-   : null) ||
    'unknown';
📝 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
const failureReason =
invoice.last_finalization_error?.message ||
invoice.status_transitions?.finalized_at ||
(invoice.status_transitions?.finalized_at ?
String(invoice.status_transitions.finalized_at)
: null) ||
'unknown';
const failureReason =
invoice.last_finalization_error?.message ||
'unknown';
🤖 Prompt for AI Agents
In `@packages/workers/src/routes/billing/handlers/invoiceHandlers.ts` around lines
170 - 175, The code builds failureReason using
invoice.last_finalization_error?.message or
invoice.status_transitions?.finalized_at (a Unix timestamp) which yields
misleading numeric strings; change the fallback to a meaningful value by
removing the timestamp fallback and instead use something like invoice.status
or, if a date is desired, format finalized_at into an ISO string via new
Date(finalized_at * 1000).toISOString(); update the failureReason assignment
(variable name failureReason in invoiceHandlers.ts) to use
invoice.last_finalization_error?.message || invoice.status || 'unknown' (or the
formatted date approach) so the displayed reason is human-readable.

Comment on lines 97 to +100
case 'customer.updated':
return handleCustomerUpdated(data.object, ctx);
return handleCustomerUpdated(data.object as Stripe.Customer, ctx);
case 'customer.deleted':
return handleCustomerDeleted(data.object, ctx);
return handleCustomerDeleted(data.object as unknown as Stripe.Customer, ctx);
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:

cd /root/repo && find . -name "webhookRouter.ts" -o -name "*billing*" -type f | grep -E "\.(ts|tsx|js)$" | head -20

Repository: InfinityBowman/corates

Length of output: 126


🏁 Script executed:

cd /root/repo && cat -n packages/workers/src/routes/billing/webhookRouter.ts | head -150

Repository: InfinityBowman/corates

Length of output: 126


🏁 Script executed:

cd /root/repo && grep -n "handleCustomerDeleted" packages/workers/src/routes/billing/webhookRouter.ts -A 10

Repository: InfinityBowman/corates

Length of output: 126


🏁 Script executed:

cd /root/repo && grep -n "Stripe\." packages/workers/src/routes/billing/webhookRouter.ts | head -20

Repository: InfinityBowman/corates

Length of output: 126


🏁 Script executed:

cd /root/repo && find . -name "package.json" -path "*/workers/*" -exec grep -l "stripe" {} \;

Repository: InfinityBowman/corates

Length of output: 126


🏁 Script executed:

git ls-files | grep -i "webhookrouter"

Repository: InfinityBowman/corates

Length of output: 118


🏁 Script executed:

git ls-files | grep -E "routes/billing" | head -20

Repository: InfinityBowman/corates

Length of output: 1297


🏁 Script executed:

cat packages/workers/src/routes/billing/webhookRouter.ts | head -150

Repository: InfinityBowman/corates

Length of output: 4763


🏁 Script executed:

grep -n "handleCustomerDeleted" packages/workers/src/routes/billing/webhookRouter.ts -A 15 -B 5

Repository: InfinityBowman/corates

Length of output: 1384


🏁 Script executed:

grep -n "Stripe\." packages/workers/src/routes/billing/webhookRouter.ts | head -30

Repository: InfinityBowman/corates

Length of output: 1347


🏁 Script executed:

cat packages/workers/src/routes/billing/handlers/customerHandlers.ts

Repository: InfinityBowman/corates

Length of output: 4239


🏁 Script executed:

grep -n "handleCustomerDeleted" packages/workers/src/routes/billing/handlers/customerHandlers.ts -A 20

Repository: InfinityBowman/corates

Length of output: 695


🏁 Script executed:

grep -rn "DeletedCustomer" packages/workers/src --include="*.ts" --include="*.js"

Repository: InfinityBowman/corates

Length of output: 180


🏁 Script executed:

grep -n "stripe" packages/workers/package.json

Repository: InfinityBowman/corates

Length of output: 197


🏁 Script executed:

cat packages/workers/src/routes/admin/stripe-tools.ts | sed -n '425,435p'

Repository: InfinityBowman/corates

Length of output: 446


🏁 Script executed:

python3 << 'EOF'
import json

# Check Stripe types for customer.deleted event
# According to Stripe docs, customer.deleted sends a DeletedCustomer object
# which has: id, object: 'customer', deleted: true
# It does NOT include fields like email, name, metadata, etc.

payload_structure = {
  "customer.updated": "Stripe.Customer (full customer object)",
  "customer.deleted": "Stripe.DeletedCustomer (minimal object with id, object, deleted)"
}

print("Stripe Webhook Payload Structures:")
print(json.dumps(payload_structure, indent=2))
print("\nThe handler only accesses customer.id, but the type should be accurate.")
EOF

Repository: InfinityBowman/corates

Length of output: 152


🏁 Script executed:

grep -n "customer" packages/workers/src/routes/billing/handlers/types.ts

Repository: InfinityBowman/corates

Length of output: 48


🏁 Script executed:

git log --oneline -5 packages/workers/src/routes/billing/webhookRouter.ts

Repository: InfinityBowman/corates

Length of output: 99


🏁 Script executed:

grep -rn "Stripe.DeletedCustomer" packages/workers/src --include="*.ts"

Repository: InfinityBowman/corates

Length of output: 180


🏁 Script executed:

grep -B5 -A5 "customer.id" packages/workers/src/routes/billing/handlers/customerHandlers.ts | head -20

Repository: InfinityBowman/corates

Length of output: 585


Update handleCustomerDeleted to accept Stripe.DeletedCustomer for type correctness.

Stripe sends a DeletedCustomer shape for customer.deleted webhooks, which contains only id, object, and deleted fields. The current cast to Stripe.Customer misrepresents the payload type. Update both the handler parameter type and the cast in the router:

Suggested fix
// In webhookRouter.ts
-    case 'customer.deleted':
-      return handleCustomerDeleted(data.object as unknown as Stripe.Customer, ctx);
+    case 'customer.deleted':
+      return handleCustomerDeleted(data.object as Stripe.DeletedCustomer, ctx);

// In customerHandlers.ts
-export async function handleCustomerDeleted(
-  customer: Stripe.Customer,
+export async function handleCustomerDeleted(
+  customer: Stripe.DeletedCustomer,
🤖 Prompt for AI Agents
In `@packages/workers/src/routes/billing/webhookRouter.ts` around lines 97 - 100,
The router currently casts data.object to Stripe.Customer for the
'customer.deleted' event; change the cast to Stripe.DeletedCustomer and update
the handler signature for handleCustomerDeleted to accept a
Stripe.DeletedCustomer (instead of Stripe.Customer). Locate
handleCustomerDeleted and its router invocation and change the parameter type to
Stripe.DeletedCustomer, and update any internal code in handleCustomerDeleted to
use only fields present on DeletedCustomer (id, object, deleted) to maintain
type correctness.

Comment thread packages/workers/src/routes/google-drive.ts
Comment thread README.md
- **Linting**: ESLint with custom CoRATES rules
- **Code Format**: Prettier
- **Type Safety**: TypeScript with jsconfig.json path aliases
- **CoRATES MCP**: Custom MCP server for CoRATES specific AI tooling
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

Minor grammar fix: add hyphen to compound adjective.

Per static analysis, "CoRATES specific" should be hyphenated when used as a compound adjective.

📝 Suggested fix
-- **CoRATES MCP**: Custom MCP server for CoRATES specific AI tooling
+- **CoRATES MCP**: Custom MCP server for CoRATES-specific AI tooling
📝 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
- **CoRATES MCP**: Custom MCP server for CoRATES specific AI tooling
- **CoRATES MCP**: Custom MCP server for CoRATES-specific AI tooling
🧰 Tools
🪛 LanguageTool

[grammar] ~56-~56: Use a hyphen to join words.
Context: ...TES MCP**: Custom MCP server for CoRATES specific AI tooling - AI Compatible:...

(QB_NEW_EN_HYPHEN)

🤖 Prompt for AI Agents
In `@README.md` at line 56, The phrase "CoRATES specific" in the README line for
"CoRATES MCP" should be hyphenated as a compound adjective; update the text "-
**CoRATES MCP**: Custom MCP server for CoRATES specific AI tooling" to read
"Custom MCP server for CoRATES-specific AI tooling" so the compound modifier is
grammatically correct.

Comment thread scripts/loc-report.mjs
Comment on lines 38 to 42
function checkCommand(command) {
const result = spawnSync('which', [command], { encoding: 'utf8' });
if (result.status !== 0) {
if (command === 'cloc') {
console.error(`cloc not found in PATH. Install it: https://github.com/AlDanial/cloc`);
} else {
console.error(`${command} not found in PATH`);
}
console.error(c('yellow', `${command} not found. Install: brew install ${command}`));
process.exit(2);
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:

cat -n scripts/loc-report.mjs | head -60

Repository: InfinityBowman/corates

Length of output: 2072


🏁 Script executed:

# Check what platform-specific code patterns exist in this repo
rg 'process\.platform' scripts/ || echo "No process.platform checks found"

Repository: InfinityBowman/corates

Length of output: 98


🏁 Script executed:

# Look for any cross-platform command execution patterns
rg 'spawnSync.*which|spawnSync.*where' scripts/ -A 2 -B 2

Repository: InfinityBowman/corates

Length of output: 403


🏁 Script executed:

# Check Node.js version to understand available APIs
cat package.json | grep -A 5 '"engines"' || echo "No engines specified"

Repository: InfinityBowman/corates

Length of output: 170


🏁 Script executed:

# Check GitHub Actions workflows for platform matrix
find .github/workflows -name "*.yml" -o -name "*.yaml" 2>/dev/null | head -5

Repository: InfinityBowman/corates

Length of output: 96


🏁 Script executed:

# Check if any workflows run on Windows
rg 'windows|win32|windows-latest' .github/workflows/ 2>/dev/null || echo "No Windows-specific workflows found"

Repository: InfinityBowman/corates

Length of output: 101


🏁 Script executed:

# Check README or docs for platform requirements
cat README.md 2>/dev/null | head -100 || echo "No README found"

Repository: InfinityBowman/corates

Length of output: 3640


🏁 Script executed:

cat package.json | grep -A 30 '"scripts"'

Repository: InfinityBowman/corates

Length of output: 1556


Make command lookup portable across OSes.
which isn't available on Windows, and the brew-only hint is Mac-specific. This script is exposed as pnpm loc in package.json for local developer use, so Windows contributors will encounter this issue. Consider a cross‑platform locator and a neutral install hint.

🔧 Suggested fix
 function checkCommand(command) {
-  const result = spawnSync('which', [command], { encoding: 'utf8' });
+  const locator = process.platform === 'win32' ? 'where' : 'which';
+  const result = spawnSync(locator, [command], { encoding: 'utf8' });
   if (result.status !== 0) {
-    console.error(c('yellow', `${command} not found. Install: brew install ${command}`));
+    console.error(
+      c('yellow', `${command} not found. Install it and ensure it is on PATH.`),
+    );
     process.exit(2);
   }
 }
🤖 Prompt for AI Agents
In `@scripts/loc-report.mjs` around lines 38 - 42, The checkCommand function
currently uses the Unix-only 'which' and a macOS-specific brew hint; make it
cross-platform by selecting the locator based on process.platform (use 'where'
on Windows, 'which' or 'command -v' on POSIX) or by using a Node package like
'which' to resolve executables, and update the spawnSync call (refer to
checkCommand and the existing spawnSync invocation) to use the chosen locator;
change the error text emitted via console.error(c('yellow', ...)) to a neutral
install hint (e.g., "<command> not found. Install it via your system package
manager or from https://<command>.org") rather than referencing brew so Windows
users aren't misled.

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: 0

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/users.ts (1)

896-931: Keep impersonation userId consistent between path and body.

The handler blocks self‑impersonation using the path param, but the downstream request uses body.userId. A mismatched body can bypass the self-check and create audit ambiguity. Validate they match (or use the path param exclusively).

🔧 Proposed fix
   const { userId } = c.req.valid('param');
   const adminUser = c.get('user') as AuthUser;
   const body = c.req.valid('json');
 
   try {
+    if (body.userId !== userId) {
+      const error = createValidationError(
+        'userId',
+        VALIDATION_ERRORS.INVALID_INPUT.code,
+        body.userId,
+        'mismatched_user_id',
+      );
+      return c.json(error, error.statusCode as ContentfulStatusCode);
+    }
+
     // Don't allow impersonating yourself
     if (adminUser.id === userId) {
       const error = createValidationError(
         'userId',
         VALIDATION_ERRORS.INVALID_INPUT.code,
         userId,
         'cannot_impersonate_self',
       );
       return c.json(error, error.statusCode as ContentfulStatusCode);
     }
@@
     const authRequest = new Request(authUrl.toString(), {
       method: 'POST',
       headers,
-      body: JSON.stringify({ userId: body.userId }),
+      body: JSON.stringify({ userId }),
     });
🧹 Nitpick comments (5)
packages/workers/src/routes/admin/billing-observability.ts (2)

393-395: Verify processedAt guard doesn't skip valid events.

The guard skips events where processedAt is null or cannot be parsed. Since this targets LedgerStatus.PROCESSED events (line 386), processedAt should always be set. However, if data integrity issues exist, legitimate stuck states could be missed silently.

Consider logging a warning when a PROCESSED event has null processedAt:

Optional: Add logging for data integrity issues
        : (event.processedAt as number | null);
-      if (!processedAtTimestamp) continue;
+      if (!processedAtTimestamp) {
+        console.warn('PROCESSED event with null processedAt:', { ledgerId: event.id, stripeEventId: event.stripeEventId });
+        continue;
+      }

658-666: Consider extracting reduce accumulator pattern to a utility.

The same reduce pattern with typed accumulator appears here and in the ledger route (lines 739-756). If this pattern is common, a small helper could reduce duplication.

This is a minor observation—current code is correct and readable.

packages/workers/src/routes/admin/billing.ts (2)

26-29: Consider importing Database type from @/db/client.js instead of redefining.

The Database type is already exported from packages/workers/src/db/client.ts (see relevant code snippets). Redefining it here creates potential drift if the source definition changes.

Import the existing type
-import type { DrizzleD1Database } from 'drizzle-orm/d1';
-import type * as schema from '@/db/schema.js';
+import { createDb, type Database } from '@/db/client.js';

-type Database = DrizzleD1Database<typeof schema>;

Note: You'd also need to remove the duplicate createDb import on line 9.


796-806: Consider typing updateData more strictly.

Using Record<string, unknown> loses type safety. Drizzle can infer the correct type from the schema. A partial type would be safer.

Optional: Use Drizzle's inferred type
-    const updateData: Record<string, unknown> = {
+    const updateData: Partial<typeof subscription.$inferInsert> = {
       updatedAt: new Date(),
     };

This ensures only valid subscription fields can be assigned.

packages/workers/src/routes/admin/database.ts (1)

477-533: Consider moving interfaces to module level.

The ColumnInfo and DrizzleColumn interfaces are defined inside the route handler. Moving them to module level would improve readability and allow reuse.

Move interfaces to module level
+interface ColumnInfo {
+  name: string;
+  type: string;
+  notNull: boolean;
+  primaryKey: boolean;
+  unique: boolean;
+  hasDefault: boolean;
+  foreignKey?: {
+    table: string;
+    column: string;
+  };
+}
+
+interface DrizzleColumn {
+  dataType?: string;
+  notNull?: boolean;
+  primary?: boolean;
+  isUnique?: boolean;
+  hasDefault?: boolean;
+  config?: {
+    references?: () => { table?: unknown; name?: string };
+  };
+}
+
 const databaseRoutes = new OpenAPIHono<{ Bindings: Env }>({

Then remove the inline definitions from lines 477-499.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6adc7bd and ffaf19c.

📒 Files selected for processing (4)
  • packages/workers/src/routes/admin/billing-observability.ts
  • packages/workers/src/routes/admin/billing.ts
  • packages/workers/src/routes/admin/database.ts
  • packages/workers/src/routes/admin/users.ts
🧰 Additional context used
📓 Path-based instructions (8)
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/yjs-sync.mdc)

**/*.{js,jsx,ts,tsx}: Use the useProject hook for managing Yjs connections with reference counting instead of creating Y.Doc instances directly
Never create Y.Doc instances directly; always use the connection registry managed by useProject
Access Y.Doc connection state via projectStore.getConnectionState(projectId) instead of checking connection state directly
Read Yjs-synced data from projectStore (using getStudies, getChecklist, etc.) rather than accessing Y.Doc maps/arrays directly
Use operation functions from useProject or projectActionsStore for writing data instead of directly modifying Y.Doc
Don't store Y.Doc references in component state; always retrieve the connection through useProject
Handle connection cleanup via onCleanup() in Solid.js components or equivalent cleanup patterns, calling disconnect() when the component unmounts

**/*.{js,jsx,ts,tsx}: For UI icons, use solid-icons library or SVGs only (never emojis)
Prefer modern ES6+ syntax and features
Use import aliases from jsconfig.json (see ui-components.mdc)
Group related components in subdirectories with barrel exports
Use Ark UI components from @corates/ui package, NOT local components
Use solid-icons icon library (e.g., solid-icons/bi, solid-icons/fi) for icons
Comments should explain WHY something is being done, not narrate what the code does
Use comments to explain why a particular approach or workaround was chosen
Use comments to clarify intent when code could be misread or misunderstood
Use comments to provide context from external systems, specs, or requirements
Use comments to document assumptions, edge cases, or limitations
Do NOT narrate what the code is doing in comments
Do NOT duplicate function or variable names in plain English in comments
Do NOT leave stale comments that contradict the code
Do NOT reference removed or obsolete code paths in comments

**/*.{js,jsx,ts,tsx}: Prefer modern ES6+ syntax and features
Comments should not repeat what the code is saying. Rese...

Files:

  • packages/workers/src/routes/admin/database.ts
  • packages/workers/src/routes/admin/billing.ts
  • packages/workers/src/routes/admin/users.ts
  • packages/workers/src/routes/admin/billing-observability.ts
**/*.{js,jsx,ts,tsx,css,scss}

📄 CodeRabbit inference engine (.cursor/rules/corates.mdc)

Ensure browser compatibility (Safari is usually problematic)

Files:

  • packages/workers/src/routes/admin/database.ts
  • packages/workers/src/routes/admin/billing.ts
  • packages/workers/src/routes/admin/users.ts
  • packages/workers/src/routes/admin/billing-observability.ts
packages/workers/**/*.{js,ts}

📄 CodeRabbit inference engine (.cursor/rules/corates.mdc)

packages/workers/**/*.{js,ts}: Use Zod for schema and input validation on the backend
Use Drizzle ORM for ALL database interactions and migrations
Use Better-Auth for authentication and user management
All project routes are org-scoped - use /api/orgs/:orgId/projects/... pattern on backend
Backend uses orgId (UUID) for all API operations
Use requireOrgMembership and requireProjectAccess middleware for auth

packages/workers/**/*.{js,ts}: Always use createDomainError from @corates/shared for backend error handling instead of manually creating error objects
Use predefined error constants from @corates/shared (PROJECT_ERRORS, AUTH_ERRORS, VALIDATION_ERRORS, SYSTEM_ERRORS, USER_ERRORS) instead of arbitrary error strings
Wrap database operations in try-catch blocks and return domain errors using createDomainError with SYSTEM_ERRORS.DB_ERROR
Use validation middleware (validateRequest) for request validation instead of manually validating request bodies in routes

Files:

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

📄 CodeRabbit inference engine (.cursor/rules/workers.mdc)

ALWAYS use db.batch() for multiple related database operations in Drizzle to ensure atomicity - all operations must succeed or all fail together

Files:

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

📄 CodeRabbit inference engine (.cursor/rules/workers.mdc)

packages/workers/src/routes/**/*.{js,ts,jsx,tsx}: ALWAYS validate request bodies using validateRequest middleware from the validation config rather than manual validation
Use validateQueryParams middleware for validating query parameters in routes

Files:

  • packages/workers/src/routes/admin/database.ts
  • packages/workers/src/routes/admin/billing.ts
  • packages/workers/src/routes/admin/users.ts
  • packages/workers/src/routes/admin/billing-observability.ts
packages/workers/src/**/*.{js,ts}

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

packages/workers/src/**/*.{js,ts}: Use Zod for schema and input validation (backend)
Use Drizzle ORM for ALL database interactions and migrations

Files:

  • packages/workers/src/routes/admin/database.ts
  • packages/workers/src/routes/admin/billing.ts
  • packages/workers/src/routes/admin/users.ts
  • packages/workers/src/routes/admin/billing-observability.ts
packages/{workers,web}/src/**/*.{js,ts,jsx,tsx}

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

Use Better-Auth for authentication and user management

Files:

  • packages/workers/src/routes/admin/database.ts
  • packages/workers/src/routes/admin/billing.ts
  • packages/workers/src/routes/admin/users.ts
  • packages/workers/src/routes/admin/billing-observability.ts
packages/{web,workers}/**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (.cursor/rules/error-handling.mdc)

Never throw string literals; always throw Error objects or return domain errors from API routes

Files:

  • packages/workers/src/routes/admin/database.ts
  • packages/workers/src/routes/admin/billing.ts
  • packages/workers/src/routes/admin/users.ts
  • packages/workers/src/routes/admin/billing-observability.ts
🧠 Learnings (22)
📚 Learning: 2026-01-06T23:56:57.354Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/error-handling.mdc:0-0
Timestamp: 2026-01-06T23:56:57.354Z
Learning: Applies to packages/{web,workers}/**/*.{js,ts,jsx,tsx} : Never throw string literals; always throw Error objects or return domain errors from API routes

Applied to files:

  • packages/workers/src/routes/admin/database.ts
  • packages/workers/src/routes/admin/billing-observability.ts
📚 Learning: 2026-01-06T23:56:57.354Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/error-handling.mdc:0-0
Timestamp: 2026-01-06T23:56:57.354Z
Learning: Applies to packages/workers/**/*.{js,ts} : Wrap database operations in try-catch blocks and return domain errors using createDomainError with SYSTEM_ERRORS.DB_ERROR

Applied to files:

  • packages/workers/src/routes/admin/database.ts
  • packages/workers/src/routes/admin/billing-observability.ts
📚 Learning: 2026-01-06T18:29:08.745Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-06T18:29:08.745Z
Learning: Applies to packages/workers/src/**/*.{js,ts} : Use Drizzle ORM for ALL database interactions and migrations

Applied to files:

  • packages/workers/src/routes/admin/database.ts
  • packages/workers/src/routes/admin/users.ts
📚 Learning: 2026-01-01T23:32:06.095Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/corates.mdc:0-0
Timestamp: 2026-01-01T23:32:06.095Z
Learning: Applies to packages/workers/**/*.{js,ts} : Use Drizzle ORM for ALL database interactions and migrations

Applied to files:

  • packages/workers/src/routes/admin/database.ts
  • packages/workers/src/routes/admin/users.ts
📚 Learning: 2026-01-01T23:31:43.756Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/api-routes.mdc:0-0
Timestamp: 2026-01-01T23:31:43.756Z
Learning: Applies to packages/workers/src/routes/**/*.js : Use error constants from `corates/shared` (`PROJECT_ERRORS.*`, `AUTH_ERRORS.*`, `VALIDATION_ERRORS.*`, `SYSTEM_ERRORS.*`, `USER_ERRORS.*`) instead of creating error objects manually

Applied to files:

  • packages/workers/src/routes/admin/database.ts
  • packages/workers/src/routes/admin/billing.ts
  • packages/workers/src/routes/admin/users.ts
  • packages/workers/src/routes/admin/billing-observability.ts
📚 Learning: 2026-01-01T23:32:06.095Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/corates.mdc:0-0
Timestamp: 2026-01-01T23:32:06.095Z
Learning: Applies to packages/workers/**/*.{js,ts} : Use Zod for schema and input validation on the backend

Applied to files:

  • packages/workers/src/routes/admin/database.ts
  • packages/workers/src/routes/admin/billing-observability.ts
📚 Learning: 2026-01-06T18:29:08.745Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-06T18:29:08.745Z
Learning: Applies to packages/workers/src/**/*.{js,ts} : Use Zod for schema and input validation (backend)

Applied to files:

  • packages/workers/src/routes/admin/database.ts
📚 Learning: 2026-01-01T23:32:23.488Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/workers.mdc:0-0
Timestamp: 2026-01-01T23:32:23.488Z
Learning: Applies to packages/workers/src/routes/**/*.{js,ts,jsx,tsx} : ALWAYS validate request bodies using `validateRequest` middleware from the validation config rather than manual validation

Applied to files:

  • packages/workers/src/routes/admin/database.ts
  • packages/workers/src/routes/admin/users.ts
📚 Learning: 2026-01-06T23:56:57.354Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/error-handling.mdc:0-0
Timestamp: 2026-01-06T23:56:57.354Z
Learning: Applies to packages/workers/**/*.{js,ts} : Use validation middleware (validateRequest) for request validation instead of manually validating request bodies in routes

Applied to files:

  • packages/workers/src/routes/admin/database.ts
📚 Learning: 2026-01-01T23:32:23.488Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/workers.mdc:0-0
Timestamp: 2026-01-01T23:32:23.488Z
Learning: Applies to packages/workers/src/routes/**/*.{js,ts,jsx,tsx} : Use `validateQueryParams` middleware for validating query parameters in routes

Applied to files:

  • packages/workers/src/routes/admin/database.ts
📚 Learning: 2026-01-01T23:31:43.756Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/api-routes.mdc:0-0
Timestamp: 2026-01-01T23:31:43.756Z
Learning: Applies to packages/workers/src/routes/**/*.js : Always create DB client from environment using `createDb` function in route handlers

Applied to files:

  • packages/workers/src/routes/admin/database.ts
  • packages/workers/src/routes/admin/users.ts
📚 Learning: 2026-01-01T23:31:43.756Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/api-routes.mdc:0-0
Timestamp: 2026-01-01T23:31:43.756Z
Learning: Applies to packages/workers/src/routes/**/*.js : ALWAYS use `createDomainError` from `corates/shared` for error handling in API routes

Applied to files:

  • packages/workers/src/routes/admin/database.ts
  • packages/workers/src/routes/admin/billing.ts
  • packages/workers/src/routes/admin/users.ts
  • packages/workers/src/routes/admin/billing-observability.ts
📚 Learning: 2026-01-01T23:31:43.756Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/api-routes.mdc:0-0
Timestamp: 2026-01-01T23:31:43.756Z
Learning: Applies to packages/workers/src/routes/**/*.js : Always use Drizzle ORM with query builders - never use raw SQL in database operations

Applied to files:

  • packages/workers/src/routes/admin/database.ts
  • packages/workers/src/routes/admin/users.ts
📚 Learning: 2026-01-01T23:31:43.756Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/api-routes.mdc:0-0
Timestamp: 2026-01-01T23:31:43.756Z
Learning: Applies to packages/workers/src/routes/**/*.js : Apply authentication and authorization middleware in correct order: Authentication → Organization membership → Project access → Authorization → Validation → Route handler

Applied to files:

  • packages/workers/src/routes/admin/database.ts
  • packages/workers/src/routes/admin/users.ts
📚 Learning: 2026-01-01T23:31:43.756Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/api-routes.mdc:0-0
Timestamp: 2026-01-01T23:31:43.756Z
Learning: Applies to packages/workers/src/routes/orgs/**/*.js : Use organization-scoped route patterns with path structure `/api/orgs/:orgId/...` and leverage `requireOrgMembership`, `requireProjectAccess`, `getOrgContext`, and `getProjectContext` middleware

Applied to files:

  • packages/workers/src/routes/admin/database.ts
  • packages/workers/src/routes/admin/billing.ts
  • packages/workers/src/routes/admin/users.ts
📚 Learning: 2026-01-01T23:32:06.095Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/corates.mdc:0-0
Timestamp: 2026-01-01T23:32:06.095Z
Learning: Applies to packages/workers/**/*.{js,ts} : All project routes are org-scoped - use `/api/orgs/:orgId/projects/...` pattern on backend

Applied to files:

  • packages/workers/src/routes/admin/database.ts
  • packages/workers/src/routes/admin/users.ts
📚 Learning: 2026-01-01T23:32:23.488Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/workers.mdc:0-0
Timestamp: 2026-01-01T23:32:23.488Z
Learning: Applies to packages/workers/src/routes/orgs/**/*.{js,ts,jsx,tsx} : Use `getOrgContext(c)` to retrieve the validated orgId from context instead of manual checks - orgId is guaranteed valid by middleware

Applied to files:

  • packages/workers/src/routes/admin/billing.ts
  • packages/workers/src/routes/admin/billing-observability.ts
📚 Learning: 2026-01-06T18:29:08.745Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-06T18:29:08.745Z
Learning: Applies to packages/{workers,web}/src/**/*.{js,ts,jsx,tsx} : Use Better-Auth for authentication and user management

Applied to files:

  • packages/workers/src/routes/admin/users.ts
📚 Learning: 2026-01-01T23:32:06.095Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/corates.mdc:0-0
Timestamp: 2026-01-01T23:32:06.095Z
Learning: Applies to packages/workers/**/*.{js,ts} : Use Better-Auth for authentication and user management

Applied to files:

  • packages/workers/src/routes/admin/users.ts
📚 Learning: 2026-01-01T23:32:23.488Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/workers.mdc:0-0
Timestamp: 2026-01-01T23:32:23.488Z
Learning: Applies to packages/workers/src/routes/orgs/**/*.{js,ts,jsx,tsx} : Use `requireOrgMembership` and `requireProjectAccess` middleware instead of manual membership checks for org-scoped routes

Applied to files:

  • packages/workers/src/routes/admin/users.ts
  • packages/workers/src/routes/admin/billing-observability.ts
📚 Learning: 2026-01-01T23:32:23.488Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/workers.mdc:0-0
Timestamp: 2026-01-01T23:32:23.488Z
Learning: Applies to packages/workers/src/config/validation.{js,ts} : Add new Zod validation schemas to `config/validation.js` and reuse `commonFields` when possible to avoid duplication

Applied to files:

  • packages/workers/src/routes/admin/users.ts
  • packages/workers/src/routes/admin/billing-observability.ts
📚 Learning: 2026-01-06T23:56:57.354Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/error-handling.mdc:0-0
Timestamp: 2026-01-06T23:56:57.354Z
Learning: Applies to packages/workers/**/*.{js,ts} : Use predefined error constants from `corates/shared` (PROJECT_ERRORS, AUTH_ERRORS, VALIDATION_ERRORS, SYSTEM_ERRORS, USER_ERRORS) instead of arbitrary error strings

Applied to files:

  • packages/workers/src/routes/admin/billing-observability.ts
🧬 Code graph analysis (3)
packages/workers/src/routes/admin/database.ts (3)
packages/workers/src/lib/honoValidationHook.ts (1)
  • validationHook (26-51)
packages/workers/src/db/schema.ts (1)
  • mediaFiles (146-162)
packages/workers/src/db/client.ts (1)
  • createDb (7-9)
packages/workers/src/routes/admin/billing.ts (5)
packages/workers/src/db/client.ts (1)
  • Database (5-5)
packages/workers/src/lib/honoValidationHook.ts (1)
  • validationHook (26-51)
packages/workers/src/lib/notify.ts (1)
  • notifyOrgMembers (67-105)
.claude/skills/api-route/examples/ExampleRoutes.js (1)
  • updateData (302-302)
packages/workers/src/db/orgAccessGrants.ts (2)
  • revokeGrant (109-120)
  • updateGrantExpiresAt (92-107)
packages/workers/src/routes/admin/users.ts (3)
packages/workers/src/lib/honoValidationHook.ts (1)
  • validationHook (26-51)
packages/workers/src/db/client.ts (1)
  • createDb (7-9)
packages/workers/src/db/schema.ts (1)
  • session (68-84)
⏰ 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 (15)
packages/workers/src/routes/admin/users.ts (1)

34-39: Schema and route setup tightening looks good.

Constraining record keys to strings and centralizing the validation hook reduce ambiguous JSON shapes and improve response consistency.

Also applies to: 128-136, 163-169, 380-381

packages/workers/src/routes/admin/billing-observability.ts (5)

6-19: LGTM! Clean OpenAPIHono setup with centralized validation.

The typed bindings OpenAPIHono<{ Bindings: Env }> and centralized validationHook align well with the pattern used across other admin routes.


265-298: Well-structured TypeScript interfaces for stuck state detection.

The StuckState and StuckOrg interfaces provide good type safety and align with the corresponding Zod schemas. The optional fields correctly mirror the schema definitions.


341-346: Good defensive guard for nullable createdAt.

The continue guard at line 341 prevents processing subscriptions with missing createdAt. This is a sensible safeguard.


516-522: Good Stripe error handling with explicit typing.

Renaming the catch variable to stripeErr and casting to Error improves clarity and avoids shadowing the outer error reference pattern used elsewhere.


710-734: Clean conditional query construction.

The ternary pattern for building queries with/without conditions is clear and avoids mutating query builders. This is a good approach for maintaining query immutability.

packages/workers/src/routes/admin/billing.ts (4)

35-78: Well-typed notification helper with proper async handling.

The SubscriptionNotificationData interface and notifySubscriptionChange function provide good type safety. The waitUntil pattern correctly handles async notifications without blocking responses.


209-217: Schema change: metadata now accepts any JSON object.

Changing from a stricter type to z.record(z.string(), z.unknown()) provides flexibility but loses type safety on metadata contents. This is acceptable for admin endpoints where flexibility is often needed.


1169-1176: Good use of as const for literal action types.

The 'extended' as const and 'created' as const assertions ensure the response types align with the GrantSuccessResponseSchema which expects z.enum(['created', 'extended']).


963-970: Verify metadata serialization handling in createGrant function.

The concern about metadata type mismatch between the JSON object being passed and GrantSchema's z.string().nullable() definition requires code inspection. Confirm whether createGrant or Drizzle ORM handles JSON serialization automatically, or if explicit JSON.stringify(body.metadata) is needed.

packages/workers/src/routes/admin/database.ts (5)

21-39: Good use of as const for type-safe table allowlist.

Deriving AllowedTableName from the ALLOWED_TABLES array ensures compile-time safety when checking table access. This is a solid pattern.


590-594: Filter injection is mitigated by allowlist but consider explicit validation.

The filterBy column is checked against tableRecord[filterBy] which implicitly validates it exists. However, an explicit check against known column names would be more defensive, especially since filterValue is used directly in the query.

Since this is an admin-only endpoint with table allowlisting, the risk is low.


732-763: Query builder pattern is correct but verbose.

The pattern of building a base query and conditionally applying .where() is correct. The approach where where is applied after orderBy/limit/offset works because Drizzle's query builder is immutable.


856-862: Good defensive filtering for nullable userId.

The .filter(row => row.userId) ensures only rows with actual user associations are included. The cast to string is safe after this filter.


6-18: Consistent setup with other admin routes.

The OpenAPIHono initialization with typed Env bindings and centralized validationHook matches the pattern in billing.ts and billing-observability.ts, ensuring consistency across the admin routes.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

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 (2)
packages/workers/src/routes/google-drive.ts (1)

511-527: Silent DB failure may cause orphaned R2 files and misleading success response.

If the mediaFiles insert fails, the file remains in R2 but isn't tracked in the database. The request still returns success: true, misleading the user into thinking the import succeeded. Consider either failing the request or cleaning up the R2 file on DB error.

🛠️ Suggested fix
     try {
       await db.insert(mediaFiles).values({
         id: mediaFileId,
         filename: uniqueFileName,
         originalName: originalFileName,
         fileType: 'application/pdf',
         fileSize: fileSize,
         uploadedBy: user!.id,
         bucketKey: r2Key,
         orgId: project.orgId,
         projectId,
         studyId,
         createdAt: new Date(),
       });
     } catch (dbError) {
       console.error('Failed to insert mediaFiles record after Google Drive import:', dbError);
+      // Clean up orphaned R2 file
+      try {
+        await c.env.PDF_BUCKET.delete(r2Key);
+      } catch (cleanupError) {
+        console.error('Failed to clean up R2 file after DB error:', cleanupError);
+      }
+      const systemError = createDomainError(SYSTEM_ERRORS.DB_ERROR, {
+        operation: 'insert_media_file_record',
+        originalError: dbError instanceof Error ? dbError.message : String(dbError),
+      });
+      return c.json(systemError, systemError.statusCode as ContentfulStatusCode);
     }
packages/workers/src/index.ts (1)

241-276: Use domain errors here to avoid leaking upstream/internal details.
Returning upstream statusText and error.message can expose internal or third‑party details, and it bypasses standardized error handling. Prefer createDomainError with SYSTEM_ERRORS.* and map to a safe status (e.g., 502/500) while logging the underlying error server‑side. As per coding guidelines, use createDomainError and predefined error constants.

🤖 Fix all issues with AI agents
In @.claude/skills/code-review/SKILL.md:
- Around line 147-185: The markdown code block is malformed around the snippet
containing "8. No-issue outcome" and "Explicit prohibitions": end the opening
```md block before the separator (the line with the horizontal separator
characters), change "8. No-issue outcome" to a proper heading (e.g., "### 8.
No-issue outcome"), wrap the “## Code review / No issues found…” example in its
own fenced ```md block, replace the separator with a proper markdown horizontal
rule (---), and add a heading "## Explicit prohibitions" before the bulleted
list; locate these edits by searching for the exact strings "8. No-issue
outcome", "Explicit prohibitions", and the opening/closing backticks in the
problematic diff to fix the structure.

In `@packages/workers/src/commands/members/updateMemberRole.ts`:
- Around line 43-53: The update currently calls db.update(projectMembers).set({
role })... but never checks the result so it can silently succeed with 0
affected rows; change the logic in updateMemberRole to capture the update result
and assert that at least one row was affected (e.g., check result.rowsAffected
or the Drizzle/D1 equivalent) and if zero, throw a domain error (use
createDomainError with SYSTEM_ERRORS.DB_ERROR or a more specific NOT_FOUND error
and operation: 'update_member_role') so callers know the member did not exist;
keep the existing catch that maps DB errors via createDomainError for other
failures.
🧹 Nitpick comments (2)
packages/workers/src/lib/honoValidationHook.ts (1)

59-79: LGTM!

Error code selection now correctly maps Zod error types to appropriate validation error codes. The fallback to INVALID_INPUT handles unknown cases gracefully.

Optional enhancement: The target field (e.g., 'body', 'query', 'params') from the validation result is currently unused. Consider incorporating it into error messages for better debugging context:

-    error.message = message;
+    error.message = result.target !== 'json' 
+      ? `${result.target} validation failed: ${message}`
+      : message;
packages/workers/src/index.ts (1)

165-170: Centralize pdf-proxy input validation via Zod + validateRequest.
Manual JSON parsing and url checks bypass the shared validation hook and URL-format validation. Prefer a Zod schema and validateRequest to keep request validation consistent. As per coding guidelines, use Zod for backend validation and the shared validation middleware.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ffaf19c and 1ec101e.

📒 Files selected for processing (7)
  • .claude/skills/code-review/SKILL.md
  • packages/workers/src/commands/members/updateMemberRole.ts
  • packages/workers/src/index.ts
  • packages/workers/src/lib/honoValidationHook.ts
  • packages/workers/src/policies/billing.ts
  • packages/workers/src/routes/__tests__/orgs-management.test.js
  • packages/workers/src/routes/google-drive.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/workers/src/policies/billing.ts
🧰 Additional context used
📓 Path-based instructions (10)
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/yjs-sync.mdc)

**/*.{js,jsx,ts,tsx}: Use the useProject hook for managing Yjs connections with reference counting instead of creating Y.Doc instances directly
Never create Y.Doc instances directly; always use the connection registry managed by useProject
Access Y.Doc connection state via projectStore.getConnectionState(projectId) instead of checking connection state directly
Read Yjs-synced data from projectStore (using getStudies, getChecklist, etc.) rather than accessing Y.Doc maps/arrays directly
Use operation functions from useProject or projectActionsStore for writing data instead of directly modifying Y.Doc
Don't store Y.Doc references in component state; always retrieve the connection through useProject
Handle connection cleanup via onCleanup() in Solid.js components or equivalent cleanup patterns, calling disconnect() when the component unmounts

**/*.{js,jsx,ts,tsx}: For UI icons, use solid-icons library or SVGs only (never emojis)
Prefer modern ES6+ syntax and features
Use import aliases from jsconfig.json (see ui-components.mdc)
Group related components in subdirectories with barrel exports
Use Ark UI components from @corates/ui package, NOT local components
Use solid-icons icon library (e.g., solid-icons/bi, solid-icons/fi) for icons
Comments should explain WHY something is being done, not narrate what the code does
Use comments to explain why a particular approach or workaround was chosen
Use comments to clarify intent when code could be misread or misunderstood
Use comments to provide context from external systems, specs, or requirements
Use comments to document assumptions, edge cases, or limitations
Do NOT narrate what the code is doing in comments
Do NOT duplicate function or variable names in plain English in comments
Do NOT leave stale comments that contradict the code
Do NOT reference removed or obsolete code paths in comments

**/*.{js,jsx,ts,tsx}: Prefer modern ES6+ syntax and features
Comments should not repeat what the code is saying. Rese...

Files:

  • packages/workers/src/routes/__tests__/orgs-management.test.js
  • packages/workers/src/index.ts
  • packages/workers/src/routes/google-drive.ts
  • packages/workers/src/commands/members/updateMemberRole.ts
  • packages/workers/src/lib/honoValidationHook.ts
packages/workers/src/routes/**/*.js

📄 CodeRabbit inference engine (.cursor/rules/api-routes.mdc)

packages/workers/src/routes/**/*.js: ALWAYS use validateRequest middleware for request body validation in API routes
Use validateQueryParams middleware for query string validation in API routes
Always create DB client from environment using createDb function in route handlers
Use db.batch() for related database operations that must be atomic
Always use Drizzle ORM with query builders - never use raw SQL in database operations
ALWAYS use createDomainError from @corates/shared for error handling in API routes
Use error constants from @corates/shared (PROJECT_ERRORS.*, AUTH_ERRORS.*, VALIDATION_ERRORS.*, SYSTEM_ERRORS.*, USER_ERRORS.*) instead of creating error objects manually
Apply authentication and authorization middleware in correct order: Authentication → Organization membership → Project access → Authorization → Validation → Route handler

Files:

  • packages/workers/src/routes/__tests__/orgs-management.test.js
**/*.{js,jsx,ts,tsx,css,scss}

📄 CodeRabbit inference engine (.cursor/rules/corates.mdc)

Ensure browser compatibility (Safari is usually problematic)

Files:

  • packages/workers/src/routes/__tests__/orgs-management.test.js
  • packages/workers/src/index.ts
  • packages/workers/src/routes/google-drive.ts
  • packages/workers/src/commands/members/updateMemberRole.ts
  • packages/workers/src/lib/honoValidationHook.ts
packages/workers/**/*.{js,ts}

📄 CodeRabbit inference engine (.cursor/rules/corates.mdc)

packages/workers/**/*.{js,ts}: Use Zod for schema and input validation on the backend
Use Drizzle ORM for ALL database interactions and migrations
Use Better-Auth for authentication and user management
All project routes are org-scoped - use /api/orgs/:orgId/projects/... pattern on backend
Backend uses orgId (UUID) for all API operations
Use requireOrgMembership and requireProjectAccess middleware for auth

packages/workers/**/*.{js,ts}: Always use createDomainError from @corates/shared for backend error handling instead of manually creating error objects
Use predefined error constants from @corates/shared (PROJECT_ERRORS, AUTH_ERRORS, VALIDATION_ERRORS, SYSTEM_ERRORS, USER_ERRORS) instead of arbitrary error strings
Wrap database operations in try-catch blocks and return domain errors using createDomainError with SYSTEM_ERRORS.DB_ERROR
Use validation middleware (validateRequest) for request validation instead of manually validating request bodies in routes

Files:

  • packages/workers/src/routes/__tests__/orgs-management.test.js
  • packages/workers/src/index.ts
  • packages/workers/src/routes/google-drive.ts
  • packages/workers/src/commands/members/updateMemberRole.ts
  • packages/workers/src/lib/honoValidationHook.ts
packages/workers/src/**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (.cursor/rules/workers.mdc)

ALWAYS use db.batch() for multiple related database operations in Drizzle to ensure atomicity - all operations must succeed or all fail together

Files:

  • packages/workers/src/routes/__tests__/orgs-management.test.js
  • packages/workers/src/index.ts
  • packages/workers/src/routes/google-drive.ts
  • packages/workers/src/commands/members/updateMemberRole.ts
  • packages/workers/src/lib/honoValidationHook.ts
packages/workers/src/routes/**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (.cursor/rules/workers.mdc)

packages/workers/src/routes/**/*.{js,ts,jsx,tsx}: ALWAYS validate request bodies using validateRequest middleware from the validation config rather than manual validation
Use validateQueryParams middleware for validating query parameters in routes

Files:

  • packages/workers/src/routes/__tests__/orgs-management.test.js
  • packages/workers/src/routes/google-drive.ts
{packages/workers/src/**/*.test.js,packages/workers/src/**/__tests__/**/*.js}

📄 CodeRabbit inference engine (.cursor/rules/workers-testing.mdc)

{packages/workers/src/**/*.test.js,packages/workers/src/**/__tests__/**/*.js}: Tests in packages/workers must run under @cloudflare/vitest-pool-workers with global setup from packages/workers/src/tests/setup.js
Use resetTestDatabase() helper to reset D1 from migration SQL before each test or describe block
Seed test data using provided seed* helpers (seedUser, seedOrganization, seedOrgMember, seedProject, etc.) from packages/workers/src/tests/helpers.js with Zod schema validation
Clear ProjectDoc Durable Objects storage between tests using clearProjectDOs() helper to prevent invalidation/reset edge cases
For integration tests, fetch against real worker env using app.fetch(req, env, ctx) with env from cloudflare:test, or use fetchApp() helper
Use shared json() helper from packages/workers/src/tests/helpers.js to safely parse response bodies in tests
Mock src/middleware/auth.js requireAuth function in route tests by setting user/session via x-test-user-id and x-test-user-email headers
Reuse provided helpers from packages/workers/src/tests/helpers.js (resetTestDatabase, clearProjectDOs, seedUser, seedOrganization, createTestEnv, fetchApp, json, createAuthHeaders) instead of implementing ad-hoc solutions
Only re-mock Postmark and Stripe modules inside a test file if you need behavior different from the global mock setup in packages/workers/src/tests/setup.js

Files:

  • packages/workers/src/routes/__tests__/orgs-management.test.js
packages/workers/src/**/*.{js,ts}

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

packages/workers/src/**/*.{js,ts}: Use Zod for schema and input validation (backend)
Use Drizzle ORM for ALL database interactions and migrations

Files:

  • packages/workers/src/routes/__tests__/orgs-management.test.js
  • packages/workers/src/index.ts
  • packages/workers/src/routes/google-drive.ts
  • packages/workers/src/commands/members/updateMemberRole.ts
  • packages/workers/src/lib/honoValidationHook.ts
packages/{workers,web}/src/**/*.{js,ts,jsx,tsx}

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

Use Better-Auth for authentication and user management

Files:

  • packages/workers/src/routes/__tests__/orgs-management.test.js
  • packages/workers/src/index.ts
  • packages/workers/src/routes/google-drive.ts
  • packages/workers/src/commands/members/updateMemberRole.ts
  • packages/workers/src/lib/honoValidationHook.ts
packages/{web,workers}/**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (.cursor/rules/error-handling.mdc)

Never throw string literals; always throw Error objects or return domain errors from API routes

Files:

  • packages/workers/src/routes/__tests__/orgs-management.test.js
  • packages/workers/src/index.ts
  • packages/workers/src/routes/google-drive.ts
  • packages/workers/src/commands/members/updateMemberRole.ts
  • packages/workers/src/lib/honoValidationHook.ts
🧠 Learnings (50)
📓 Common learnings
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-06T18:29:08.745Z
Learning: Applies to packages/workers/src/**/*.{js,ts} : Use Zod for schema and input validation (backend)
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/corates.mdc:0-0
Timestamp: 2026-01-01T23:32:06.095Z
Learning: Applies to packages/workers/**/*.{js,ts} : Use Zod for schema and input validation on the backend
📚 Learning: 2026-01-01T23:31:43.756Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/api-routes.mdc:0-0
Timestamp: 2026-01-01T23:31:43.756Z
Learning: Applies to packages/workers/src/routes/orgs/**/*.js : Use organization-scoped route patterns with path structure `/api/orgs/:orgId/...` and leverage `requireOrgMembership`, `requireProjectAccess`, `getOrgContext`, and `getProjectContext` middleware

Applied to files:

  • packages/workers/src/routes/__tests__/orgs-management.test.js
  • packages/workers/src/index.ts
  • packages/workers/src/routes/google-drive.ts
📚 Learning: 2026-01-01T23:32:23.488Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/workers.mdc:0-0
Timestamp: 2026-01-01T23:32:23.488Z
Learning: Applies to packages/workers/src/routes/orgs/**/*.{js,ts,jsx,tsx} : Use `getOrgContext(c)` to retrieve the validated orgId from context instead of manual checks - orgId is guaranteed valid by middleware

Applied to files:

  • packages/workers/src/routes/__tests__/orgs-management.test.js
  • packages/workers/src/index.ts
📚 Learning: 2026-01-01T23:32:23.488Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/workers.mdc:0-0
Timestamp: 2026-01-01T23:32:23.488Z
Learning: Applies to packages/workers/src/routes/orgs/**/*.{js,ts,jsx,tsx} : Use `requireOrgMembership` and `requireProjectAccess` middleware instead of manual membership checks for org-scoped routes

Applied to files:

  • packages/workers/src/routes/__tests__/orgs-management.test.js
  • packages/workers/src/index.ts
  • packages/workers/src/routes/google-drive.ts
  • packages/workers/src/commands/members/updateMemberRole.ts
📚 Learning: 2026-01-05T01:00:58.113Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/workers-testing.mdc:0-0
Timestamp: 2026-01-05T01:00:58.113Z
Learning: Applies to {packages/workers/src/**/*.test.js,packages/workers/src/**/__tests__/**/*.js} : Reuse provided helpers from packages/workers/src/__tests__/helpers.js (resetTestDatabase, clearProjectDOs, seedUser, seedOrganization, createTestEnv, fetchApp, json, createAuthHeaders) instead of implementing ad-hoc solutions

Applied to files:

  • packages/workers/src/routes/__tests__/orgs-management.test.js
📚 Learning: 2026-01-05T01:00:58.113Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/workers-testing.mdc:0-0
Timestamp: 2026-01-05T01:00:58.113Z
Learning: Applies to {packages/workers/src/**/*.test.js,packages/workers/src/**/__tests__/**/*.js} : Mock src/middleware/auth.js requireAuth function in route tests by setting user/session via x-test-user-id and x-test-user-email headers

Applied to files:

  • packages/workers/src/routes/__tests__/orgs-management.test.js
📚 Learning: 2026-01-05T01:00:58.113Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/workers-testing.mdc:0-0
Timestamp: 2026-01-05T01:00:58.113Z
Learning: Applies to {packages/workers/src/**/*.test.js,packages/workers/src/**/__tests__/**/*.js} : Seed test data using provided seed* helpers (seedUser, seedOrganization, seedOrgMember, seedProject, etc.) from packages/workers/src/__tests__/helpers.js with Zod schema validation

Applied to files:

  • packages/workers/src/routes/__tests__/orgs-management.test.js
📚 Learning: 2026-01-01T23:32:06.095Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/corates.mdc:0-0
Timestamp: 2026-01-01T23:32:06.095Z
Learning: Applies to packages/workers/**/*.{js,ts} : All project routes are org-scoped - use `/api/orgs/:orgId/projects/...` pattern on backend

Applied to files:

  • packages/workers/src/routes/__tests__/orgs-management.test.js
  • packages/workers/src/index.ts
  • packages/workers/src/routes/google-drive.ts
📚 Learning: 2026-01-01T23:32:17.698Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/organizations.mdc:0-0
Timestamp: 2026-01-01T23:32:17.698Z
Learning: Applies to packages/workers/src/routes/orgs/** : Use UUID for backend API endpoints: /api/orgs/:orgId/...

Applied to files:

  • packages/workers/src/routes/__tests__/orgs-management.test.js
📚 Learning: 2026-01-01T23:31:43.756Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/api-routes.mdc:0-0
Timestamp: 2026-01-01T23:31:43.756Z
Learning: Applies to packages/workers/src/routes/**/*.js : Use error constants from `corates/shared` (`PROJECT_ERRORS.*`, `AUTH_ERRORS.*`, `VALIDATION_ERRORS.*`, `SYSTEM_ERRORS.*`, `USER_ERRORS.*`) instead of creating error objects manually

Applied to files:

  • packages/workers/src/routes/__tests__/orgs-management.test.js
  • packages/workers/src/index.ts
  • packages/workers/src/routes/google-drive.ts
📚 Learning: 2026-01-01T23:32:17.698Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/organizations.mdc:0-0
Timestamp: 2026-01-01T23:32:17.698Z
Learning: Applies to packages/workers/src/routes/orgs/** : Import and use requireOrgMembership middleware to verify org membership and optionally enforce minimum role requirements

Applied to files:

  • packages/workers/src/routes/__tests__/orgs-management.test.js
  • packages/workers/src/routes/google-drive.ts
📚 Learning: 2025-12-27T03:01:35.601Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/durable-objects.mdc:0-0
Timestamp: 2025-12-27T03:01:35.601Z
Learning: Applies to packages/workers/src/durable-objects/**/ProjectDoc.{js,ts} : Handle WebSocket upgrade requests by checking for 'Upgrade: websocket' header in ProjectDoc fetch method

Applied to files:

  • packages/workers/src/index.ts
📚 Learning: 2025-12-27T03:01:35.601Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/durable-objects.mdc:0-0
Timestamp: 2025-12-27T03:01:35.601Z
Learning: Applies to packages/workers/src/durable-objects/**/ProjectDoc.{js,ts} : Use y-websocket protocol with message type 1 for awareness (presence) messages in ProjectDoc

Applied to files:

  • packages/workers/src/index.ts
📚 Learning: 2026-01-06T18:29:08.745Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-06T18:29:08.745Z
Learning: Applies to packages/{workers,web}/src/**/*.{js,ts,jsx,tsx} : Use Better-Auth for authentication and user management

Applied to files:

  • packages/workers/src/index.ts
  • packages/workers/src/routes/google-drive.ts
📚 Learning: 2025-12-27T03:01:35.601Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/durable-objects.mdc:0-0
Timestamp: 2025-12-27T03:01:35.601Z
Learning: Applies to packages/workers/src/durable-objects/**/ProjectDoc.{js,ts} : Use y-websocket protocol with message type 0 for Yjs sync messages in ProjectDoc

Applied to files:

  • packages/workers/src/index.ts
📚 Learning: 2025-12-27T03:01:35.601Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/durable-objects.mdc:0-0
Timestamp: 2025-12-27T03:01:35.601Z
Learning: Applies to packages/workers/src/durable-objects/**/ProjectDoc.{js,ts} : Verify authentication before WebSocket upgrade in ProjectDoc

Applied to files:

  • packages/workers/src/index.ts
  • packages/workers/src/routes/google-drive.ts
📚 Learning: 2026-01-01T23:32:06.095Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/corates.mdc:0-0
Timestamp: 2026-01-01T23:32:06.095Z
Learning: Applies to packages/workers/**/*.{js,ts} : Use Better-Auth for authentication and user management

Applied to files:

  • packages/workers/src/index.ts
📚 Learning: 2025-12-27T03:01:35.601Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/durable-objects.mdc:0-0
Timestamp: 2025-12-27T03:01:35.601Z
Learning: Applies to packages/workers/src/durable-objects/**/*.{js,ts} : Do not expose internal sync endpoints (/sync, /sync-member, /sync-pdf, /disconnect-all) publicly without X-Internal-Request verification

Applied to files:

  • packages/workers/src/index.ts
📚 Learning: 2026-01-01T23:32:06.095Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/corates.mdc:0-0
Timestamp: 2026-01-01T23:32:06.095Z
Learning: Applies to **/*.{js,jsx,ts,tsx} : Use comments to document assumptions, edge cases, or limitations

Applied to files:

  • packages/workers/src/index.ts
📚 Learning: 2026-01-01T23:32:06.095Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/corates.mdc:0-0
Timestamp: 2026-01-01T23:32:06.095Z
Learning: Applies to **/*.{js,jsx,ts,tsx} : Use comments to explain why a particular approach or workaround was chosen

Applied to files:

  • packages/workers/src/index.ts
📚 Learning: 2026-01-01T23:32:06.095Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/corates.mdc:0-0
Timestamp: 2026-01-01T23:32:06.095Z
Learning: Applies to **/*.{js,jsx,ts,tsx} : Do NOT reference removed or obsolete code paths in comments

Applied to files:

  • packages/workers/src/index.ts
📚 Learning: 2026-01-01T23:31:43.756Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/api-routes.mdc:0-0
Timestamp: 2026-01-01T23:31:43.756Z
Learning: Applies to packages/workers/src/routes/**/*.js : Apply authentication and authorization middleware in correct order: Authentication → Organization membership → Project access → Authorization → Validation → Route handler

Applied to files:

  • packages/workers/src/index.ts
  • packages/workers/src/routes/google-drive.ts
📚 Learning: 2025-12-27T03:02:05.951Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/pdf-handling.mdc:0-0
Timestamp: 2025-12-27T03:02:05.951Z
Learning: Applies to packages/web/src/**/*.{js,jsx,ts,tsx} : Use `downloadPdf` from `api/pdf-api.js` to download PDFs from server, then cache the result using `cachePdf`

Applied to files:

  • packages/workers/src/index.ts
📚 Learning: 2025-12-27T03:01:35.601Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/durable-objects.mdc:0-0
Timestamp: 2025-12-27T03:01:35.601Z
Learning: Applies to packages/workers/src/durable-objects/**/ProjectDoc.{js,ts} : Structure ProjectDoc Y.Doc with meta (Map), members (Map), and reviews (Map) containing checklists (Map), pdfs (Array), and reconciliation (Map)

Applied to files:

  • packages/workers/src/index.ts
📚 Learning: 2025-12-27T03:01:35.601Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/durable-objects.mdc:0-0
Timestamp: 2025-12-27T03:01:35.601Z
Learning: Applies to packages/workers/src/durable-objects/**/ProjectDoc.{js,ts} : Close WebSocket connection with code 1008 when user access to project is denied

Applied to files:

  • packages/workers/src/index.ts
📚 Learning: 2025-12-27T03:02:05.951Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/pdf-handling.mdc:0-0
Timestamp: 2025-12-27T03:02:05.951Z
Learning: Applies to packages/web/src/**/*.{js,jsx,ts,tsx} : Use PDF operations from `useProject` hook instead of bypassing through direct API calls

Applied to files:

  • packages/workers/src/index.ts
📚 Learning: 2025-12-27T03:02:50.087Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/yjs-sync.mdc:0-0
Timestamp: 2025-12-27T03:02:50.087Z
Learning: Applies to packages/workers/src/durable-objects/ProjectDoc.js, packages/web/src/primitives/useProject/** : Y.Doc structure in ProjectDoc follows a specific hierarchy: meta (Map), members (Map), reviews (Map containing studies with checklists, pdfs, and reconciliation)

Applied to files:

  • packages/workers/src/index.ts
📚 Learning: 2025-12-27T03:02:50.087Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/yjs-sync.mdc:0-0
Timestamp: 2025-12-27T03:02:50.087Z
Learning: Applies to **/*.{js,jsx,ts,tsx} : Never create Y.Doc instances directly; always use the connection registry managed by useProject

Applied to files:

  • packages/workers/src/index.ts
📚 Learning: 2026-01-05T01:00:58.113Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/workers-testing.mdc:0-0
Timestamp: 2026-01-05T01:00:58.113Z
Learning: Applies to {packages/workers/src/**/*.test.js,packages/workers/src/**/__tests__/**/*.js} : Clear ProjectDoc Durable Objects storage between tests using clearProjectDOs() helper to prevent invalidation/reset edge cases

Applied to files:

  • packages/workers/src/index.ts
📚 Learning: 2025-12-27T03:01:35.601Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/durable-objects.mdc:0-0
Timestamp: 2025-12-27T03:01:35.601Z
Learning: Applies to packages/workers/src/durable-objects/**/ProjectDoc.{js,ts} : Check user membership in project before allowing WebSocket connection in ProjectDoc

Applied to files:

  • packages/workers/src/index.ts
  • packages/workers/src/routes/google-drive.ts
  • packages/workers/src/commands/members/updateMemberRole.ts
📚 Learning: 2025-12-27T03:01:35.601Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/durable-objects.mdc:0-0
Timestamp: 2025-12-27T03:01:35.601Z
Learning: Applies to packages/workers/src/durable-objects/**/*.{js,ts} : Track active WebSocket sessions in a Map with session structure { user, awarenessClientId }

Applied to files:

  • packages/workers/src/index.ts
📚 Learning: 2025-12-27T03:01:35.601Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/durable-objects.mdc:0-0
Timestamp: 2025-12-27T03:01:35.601Z
Learning: Applies to packages/workers/src/durable-objects/**/*.{js,ts} : Remove WebSocket sessions from tracking on disconnect and perform cleanup

Applied to files:

  • packages/workers/src/index.ts
📚 Learning: 2026-01-01T23:31:43.756Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/api-routes.mdc:0-0
Timestamp: 2026-01-01T23:31:43.756Z
Learning: Applies to packages/workers/src/routes/**/*.js : ALWAYS use `createDomainError` from `corates/shared` for error handling in API routes

Applied to files:

  • packages/workers/src/index.ts
  • packages/workers/src/routes/google-drive.ts
  • packages/workers/src/commands/members/updateMemberRole.ts
📚 Learning: 2026-01-06T23:56:57.354Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/error-handling.mdc:0-0
Timestamp: 2026-01-06T23:56:57.354Z
Learning: Applies to packages/{web,workers}/**/*.{js,ts,jsx,tsx} : Never throw string literals; always throw Error objects or return domain errors from API routes

Applied to files:

  • packages/workers/src/index.ts
  • packages/workers/src/routes/google-drive.ts
  • packages/workers/src/commands/members/updateMemberRole.ts
📚 Learning: 2026-01-06T23:56:57.354Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/error-handling.mdc:0-0
Timestamp: 2026-01-06T23:56:57.354Z
Learning: Applies to packages/workers/**/*.{js,ts} : Always use `createDomainError` from `corates/shared` for backend error handling instead of manually creating error objects

Applied to files:

  • packages/workers/src/index.ts
  • packages/workers/src/commands/members/updateMemberRole.ts
📚 Learning: 2025-12-27T03:02:05.951Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/pdf-handling.mdc:0-0
Timestamp: 2025-12-27T03:02:05.951Z
Learning: Applies to packages/web/src/**/*.{js,jsx,ts,tsx} : Use `importFromGoogleDrive` from `api/google-drive.js` for importing PDFs from Google Drive. Pass fileId, projectId, studyId, and tag

Applied to files:

  • packages/workers/src/routes/google-drive.ts
📚 Learning: 2025-12-27T03:02:05.951Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/pdf-handling.mdc:0-0
Timestamp: 2025-12-27T03:02:05.951Z
Learning: Applies to packages/web/src/**/*.{js,jsx,ts,tsx} : Save form state using `saveFormState` from `@/lib/formStatePersistence.js` before triggering OAuth redirects for Google Drive

Applied to files:

  • packages/workers/src/routes/google-drive.ts
📚 Learning: 2026-01-01T23:32:06.095Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/corates.mdc:0-0
Timestamp: 2026-01-01T23:32:06.095Z
Learning: Applies to packages/workers/**/*.{js,ts} : Use `requireOrgMembership` and `requireProjectAccess` middleware for auth

Applied to files:

  • packages/workers/src/routes/google-drive.ts
  • packages/workers/src/commands/members/updateMemberRole.ts
📚 Learning: 2026-01-01T23:32:17.698Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/organizations.mdc:0-0
Timestamp: 2026-01-01T23:32:17.698Z
Learning: Applies to packages/workers/src/routes/orgs/** : Use requireProjectAccess middleware after requireOrgMembership to verify project membership and optionally enforce minimum role requirements

Applied to files:

  • packages/workers/src/routes/google-drive.ts
  • packages/workers/src/commands/members/updateMemberRole.ts
📚 Learning: 2026-01-01T23:32:17.698Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/organizations.mdc:0-0
Timestamp: 2026-01-01T23:32:17.698Z
Learning: Applies to packages/workers/src/routes/orgs/** : Enforce middleware chain order: requireAuth → requireOrgMembership → requireProjectAccess → requireEntitlement → validateRequest

Applied to files:

  • packages/workers/src/routes/google-drive.ts
📚 Learning: 2026-01-01T23:31:43.756Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/api-routes.mdc:0-0
Timestamp: 2026-01-01T23:31:43.756Z
Learning: Applies to packages/workers/src/routes/**/*.js : ALWAYS use `validateRequest` middleware for request body validation in API routes

Applied to files:

  • packages/workers/src/routes/google-drive.ts
📚 Learning: 2026-01-01T23:32:23.488Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/workers.mdc:0-0
Timestamp: 2026-01-01T23:32:23.488Z
Learning: Applies to packages/workers/src/routes/**/*.{js,ts,jsx,tsx} : ALWAYS validate request bodies using `validateRequest` middleware from the validation config rather than manual validation

Applied to files:

  • packages/workers/src/routes/google-drive.ts
  • packages/workers/src/lib/honoValidationHook.ts
📚 Learning: 2026-01-01T23:32:17.698Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/organizations.mdc:0-0
Timestamp: 2026-01-01T23:32:17.698Z
Learning: Applies to packages/workers/src/routes/orgs/** : Ensure project invitation flow creates org membership before project membership, including orgRole and projectRole in invitation data

Applied to files:

  • packages/workers/src/commands/members/updateMemberRole.ts
📚 Learning: 2026-01-01T23:32:17.698Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/organizations.mdc:0-0
Timestamp: 2026-01-01T23:32:17.698Z
Learning: Applies to packages/workers/src/routes/orgs/** : Use project role hierarchy: owner > collaborator > member > viewer

Applied to files:

  • packages/workers/src/commands/members/updateMemberRole.ts
📚 Learning: 2026-01-06T23:56:57.354Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/error-handling.mdc:0-0
Timestamp: 2026-01-06T23:56:57.354Z
Learning: Applies to packages/workers/**/*.{js,ts} : Wrap database operations in try-catch blocks and return domain errors using createDomainError with SYSTEM_ERRORS.DB_ERROR

Applied to files:

  • packages/workers/src/commands/members/updateMemberRole.ts
📚 Learning: 2026-01-01T23:32:23.488Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/workers.mdc:0-0
Timestamp: 2026-01-01T23:32:23.488Z
Learning: Applies to packages/workers/src/config/validation.{js,ts} : Add new Zod validation schemas to `config/validation.js` and reuse `commonFields` when possible to avoid duplication

Applied to files:

  • packages/workers/src/lib/honoValidationHook.ts
📚 Learning: 2026-01-06T18:29:08.745Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-06T18:29:08.745Z
Learning: Applies to packages/workers/src/**/*.{js,ts} : Use Zod for schema and input validation (backend)

Applied to files:

  • packages/workers/src/lib/honoValidationHook.ts
📚 Learning: 2026-01-01T23:32:06.095Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/corates.mdc:0-0
Timestamp: 2026-01-01T23:32:06.095Z
Learning: Applies to packages/workers/**/*.{js,ts} : Use Zod for schema and input validation on the backend

Applied to files:

  • packages/workers/src/lib/honoValidationHook.ts
📚 Learning: 2026-01-01T23:31:43.756Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/api-routes.mdc:0-0
Timestamp: 2026-01-01T23:31:43.756Z
Learning: Applies to packages/workers/src/config/validation.js : Add new validation schemas to `src/config/validation.js` and reuse `commonFields` when possible

Applied to files:

  • packages/workers/src/lib/honoValidationHook.ts
📚 Learning: 2026-01-06T23:56:57.354Z
Learnt from: CR
Repo: InfinityBowman/corates PR: 0
File: .cursor/rules/error-handling.mdc:0-0
Timestamp: 2026-01-06T23:56:57.354Z
Learning: Applies to packages/workers/**/*.{js,ts} : Use validation middleware (validateRequest) for request validation instead of manually validating request bodies in routes

Applied to files:

  • packages/workers/src/lib/honoValidationHook.ts
🧬 Code graph analysis (3)
packages/workers/src/index.ts (1)
packages/workers/src/docs.ts (1)
  • getDocsHtml (3-160)
packages/workers/src/commands/members/updateMemberRole.ts (4)
packages/workers/src/db/client.ts (1)
  • createDb (7-9)
packages/workers/src/db/schema.ts (1)
  • projectMembers (134-144)
packages/workers/src/commands/lib/doSync.ts (1)
  • syncMemberToDO (12-12)
packages/workers/src/commands/lib/notifications.ts (2)
  • notifyUser (29-47)
  • NotificationTypes (12-17)
packages/workers/src/lib/honoValidationHook.ts (1)
packages/workers/src/config/validation.ts (1)
  • ValidationResult (230-234)
⏰ 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 (19)
packages/workers/src/routes/__tests__/orgs-management.test.js (1)

490-491: Reasonable workaround for enum validation variability.

The regex approach handles Zod's behavior where missing enum fields can be reported as either missing or invalid. However, this introduces inconsistency with other validation tests in the same file (lines 188 and 410) that use exact toBe('VALIDATION_FIELD_REQUIRED') assertions.

Consider whether the underlying Zod schema could be configured to report missing enum fields consistently, or alternatively, apply the same regex pattern to other similar tests for uniformity.

packages/workers/src/lib/honoValidationHook.ts (3)

1-12: LGTM!

Module documentation is clear, and imports are well-organized using type-only imports where appropriate. Aligns with coding guidelines for using Zod and @corates/shared error utilities.


14-18: LGTM!

Interface correctly models the Hono/OpenAPIHono validation hook result contract with success, error, and target fields.


31-57: LGTM!

Robust handling of Zod validation errors:

  • Optional chaining safely handles edge cases (empty issues array)
  • Cross-version compatibility for detecting undefined values (actual vs string representation)
  • Comprehensive missing-field detection covering invalid_type, invalid_enum_value, and message patterns
packages/workers/src/routes/google-drive.ts (10)

6-28: LGTM!

The imports are well-organized, and the centralized validationHook replaces the inlined defaultHook appropriately. Using Env type bindings and applying requireAuth globally is consistent with the codebase patterns.


69-76: LGTM!

The details field typing update to z.record(z.string(), z.unknown()) properly constrains the error payload structure while maintaining flexibility for various error contexts.


78-102: LGTM!

The GoogleTokens interface and getGoogleTokens function are well-typed. The function properly handles the case where no Google account exists by returning undefined.


107-138: LGTM!

The function is well-typed with proper error handling using createDomainError. The type assertion on line 133 for the Google OAuth response is acceptable for external API responses.


143-182: LGTM!

The token refresh logic is sound. The non-null assertion on line 154 is safe since callers verify tokens?.accessToken is truthy before invoking this function. The edge case handling for invalid expiresAt values (lines 157-159) is appropriate defensive coding.


200-210: LGTM!

The status route correctly returns the connection status. The user! assertion is safe due to the requireAuth middleware applied globally.


232-277: LGTM!

The picker token route properly handles token retrieval with comprehensive error handling. The @ts-expect-error annotation for OpenAPIHono strict return types is an acceptable workaround, and ContentfulStatusCode casting maintains type consistency.


295-315: LGTM!

The disconnect route correctly removes the Google account association with proper error handling using SYSTEM_ERRORS.DB_ERROR per guidelines.


367-375: LGTM!

The project access check using requireProjectEdit properly validates user permissions before proceeding with the import. Domain errors are correctly surfaced to the client.


529-551: LGTM!

The success response is well-structured with proper as const annotations for literal types, and the outer catch block correctly handles and surfaces domain errors.

packages/workers/src/index.ts (1)

39-39: Typed OpenAPIHono bindings look solid.
Improves compile-time safety for env bindings without changing runtime behavior.

packages/workers/src/commands/members/updateMemberRole.ts (4)

1-15: LGTM!

The imports are well-organized and include the required createDomainError and SYSTEM_ERRORS from @corates/shared per coding guidelines. The JSDoc clearly documents the potential error case.


17-31: LGTM!

The interfaces establish a clear typed contract for the command, consistent with the other member command typings mentioned in the AI summary.


55-78: LGTM!

The non-fatal error handling for DO sync and user notifications is appropriate—the core operation (DB update) succeeds, and failures in secondary operations are logged without blocking the response. This aligns with the pattern used across other member commands.


33-37: Ensure route-level validation exists for all updateMemberRole inputs.

This command accepts parameters directly without validation. Per the backend guidelines, all input validation must happen at the route layer using validateRequest middleware with a Zod schema that validates orgId, projectId, userId, and role before invoking this command function.

Verify that the route calling this command includes proper Zod schema validation and validateRequest middleware.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +147 to +185
```md
### Issue: <short title>

**Type:** Bug | Logic Error | CLAUDE.md Violation
**Severity:** High
**Reason flagged:** <agent-provided reason>

**Description:**
Clear explanation of the problem.

**Evidence:**

- File path and line range (for example: `src/foo/bar.ts:42-58`)
- Include at least one line of context before and after the relevant lines

**Relevant Rule (if applicable):**
Quoted CLAUDE.md rule with its file path.

**Suggested Fix:**
Description of the fix. Include a committable suggestion only if it fully resolves the issue.


8. No-issue outcome

If no validated issues remain, the Markdown file must contain:

## Code review

No issues found. Checked for bugs and CLAUDE.md compliance.


Explicit prohibitions
• Do not post GitHub comments
• Do not post inline comments
• Do not modify the repository
• Output only the Markdown report
```
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 | 🟠 Major

Fix markdown code block structure and heading formats.

The code block starting at line 147 has structural issues:

  1. Step 8 missing proper heading format (line 170): The text "8. No-issue outcome" should use ### 8. No-issue outcome to match the format of steps 0-7.

  2. "Explicit prohibitions" missing heading (line 180): This section lacks a markdown heading (should be ## or ###).

  3. Code block boundaries unclear (lines 147-185): The current structure suggests lines 147-185 are all within one code block, but lines 168-184 appear to be documentation rather than example output. This creates ambiguity about whether:

    • The code block should end at line 167 (before the separator), OR
    • Lines 168-184 are meant as part of the example format

The separator at line 168 and the formatting of lines 169-184 suggest these should be outside the code block, but the closing backticks are at line 185.

📝 Proposed fix for markdown structure

Option 1: If lines 168-184 are meant to be documentation (recommended):

 **Suggested Fix:**  
 Description of the fix. Include a committable suggestion only if it fully resolves the issue.
+```
 
-
+---
 
-8. No-issue outcome
+### 8. No-issue outcome
 
 If no validated issues remain, the Markdown file must contain:
 
+```md
 ## Code review
 
 No issues found. Checked for bugs and CLAUDE.md compliance.
+```
 
-
+---
 
-Explicit prohibitions
+## Explicit prohibitions
+
 • Do not post GitHub comments
 • Do not post inline comments
 • Do not modify the repository
 • Output only the Markdown report
-```

Option 2: If lines 168-184 are meant to be part of the example (inside the code block):

 **Suggested Fix:**  
 Description of the fix. Include a committable suggestion only if it fully resolves the issue.
 
 ⸻
 
-8. No-issue outcome
+### 8. No-issue outcome
 
 If no validated issues remain, the Markdown file must contain:
 
 ## Code review
 
 No issues found. Checked for bugs and CLAUDE.md compliance.
 
 ⸻
 
-Explicit prohibitions
+## Explicit prohibitions
+
 • Do not post GitHub comments
 • Do not post inline comments
 • Do not modify the repository
 • Output only the Markdown report

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

In @.claude/skills/code-review/SKILL.md around lines 147 - 185, The markdown
code block is malformed around the snippet containing "8. No-issue outcome" and
"Explicit prohibitions": end the opening md block before the separator (the line with the horizontal separator characters), change "8. No-issue outcome" to a proper heading (e.g., "### 8. No-issue outcome"), wrap the “## Code review / No issues found…” example in its own fenced md block, replace the separator
with a proper markdown horizontal rule (---), and add a heading "## Explicit
prohibitions" before the bulleted list; locate these edits by searching for the
exact strings "8. No-issue outcome", "Explicit prohibitions", and the
opening/closing backticks in the problematic diff to fix the structure.


</details>

<!-- fingerprinting:phantom:triton:puma -->

<!-- This is an auto-generated comment by CodeRabbit -->

Comment on lines +43 to +53
try {
await db
.update(projectMembers)
.set({ role })
.where(and(eq(projectMembers.projectId, projectId), eq(projectMembers.userId, userId)));
} catch (err) {
throw createDomainError(SYSTEM_ERRORS.DB_ERROR, {
operation: 'update_member_role',
cause: err instanceof Error ? err.message : String(err),
});
}
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

Update may silently succeed if user is not a project member.

The update query will succeed with 0 affected rows if the user is not a member of the project, returning a successful result despite no actual change occurring. Consider checking the result to ensure the member exists.

🔧 Suggested fix
   try {
-    await db
+    const result = await db
       .update(projectMembers)
       .set({ role })
       .where(and(eq(projectMembers.projectId, projectId), eq(projectMembers.userId, userId)));
+    
+    if (result.rowsAffected === 0) {
+      throw createDomainError(VALIDATION_ERRORS.NOT_FOUND, {
+        resource: 'project_member',
+        userId,
+        projectId,
+      });
+    }
   } catch (err) {
+    if (err && typeof err === 'object' && 'code' in err) {
+      throw err; // Re-throw domain errors
+    }
     throw createDomainError(SYSTEM_ERRORS.DB_ERROR, {
       operation: 'update_member_role',
       cause: err instanceof Error ? err.message : String(err),
     });
   }

Verify if rowsAffected is available on Drizzle's update result for D1:

#!/bin/bash
# Check how other commands handle update results in this codebase
rg -n -A5 'rowsAffected|\.update\(' packages/workers/src/commands/ --type ts
🤖 Prompt for AI Agents
In `@packages/workers/src/commands/members/updateMemberRole.ts` around lines 43 -
53, The update currently calls db.update(projectMembers).set({ role })... but
never checks the result so it can silently succeed with 0 affected rows; change
the logic in updateMemberRole to capture the update result and assert that at
least one row was affected (e.g., check result.rowsAffected or the Drizzle/D1
equivalent) and if zero, throw a domain error (use createDomainError with
SYSTEM_ERRORS.DB_ERROR or a more specific NOT_FOUND error and operation:
'update_member_role') so callers know the member did not exist; keep the
existing catch that maps DB errors via createDomainError for other failures.

@InfinityBowman InfinityBowman merged commit 88f0f18 into main Jan 17, 2026
3 checks passed
@InfinityBowman InfinityBowman deleted the 297-finish-backend-typescript-migration-and-command-pattern branch January 17, 2026 00:26
@coderabbitai coderabbitai Bot mentioned this pull request Apr 18, 2026
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.

Finish backend typescript migration and command pattern

2 participants