Skip to content

Support CLI RLS tokens and fix categories index#12

Merged
gentamura merged 3 commits intomainfrom
feature-cli-rls
Oct 22, 2025
Merged

Support CLI RLS tokens and fix categories index#12
gentamura merged 3 commits intomainfrom
feature-cli-rls

Conversation

@gentamura
Copy link
Member

@gentamura gentamura commented Oct 22, 2025

Summary

  • allow the RLS client to accept raw Supabase access tokens and surface detailed Postgres errors
  • keep provisioning logic in sync so the CLI can reuse the same token handling
  • generate the categories_system_name_idx filter as a literal to avoid placeholders in migrations

Testing

  • bun test packages/db/src/index.test.ts
  • bun run build --filter @listee/db

Summary by CodeRabbit

  • New Features

    • APIs now accept JWT strings as well as token objects.
    • Added parseSupabaseAccessToken for explicit JWT parsing.
  • Improvements

    • Stronger RLS transaction lifecycle: setup/cleanup of JWT and role context with clearer error reporting.
    • createRlsClient/createDrizzle updated to handle token strings.
  • Tests

    • Expanded tests for token parsing, role handling, and permission-error scenarios.

@coderabbitai
Copy link

coderabbitai bot commented Oct 22, 2025

Walkthrough

The PR widens token parameters to accept SupabaseToken | string across auth and db packages, adds JWT parsing utilities and token resolution, enhances RLS transaction setup/teardown and error reporting, updates tests to cover JWT-string scenarios and execution interception, and fixes an import path to include .js.

Changes

Cohort / File(s) Summary
Auth: token type widening
packages/auth/src/account/provision-account.ts
ProvisionAccountParams.token, AccountProvisionerDependencies.createRlsClient, and resolveCreateRlsClient signatures updated to accept `SupabaseToken
DB: JWT handling & RLS lifecycle
packages/db/src/index.ts
Added utilities: parseSupabaseAccessToken, resolveSupabaseToken, type guards, base64/JWT helpers, and error detail formatting. createRlsClient / createDrizzle now accept `SupabaseToken
DB: tests — interceptor & JWT tests
packages/db/src/index.test.ts
Introduced ExecuteInterceptor and setExecuteInterceptor, local PostgresPermissionError test helper, helpers to build/encode Supabase-style JWTs, wired parseSupabaseAccessToken into tests, reset interceptor between tests, and extended createRlsClient tests to assert expanded SQL sequences, role handling, and token-string parsing.
Schema import/path change
packages/db/src/schema/index.ts
Changed import to ../constants/category.js; removed eq import and replaced parameterized WHERE with sql + sql.raw literal injection for DEFAULT_CATEGORY_KIND.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant createRlsClient
    participant resolveSupabaseToken
    participant parseSupabaseAccessToken
    participant Database
    participant Transaction

    Client->>createRlsClient: call with token (string | SupabaseToken)
    createRlsClient->>resolveSupabaseToken: resolve token
    alt token is string
        resolveSupabaseToken->>parseSupabaseAccessToken: parse JWT string
        parseSupabaseAccessToken-->>resolveSupabaseToken: SupabaseToken claims
    else token is SupabaseToken
        resolveSupabaseToken-->>createRlsClient: pass-through claims
    end
    createRlsClient->>Database: set request.jwt.claims / sub / role
    createRlsClient->>Database: optionally apply local role
    createRlsClient->>Transaction: execute user transaction
    alt success
        Transaction-->>createRlsClient: result
    else failure
        Transaction-->>createRlsClient: error (combined details)
    end
    createRlsClient->>Database: cleanup jwt claims & role
    createRlsClient-->>Client: return result or throw
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly Related PRs

Poem

🐰 I sniffed a token, furry and bright,

I parsed its claims by moonlit night.
RLS doors open, then softly close,
Hops of cleanup where the wild wind blows.
A tiny rabbit applauds these bytes.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The PR title "Support CLI RLS tokens and fix categories index" directly addresses the two primary changes in the changeset. The first part accurately reflects the substantial updates to allow RLS clients and provisioning logic to accept Supabase access tokens as strings (in addition to SupabaseToken objects), which spans multiple files with token handling enhancements. The second part correctly identifies the categories index fix where the migration query was modified to use raw SQL to avoid placeholders. The title is concise, clear, and specific enough for teammates to understand the main objectives without being overly broad or vague.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature-cli-rls

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between de79260 and 5019384.

