Skip to content

Conversation

@lourou
Copy link
Member

@lourou lourou commented Oct 29, 2025

This is done to support JWT verification on the payer gateway using a public key

Summary by CodeRabbit

  • Documentation

    • Added instructions and a command to generate ECDSA P-256 (ES256) JWT key pairs and updated the .env example, deprecating the old symmetric JWT secret.
  • New Features

    • Migrated JWT authentication to ES256 with issuer enforcement.
    • Added a key-generation utility that outputs backend and gateway-ready key values.
  • Behavior

    • Startup now validates JWT keys and fails early with clear errors if keys are missing or invalid.

@lourou lourou requested a review from a team as a code owner October 29, 2025 17:08
@claude
Copy link

claude bot commented Oct 29, 2025

Claude finished @lourou's task —— View job


Code Review Complete ✅

I've completed a thorough review of this PR that migrates JWT authentication from symmetric HS256 to asymmetric ES256 (ECDSA P-256). Overall, this is a well-executed security improvement with proper key caching and validation. Here's my feedback:


🔒 Security Considerations

✅ Strengths:

  • Excellent choice of ES256 (ECDSA P-256) for asymmetric JWT - industry standard and appropriate for this use case
  • Public key can be safely shared with the gateway for verification without exposing signing capability
  • Issuer validation (convos.org) properly enforced in verifyV2JwtToken at src/utils/v2/jwt.ts:165
  • Algorithm is explicitly specified (algorithms: ["ES256"]) preventing algorithm confusion attacks
  • Keys validated at startup before server accepts traffic - fail-fast approach prevents runtime errors
  • Good key generation script with clear security warnings about keeping private keys secret

⚠️ Recommendations:

  1. Key rotation strategy: Consider documenting a key rotation plan. Currently, there's no mechanism to rotate keys or support multiple public keys for zero-downtime rotation.
  2. Key storage: The README should mention using secrets management (AWS Secrets Manager, HashiCorp Vault, etc.) in production rather than .env files.
  3. Environment separation: Consider using different key pairs per environment (dev/staging/prod) to limit blast radius if keys are compromised.

⚡ Performance

✅ Major Improvement (addressed in latest commit):

  • Keys are now cached at module level (src/utils/v2/jwt.ts:27-28) - excellent fix!
  • loadPrivateKey() and loadPublicKey() functions (src/utils/v2/jwt.ts:34-64) implement lazy-loading with caching
  • Keys are imported once at startup via validateJWTKeys() and reused for all subsequent requests
  • This eliminates the performance concern raised by CodeRabbit about re-parsing PEM on every request

📊 Performance characteristics:

  • ES256 signing is slightly slower than HS256 but negligible for your throughput requirements
  • Verification is fast and now cached properly
  • The startup validation adds ~100ms but provides valuable fail-fast behavior

🏗️ Code Quality & Best Practices

✅ Strengths:

  • Clean separation of concerns: config validation, key loading, token operations
  • Proper use of Zod for JWT payload validation (src/utils/v2/jwt.ts:18-25)
  • Good use of tryCatch utility for error handling
  • Clear JSDoc comments on key functions
  • Metadata size validation prevents JWT bloat (src/utils/v2/jwt.ts:100-110)
  • Backward compatibility maintained for v1 JWT (src/config.ts:20-25)

⚠️ Minor Issues:

  1. Inconsistent error handling in config.ts (src/config.ts:9-14):

    // Current - uses plain Error
    throw new Error("XMTP_NOTIFICATION_SECRET is not configured");
    
    // Should use AppError per codebase conventions
    import { AppError } from "./utils/errors";
    throw new AppError(500, "XMTP_NOTIFICATION_SECRET is not configured");

    The coding guidelines specify using AppError class consistently, but config.ts uses plain Error for validation failures. This should be updated for consistency.

  2. Inconsistent error types in jwt.ts (src/utils/v2/jwt.ts:39-41, 77-79):

    • loadPrivateKey() and validateJWTKeys() throw plain Error instances
    • While createV2JwtToken() and verifyV2JwtToken() properly use AppError
    • Consider standardizing to AppError throughout for consistency
  3. Structured logging improvement (src/index.ts:84):

    // Current
    logger.error({ error }, "Failed to validate JWT keys at startup");
    // This is actually CORRECT ✅ - wrapping error in object preserves stack trace

    Actually, this is properly structured! Good job here.


