Skip to content

Conversation

@TejaBudumuru3
Copy link
Contributor

@TejaBudumuru3 TejaBudumuru3 commented Jan 4, 2026

…ling

  • Added TokenScheduler to manage periodic token refreshes.
  • Introduced tokenRefreshService for handling token refresh logic.
  • Enhanced Google OAuth callback with detailed logging and error handling.
  • Implemented update functionality for nodes and triggers in user routes.
  • Updated workflow slice to accommodate new ID structures.
  • Improved error handling and logging across various components.
  • Refactored Google Sheets service to conditionally include refresh tokens.
  • Added debug endpoints for credential management and token status.

Summary by CodeRabbit

  • New Features

    • Automatic token refresh scheduler that periodically validates and refreshes expiring OAuth credentials
    • New endpoints for updating node and trigger configurations
    • Enhanced workflow editor to support updating Google Sheet configurations
  • Improvements

    • Improved token field handling to prevent overwriting valid credentials
    • Better restoration of previously selected documents and sheets in workflows
    • Enhanced workflow data synchronization and loading

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

…ling

- Added TokenScheduler to manage periodic token refreshes.
- Introduced tokenRefreshService for handling token refresh logic.
- Enhanced Google OAuth callback with detailed logging and error handling.
- Implemented update functionality for nodes and triggers in user routes.
- Updated workflow slice to accommodate new ID structures.
- Improved error handling and logging across various components.
- Refactored Google Sheets service to conditionally include refresh tokens.
- Added debug endpoints for credential management and token status.
@TejaBudumuru3 TejaBudumuru3 requested a review from Vamsi-o as a code owner January 4, 2026 16:33
@coderabbitai
Copy link

coderabbitai bot commented Jan 4, 2026

📝 Walkthrough

Walkthrough

This PR introduces automated token refresh infrastructure for Google OAuth credentials. It adds a periodic scheduler and refresh service to detect and refresh expiring tokens, creates backend endpoints for updating node and trigger configurations, and refactors frontend workflow components to support configuration updates with revised ID formats.

Changes

Cohort / File(s) Summary
Backend Token Refresh Infrastructure
apps/http-backend/src/scheduler/token-scheduler.ts, apps/http-backend/src/services/token-refresh.service.ts
New TokenScheduler class runs periodic refresh checks (default 60-min interval). TokenRefreshService detects expiring tokens (10-min buffer), refreshes via GoogleSheetsService, handles invalid_grant errors by deleting credentials and clearing references from triggers/nodes, and aggregates refresh metrics.
Backend API Integration
apps/http-backend/src/index.ts, apps/http-backend/src/routes/google_callback.ts
Index now imports and starts tokenScheduler on server startup. Google callback route adds verbose debug logging, persists tokens to database, and exposes GET /debug/credentials endpoint to query and return stored OAuth credentials.
Backend Update Endpoints
apps/http-backend/src/routes/userRoutes/userRoutes.ts
Added PUT /update/node and PUT /update/trigger endpoints, each validating input via NodeUpdateSchema and TriggerUpdateSchema, requiring authentication, and returning 201 on success.
Backend Service Enhancements
packages/nodes/src/google-oauth-service.ts, packages/nodes/src/google-sheets/google-sheets.service.ts
OAuth service now filters token fields before updating to avoid overwriting valid tokens with falsy values. Google Sheets service conditionally includes refresh_token only when returned by Google. Minor typo fix in error message.
Frontend Workflow Components
apps/web/app/components/nodes/CreateWorkFlow.tsx, apps/web/app/components/nodes/GoogleSheetFormClient.tsx, apps/web/app/components/nodes/actions.ts
Node ID formats changed from trigger-{dbId}/action-{dbId}-{index} to trigger~{id}/action~{id}~{index}. GoogleSheetFormClient now supports update flow via new handleUpdateConfig action, accepts avlNode prop, and updates button label based on operation mode. Enhanced state restoration for credentials and sheets.
Frontend Config & State Management
apps/web/app/workflow/lib/config.ts, apps/web/store/slices/workflowSlice.ts
Added updateTrigger and updateNode functions performing PUT requests to backend. Trigger and NodeItem interfaces replaced dbId with id and added AvailableTriggerID / AvailableNodeID fields. Workflow creation now uses date.toString() instead of date.getTime().
Frontend UI & Sidebar
apps/web/app/components/ui/app-sidebar.tsx
Added workflow data fetching and Redux synchronization. Introduced getWorkflowData to populate nodes and trigger state, and getWorkflows for fetching all workflows. Enhanced initialization with conditional workflow creation and post-creation refresh.
Shared Exports & Types
packages/common/src/index.ts, packages/nodes/src/index.ts
Common exports new NodeUpdateSchema and TriggerUpdateSchema (Zod objects for validation). Added FOUND (302) status code to enum. Nodes package now exports GoogleSheetsService.

Sequence Diagram(s)

sequenceDiagram
    participant Scheduler as TokenScheduler
    participant Service as TokenRefreshService
    participant Sheets as GoogleSheetsService
    participant OAuth as GoogleOAuthService
    participant DB as Database
    
    rect rgb(200, 220, 255)
        Note over Scheduler,DB: Periodic Refresh Flow (every 60 minutes)
    end
    
    Scheduler->>Service: refreshAllExpiringTokens()
    activate Service
    
    Service->>DB: fetch all google_oauth credentials
    DB-->>Service: credentials list
    
    loop For each expiring credential
        rect rgb(220, 240, 220)
            Note over Service,DB: Token Refresh for Single Credential
        end
        
        Service->>Sheets: refreshAccessToken(currentTokens)
        activate Sheets
        Sheets-->>Sheets: call Google API
        Sheets-->>Service: { access_token, refresh_token?, expiry_date }
        deactivate Sheets
        
        Service->>OAuth: updateCredentials(credId, newTokens)
        activate OAuth
        OAuth->>OAuth: filter truthy token fields
        OAuth->>DB: update credential record
        DB-->>OAuth: updated
        OAuth-->>Service: success
        deactivate OAuth
    end
    
    rect rgb(255, 200, 200)
        Note over Service,DB: Error Handling: invalid_grant
    end
    
    alt token refresh fails with invalid_grant
        Service->>OAuth: credential is invalid
        OAuth->>DB: delete credential
        OAuth->>DB: clear credId from triggers & nodes
        DB-->>OAuth: references cleared
        Service-->>Service: log deletion
    else other error
        Service-->>Service: log error, continue
    end
    
    Service-->>Scheduler: { total, refreshed, failed, deleted }
    deactivate Service
    Scheduler->>Scheduler: log results
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • Vamsi-o