📒 Files selected for processing (4)
  • packages/auth/src/account/provision-account.ts (3 hunks)
  • packages/db/src/index.test.ts (9 hunks)
  • packages/db/src/index.ts (4 hunks)
  • packages/db/src/schema/index.ts (3 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
packages/*/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

packages/*/src/**/*.{ts,tsx}: Avoid implicit any; TypeScript is run with strict enabled
Prefer type guards or the satisfies operator over as casts where appropriate
Prefer unknown for external inputs
Use PascalCase for types and enums
Use camelCase for variables and functions
Ensure source comments are written in English

Files:

  • packages/db/src/index.ts
  • packages/db/src/schema/index.ts
  • packages/auth/src/account/provision-account.ts
  • packages/db/src/index.test.ts
packages/*/src/**/{*.test.ts,__tests__/**/*.ts}

📄 CodeRabbit inference engine (AGENTS.md)

packages/*/src/**/{*.test.ts,__tests__/**/*.ts}: Write test names and descriptions in English
Co-locate tests as *.test.ts or under tests/ using Bun’s test runner

Files:

  • packages/db/src/index.test.ts
🧠 Learnings (3)
📓 Common learnings
Learnt from: CR
PR: listee-dev/listee-libs#0
File: AGENTS.md:0-0
Timestamp: 2025-10-02T12:40:33.718Z
Learning: Applies to packages/auth/src/authentication/**/*.ts : Extend Supabase JWT verification (createSupabaseAuthentication using .well-known/jwks.json) only via dedicated modules to keep caching and claim validation centralized
📚 Learning: 2025-10-02T12:40:33.718Z
Learnt from: CR
PR: listee-dev/listee-libs#0
File: AGENTS.md:0-0
Timestamp: 2025-10-02T12:40:33.718Z
Learning: Applies to packages/auth/src/authentication/**/*.ts : Extend Supabase JWT verification (createSupabaseAuthentication using .well-known/jwks.json) only via dedicated modules to keep caching and claim validation centralized

Applied to files:

  • packages/db/src/index.ts
📚 Learning: 2025-09-30T14:08:23.213Z
Learnt from: gentamura
PR: listee-dev/listee-libs#2
File: packages/db/src/index.ts:132-137
Timestamp: 2025-09-30T14:08:23.213Z
Learning: In packages/db/src/index.ts, multi-statement SQL execution using tx.execute(sql`...`) with Drizzle and Postgres.js works correctly, so multiple statements can be combined in a single execute call.

Applied to files:

  • packages/db/src/index.test.ts
🧬 Code graph analysis (3)
packages/db/src/schema/index.ts (1)
packages/db/src/constants/category.ts (1)
  • DEFAULT_CATEGORY_KIND (2-2)
packages/auth/src/account/provision-account.ts (1)
packages/db/src/index.ts (2)
  • SupabaseToken (154-163)
  • RlsClient (460-462)
packages/db/src/index.test.ts (1)
packages/db/src/index.ts (2)
  • createRlsClient (464-572)
  • parseSupabaseAccessToken (406-413)
🔇 Additional comments (16)
packages/db/src/schema/index.ts (2)

1-1: LGTM! ESM import path and unused import cleanup.

The removal of the unused eq import and the addition of the .js extension to the relative import path align with ESM module resolution requirements.

Also applies to: 13-13


100-102: LGTM! Literal injection for migration clarity.

Using sql.raw to inject the literal value avoids generating placeholder parameters in the migration file, which is the intended goal. Since DEFAULT_CATEGORY_KIND is a build-time constant ("system"), this approach is safe.

packages/auth/src/account/provision-account.ts (1)

18-18: LGTM! Consistent token type widening.

The changes widen the token parameter type from SupabaseToken to SupabaseToken | string across all relevant interfaces and functions. This aligns with the PR objective to accept raw Supabase access tokens and is backward compatible.

Also applies to: 28-28, 53-58

packages/db/src/index.test.ts (5)

8-8: LGTM! Robust test infrastructure.

The addition of ExecuteInterceptor, PostgresPermissionError, and the interceptor wiring provides a clean mechanism to simulate database errors and test error paths. The proper cleanup in beforeEach prevents test pollution.

Also applies to: 31-40, 52-52, 130-132, 149-151, 155-155, 162-162, 171-171


220-233: LGTM! Correct base64url encoding for test tokens.

The encodeSegment and createAccessToken helpers correctly implement base64url encoding and JWT structure for testing purposes. The empty signature is appropriate since token verification is not under test.


235-288: LGTM! Enhanced RLS lifecycle testing.

The test now validates the complete RLS transaction lifecycle with 8 queries, including setup (claims, subject, role) and teardown (in reverse order). The verification that "role-with-hyphen" is sanitized to "anon" correctly tests the role sanitization logic.


302-322: LGTM! Permission error handling coverage.

This test validates that role permission errors are caught and wrapped with a descriptive message that guides users to grant the necessary role membership. The use of error code 42501 accurately simulates Postgres insufficient privilege errors.


324-349: LGTM! Comprehensive token string handling tests.

These tests validate the new capability to accept JWT access tokens as strings, both through createRlsClient integration and via the exported parseSupabaseAccessToken function. The tests correctly verify claim extraction from the token payload.

packages/db/src/index.ts (8)

1-1: LGTM! Node.js built-in import.

Using the node: protocol for the Buffer import follows Node.js best practices for built-in modules.


165-246: LGTM! Thorough type guard implementation.

The type guards (isRecord, isStringArray, isSupabaseToken) are well-structured and defensive. The isSupabaseToken function correctly validates all optional JWT claims with appropriate type checking, including the union type handling for aud.


248-372: LGTM! Comprehensive error detail extraction.

The error handling utilities effectively extract and combine error information from Postgres errors and error chains. The cycle detection in extractCombinedErrorDetails prevents infinite loops, and the deduplication ensures clean error messages. This aligns with the PR objective to surface detailed Postgres errors.


374-421: LGTM! JWT parsing without signature verification.

The JWT parsing utilities correctly handle base64url decoding and payload extraction. Note that signature verification is not performed, which is appropriate for this use case where tokens are pre-authenticated by Supabase. The validation focuses on structural integrity and claim type checking.


423-450: LGTM! Comprehensive role permission error detection.

The isRolePermissionError function uses appropriate heuristics to identify role permission errors, checking both error messages and Postgres error codes (42501, 0A000, 28000). This enables the descriptive error messaging added in the RLS transaction logic.


464-472: LGTM! Token resolution and claim extraction.

The widened token parameter type and the use of resolveSupabaseToken enable accepting both token objects and JWT strings. The claim extraction prepares the necessary values for RLS context setup, with appropriate defaults for missing values.


478-552: LGTM! Robust RLS transaction lifecycle management.

The setup and cleanup mechanism is well-designed with:

  • Proper cleanup task registration in LIFO order
  • Conditional error propagation (suppress during rollback, propagate during commit)
  • Descriptive error messaging for role permission failures
  • Comprehensive RLS context configuration

The pattern of setting both request.jwt.claim.role and the actual role via set local role provides compatibility with different RLS policy patterns.


554-572: LGTM! Comprehensive error handling in RLS transactions.

The transaction execution correctly handles both success and failure paths:

  • Cleanup runs in both cases with appropriate error propagation behavior
  • Error details are extracted and combined using the utility functions
  • The wrapped error preserves the original cause for debugging
  • The "RLS transaction failed:" prefix clearly identifies RLS-related errors

This implementation aligns with the PR objective to surface detailed Postgres errors.


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.

Copy link

@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

🧹 Nitpick comments (1)
packages/db/src/schema/index.ts (1)

100-104: Add validation to prevent SQL injection if constant changes.

The sql.raw injection assumes DEFAULT_CATEGORY_KIND will always contain SQL-safe values. While the current value "system" is safe, future changes could introduce SQL metacharacters.

Consider adding a runtime validation to ensure safety:

+      // Validate that DEFAULT_CATEGORY_KIND is SQL-safe
+      if (!/^[a-zA-Z0-9_]+$/.test(DEFAULT_CATEGORY_KIND)) {
+        throw new Error(`DEFAULT_CATEGORY_KIND must be alphanumeric: ${DEFAULT_CATEGORY_KIND}`);
+      }
       uniqueIndex("categories_system_name_idx")
         .on(table.createdBy, table.name)
         // drizzle-kit stringifies template parameters as placeholders, so use sql.raw
         // to inject the literal value without producing $1 in the generated migration.
         .where(
           sql`${table.kind} = ${sql.raw(`'${DEFAULT_CATEGORY_KIND}'`)}`,
         ),

Alternatively, if drizzle-kit supports it, consider using a WHERE clause with a proper escaped literal rather than raw SQL injection.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5559b5d and de79260.

📒 Files selected for processing (4)
  • packages/auth/src/account/provision-account.ts (3 hunks)
  • packages/db/src/index.test.ts (9 hunks)
  • packages/db/src/index.ts (4 hunks)
  • packages/db/src/schema/index.ts (2 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
packages/*/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

packages/*/src/**/*.{ts,tsx}: Avoid implicit any; TypeScript is run with strict enabled
Prefer type guards or the satisfies operator over as casts where appropriate
Prefer unknown for external inputs
Use PascalCase for types and enums
Use camelCase for variables and functions
Ensure source comments are written in English

Files:

  • packages/auth/src/account/provision-account.ts
  • packages/db/src/schema/index.ts
  • packages/db/src/index.test.ts
  • packages/db/src/index.ts
packages/*/src/**/{*.test.ts,__tests__/**/*.ts}

📄 CodeRabbit inference engine (AGENTS.md)

packages/*/src/**/{*.test.ts,__tests__/**/*.ts}: Write test names and descriptions in English
Co-locate tests as *.test.ts or under tests/ using Bun’s test runner

Files:

  • packages/db/src/index.test.ts
🧠 Learnings (2)
📓 Common learnings
Learnt from: CR
PR: listee-dev/listee-libs#0
File: AGENTS.md:0-0
Timestamp: 2025-10-02T12:40:33.718Z
Learning: Applies to packages/auth/src/authentication/**/*.ts : Extend Supabase JWT verification (createSupabaseAuthentication using .well-known/jwks.json) only via dedicated modules to keep caching and claim validation centralized
📚 Learning: 2025-09-30T14:08:23.213Z
Learnt from: gentamura
PR: listee-dev/listee-libs#2
File: packages/db/src/index.ts:132-137
Timestamp: 2025-09-30T14:08:23.213Z
Learning: In packages/db/src/index.ts, multi-statement SQL execution using tx.execute(sql`...`) with Drizzle and Postgres.js works correctly, so multiple statements can be combined in a single execute call.

Applied to files:

  • packages/db/src/index.test.ts
🧬 Code graph analysis (3)
packages/auth/src/account/provision-account.ts (1)
packages/db/src/index.ts (2)
  • SupabaseToken (154-163)
  • RlsClient (430-432)
packages/db/src/schema/index.ts (1)
packages/db/src/constants/category.ts (1)
  • DEFAULT_CATEGORY_KIND (2-2)
packages/db/src/index.test.ts (1)
packages/db/src/index.ts (2)
  • createRlsClient (434-542)
  • parseSupabaseAccessToken (380-387)
🔇 Additional comments (15)
packages/db/src/schema/index.ts (1)

13-13: LGTM: Explicit .js extension follows ESM conventions.

The import path now includes the explicit .js extension, which is required for proper ESM module resolution.

packages/db/src/index.test.ts (4)

212-225: LGTM: Test helpers correctly implement base64url encoding for JWTs.

The encodeSegment and createAccessToken functions properly implement base64url encoding and JWT structure for test purposes. The empty signature (trailing .) is appropriate since these tests focus on payload parsing rather than signature verification.


227-280: LGTM: Comprehensive test coverage for RLS setup and teardown.

The test properly validates the expanded RLS transaction sequence (8 queries) including:

  • JWT claims configuration (request.jwt.claims, request.jwt.claim.sub, request.jwt.claim.role)
  • Local role assignment
  • Proper cleanup in reverse order

The test also verifies that invalid role values (containing hyphens) are correctly sanitized to "anon" as expected by the sanitizeRole function.


294-313: LGTM: Good coverage for role permission errors.

This test properly simulates a PostgreSQL permission denied error (code 42501) and verifies that the enhanced error handling provides a descriptive message to guide developers toward granting the necessary role membership.


315-340: LGTM: Comprehensive coverage for string token handling.

These tests validate both the integration (passing JWT strings to createRlsClient) and the unit behavior (direct parseSupabaseAccessToken calls), ensuring the new token parsing functionality works correctly.

packages/db/src/index.ts (9)

1-1: LGTM: Proper use of Node.js Buffer.

Correctly imports Buffer from the node:buffer built-in module for JWT decoding.


165-218: LGTM: Robust type guards for token validation.

The type guards properly validate the structure of Supabase tokens with correct handling of optional fields and the union type for aud (string or string array).


220-344: LGTM: Well-designed error handling utilities.

The error extraction logic properly:

  • Traverses error cause chains
  • Prevents infinite loops with a visited set
  • Deduplicates messages
  • Formats PostgreSQL error details comprehensively

This will provide much better error diagnostics for RLS transactions.


346-387: Verify that tokens are already authenticated upstream.

The JWT parsing functions correctly decode and validate the structure of Supabase access tokens, but they do not verify cryptographic signatures. This is acceptable only if tokens are already verified by Supabase authentication before reaching this code.

Based on learnings from the authentication module, ensure that JWT verification is handled by the dedicated authentication layer (using .well-known/jwks.json) before tokens reach the database layer.


389-395: LGTM: Clean union type resolution.

The resolveSupabaseToken function properly handles both token input types, leveraging the type guard to determine which code path to take.


397-420: LGTM: Comprehensive role permission error detection.

The function correctly identifies PostgreSQL role permission errors using both message patterns and standard error codes (42501, 0A000, 28000).


434-502: LGTM: Well-structured RLS context setup with proper error handling.

The implementation correctly:

  • Resolves token claims from either SupabaseToken objects or JWT strings
  • Sets all required JWT-related PostgreSQL session parameters
  • Registers cleanup tasks immediately after each setup operation
  • Provides actionable error messages for role permission failures
  • Chains errors properly for debugging

503-522: LGTM: Robust cleanup logic with configurable error propagation.

The cleanup mechanism properly:

  • Executes tasks in reverse (LIFO) order
  • Continues cleanup even if individual tasks fail
  • Propagates cleanup errors on the success path
  • Suppresses cleanup errors on the failure path to preserve the original error context

524-537: LGTM: Enhanced error reporting for RLS transactions.

The error handling properly:

  • Attempts cleanup even on transaction failure
  • Suppresses cleanup errors to preserve the original failure context
  • Aggregates error details from the cause chain
  • Formats PostgreSQL-specific error information (code, detail, hint, etc.)
  • Maintains error causality for debugging

This will significantly improve the developer experience when debugging RLS issues.

packages/auth/src/account/provision-account.ts (1)

19-19: LGTM: Consistent type widening for flexible token handling.

The parameter type changes from SupabaseToken to SupabaseToken | string are consistent across the module and align with the enhanced token handling in the database layer. This maintains backward compatibility while enabling CLI and other consumers to pass JWT strings directly.

Also applies to: 29-29, 54-60

@gentamura gentamura self-assigned this Oct 22, 2025
Comment on lines +500 to +507
await tx.execute(sql`
select set_config('request.jwt.claim.role', ${roleSetting}, TRUE);
`);
cleanupTasks.push(async () => {
await tx.execute(sql`
select set_config('request.jwt.claim.role', NULL, TRUE);
`);
});
Copy link
Member Author

Choose a reason for hiding this comment

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

Supabase’s auth.role() helper reads request.jwt.claim.role, so we mirror PostgREST and populate the claim in addition to set local role.

@gentamura gentamura merged commit 4faf6a3 into main Oct 22, 2025
4 checks passed
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.

1 participant