🐛 Potential Issues

🔴 Critical:
None found - the implementation is solid.

🟡 Minor:

  1. Race condition edge case (Low risk, but worth noting):
    In loadPrivateKey() and loadPublicKey(), there's a theoretical race where multiple concurrent calls could import the key multiple times before caching. In practice, this is mitigated by:

    • Startup validation pre-caches keys before any requests
    • JavaScript's single-threaded nature
    • The race would only waste a few ms, not cause errors

    If you want to be extra defensive, you could use a promise-based lock:

    let privateKeyPromise: Promise<jose.KeyLike> | null = null;
    const loadPrivateKey = async (): Promise<jose.KeyLike> => {
      if (cachedPrivateKey) return cachedPrivateKey;
      if (!privateKeyPromise) {
        privateKeyPromise = jose.importPKCS8(JWT_PRIVATE_KEY, "ES256")
          .then(key => {
            cachedPrivateKey = key;
            return key;
          });
      }
      return privateKeyPromise;
    };
  2. Empty string keys pass config validation (src/config.ts:29-30):

    export const JWT_PRIVATE_KEY = process.env.JWT_PRIVATE_KEY || "";
    export const JWT_PUBLIC_KEY = process.env.JWT_PUBLIC_KEY || "";

    Keys default to empty strings and are validated later at startup. This is intentional for backward compatibility, but could be clearer with a comment explaining why validation is deferred.


🧪 Test Coverage

❌ Major Gap:

  • No test files found for JWT functionality (searched for *jwt*.test.ts, *jwt*.spec.ts)
  • Critical paths that should have tests:
    • createV2JwtToken() - token creation with various metadata
    • verifyV2JwtToken() - verification success/failure cases
    • validateJWTKeys() - startup validation with valid/invalid keys
    • Issuer validation enforcement
    • Expired token handling
    • Metadata size limit enforcement
    • Key caching behavior

Recommendation:
Add test coverage for src/utils/v2/jwt.ts to verify:

describe('v2 JWT', () => {
  describe('createV2JwtToken', () => {
    it('creates valid ES256 token with deviceId')
    it('includes metadata when provided')
    it('rejects metadata exceeding size limit')
    it('sets correct issuer and subject claims')
  });
  
  describe('verifyV2JwtToken', () => {
    it('verifies valid token successfully')
    it('rejects token with wrong issuer')
    it('rejects expired token')
    it('rejects token signed with wrong key')
    it('rejects malformed token')
  });
  
  describe('validateJWTKeys', () => {
    it('validates correct PEM keys')
    it('rejects invalid private key format')
    it('rejects invalid public key format')
  });
});

✅ Backward Compatibility

Excellent work here:

  • V1 JWT (JWT_SECRET) remains optional and functional (src/config.ts:20-25)
  • V2 keys are validated separately without breaking v1
  • No breaking changes to existing endpoints
  • .env.example documents both with clear deprecation notice

📋 Summary & Recommendations

This PR is production-ready with the following minor improvements suggested:

Priority Item Location Effort
🟢 Low Standardize error handling to use AppError src/config.ts:9-14 5 min
🟢 Low Use AppError in key loading functions src/utils/v2/jwt.ts:39-41, 77-79 5 min
🟡 Medium Add test coverage for JWT utilities src/utils/v2/jwt.ts 2-3 hours
🟡 Medium Document key rotation strategy README.md 15 min
🔵 Nice-to-have Add production secrets management guidance README.md 10 min

What's working great:

  • ✅ Key caching implementation (performance optimized)
  • ✅ Fail-fast validation at startup
  • ✅ Security best practices (issuer validation, algorithm enforcement)
  • ✅ Backward compatibility
  • ✅ Clear documentation and key generation tooling

Approval recommendation:Approve with optional follow-ups

The core implementation is solid and secure. The suggested improvements (error handling consistency, tests, documentation) can be addressed in follow-up PRs and don't block this security enhancement.


@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 29, 2025

Warning

Rate limit exceeded

@lourou has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 6 minutes and 29 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 25d273e and 18d8caa.