Poem

🐰 Tokens refresh on schedule's beat,
No expiry shall we greet,
Invalid grants we swiftly purge,
While workers update, nodes emerge—
A hop, a skip, credentials stay fresh!

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 title accurately describes the main changes: implementing a token refresh scheduler and enhancing Google OAuth handling, which aligns with the core functionality additions across multiple files.
✨ 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.

@coderabbitai
Copy link

coderabbitai bot commented Jan 4, 2026

Note

Docstrings generation - SUCCESS
Generated docstrings for this pull request at #49

coderabbitai bot added a commit that referenced this pull request Jan 4, 2026
Docstrings generation was requested by @TejaBudumuru3.

* #48 (comment)

The following files were modified:

* `apps/http-backend/src/index.ts`
* `apps/web/app/components/nodes/CreateWorkFlow.tsx`
* `apps/web/app/components/nodes/GoogleSheetFormClient.tsx`
* `apps/web/app/components/nodes/actions.ts`
* `apps/web/app/components/ui/app-sidebar.tsx`
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: 13

Caution

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

⚠️ Outside diff range comments (1)
apps/web/app/components/nodes/CreateWorkFlow.tsx (1)

155-223: Remove workflowId and dispatch from the dependency array.

The second useEffect rebuilds nodes/edges from Redux state (existingNodes, existingTrigger). Neither workflowId nor dispatch are used in this effect's body. Additionally, the first effect already fetches workflow data when workflowId changes and updates Redux state, so the second effect will re-run indirectly through its actual dependencies. Including workflowId directly causes unnecessary re-renders. Remove both from [existingNodes, existingTrigger, dispatch, workflowId] to [existingNodes, existingTrigger].

🧹 Nitpick comments (8)
packages/common/src/index.ts (1)

37-45: Consider stricter typing for Config fields.

Both schemas use z.any() for the Config field, which bypasses validation. If the config structure is known or partially known, consider using z.record(z.unknown()) or a more specific schema for better type safety and runtime validation.

💡 Example alternative
export const NodeUpdateSchema = z.object({
  NodeId: z.string(),
  Config: z.record(z.unknown()) // or z.object({ ... }) if structure is known
})

export const TriggerUpdateSchema = z.object({
  TriggerId: z.string(),
  Config: z.record(z.unknown())
})
apps/web/app/components/nodes/CreateWorkFlow.tsx (1)

50-51: Remove debug console logs before merging.

These console logs appear to be for debugging purposes and should be removed or wrapped in a development-only conditional before merging to production.

apps/web/app/components/ui/app-sidebar.tsx (2)

32-32: Unused import: workflowReducer.

workflowReducer is imported but not used in this file.

🔎 Proposed fix
-import { workflowActions, workflowReducer } from '@/store/slices/workflowSlice'
+import { workflowActions } from '@/store/slices/workflowSlice'

41-41: Remove debug console log.

This console log should be removed before merging.

apps/http-backend/src/scheduler/token-scheduler.ts (1)

55-61: triggerManualRefresh propagates errors from runRefreshJob.

While runRefreshJob catches errors internally, triggerManualRefresh calls it with await and could still throw if something unexpected happens before the try-catch. Consider wrapping it or documenting that callers should handle errors.

🔎 Proposed fix
     async triggerManualRefresh(): Promise<void> {
         console.log('\n🔧 Manual token refresh triggered...');
-        await this.runRefreshJob();
+        try {
+            await this.runRefreshJob();
+        } catch (error) {
+            console.error('❌ Manual token refresh failed:', error);
+            throw error;
+        }
     }
apps/web/app/components/nodes/GoogleSheetFormClient.tsx (1)

20-20: Unused prop: avlNode is declared but never used.

The avlNode prop is added to the interface and component signature but is not referenced anywhere in the component body.

If this is for future use, consider removing it until needed to avoid confusion. If it's intended to be used, the implementation appears incomplete.

apps/http-backend/src/services/token-refresh.service.ts (2)

4-10: Consider importing OAuthTokens from @repo/nodes instead of duplicating.

Per the relevant snippets, OAuthTokens is already exported from @repo/nodes. Duplicating the interface can lead to drift if the original is updated.

🔎 Proposed fix
 import { prismaClient } from "@repo/db/client";
-import { GoogleSheetsService, GoogleOAuthService } from "@repo/nodes";
-
-interface OAuthTokens {
-    access_token: string;
-    refresh_token: string;
-    token_type: string;
-    expiry_date: number;
-    scope?: string;
-}
+import { GoogleSheetsService, GoogleOAuthService, OAuthTokens } from "@repo/nodes";

168-174: Fragile string matching for detecting deleted credentials.

The check result.error?.includes('deleted') relies on the specific wording of the error message set at line 123. If that message changes, this logic breaks silently.

Consider using a structured result type instead:

🔎 Proposed fix
 interface RefreshResult {
     credentialId: string;
     success: boolean;
     error?: string;
+    deleted?: boolean;
 }

 // In refreshToken, when returning after deletion:
 return { 
     credentialId, 
     success: false, 
+    deleted: true,
     error: 'Credential deleted. Please reconnect your Google account.' 
 };

 // In refreshAllExpiringTokens:
-} else if (result.error?.includes('deleted')) {
+} else if (result.deleted) {
     deleted++;
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2372ee0 and 6f36b4d.

📒 Files selected for processing (15)
  • apps/http-backend/src/index.ts
  • apps/http-backend/src/routes/google_callback.ts
  • apps/http-backend/src/routes/userRoutes/userRoutes.ts
  • apps/http-backend/src/scheduler/token-scheduler.ts
  • apps/http-backend/src/services/token-refresh.service.ts
  • apps/web/app/components/nodes/CreateWorkFlow.tsx
  • apps/web/app/components/nodes/GoogleSheetFormClient.tsx
  • apps/web/app/components/nodes/actions.ts
  • apps/web/app/components/ui/app-sidebar.tsx
  • apps/web/app/workflow/lib/config.ts
  • apps/web/store/slices/workflowSlice.ts
  • packages/common/src/index.ts
  • packages/nodes/src/common/google-oauth-service.ts
  • packages/nodes/src/google-sheets/google-sheets.service.ts
  • packages/nodes/src/index.ts
🧰 Additional context used
🧬 Code graph analysis (8)
apps/http-backend/src/routes/userRoutes/userRoutes.ts (4)
apps/http-backend/src/routes/userRoutes/userMiddleware.ts (2)
  • userMiddleware (18-42)
  • AuthRequest (8-10)
packages/common/src/index.ts (2)
  • NodeUpdateSchema (37-40)
  • TriggerUpdateSchema (42-45)
apps/web/app/workflow/lib/config.ts (1)
  • updateNode (183-208)
packages/db/src/index.ts (1)
  • prismaClient (17-18)
packages/nodes/src/common/google-oauth-service.ts (1)
packages/nodes/src/index.ts (1)
  • OAuthTokens (7-7)
apps/http-backend/src/index.ts (1)
apps/http-backend/src/scheduler/token-scheduler.ts (1)
  • tokenScheduler (65-65)
apps/http-backend/src/services/token-refresh.service.ts (4)
packages/nodes/src/common/google-oauth-service.ts (2)
  • OAuthTokens (161-161)
  • GoogleOAuthService (160-160)
packages/nodes/src/index.ts (3)
  • OAuthTokens (7-7)
  • GoogleOAuthService (9-9)
  • GoogleSheetsService (11-11)
packages/nodes/src/google-sheets/google-sheets.service.ts (1)
  • GoogleSheetsService (138-138)
packages/db/src/index.ts (1)
  • prismaClient (17-18)
apps/web/app/workflow/lib/config.ts (1)
packages/common/src/index.ts (1)
  • BACKEND_URL (5-5)
apps/web/app/components/ui/app-sidebar.tsx (3)
apps/web/app/hooks/redux.ts (1)
  • useAppDispatch (6-6)
apps/web/app/workflow/lib/config.ts (3)
  • getAllWorkflows (64-76)
  • getAllCredentials (48-61)
  • getworkflowData (270-286)
apps/web/store/slices/workflowSlice.ts (1)
  • workflowActions (61-61)
apps/web/app/components/nodes/actions.ts (1)
apps/web/app/workflow/lib/config.ts (2)
  • updateTrigger (157-181)
  • updateNode (183-208)
apps/web/app/components/nodes/GoogleSheetFormClient.tsx (5)
apps/web/app/hooks/redux.ts (1)
  • useAppSelector (7-7)
apps/web/app/hooks/useCredential.ts (1)
  • useCredentials (5-34)
packages/common/src/index.ts (1)
  • BACKEND_URL (5-5)
apps/web/app/components/nodes/actions.ts (2)
  • handleUpdateConfig (76-106)
  • handleSaveConfig (29-74)
apps/web/store/slices/workflowSlice.ts (1)
  • workflowActions (61-61)
🔇 Additional comments (11)
packages/nodes/src/common/google-oauth-service.ts (1)

151-151: LGTM: Helpful debug logging.

The success log aids in tracing credential updates during token refresh operations.

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

11-11: LGTM: Exposes GoogleSheetsService for token refresh integration.

The new export enables the token refresh service (apps/http-backend/src/services/token-refresh.service.ts) to access GoogleSheetsService methods for refreshing credentials.

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

58-58: LGTM: Valid HTTP status code addition.

HTTP 302 FOUND is correctly added for redirect scenarios.

packages/nodes/src/google-sheets/google-sheets.service.ts (1)

133-133: LGTM: Typo correction.

Fixed "Falied" → "Failed" in error message.

apps/web/app/components/nodes/actions.ts (1)

76-106: LGTM! Clean implementation of the update flow.

The handleUpdateConfig function follows the same pattern as handleSaveConfig, maintains good code consistency, and properly delegates to the appropriate update function based on type.

apps/web/app/workflow/lib/config.ts (1)

183-208: LGTM! Correct implementation.

The updateNode function correctly implements the update operation with proper error handling and accurate logging.

apps/web/store/slices/workflowSlice.ts (2)

3-9: All references to dbId have been successfully updated.

The field rename from dbId to id in the Trigger interface is complete. No remaining references to dbId were found in the codebase, and all code consuming this interface is using the new id field.


11-18: No action required. All references to node.dbId have been successfully updated. The field rename from dbId to id in the NodeItem interface is complete, with no remaining references to the old field name found in the codebase.

apps/http-backend/src/scheduler/token-scheduler.ts (1)

14-27: LGTM on the core scheduler logic.

The scheduler implementation is straightforward and follows a reasonable pattern: immediate execution on start, then periodic runs via setInterval. The interval conversion and cleanup are handled correctly.

apps/web/app/components/nodes/GoogleSheetFormClient.tsx (1)

157-188: Nested fetch logic is complex but handles restoration well.

The restoration logic for preserving previously selected document and sheet when credential changes is thorough. However, consider extracting the nested fetch into a separate function for readability.

apps/http-backend/src/services/token-refresh.service.ts (1)

36-58: Token refresh logic is well-structured.

The refresh flow correctly:

  • Uses existing services (GoogleSheetsService, GoogleOAuthService)
  • Falls back to the old refresh token if a new one isn't provided (line 52)
  • Properly updates all token fields in the credential store

async function startServer() {
await NodeRegistry.registerAll()

tokenScheduler.start();
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add error handling and graceful shutdown for the token scheduler.

The scheduler starts without error handling, and there's no cleanup on server shutdown. If the scheduler fails to start, the server continues anyway, potentially leaving tokens unrefreshed. Additionally, without graceful shutdown, the scheduler may continue running after the server stops.

🔎 Recommended improvements
 async function startServer() {
   await NodeRegistry.registerAll()
-  tokenScheduler.start();
+  try {
+    tokenScheduler.start();
+    console.log('✅ Token scheduler started');
+  } catch (error) {
+    console.error('❌ Failed to start token scheduler:', error);
+    // Decide: should this be fatal? For now, log and continue
+  }
+  
   app.listen(PORT, () => {
     console.log(`Server running on port ${PORT}`);
   })
+  
+  // Graceful shutdown
+  process.on('SIGTERM', () => {
+    console.log('SIGTERM received, shutting down gracefully');
+    tokenScheduler.stop();
+    process.exit(0);
+  });
+  
+  process.on('SIGINT', () => {
+    console.log('SIGINT received, shutting down gracefully');
+    tokenScheduler.stop();
+    process.exit(0);
+  });
 }
📝 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
tokenScheduler.start();
async function startServer() {
await NodeRegistry.registerAll()
try {
tokenScheduler.start();
console.log('✅ Token scheduler started');
} catch (error) {
console.error('❌ Failed to start token scheduler:', error);
// Decide: should this be fatal? For now, log and continue
}
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
})
// Graceful shutdown
process.on('SIGTERM', () => {
console.log('SIGTERM received, shutting down gracefully');
tokenScheduler.stop();
process.exit(0);
});
process.on('SIGINT', () => {
console.log('SIGINT received, shutting down gracefully');
tokenScheduler.stop();
process.exit(0);
});
}
🤖 Prompt for AI Agents
In apps/http-backend/src/index.ts around line 44, tokenScheduler.start() is
invoked with no error handling or shutdown cleanup; wrap the start call in a
try/catch (await if start is async) and log the error and stop server or exit if
it fails to start, and register graceful shutdown handlers (process
SIGINT/SIGTERM and server close) that call tokenScheduler.stop() (or
cancel/cleanup) to ensure the scheduler is stopped before exit; finally ensure
start/stop calls are idempotent and errors during shutdown are caught and
logged.