📒 Files selected for processing (3)
  • src/config.ts (1 hunks)
  • src/index.ts (2 hunks)
  • src/utils/v2/jwt.ts (3 hunks)

Walkthrough

Adds asymmetric JWT v2 (ES256): new ECDSA key-generation script and README updates; config now validates/exports PEM private/public keys and issuer; JWT utils import/validate keys, sign with ES256 and verify enforcing issuer; startup validates keys before launching the server.

Changes

Cohort / File(s) Summary
Documentation
README.md, .env.example
Added instructions and command to generate ECDSA P-256 keys, show env propagation for JWT_PRIVATE_KEY/JWT_PUBLIC_KEY, and deprecate v1 JWT_SECRET.
Key Generation Script
dev/scripts/generateEcdsaKeys.ts
New script generating ECDSA P-256 (ES256) key pair via jose, exporting PKCS#8 private and SPKI public PEMs, and printing env-ready escaped values and warnings.
Configuration
src/config.ts
Replaces required symmetric JWT_SECRET flow with validation/exports for JWT_PRIVATE_KEY, JWT_PUBLIC_KEY, JWT_ISSUER ("convos.org"), adds IS_DEVELOPMENT, and preserves optional JWT_SECRET/JWT_SECRET_BYTES compatibility.
JWT Implementation
src/utils/v2/jwt.ts
Switched signing/verification to ES256 using PEM keys, added validateJWTKeys() to import/validate PEMs at startup, include sub/iss claims, and improved error handling/logging.
Startup Flow
src/index.ts
Calls validateJWTKeys() before starting the HTTP server; aborts startup on validation failure and adjusts startup/SIGTERM ordering accordingly.

Sequence Diagram

sequenceDiagram
  autonumber
  rect rgb(245,250,255)
  Note over Dev,Backend: ES256 JWT key generation & usage
  end

  Dev->>Backend: run `dev/scripts/generateEcdsaKeys.ts`
  Backend->>Backend: store `JWT_PRIVATE_KEY` in .env
  Backend->>Backend: validateJWTKeys() — import & validate PEMs
  Backend->>Backend: createToken(deviceId) with sub, iss
  Backend->>Backend: sign token using ES256 (private PEM)
  Backend->>Gateway: send JWT
  Gateway->>Gateway: import `JWT_PUBLIC_KEY`
  Gateway->>Gateway: verify JWT (ES256, iss="convos.org")
  alt verification success
    Gateway->>Gateway: extract sub → deviceId
  else verification failure
    Gateway->>Gateway: log & reject request
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Inspect src/utils/v2/jwt.ts for correct PEM import, algorithm enforcement, issuer checks, and AppError handling.
  • Verify src/config.ts environment parsing and JWT_SECRET compatibility behavior.
  • Confirm dev/scripts/generateEcdsaKeys.ts PEM formats, escaping, and jose usage.
  • Check src/index.ts startup ordering and process exit behavior on validation failure.

Suggested reviewers

  • nplasterer

Poem

🐰 I hopped and hammered curves of key,
I baked a PEM for you and me.
ES256 signed the midnight sky,
Public hops free — private stays shy.
Keep secrets snug beneath the tree. 🌿

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 pull request title "Switch to asymmetric JWT auth for v2 endpoints" directly and clearly describes the primary objective of the changeset. The changes throughout the PR—including the new ECDSA key generation script, configuration updates replacing JWT_SECRET with JWT_PRIVATE_KEY and JWT_PUBLIC_KEY, the switch to ES256 signing in the JWT utilities, and the corresponding documentation—all align precisely with this asymmetric JWT authentication shift. The title is specific and meaningful, clearly distinguishing the architectural change for a future developer reviewing the git history.

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.

@macroscopeapp
Copy link

macroscopeapp bot commented Oct 29, 2025

Switch v2 JWT on backend and gateway to ES256 with asymmetric keys for token signing and verification

Introduce ES256 signing and verification for v2 JWTs using JWT_PRIVATE_KEY and JWT_PUBLIC_KEY, add key validation at startup in src/index.ts, and update v2 token creation and verification in src/utils/v2/jwt.ts with issuer convos.org and sub set to deviceId.

📍Where to Start

Start with validateJWTKeys() and related helpers in jwt.ts, then review the startup gate in index.ts.


Macroscope summarized 18d8caa.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/utils/v2/jwt.ts (2)