Comment on lines +26 to +37
// DEBUG: Log tokens received from Google
console.log('\n🔐 Google OAuth Callback - Tokens received:');
console.log(' access_token:', tokens.access_token ? '✅ Present' : '❌ Missing');
console.log(' refresh_token:', tokens.refresh_token ? '✅ Present' : '❌ Missing');
console.log(' expiry_date:', tokens.expiry_date);
console.log(' token_type:', tokens.token_type);
console.log(' scope:', tokens.scope);

if (!tokens.refresh_token) {
console.warn('⚠️ WARNING: No refresh_token received! User may have already authorized this app.');
console.warn(' To force new refresh_token, user needs to revoke access at: https://myaccount.google.com/permissions');
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Remove or sanitize debug logging in production.

The debug logging exposes detailed OAuth token information to console logs. In production environments, this could leak sensitive data to log aggregation systems.

Consider:

  • Using a debug flag to conditionally enable verbose logging
  • Sanitizing logs to avoid exposing token presence/length details
  • Moving these logs behind a proper debugging configuration
🔎 Suggested approach with environment-based logging
+        const isDebug = process.env.NODE_ENV === 'development';
+        
-        // DEBUG: Log tokens received from Google
-        console.log('\n🔐 Google OAuth Callback - Tokens received:');
-        console.log('   access_token:', tokens.access_token ? '✅ Present' : '❌ Missing');
-        console.log('   refresh_token:', tokens.refresh_token ? '✅ Present' : '❌ Missing');
-        console.log('   expiry_date:', tokens.expiry_date);
-        console.log('   token_type:', tokens.token_type);
-        console.log('   scope:', tokens.scope);
+        if (isDebug) {
+            console.log('\n🔐 Google OAuth Callback - Tokens received:');
+            console.log('   access_token:', tokens.access_token ? '✅ Present' : '❌ Missing');
+            console.log('   refresh_token:', tokens.refresh_token ? '✅ Present' : '❌ Missing');
+            console.log('   expiry_date:', tokens.expiry_date);
+        }
         
         if (!tokens.refresh_token) {
-            console.warn('⚠️  WARNING: No refresh_token received! User may have already authorized this app.');
-            console.warn('   To force new refresh_token, user needs to revoke access at: https://myaccount.google.com/permissions');
+            if (isDebug) {
+                console.warn('⚠️  WARNING: No refresh_token received!');
+            }
         }
📝 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
// DEBUG: Log tokens received from Google
console.log('\n🔐 Google OAuth Callback - Tokens received:');
console.log(' access_token:', tokens.access_token ? '✅ Present' : '❌ Missing');
console.log(' refresh_token:', tokens.refresh_token ? '✅ Present' : '❌ Missing');
console.log(' expiry_date:', tokens.expiry_date);
console.log(' token_type:', tokens.token_type);
console.log(' scope:', tokens.scope);
if (!tokens.refresh_token) {
console.warn('⚠️ WARNING: No refresh_token received! User may have already authorized this app.');
console.warn(' To force new refresh_token, user needs to revoke access at: https://myaccount.google.com/permissions');
}
const isDebug = process.env.NODE_ENV === 'development';
if (isDebug) {
console.log('\n🔐 Google OAuth Callback - Tokens received:');
console.log(' access_token:', tokens.access_token ? '✅ Present' : '❌ Missing');
console.log(' refresh_token:', tokens.refresh_token ? '✅ Present' : '❌ Missing');
console.log(' expiry_date:', tokens.expiry_date);
}
if (!tokens.refresh_token) {
if (isDebug) {
console.warn('⚠️ WARNING: No refresh_token received!');
}
}
🤖 Prompt for AI Agents
In apps/http-backend/src/routes/google_callback.ts around lines 26 to 37, remove
or guard the DEBUG console.log/console.warn calls that print OAuth token
details; instead wrap verbose token logs behind a configurable debug flag (e.g.,
process.env.DEBUG_OAUTH === 'true') or the app logger's debug level, and if you
must log, only emit non-sensitive, sanitized info (e.g., token existence as
boolean or masked/length-only indicators) while never printing raw tokens,
refresh_token, or expiry values to production logs. Ensure the warning about
missing refresh_token remains but is also routed through the logger at an
appropriate level and sanitized, and update any related tests/config to set the
debug flag during local dev only.

Comment on lines +56 to +93
// Debug endpoint to check stored credentials
googleAuth.get('/debug/credentials', async(req: Request, res: Response)=>{
try {
const { prismaClient } = await import('@repo/db/client');

const credentials = await prismaClient.credential.findMany({
where: { type: 'google_oauth' },
select: {
id: true,
userId: true,
type: true,
config: true
}
});

const debugInfo = credentials.map(cred => {
const config = cred.config as any;
return {
id: cred.id,
userId: cred.userId,
hasAccessToken: !!config?.access_token,
hasRefreshToken: !!config?.refresh_token,
refreshTokenLength: config?.refresh_token?.length || 0,
expiryDate: config?.expiry_date,
expiresIn: config?.expiry_date ? Math.round((config.expiry_date - Date.now()) / 1000 / 60) + ' minutes' : 'N/A',
isInvalid: config?.invalid || false,
scope: config?.scope
};
});

console.log('\n📋 Stored Credentials Debug:');
console.table(debugInfo);

return res.json({ credentials: debugInfo });
} catch (err) {
return res.status(500).json({ error: err instanceof Error ? err.message : 'Unknown error' });
}
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

CRITICAL: Debug endpoint exposes credentials without authentication.

The /debug/credentials endpoint returns sensitive OAuth credential information without any authentication middleware. This allows anyone to access stored credentials including token metadata and expiry information.

🔎 Required fix: Add authentication and restrict access
-// Debug endpoint to check stored credentials
-googleAuth.get('/debug/credentials', async(req: Request, res: Response)=>{
+// Debug endpoint to check stored credentials (protected)
+googleAuth.get('/debug/credentials', userMiddleware, async(req: AuthRequest, res: Response)=>{
     try {
+        // Only allow in development or for admin users
+        if (process.env.NODE_ENV === 'production') {
+            return res.status(403).json({ error: 'Debug endpoints disabled in production' });
+        }
+        
+        if (!req.user) {
+            return res.status(401).json({ error: 'Authentication required' });
+        }
+        
         const { prismaClient } = await import('@repo/db/client');
         
         const credentials = await prismaClient.credential.findMany({
-            where: { type: 'google_oauth' },
+            where: { 
+                type: 'google_oauth',
+                userId: req.user.id  // Only show user's own credentials
+            },
             select: {
                 id: true,
                 userId: true,

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apps/http-backend/src/routes/google_callback.ts around lines 56 to 93, the
/debug/credentials endpoint exposes sensitive OAuth credential data without any
authentication; secure it by removing or disabling the public debug route and/or
wrapping it with the application’s auth middleware, verifying the requester is
authenticated and has an admin or ops role before continuing, and ensure
returned data omits secrets (no access_token, refresh_token, or full config) —
only return non-sensitive metadata (e.g., id, userId, hasTokens true/false,
expiryDate) for authorized users; if you keep the endpoint, log full details
only server-side and never send tokens to clients.

Comment on lines +477 to +512
router.put('/update/node', userMiddleware, async(req: AuthRequest, res: Response)=>{
try{
if(!req.user){
return res.status(statusCodes.BAD_REQUEST).json({
message: "User is not logged in ",
});
}
const data = req.body;
const dataSafe = NodeUpdateSchema.safeParse(data)

if(!dataSafe.success) {
return res.status(statusCodes.BAD_REQUEST).json({
message: "Invalid input"
})
}

const updateNode = await prismaClient.node.update({
where: {id: dataSafe.data.NodeId},
data:{
config: dataSafe.data.Config
}
})

if(updateNode)
return res.status(statusCodes.CREATED).json({
message: "Node updated",
data: updateNode
})

}catch(e){
console.log("This is the error from Node updating", e);
return res.status(statusCodes.INTERNAL_SERVER_ERROR).json({
message: "Internal server Error from Node Updation.",
});
}
})
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Use HTTP 200 instead of 201 for update operations.

The endpoint returns 201 CREATED on successful update (line 501), but 201 is semantically for resource creation. For update operations, 200 OK is the correct status code.

🔎 Proposed fix
     if(updateNode)
-      return res.status(statusCodes.CREATED).json({
+      return res.status(statusCodes.OK).json({
         message: "Node updated",
         data: updateNode
       })
📝 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
router.put('/update/node', userMiddleware, async(req: AuthRequest, res: Response)=>{
try{
if(!req.user){
return res.status(statusCodes.BAD_REQUEST).json({
message: "User is not logged in ",
});
}
const data = req.body;
const dataSafe = NodeUpdateSchema.safeParse(data)
if(!dataSafe.success) {
return res.status(statusCodes.BAD_REQUEST).json({
message: "Invalid input"
})
}
const updateNode = await prismaClient.node.update({
where: {id: dataSafe.data.NodeId},
data:{
config: dataSafe.data.Config
}
})
if(updateNode)
return res.status(statusCodes.CREATED).json({
message: "Node updated",
data: updateNode
})
}catch(e){
console.log("This is the error from Node updating", e);
return res.status(statusCodes.INTERNAL_SERVER_ERROR).json({
message: "Internal server Error from Node Updation.",
});
}
})
router.put('/update/node', userMiddleware, async(req: AuthRequest, res: Response)=>{
try{
if(!req.user){
return res.status(statusCodes.BAD_REQUEST).json({
message: "User is not logged in ",
});
}
const data = req.body;
const dataSafe = NodeUpdateSchema.safeParse(data)
if(!dataSafe.success) {
return res.status(statusCodes.BAD_REQUEST).json({
message: "Invalid input"
})
}
const updateNode = await prismaClient.node.update({
where: {id: dataSafe.data.NodeId},
data:{
config: dataSafe.data.Config
}
})
if(updateNode)
return res.status(statusCodes.OK).json({
message: "Node updated",
data: updateNode
})
}catch(e){
console.log("This is the error from Node updating", e);
return res.status(statusCodes.INTERNAL_SERVER_ERROR).json({
message: "Internal server Error from Node Updation.",
});
}
})
🤖 Prompt for AI Agents
In apps/http-backend/src/routes/userRoutes/userRoutes.ts around lines 477 to
512, the handler for PUT /update/node currently returns statusCodes.CREATED
(201) on successful update; change this to statusCodes.OK (200) to correctly
indicate an update operation; update the response line to use statusCodes.OK
while keeping the response body the same.

Comment on lines +514 to +547
router.put('/update/trigger', userMiddleware, async(req: AuthRequest, res: Response)=>{
try{
if(!req.user){
return res.status(statusCodes.BAD_REQUEST).json({
message: "User is not logged in ",
});
}
const data = req.body;
const dataSafe = TriggerUpdateSchema.safeParse(data)

if(!dataSafe.success)
return res.status(statusCodes.BAD_REQUEST).json({
message: "Invalid input"
})

const updatedTrigger = await prismaClient.trigger.update({
where:{id: dataSafe.data.TriggerId},
data:{
config: dataSafe.data.Config
}
})

if(updatedTrigger)
return res.status(statusCodes.CREATED).json({
message: "Trigger updated",
data: updatedTrigger
})
}catch(e){
console.log("This is the error from Trigger updating", e);
return res.status(statusCodes.INTERNAL_SERVER_ERROR).json({
message: "Internal server Error from Trigger Updation",
});
}
})
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Use HTTP 200 instead of 201 for update operations.

Similar to the node update endpoint, this trigger update endpoint returns 201 CREATED (line 537) but should return 200 OK for successful updates.

🔎 Proposed fix
     if(updatedTrigger)
-      return res.status(statusCodes.CREATED).json({
+      return res.status(statusCodes.OK).json({
         message: "Trigger updated",
         data: updatedTrigger
       })
📝 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
router.put('/update/trigger', userMiddleware, async(req: AuthRequest, res: Response)=>{
try{
if(!req.user){
return res.status(statusCodes.BAD_REQUEST).json({
message: "User is not logged in ",
});
}
const data = req.body;
const dataSafe = TriggerUpdateSchema.safeParse(data)
if(!dataSafe.success)
return res.status(statusCodes.BAD_REQUEST).json({
message: "Invalid input"
})
const updatedTrigger = await prismaClient.trigger.update({
where:{id: dataSafe.data.TriggerId},
data:{
config: dataSafe.data.Config
}
})
if(updatedTrigger)
return res.status(statusCodes.CREATED).json({
message: "Trigger updated",
data: updatedTrigger
})
}catch(e){
console.log("This is the error from Trigger updating", e);
return res.status(statusCodes.INTERNAL_SERVER_ERROR).json({
message: "Internal server Error from Trigger Updation",
});
}
})
router.put('/update/trigger', userMiddleware, async(req: AuthRequest, res: Response)=>{
try{
if(!req.user){
return res.status(statusCodes.BAD_REQUEST).json({
message: "User is not logged in ",
});
}
const data = req.body;
const dataSafe = TriggerUpdateSchema.safeParse(data)
if(!dataSafe.success)
return res.status(statusCodes.BAD_REQUEST).json({
message: "Invalid input"
})
const updatedTrigger = await prismaClient.trigger.update({
where:{id: dataSafe.data.TriggerId},
data:{
config: dataSafe.data.Config
}
})
if(updatedTrigger)
return res.status(statusCodes.OK).json({
message: "Trigger updated",
data: updatedTrigger
})
}catch(e){
console.log("This is the error from Trigger updating", e);
return res.status(statusCodes.INTERNAL_SERVER_ERROR).json({
message: "Internal server Error from Trigger Updation",
});
}
})
🤖 Prompt for AI Agents
In apps/http-backend/src/routes/userRoutes/userRoutes.ts around lines 514 to
547, the trigger update handler returns HTTP 201 CREATED on success; change it
to return HTTP 200 OK instead. Update the response that currently sends
statusCodes.CREATED to use statusCodes.OK and keep the same message and payload;
ensure the rest of the logic and error handling remain unchanged.

Comment on lines 89 to +95
if(!creds) getCreds()
if(!workflow) getWorkflows()
},[selectedWorkflow])
if(!workflow) {
getWorkflows()
createNewWorkflow()
}
getWorkflowData()
},[flow.workflow_id, dispatch])
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Potential race condition and missing state guards.

When !workflow, both getWorkflows() and createNewWorkflow() are called simultaneously. createNewWorkflow() may call getWorkflows() again in its else-branch (line 128), leading to duplicate fetches. Additionally, getWorkflowData() runs on every effect trigger, even when flow.workflow_id hasn't changed meaningfully.

Consider:

  1. Using a ref or state flag to prevent duplicate initialization
  2. Separating concerns into distinct effects (credentials, workflows, workflow data)
  3. Adding cleanup logic for unmounted components
🔎 Suggested approach
+  const [initialized, setInitialized] = useState(false);
+
   useEffect(()=>{
     // ... existing code
-    if(!workflow) {
+    if(!workflow && !initialized) {
+      setInitialized(true);
       getWorkflows()
       createNewWorkflow()
     }
-    getWorkflowData()
   },[flow.workflow_id, dispatch])
+
+  // Separate effect for workflow data
+  useEffect(() => {
+    if (flow.workflow_id) {
+      getWorkflowData()
+    }
+  }, [flow.workflow_id])
🤖 Prompt for AI Agents
In apps/web/app/components/ui/app-sidebar.tsx around lines 89-95, the effect may
trigger duplicate/racing workflow initialization because getWorkflows() and
createNewWorkflow() can run concurrently and getWorkflowData() runs on every
trigger; refactor into three focused effects (1) credentials: run getCreds()
once on mount with a mounted-ref cleanup, (2) workflows: on mount or when a
guarded-initialize ref/state indicates "not initialized" call getWorkflows() OR
createNewWorkflow() but never both—use a useRef boolean (e.g., initializedRef)
or local state to ensure only one initialization path runs and avoid calling
getWorkflows() from createNewWorkflow() if already initializing, and (3)
workflow data: run getWorkflowData() only when flow.workflow_id actually changes
(compare prev id via ref or include flow.workflow_id in deps and short-circuit
if unchanged). Also add a cleanup flag to ignore results for unmounted
components to prevent state updates after unmount.

Comment on lines +157 to +181
export const updateTrigger = async(context: updateContext)=>{
try{
const res = await axios.put(`${BACKEND_URL}/user/update/trigger`,
{
TriggerId: context.id,
Config: context.config
},{
headers: {"Content-Type": "application/json"},
withCredentials: true
}
)
const trigger = (res.data)
console.log('trigger created: ', trigger);
return {
success: true,
data: trigger
}
}catch(e){
console.error("Error in updating Trigger:", e);
return {
success: false,
data: e instanceof Error ? e.message : `unknown error ${e}`
}
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix misleading log message.

Line 169 logs 'trigger created: ' but this function updates an existing trigger. The message should say "trigger updated" to accurately reflect the operation.

🔎 Proposed fix
     const trigger = (res.data)
-    console.log('trigger created: ', trigger);
+    console.log('trigger updated: ', trigger);
     return { 
       success: true,
       data: trigger
📝 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
export const updateTrigger = async(context: updateContext)=>{
try{
const res = await axios.put(`${BACKEND_URL}/user/update/trigger`,
{
TriggerId: context.id,
Config: context.config
},{
headers: {"Content-Type": "application/json"},
withCredentials: true
}
)
const trigger = (res.data)
console.log('trigger created: ', trigger);
return {
success: true,
data: trigger
}
}catch(e){
console.error("Error in updating Trigger:", e);
return {
success: false,
data: e instanceof Error ? e.message : `unknown error ${e}`
}
}
}
export const updateTrigger = async(context: updateContext)=>{
try{
const res = await axios.put(`${BACKEND_URL}/user/update/trigger`,
{
TriggerId: context.id,
Config: context.config
},{
headers: {"Content-Type": "application/json"},
withCredentials: true
}
)
const trigger = (res.data)
console.log('trigger updated: ', trigger);
return {
success: true,
data: trigger
}
}catch(e){
console.error("Error in updating Trigger:", e);
return {
success: false,
data: e instanceof Error ? e.message : `unknown error ${e}`
}
}
}
🤖 Prompt for AI Agents
In apps/web/app/workflow/lib/config.ts around lines 157 to 181, the console.log
message incorrectly says 'trigger created' inside updateTrigger; change that
message to 'trigger updated' (optionally include the trigger id or brief
context) so the log accurately reflects the operation and aids debugging.

@@ -1,4 +1,5 @@
import z from "zod";
import { string } from "zod/v4";
Copy link

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 for all imports from zod/v4
echo "=== Checking zod/v4 imports ==="
rg -n "from ['\"]zod" packages/common/src/index.ts

# Check zod version in package.json
echo -e "\n=== Checking zod in package.json ==="
fd -e json "package.json" | head -5 | xargs grep -H "zod" 2>/dev/null || true

# View the entire file to check if string is used
echo -e "\n=== Entire file content ==="
cat -n packages/common/src/index.ts

# Check if string identifier is used anywhere in the file
echo -e "\n=== Search for 'string' usage in file ==="
rg -n "string" packages/common/src/index.ts

Repository: Dev-Pross/BuildFlow

Length of output: 2653


🏁 Script executed:

#!/bin/bash
# Check if zod package has any exports for subpaths
echo "=== Checking zod package exports/structure ==="
fd -e json "package.json" | xargs grep -l "zod" | head -1 | xargs cat | grep -A 20 "dependencies\|devDependencies" | grep "zod"

# Check if there are any other zod/v4 imports in the entire codebase
echo -e "\n=== Searching for zod/v4 imports across codebase ==="
rg "from ['\"]zod/v4['\"]" --type=ts --type=js

Repository: Dev-Pross/BuildFlow

Length of output: 225


Remove the unused import from line 2.

The string import from "zod/v4" is never used in this file. All schema definitions correctly use z.string() from the standard zod import on line 1. Additionally, "zod/v4" is not a valid import path for the installed zod version (3.25.76).

Proposed fix
 import z from "zod";
-import { string } from "zod/v4";
📝 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
import { string } from "zod/v4";
import z from "zod";
🤖 Prompt for AI Agents
In packages/common/src/index.ts around line 2, remove the unused and invalid
import "import { string } from \"zod/v4\""; it is not referenced anywhere and
the project uses zod v3 where "zod/v4" is not a valid path—delete that line so
only the correct zod import remains and run typecheck to ensure no remaining
references to that symbol.

Comment on lines +129 to +135
// Filter out empty/falsy values to prevent overwriting valid tokens
const filteredTokens: Partial<OAuthTokens> = {};
if (tokens.access_token) filteredTokens.access_token = tokens.access_token;
if (tokens.refresh_token) filteredTokens.refresh_token = tokens.refresh_token;
if (tokens.token_type) filteredTokens.token_type = tokens.token_type;
if (tokens.expiry_date) filteredTokens.expiry_date = tokens.expiry_date;
if (tokens.scope) filteredTokens.scope = tokens.scope;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: Falsy check incorrectly rejects expiry_date = 0.

The truthy check if (tokens.expiry_date) treats 0 as falsy and excludes it from filteredTokens. Since expiry_date is a Unix timestamp (number), 0 is a valid value (epoch time) that should be preserved. This bug could cause tokens to lose their expiry metadata.

🔎 Proposed fix: Use explicit undefined/null checks
-            // Filter out empty/falsy values to prevent overwriting valid tokens
             const filteredTokens: Partial<OAuthTokens> = {};
             if (tokens.access_token) filteredTokens.access_token = tokens.access_token;
             if (tokens.refresh_token) filteredTokens.refresh_token = tokens.refresh_token;
             if (tokens.token_type) filteredTokens.token_type = tokens.token_type;
-            if (tokens.expiry_date) filteredTokens.expiry_date = tokens.expiry_date;
+            if (tokens.expiry_date !== undefined && tokens.expiry_date !== null) {
+                filteredTokens.expiry_date = tokens.expiry_date;
+            }
             if (tokens.scope) filteredTokens.scope = tokens.scope;
🤖 Prompt for AI Agents
In packages/nodes/src/common/google-oauth-service.ts around lines 129 to 135,
the current truthy checks (e.g., if (tokens.expiry_date)) incorrectly drop valid
numeric values such as 0; change those checks to explicit null/undefined checks
(e.g., if (tokens.expiry_date !== undefined && tokens.expiry_date !== null)) so
expiry_date (and any other numeric fields) are preserved even when zero, thereby
preventing accidental loss of token expiry metadata.

Comment on lines +118 to 128
const result: GoogleSheetsCredentials = {
access_token: credentials.access_token || '',
refresh_token: credentials.refresh_token || '',
refresh_token: '', // Will be set below if present
token_type: credentials.token_type || '',
expiry_date: credentials.expiry_date || 0
};

// Only include refresh_token if Google actually returned one
if (credentials.refresh_token) {
result.refresh_token = credentials.refresh_token;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Returning empty string for missing refresh_token creates inconsistency.

When Google doesn't return a new refresh_token, the current code returns an empty string (''). This creates problems:

  1. The GoogleSheetsCredentials interface requires refresh_token: string, but semantically an empty string is ambiguous.
  2. The filtering logic in google-oauth-service.ts (lines 132-135) treats empty strings as falsy and won't persist them, potentially causing the existing refresh_token to be lost.
  3. Consumers checking if (creds.refresh_token) will get false for empty strings.
🔎 Recommended solutions

Option 1 (Preferred): Make refresh_token optional and use undefined

 interface GoogleSheetsCredentials{
     access_token: string,
-    refresh_token: string,
+    refresh_token?: string,
     token_type: string,
     expiry_date: number
 }
             const result: GoogleSheetsCredentials = {
                 access_token: credentials.access_token || '',
-                refresh_token: '', // Will be set below if present
                 token_type: credentials.token_type || '',
                 expiry_date: credentials.expiry_date || 0
             };

             // Only include refresh_token if Google actually returned one
             if (credentials.refresh_token) {
                 result.refresh_token = credentials.refresh_token;
             }

Option 2: Preserve the existing refresh_token from constructor

Store the original refresh_token in the constructor and return it when Google doesn't provide a new one:

 class GoogleSheetsService{
     private sheets : sheets_v4.Sheets;
     private auth: OAuth2Client;
     private drive: drive_v3.Drive;
+    private originalRefreshToken: string;
     
     constructor(credentials: GoogleSheetsCredentials){
+        this.originalRefreshToken = credentials.refresh_token;
         // ... rest of constructor
     }
     
     async refreshAccessToken(): Promise <GoogleSheetsCredentials>{
         try{
             const {credentials} = await this.auth.refreshAccessToken();

             const result: GoogleSheetsCredentials = {
                 access_token: credentials.access_token || '',
-                refresh_token: '',
+                refresh_token: credentials.refresh_token || this.originalRefreshToken,
                 token_type: credentials.token_type || '',
                 expiry_date: credentials.expiry_date || 0
             };
-
-            // Only include refresh_token if Google actually returned one
-            if (credentials.refresh_token) {
-                result.refresh_token = credentials.refresh_token;
-            }

             return result;
         }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In packages/nodes/src/google-sheets/google-sheets.service.ts around lines 118 to
128, the code currently sets refresh_token to an empty string when Google
doesn't return one; change this so that refresh_token is optional/undefined
instead of an empty string by updating the GoogleSheetsCredentials type to make
refresh_token?: string and return result without setting refresh_token (or
explicitly set it to undefined) when credentials.refresh_token is absent; ensure
callers that check for creds.refresh_token use truthy checks and update any
typings where needed so no empty-string sentinel is used.

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.

3 participants