54-81: Consider caching the imported private key at module level.

The private key is imported on every call to createV2JwtToken, which involves parsing the PEM string repeatedly. For better performance, import and cache the key once at module initialization.

Apply this pattern:

+// Import and cache keys at module initialization
+let cachedPrivateKey: jose.KeyLike | null = null;
+
+async function getPrivateKey() {
+  if (!cachedPrivateKey) {
+    const { data, error } = await tryCatch(
+      jose.importPKCS8(JWT_PRIVATE_KEY, "ES256"),
+    );
+    if (error) {
+      logger.error("Failed to import JWT private key", error);
+      throw new AppError(500, "Failed to import JWT private key", error);
+    }
+    cachedPrivateKey = data;
+  }
+  return cachedPrivateKey;
+}
+
 export const createV2JwtToken = async (args: {
   deviceId: string;
   metadata?: V2JWTMetadata;
   expirationTime?: string;
 }) => {
   // ... validation code ...

-  // Import ECDSA private key
-  const { data: privateKey, error: importError } = await tryCatch(
-    jose.importPKCS8(JWT_PRIVATE_KEY, "ES256"),
-  );
-
-  if (importError) {
-    logger.error("Failed to import JWT private key", importError);
-    throw new AppError(500, "Failed to import JWT private key", importError);
-  }
+  const privateKey = await getPrivateKey();

   // Create JWT token with ECDSA ES256
   const { data: jwt, error: jwtError } = await tryCatch(

84-116: Consider caching the imported public key at module level.

The public key is imported on every call to verifyV2JwtToken. Since this function is called on every authenticated request, the repeated key import could significantly impact performance.

Apply this pattern:

+let cachedPublicKey: jose.KeyLike | null = null;
+
+async function getPublicKey() {
+  if (!cachedPublicKey) {
+    const { data, error } = await tryCatch(
+      jose.importSPKI(JWT_PUBLIC_KEY, "ES256"),
+    );
+    if (error) {
+      logger.error("Failed to import JWT public key for verification", error);
+      throw new AppError(500, "Failed to import JWT public key", error);
+    }
+    cachedPublicKey = data;
+  }
+  return cachedPublicKey;
+}
+
 export const verifyV2JwtToken = async (args: { token: string }) => {
-  // Import ECDSA public key for verification
-  const { data: publicKey, error: importError } = await tryCatch(
-    jose.importSPKI(JWT_PUBLIC_KEY, "ES256"),
-  );
-
-  if (importError) {
-    logger.error(
-      "Failed to import JWT public key for verification",
-      importError,
-    );
-    throw new AppError(500, "Failed to import JWT public key", importError);
-  }
+  const publicKey = await getPublicKey();

   // Verify JWT token using the public key
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 788af7b and 5fbf0e8.

📒 Files selected for processing (4)
  • README.md (2 hunks)
  • dev/scripts/generateEcdsaKeys.ts (1 hunks)
  • src/config.ts (2 hunks)
  • src/utils/v2/jwt.ts (3 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*

📄 CodeRabbit inference engine (.cursorrules)

Follow consistent file naming (kebab-case)

Files:

  • README.md
  • src/utils/v2/jwt.ts
  • dev/scripts/generateEcdsaKeys.ts
  • src/config.ts
**/README.md

📄 CodeRabbit inference engine (.cursorrules)

**/README.md: Document environment setup
Keep README up to date
Document API endpoints
Include example usage

Files:

  • README.md
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursorrules)

**/*.{ts,tsx}: TypeScript with strict mode and ESNext features
Implement consistent error handling using AppError class
Use object parameter syntax for function arguments: '(args: { paramOne: string }) => {}' instead of '(paramOne: string) => {}' for better readability and extensibility
Keep functions focused and avoid optional parameters. If a function has many optional parameters, consider splitting it into multiple specialized functions for clearer intent and better maintainability
Prefer type inference from Prisma models
Keep types close to their usage (same directory)
Use type exports for shared types
Leverage path aliases (@/) for imports
Don't specify return type on functions. Prefer inferring the value.
Use transactions for multi-table operations
Include necessary relations in queries using include
Use custom AppError class for application errors
Log errors with context using logError utility
Log security-relevant events
Implement caching where needed
Optimize database queries
Use appropriate batch operations
Add JSDoc comments for public APIs

Files:

  • src/utils/v2/jwt.ts
  • dev/scripts/generateEcdsaKeys.ts
  • src/config.ts
🧠 Learnings (1)
📚 Learning: 2025-09-04T14:37:41.542Z
Learnt from: nmalzieu
PR: ephemeraHQ/convos-backend#133
File: src/api/v1/devices/handlers/get-device.handler.ts:15-16
Timestamp: 2025-09-04T14:37:41.542Z
Learning: For authenticated endpoints in the convos-backend Express.js application, res.locals.xmtpId is guaranteed to be present when handlers execute because the auth middleware validates JWT tokens and returns 401 before reaching handlers if authentication fails. Additional guards for res.locals.xmtpId are not needed in authenticated route handlers.

Applied to files:

  • src/config.ts
🧬 Code graph analysis (1)
src/utils/v2/jwt.ts (3)
src/utils/try-catch.ts (1)
  • tryCatch (14-23)
src/config.ts (3)
  • JWT_PRIVATE_KEY (69-69)
  • JWT_ISSUER (72-72)
  • JWT_PUBLIC_KEY (70-70)
src/utils/errors.ts (1)
  • AppError (1-11)
🪛 GitHub Check: Typecheck, format check, lint
src/config.ts

[failure] 19-19: error: JWT_PRIVATE_KEY is not configured

  at /home/runner/work/convos-backend/convos-backend/src/config.ts:19:9
⏰ 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). (3)
  • GitHub Check: claude-review
  • GitHub Check: push_to_registry
  • GitHub Check: Review for correctness
🔇 Additional comments (3)
dev/scripts/generateEcdsaKeys.ts (1)

1-57: LGTM! Key generation script is well-structured.

The script correctly generates ES256 key pairs, exports them in the proper PEM formats (PKCS#8 for private, SPKI for public), and provides clear output with security warnings.

README.md (1)

38-44: LGTM! Clear documentation for key generation.

The instructions provide a clear workflow for generating and distributing the JWT keys.

src/config.ts (1)

18-70: LGTM! Thorough validation with clear error messages.

The validation logic properly checks for the presence and non-emptiness of the JWT keys, with descriptive error messages that guide developers on what's needed.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/index.ts (1)

83-85: Log startup validation failures with structured error context.

Line 84 currently passes the error as a second positional argument, so pino treats it as string interpolation and we lose the structured stack trace in logs. Wrap the error in an object to keep the context and comply with our logging conventions.

-    logger.error("Failed to validate JWT keys at startup", error);
+    logger.error({ error }, "Failed to validate JWT keys at startup");
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5fbf0e8 and 4309594.

📒 Files selected for processing (3)
  • .env.example (1 hunks)
  • src/index.ts (2 hunks)
  • src/utils/v2/jwt.ts (4 hunks)
✅ Files skipped from review due to trivial changes (1)
  • .env.example
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursorrules)

**/*.{ts,tsx}: TypeScript with strict mode and ESNext features
Implement consistent error handling using AppError class
Use object parameter syntax for function arguments: '(args: { paramOne: string }) => {}' instead of '(paramOne: string) => {}' for better readability and extensibility
Keep functions focused and avoid optional parameters. If a function has many optional parameters, consider splitting it into multiple specialized functions for clearer intent and better maintainability
Prefer type inference from Prisma models
Keep types close to their usage (same directory)
Use type exports for shared types
Leverage path aliases (@/) for imports
Don't specify return type on functions. Prefer inferring the value.
Use transactions for multi-table operations
Include necessary relations in queries using include
Use custom AppError class for application errors
Log errors with context using logError utility
Log security-relevant events
Implement caching where needed
Optimize database queries
Use appropriate batch operations
Add JSDoc comments for public APIs

Files:

  • src/utils/v2/jwt.ts
  • src/index.ts
**/*

📄 CodeRabbit inference engine (.cursorrules)

Follow consistent file naming (kebab-case)

Files:

  • src/utils/v2/jwt.ts
  • src/index.ts
**/index.ts

📄 CodeRabbit inference engine (.cursorrules)

Use index.ts files for exports

Files:

  • src/index.ts
🧠 Learnings (1)
📚 Learning: 2025-08-05T14:39:39.983Z
Learnt from: CR
PR: ephemeraHQ/convos-backend#0
File: .cursorrules:0-0
Timestamp: 2025-08-05T14:39:39.983Z
Learning: Applies to **/{middleware,api,routes}/**/*.{ts,tsx} : Implement rate limiting

Applied to files:

  • src/index.ts
🧬 Code graph analysis (2)
src/utils/v2/jwt.ts (3)
src/config.ts (3)
  • JWT_PRIVATE_KEY (69-69)
  • JWT_PUBLIC_KEY (70-70)
  • JWT_ISSUER (72-72)
src/utils/try-catch.ts (1)
  • tryCatch (14-23)
src/utils/errors.ts (1)
  • AppError (1-11)
src/index.ts (2)
src/utils/v2/jwt.ts (1)
  • validateJWTKeys (31-53)
src/config.ts (1)
  • IS_DEVELOPMENT (76-76)
⏰ 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: claude-review
  • GitHub Check: Review for correctness

Copy link
Contributor

@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

♻️ Duplicate comments (3)
src/config.ts (3)

9-15: Replace plain Error with AppError for consistent error handling.

These lines throw plain Error instances, which violates the coding guideline requiring the AppError class for application errors. As previously noted, all configuration validation errors should use AppError.

Apply this diff:

+import { AppError } from "./utils/errors";
+
 // Validate required environment variables
 if (!process.env.XMTP_NOTIFICATION_SECRET) {
-  throw new Error("XMTP_NOTIFICATION_SECRET is not configured");
+  throw new AppError(500, "XMTP_NOTIFICATION_SECRET is not configured");
 }

 if (!process.env.NOTIFICATION_SERVER_URL) {
-  throw new Error("NOTIFICATION_SERVER_URL is not configured");
+  throw new AppError(500, "NOTIFICATION_SERVER_URL is not configured");
 }

Based on coding guidelines.


32-39: Replace plain Error with AppError for JWT private key validation.

Line 36 throws a plain Error instance, which violates the coding guideline. All configuration validation should use the AppError class.

Apply this diff:

 if (
   typeof JWT_PRIVATE_KEY_RAW !== "string" ||
   JWT_PRIVATE_KEY_RAW.trim().length === 0
 ) {
-  throw new Error(
+  throw new AppError(
+    500,
     "Missing `JWT_PRIVATE_KEY`: set a non-empty PEM-encoded ECDSA private key in environment before starting the app",
   );
 }

Based on coding guidelines.


42-49: Replace plain Error with AppError for JWT public key validation.

Line 46 throws a plain Error instance, which violates the coding guideline requiring consistent error handling via the AppError class.

Apply this diff:

 if (
   typeof JWT_PUBLIC_KEY_RAW !== "string" ||
   JWT_PUBLIC_KEY_RAW.trim().length === 0
 ) {
-  throw new Error(
+  throw new AppError(
+    500,
     "Missing `JWT_PUBLIC_KEY`: set a non-empty PEM-encoded ECDSA public key in environment before starting the app",
   );
 }

Based on coding guidelines.

🧹 Nitpick comments (1)
src/config.ts (1)

32-49: Consider extracting validation logic to reduce duplication.

The validation logic for JWT_PRIVATE_KEY and JWT_PUBLIC_KEY is nearly identical. While this duplication is minor at module load time, extracting it to a helper could improve maintainability.

Example helper:

function validatePEMKey(key: string | undefined, keyName: string): string {
  if (typeof key !== "string" || key.trim().length === 0) {
    throw new AppError(
      500,
      `Missing \`${keyName}\`: set a non-empty PEM-encoded ECDSA ${keyName.toLowerCase().includes('private') ? 'private' : 'public'} key in environment before starting the app`,
    );
  }
  return key;
}

// Usage:
export const JWT_PRIVATE_KEY = validatePEMKey(process.env.JWT_PRIVATE_KEY, "JWT_PRIVATE_KEY");
export const JWT_PUBLIC_KEY = validatePEMKey(process.env.JWT_PUBLIC_KEY, "JWT_PUBLIC_KEY");
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 085eb79 and 25d273e.

📒 Files selected for processing (1)
  • src/config.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursorrules)

**/*.{ts,tsx}: TypeScript with strict mode and ESNext features
Implement consistent error handling using AppError class
Use object parameter syntax for function arguments: '(args: { paramOne: string }) => {}' instead of '(paramOne: string) => {}' for better readability and extensibility
Keep functions focused and avoid optional parameters. If a function has many optional parameters, consider splitting it into multiple specialized functions for clearer intent and better maintainability
Prefer type inference from Prisma models
Keep types close to their usage (same directory)
Use type exports for shared types
Leverage path aliases (@/) for imports
Don't specify return type on functions. Prefer inferring the value.
Use transactions for multi-table operations
Include necessary relations in queries using include
Use custom AppError class for application errors
Log errors with context using logError utility
Log security-relevant events
Implement caching where needed
Optimize database queries
Use appropriate batch operations
Add JSDoc comments for public APIs

Files:

  • src/config.ts
**/*

📄 CodeRabbit inference engine (.cursorrules)

Follow consistent file naming (kebab-case)

Files:

  • src/config.ts
🧠 Learnings (1)
📚 Learning: 2025-09-04T14:37:41.542Z
Learnt from: nmalzieu
PR: ephemeraHQ/convos-backend#133
File: src/api/v1/devices/handlers/get-device.handler.ts:15-16
Timestamp: 2025-09-04T14:37:41.542Z
Learning: For authenticated endpoints in the convos-backend Express.js application, res.locals.xmtpId is guaranteed to be present when handlers execute because the auth middleware validates JWT tokens and returns 401 before reaching handlers if authentication fails. Additional guards for res.locals.xmtpId are not needed in authenticated route handlers.

Applied to files:

  • src/config.ts
🪛 GitHub Check: Typecheck, format check, lint
src/config.ts

[failure] 36-36: error: Missing JWT_PRIVATE_KEY: set a non-empty PEM-encoded ECDSA private key in environment before starting the app

  at /home/runner/work/convos-backend/convos-backend/src/config.ts:36:9
⏰ 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: push_to_registry
  • GitHub Check: claude-review
🔇 Additional comments (3)
src/config.ts (3)

20-25: Good backward compatibility approach.

The optional v1 JWT handling is well-structured. Making JWT_SECRET optional while conditionally deriving JWT_SECRET_BYTES properly maintains backward compatibility for any existing v1 endpoints without forcing migration.


51-55: Clean export pattern after validation.

The type assertions to string are safe here since the validation above ensures these values are non-empty strings. The hardcoded issuer is appropriate for this stage.


27-49: JWT key validation configuration is properly set up—no action required.

Verification confirms both requirements are met:

  • .env.example includes JWT_PRIVATE_KEY and JWT_PUBLIC_KEY placeholders
  • validateJWTKeys() is imported and invoked at startup (src/index.ts, line 60)

The two-layer validation approach is correct: basic non-empty string checks in src/config.ts, with deeper PEM format validation performed at application startup via validateJWTKeys().

export const validateJWTKeys = async () => {
try {
// Validate and cache private key
await loadPrivateKey();
Copy link

Choose a reason for hiding this comment

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

validateJWTKeys never checks that the public key matches the private key. This can pass startup yet cause every verifyV2JwtToken to fail (401). Consider signing a throwaway JWT with the private key and verifying it with the public key during startup to fail fast on mismatched pairs.

-    await loadPrivateKey();
+    const privateKey = await loadPrivateKey();
@@
-    await loadPublicKey();
+    const publicKey = await loadPublicKey();
+  try {
+    const testJwt = await new jose.SignJWT({ k: "chk" })
+      .setProtectedHeader({ alg: "ES256" })
+      .setIssuer(JWT_ISSUER)
+      .setIssuedAt()
+      .setExpirationTime("1m")
+      .sign(privateKey);
+    await jose.jwtVerify(testJwt, publicKey, { issuer: JWT_ISSUER, algorithms: ["ES256"] });
+    logger.info("JWT key pair match validation successful");
+  } catch (error) {
+    logger.error({ error }, "JWT key pair validation failed (mismatch)");
+    throw new Error("JWT_PRIVATE_KEY and JWT_PUBLIC_KEY do not form a valid pair");
+  }

🚀 Reply to ask Macroscope to explain or update this suggestion.

👍 Helpful? React to give us feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants