Skip to content

feat(cli): Add support for federated auth database integrations#327

Draft
tkislan wants to merge 11 commits intotk/cli-integrations-add-edit-fullfrom
tk/cli-federated-auth
Draft

feat(cli): Add support for federated auth database integrations#327
tkislan wants to merge 11 commits intotk/cli-integrations-add-edit-fullfrom
tk/cli-federated-auth

Conversation

@tkislan
Copy link
Contributor

@tkislan tkislan commented Feb 27, 2026

Summary by CodeRabbit

  • New Features

    • Federated authentication for database integrations (Trino OAuth) with an interactive browser flow.
    • New CLI command to authenticate integrations via interactive selection or direct integration ID.
    • Automatic token management: secure local token storage, refresh-on-expiry, and token upsert.
  • Tests

    • Comprehensive unit and integration tests covering token schema, storage, refresh logic, and the OAuth local flow.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 27, 2026

📝 Walkthrough

Walkthrough

This PR adds federated authentication to the CLI: a new integrations auth command, an in-process OAuth2 local server, token management (read/write/refresh) persisted to a YAML tokens file, and wiring to resolve federated tokens when injecting integration env vars. It also updates several APIs to async, adds tests for tokens and OAuth flows, exposes Trino OAuth SQLAlchemy input builders, and adds related dependencies and secure file utilities.

Sequence Diagram

sequenceDiagram
    participant User
    participant CLI as "CLI (integrations auth)"
    participant AuthCmd as "auth-integration.ts"
    participant OAuthServer as "oauth-local-server.ts"
    participant Browser
    participant OAuthProvider
    participant TokenStore as "federated-auth-tokens.ts"
    participant Integrations as "integrations YAML"

    User->>CLI: run `integrations auth [id]`
    CLI->>AuthCmd: invoke action handler
    AuthCmd->>Integrations: load integrations file
    AuthCmd->>AuthCmd: select target federated integration (id or prompt)
    AuthCmd->>OAuthServer: runOAuthFlow(params)
    OAuthServer->>Browser: open /start URL
    Browser->>OAuthProvider: auth request (redirect)
    OAuthProvider->>Browser: redirect back with code
    Browser->>OAuthServer: /callback?code=...
    OAuthServer->>OAuthProvider: exchange code -> tokens
    OAuthProvider-->>OAuthServer: access_token, refresh_token, expires_in
    OAuthServer->>TokenStore: saveTokenForIntegration(tokenEntry)
    TokenStore->>TokenStore: write federated-auth-tokens.yaml
    OAuthServer-->>AuthCmd: return FederatedAuthTokenEntry
    AuthCmd->>User: log success
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 52.78% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Updates Docs ⚠️ Warning New integrations auth CLI command lacks user-facing documentation; no CLI guide or command reference found. Create packages/cli/README.md documenting the new integrations auth command with usage syntax, prerequisites, and examples.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Title accurately summarizes the primary change: adding federated auth support to database integrations in the CLI.

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

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


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

@codecov
Copy link

codecov bot commented Feb 27, 2026

Codecov Report

❌ Patch coverage is 40.19934% with 180 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.18%. Comparing base (0e24ecc) to head (3af7ee6).
⚠️ Report is 1 commits behind head on tk/cli-integrations-add-edit-full.

Files with missing lines Patch % Lines
.../cli/src/commands/integrations/auth-integration.ts 2.43% 80 Missing ⚠️
...kages/cli/src/federated-auth/oauth-local-server.ts 4.76% 60 Missing ⚠️
...es/cli/src/federated-auth/federated-auth-tokens.ts 76.36% 26 Missing ⚠️
...-integrations/src/database-integration-env-vars.ts 65.38% 9 Missing ⚠️
...li/src/integrations/inject-integration-env-vars.ts 0.00% 4 Missing ⚠️
.../cli/src/commands/integrations/edit-integration.ts 50.00% 1 Missing ⚠️
Additional details and impacted files
@@                          Coverage Diff                          @@
##           tk/cli-integrations-add-edit-full     #327      +/-   ##
=====================================================================
- Coverage                              83.75%   82.18%   -1.58%     
=====================================================================
  Files                                    140      146       +6     
  Lines                                   7707     7986     +279     
  Branches                                2145     2140       -5     
=====================================================================
+ Hits                                    6455     6563     +108     
- Misses                                  1252     1423     +171     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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

Caution

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

⚠️ Outside diff range comments (1)
packages/database-integrations/README.md (1)

160-162: ⚠️ Potential issue | 🟡 Minor

Update example to await the async API.

The sample still uses synchronous destructuring even though the function now returns a Promise.

💡 Suggested fix
-const { envVars } = getEnvironmentVariablesForIntegrations(integrations, {
+const { envVars } = await getEnvironmentVariablesForIntegrations(integrations, {
   projectRootDirectory: "/path/to/project",
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/database-integrations/README.md` around lines 160 - 162, The example
calls getEnvironmentVariablesForIntegrations synchronously but that function now
returns a Promise; update the example to await the async API by awaiting the
call (or using .then) before destructuring the returned object so envVars is
obtained from the resolved value of
getEnvironmentVariablesForIntegrations(integrations, { projectRootDirectory:
"/path/to/project" }); reference getEnvironmentVariablesForIntegrations,
envVars, integrations, and projectRootDirectory when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/cli/src/commands/integrations/auth-integration.ts`:
- Around line 145-149: The catch around resolveEnvVarRefsFromMap silently
swallows errors causing later cryptic validation failures; update the catch in
the block that calls resolveEnvVarRefsFromMap(integrationRawJson, envVars) so it
either re-throws a new Error with a clear message referencing the env:
resolution failure (including the original error) or emits a user-visible
warning via the CLI logger (e.g., logger.warn or console.error) that explains
env var resolution failed for integrationRawJson and advises the user to check
environment variables; ensure you reference the original error when re-throwing
or logging so the root cause is preserved.
- Line 1: The file imports debug from the wrong module ("node:console") which
bypasses the CLI's --quiet/--debug flags; replace the import statement `import {
debug } from 'node:console'` with an import from the CLI output wrapper (e.g.
`import { debug } from '../../output'`) and ensure all uses of the debug
identifier (the call sites in this module) now call the exported debug from
../../output so the CLI flags are respected.

In `@packages/cli/src/federated-auth/federated-auth-tokens-schema.test.ts`:
- Around line 8-69: The tests miss edge cases for invalid expiresAt and
empty-string credentials; update the
federated-auth/federated-auth-tokens-schema.test.ts to add tests that assert
federatedAuthTokenEntrySchema rejects entries with a non-ISO or invalid
expiresAt (e.g., 'not-a-date' or malformed timestamp) and rejects entries where
accessToken or refreshToken are empty strings (''), and similarly add a case to
baseTokensFileSchema to ensure tokens containing such invalid entries are
rejected; use the existing FederatedAuthTokenEntry shape and the
federatedAuthTokenEntrySchema and baseTokensFileSchema identifiers to locate
where to add these test cases.

In `@packages/cli/src/federated-auth/federated-auth-tokens-schema.ts`:
- Around line 10-14: The schema federatedAuthTokenEntrySchema currently allows
empty strings for integrationId, accessToken, refreshToken (and for expiresAt
when present); update those fields to disallow empty strings by using Zod's
nonempty (or .min(1)) validator: change integrationId, accessToken, and
refreshToken to z.string().nonempty(), and make expiresAt
z.string().datetime().nonempty().optional() so an empty string does not pass
validation when provided.

In `@packages/cli/src/federated-auth/federated-auth-tokens.test.ts`:
- Around line 15-89: Add two tests to federated-auth-tokens.test.ts that
exercise readTokensFile's error paths: one "malformed YAML" test that writes
syntactically invalid YAML to a test file (use fs.writeFile) then calls
readTokensFile(testPath) and asserts result.tokens is [] and result.issues
contains a parse/error entry; and one "invalid-entry schema" test that writes
YAML with entries missing required fields or wrong types (e.g., an object
without integrationId or with non-string accessToken), then calls
readTokensFile(testPath) and asserts result.tokens excludes the invalid entries
and result.issues lists the schema validation problems. Use the existing testDir
pattern and reference the readTokensFile (and writeTokensFile if helpful)
functions to locate the logic under test.

In `@packages/cli/src/federated-auth/federated-auth-tokens.ts`:
- Around line 132-143: saveTokenForIntegration currently calls readTokensFile
which returns only valid tokens, so when you upsert via saveTokenForIntegration
(function) any malformed/raw entries from the original file are dropped
silently; change saveTokenForIntegration to preserve non-parsed/raw entries or
at minimum warn the user when malformed entries exist: modify readTokensFile (or
call a new reader) to return both parsed tokens and rawEntries, then in
saveTokenForIntegration merge the updated/inserted FederatedAuthTokenEntry into
the parsed tokens list while reassembling the final file contents to include
untouched rawEntries (or log a warning if rawEntries are present) before calling
writeTokensFile; reference tokens, entry, writeTokensFile and
getDefaultTokensFilePath when implementing this behavior.

In `@packages/cli/src/federated-auth/oauth-local-server.ts`:
- Around line 125-134: The current app.listen(...) call that creates server does
not handle bind errors (e.g., EADDRINUSE) so failures are ambiguous; attach an
explicit error handler to the returned server from app.listen (the server
variable) and in that handler check err.code (e.g., 'EADDRINUSE') to log a clear
message including OAUTH_PORT and startURL and exit the process (or suggest an
alternative port) rather than letting the process crash silently; ensure any
cleanup (closing server) is done as needed and keep the existing browser-open
logic (open(startURL)) in the listen callback.
- Around line 114-115: The HTML response embeds err.name and err.message
unescaped (the html variable), which allows XSS; update the OAuth error handler
to HTML-escape these values before interpolation (e.g., implement/use an
escapeHtml helper that replaces &, <, >, ", ' and /) and use escapedName =
escapeHtml(err.name) and escapedMessage = escapeHtml(err.message) when building
html, or alternatively render the error as plain text/JSON via
res.status(400).send to avoid HTML interpolation.
- Around line 107-112: The Express app is missing Passport initialization; call
passport.initialize() on the Express instance before any route handlers to
ensure Passport sets up req._passport even when using session: false. Add
app.use(passport.initialize()) immediately after creating app (before the routes
that reference START_PATH and CALLBACK_PATH and before the
passport.authenticate(...) calls) so passport.authenticate('trino-oauth', {
session: false }) runs with Passport properly initialized.

In `@packages/cli/src/federated-auth/token-refresh.test.ts`:
- Around line 47-126: Add unit tests in
packages/cli/src/federated-auth/token-refresh.test.ts to cover the error paths
of refreshAccessToken: simulate token endpoint responses for (1) OAuth
"invalid_grant" body (ok: true but error payload), (2) OAuth "invalid_client"
body, (3) non-OK HTTP response (ok: false, status/text), and (4) invalid JSON
(mock fetch.json to throw). For each case call refreshAccessToken with a valid
FederatedAuthTokenEntry and DatabaseIntegrationConfig, stub global fetch with
vi.stubGlobal/vi.fn() (as in the existing test), assert that refreshAccessToken
rejects with an appropriate error message or type, and clean up globals with
vi.unstubAllGlobals(); keep test structure consistent with the existing "should
call token endpoint..." test and use the same tokenUrl/clientId/clientSecret
setup.
- Around line 96-125: The fetch stub created with vi.stubGlobal('fetch') in the
test around mockFetch can leak if assertions fail; wrap the test execution and
assertions in a try/finally so vi.unstubAllGlobals() always runs. Locate the
test in token-refresh.test.ts that calls refreshAccessToken(token, integration,
'/tmp/test-tokens.yaml') and replace the current linear setup/assert/cleanup
with a try { ... assertions including
expect(mockFetch).toHaveBeenCalledWith(...); ... } finally {
vi.unstubAllGlobals(); } to guarantee the global stub is removed even on
failure.

In `@packages/cli/src/federated-auth/token-refresh.ts`:
- Around line 75-77: The code computes expiresAt using responseBody.expires_in
without validating it; update the logic around responseBody/expires_in
(affecting variables newRefreshToken, expiresIn, expiresAt) to first coerce and
validate expires_in is a finite positive number (e.g.,
Number(responseBody.expires_in)), and if it is NaN, <=0, or not finite, fall
back to the default (3600) before computing expiresAt; then compute expiresAt
from the validated expiresIn so negative/invalid values cannot produce bad
timestamps.
- Around line 42-46: The fetch call that posts to tokenUrl (using
getOAuth2ClientBasicAuthHeaders and tokenEntry.refreshToken) needs a timeout to
avoid hanging; wrap the fetch in an AbortController, pass controller.signal into
fetch, start a timer (e.g., 5–10s) that calls controller.abort() on expiry, and
clear the timer after fetch completes; also catch/handle the abort error path so
token refresh fails fast and surfaces a clear timeout error instead of hanging.
- Around line 23-27: In refreshAccessToken ensure you validate that the
tokenEntry is intended for the provided integration before using integration
metadata: check a stable identifier (e.g. tokenEntry.integrationId or
tokenEntry.id) matches integration.id (or integration.integrationId) at the
start of refreshAccessToken, and if it does not match, abort the flow (throw an
error or return a rejected Promise) rather than sending tokenEntry.refreshToken
to the integration endpoint; update any callers/tests to handle the new error
path if needed.
- Around line 93-99: The isTokenExpired function can compute expiryTime as NaN
for invalid expiresAt values, causing the subsequent comparison to incorrectly
mark an invalid timestamp as not expired; update isTokenExpired to detect
invalid dates (e.g., check Number.isNaN(expiryTime) or isNaN(new
Date(expiresAt).getTime())) and return true when expiryTime is invalid so any
malformed or non-parsable expiresAt is treated as expired; keep the existing
bufferSeconds logic and references to tokenEntry.expiresAt and expiryTime when
implementing this guard.

In `@packages/database-integrations/src/database-integration-env-vars.ts`:
- Around line 73-93: The federated detection and unsafe cast are causing Trino
OAuth configs stored only in integration.metadata to be missed and bypassing
runtime validation; update the logic in the block around isFederated and the
call to getValidFederatedAuthToken so that you check both
integration.federated_auth_method and integration.metadata.authMethod (or
equivalent) for 'trino-oauth' instead of relying solely on the top-level field,
and validate metadata at runtime before calling buildTrinoOAuthSqlAlchemyInput:
fetch metadata as DatabaseIntegrationMetadataByType['trino'], assert/guard that
metadata.authMethod === TrinoAuthMethods.Oauth, then call
buildTrinoOAuthSqlAlchemyInput with that validated metadata and push the env var
via getSqlEnvVarName only when validation succeeds.

In `@packages/database-integrations/src/federated-auth-tokens.ts`:
- Around line 171-177: The current try/catch around
refreshAccessToken(tokenEntry, integration, tokensFilePath) swallows failures
and returns undefined; instead let refresh errors propagate (or rethrow with
context) so callers can handle/report them. Remove the empty catch block (or
change it to catch (err) { throw new Error(`refreshAccessToken failed for
${integration}: ${err}`); }) and keep finalToken assignment as before; keep
references to tokenEntry, isTokenExpired, refreshAccessToken, finalToken and
tokensFilePath to locate the change.
- Around line 107-114: The token refresh fetch to tokenUrl should use an
AbortController with a timeout to avoid hanging: create an AbortController, pass
controller.signal to fetch, start a setTimeout (e.g., 10s) that calls
controller.abort(), and clear the timer when the response arrives; ensure fetch
rejections (including AbortError) are caught and the error is thrown/rejected up
(do not suppress) so callers see failures. Update the refresh logic that uses
tokenEntry.refreshToken and the fetch call to propagate errors instead of
swallowing them so token refresh failures are visible to callers.

---

Outside diff comments:
In `@packages/database-integrations/README.md`:
- Around line 160-162: The example calls getEnvironmentVariablesForIntegrations
synchronously but that function now returns a Promise; update the example to
await the async API by awaiting the call (or using .then) before destructuring
the returned object so envVars is obtained from the resolved value of
getEnvironmentVariablesForIntegrations(integrations, { projectRootDirectory:
"/path/to/project" }); reference getEnvironmentVariablesForIntegrations,
envVars, integrations, and projectRootDirectory when making the change.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 7b16bb8 and 42c43e0.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (19)
  • packages/cli/package.json
  • packages/cli/src/cli.ts
  • packages/cli/src/commands/integrations/auth-integration.ts
  • packages/cli/src/commands/integrations/edit-integration.ts
  • packages/cli/src/commands/run.ts
  • packages/cli/src/constants.ts
  • packages/cli/src/federated-auth/federated-auth-tokens-schema.test.ts
  • packages/cli/src/federated-auth/federated-auth-tokens-schema.ts
  • packages/cli/src/federated-auth/federated-auth-tokens.test.ts
  • packages/cli/src/federated-auth/federated-auth-tokens.ts
  • packages/cli/src/federated-auth/oauth-local-server.ts
  • packages/cli/src/federated-auth/token-refresh.test.ts
  • packages/cli/src/federated-auth/token-refresh.ts
  • packages/database-integrations/README.md
  • packages/database-integrations/package.json
  • packages/database-integrations/src/database-integration-env-vars.test.ts
  • packages/database-integrations/src/database-integration-env-vars.ts
  • packages/database-integrations/src/federated-auth-tokens.ts
  • packages/database-integrations/src/index.ts

Comment on lines +8 to +69
describe('federatedAuthTokenEntrySchema', () => {
it('should accept valid token entry', () => {
const valid: FederatedAuthTokenEntry = {
integrationId: 'abc-123',
accessToken: 'eyJhbG...',
refreshToken: 'dGhpcyBpcyBhIHJl...',
expiresAt: '2026-02-25T15:30:00.000Z',
}
expect(federatedAuthTokenEntrySchema.parse(valid)).toEqual(valid)
})

it('should accept entry without expiresAt', () => {
const valid = {
integrationId: 'abc-123',
accessToken: 'token',
refreshToken: 'refresh',
}
expect(federatedAuthTokenEntrySchema.parse(valid)).toEqual(valid)
})

it('should reject missing integrationId', () => {
const invalid = {
accessToken: 'token',
refreshToken: 'refresh',
}
expect(() => federatedAuthTokenEntrySchema.parse(invalid)).toThrow()
})

it('should reject missing accessToken', () => {
const invalid = {
integrationId: 'abc',
refreshToken: 'refresh',
}
expect(() => federatedAuthTokenEntrySchema.parse(invalid)).toThrow()
})

it('should reject missing refreshToken', () => {
const invalid = {
integrationId: 'abc',
accessToken: 'token',
}
expect(() => federatedAuthTokenEntrySchema.parse(invalid)).toThrow()
})
})

describe('baseTokensFileSchema', () => {
it('should accept empty tokens array', () => {
expect(baseTokensFileSchema.parse({ tokens: [] })).toEqual({ tokens: [] })
})

it('should accept missing tokens (defaults to [])', () => {
expect(baseTokensFileSchema.parse({})).toEqual({ tokens: [] })
})

it('should accept array of records', () => {
const parsed = baseTokensFileSchema.parse({
tokens: [{ integrationId: 'x', accessToken: 'a', refreshToken: 'r' }],
})
expect(parsed.tokens).toHaveLength(1)
expect(parsed.tokens[0]).toEqual({ integrationId: 'x', accessToken: 'a', refreshToken: 'r' })
})
})
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add edge-case rejection tests for token schema.

Current suite misses invalid expiresAt and empty-string credential cases.

Suggested additions
 describe('federatedAuthTokenEntrySchema', () => {
+  it('should reject invalid expiresAt format', () => {
+    const invalid = {
+      integrationId: 'abc-123',
+      accessToken: 'token',
+      refreshToken: 'refresh',
+      expiresAt: 'not-a-date',
+    }
+    expect(() => federatedAuthTokenEntrySchema.parse(invalid)).toThrow()
+  })
+
+  it('should reject empty required string fields', () => {
+    const invalid = {
+      integrationId: '',
+      accessToken: '',
+      refreshToken: '',
+    }
+    expect(() => federatedAuthTokenEntrySchema.parse(invalid)).toThrow()
+  })
+
   it('should accept valid token entry', () => {
As per coding guidelines: "Test edge cases, error handling, and special characters in test files".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/federated-auth/federated-auth-tokens-schema.test.ts` around
lines 8 - 69, The tests miss edge cases for invalid expiresAt and empty-string
credentials; update the federated-auth/federated-auth-tokens-schema.test.ts to
add tests that assert federatedAuthTokenEntrySchema rejects entries with a
non-ISO or invalid expiresAt (e.g., 'not-a-date' or malformed timestamp) and
rejects entries where accessToken or refreshToken are empty strings (''), and
similarly add a case to baseTokensFileSchema to ensure tokens containing such
invalid entries are rejected; use the existing FederatedAuthTokenEntry shape and
the federatedAuthTokenEntrySchema and baseTokensFileSchema identifiers to locate
where to add these test cases.

Comment on lines +75 to +77
const newRefreshToken = responseBody.refresh_token ?? tokenEntry.refreshToken
const expiresIn = responseBody.expires_in ?? 3600
const expiresAt = new Date(Date.now() + expiresIn * 1000).toISOString()
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Validate expires_in before computing expiry timestamp.

Line 76 trusts expires_in as a valid positive number. Invalid/negative values can produce bad expiry behavior.

Suggested fix
-  const expiresIn = responseBody.expires_in ?? 3600
+  const expiresIn =
+    typeof responseBody.expires_in === 'number' &&
+    Number.isFinite(responseBody.expires_in) &&
+    responseBody.expires_in > 0
+      ? responseBody.expires_in
+      : 3600
   const expiresAt = new Date(Date.now() + expiresIn * 1000).toISOString()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const newRefreshToken = responseBody.refresh_token ?? tokenEntry.refreshToken
const expiresIn = responseBody.expires_in ?? 3600
const expiresAt = new Date(Date.now() + expiresIn * 1000).toISOString()
const newRefreshToken = responseBody.refresh_token ?? tokenEntry.refreshToken
const expiresIn =
typeof responseBody.expires_in === 'number' &&
Number.isFinite(responseBody.expires_in) &&
responseBody.expires_in > 0
? responseBody.expires_in
: 3600
const expiresAt = new Date(Date.now() + expiresIn * 1000).toISOString()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/federated-auth/token-refresh.ts` around lines 75 - 77, The
code computes expiresAt using responseBody.expires_in without validating it;
update the logic around responseBody/expires_in (affecting variables
newRefreshToken, expiresIn, expiresAt) to first coerce and validate expires_in
is a finite positive number (e.g., Number(responseBody.expires_in)), and if it
is NaN, <=0, or not finite, fall back to the default (3600) before computing
expiresAt; then compute expiresAt from the validated expiresIn so
negative/invalid values cannot produce bad timestamps.

Comment on lines +73 to +93
const isFederated =
integration.federated_auth_method != null && isFederatedAuthMethod(integration.federated_auth_method)

if (isFederated && params.federatedAuthTokensFilePath) {
try {
const accessToken = await getValidFederatedAuthToken(integration, params.federatedAuthTokensFilePath)
if (accessToken && integration.type === 'trino' && integration.federated_auth_method === 'trino-oauth') {
const metadata = integration.metadata as Extract<
DatabaseIntegrationMetadataByType['trino'],
{ authMethod: typeof TrinoAuthMethods.Oauth }
>
const sqlAlchemyInput = buildTrinoOAuthSqlAlchemyInput(
integration.id,
metadata,
accessToken,
params.projectRootDirectory
)
envVarsForThisIntegration.push({
name: getSqlEnvVarName(integration.id),
value: JSON.stringify({ integration_id: integration.id, ...sqlAlchemyInput }),
})
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Federated detection is incomplete and the OAuth cast is unsafe.

Current logic only trusts top-level federated_auth_method; metadata-only Trino OAuth configs won’t get token-backed SQL env vars. The forced cast also skips runtime validation of metadata auth method.

💡 Suggested fix
-      const isFederated =
-        integration.federated_auth_method != null && isFederatedAuthMethod(integration.federated_auth_method)
+      const topLevelFederated =
+        integration.federated_auth_method != null && isFederatedAuthMethod(integration.federated_auth_method)
+      const trinoOauthFromMetadata =
+        integration.type === 'trino' && integration.metadata.authMethod === TrinoAuthMethods.Oauth
+      const isFederated = topLevelFederated || trinoOauthFromMetadata
 
       if (isFederated && params.federatedAuthTokensFilePath) {
         try {
           const accessToken = await getValidFederatedAuthToken(integration, params.federatedAuthTokensFilePath)
-          if (accessToken && integration.type === 'trino' && integration.federated_auth_method === 'trino-oauth') {
-            const metadata = integration.metadata as Extract<
-              DatabaseIntegrationMetadataByType['trino'],
-              { authMethod: typeof TrinoAuthMethods.Oauth }
-            >
+          if (accessToken && integration.type === 'trino' && integration.metadata.authMethod === TrinoAuthMethods.Oauth) {
+            const metadata = integration.metadata
             const sqlAlchemyInput = buildTrinoOAuthSqlAlchemyInput(
               integration.id,
               metadata,
               accessToken,
               params.projectRootDirectory
             )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/database-integrations/src/database-integration-env-vars.ts` around
lines 73 - 93, The federated detection and unsafe cast are causing Trino OAuth
configs stored only in integration.metadata to be missed and bypassing runtime
validation; update the logic in the block around isFederated and the call to
getValidFederatedAuthToken so that you check both
integration.federated_auth_method and integration.metadata.authMethod (or
equivalent) for 'trino-oauth' instead of relying solely on the top-level field,
and validate metadata at runtime before calling buildTrinoOAuthSqlAlchemyInput:
fetch metadata as DatabaseIntegrationMetadataByType['trino'], assert/guard that
metadata.authMethod === TrinoAuthMethods.Oauth, then call
buildTrinoOAuthSqlAlchemyInput with that validated metadata and push the env var
via getSqlEnvVarName only when validation succeeds.

Comment on lines +107 to +114
const response = await fetch(tokenUrl, {
method: 'POST',
headers: {
Authorization: `Basic ${authString}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `grant_type=refresh_token&refresh_token=${encodeURIComponent(tokenEntry.refreshToken)}`,
})
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify there is a fetch call and no abort signal wiring.
rg -nP -C2 "refreshAccessToken|fetch\(|AbortController|signal" packages/database-integrations/src/federated-auth-tokens.ts

Repository: deepnote/deepnote

Length of output: 570


🏁 Script executed:

# Get full context of the file and identify the function
wc -l packages/database-integrations/src/federated-auth-tokens.ts

Repository: deepnote/deepnote

Length of output: 124


🏁 Script executed:

# Read the entire file to understand the context
cat -n packages/database-integrations/src/federated-auth-tokens.ts

Repository: deepnote/deepnote

Length of output: 6939


Add timeout/abort for token refresh HTTP call.

The fetch call at line 107 can hang indefinitely without a timeout mechanism, blocking token refresh operations. Additionally, errors are suppressed in the caller (lines 175–176), making failures invisible.

💡 Suggested fix
-  const response = await fetch(tokenUrl, {
-    method: 'POST',
-    headers: {
-      Authorization: `Basic ${authString}`,
-      'Content-Type': 'application/x-www-form-urlencoded',
-    },
-    body: `grant_type=refresh_token&refresh_token=${encodeURIComponent(tokenEntry.refreshToken)}`,
-  })
+  const timeoutMs = 10_000
+  const controller = new AbortController()
+  const timeout = setTimeout(() => controller.abort(), timeoutMs)
+  let response: Response
+  try {
+    response = await fetch(tokenUrl, {
+      method: 'POST',
+      headers: {
+        Authorization: `Basic ${authString}`,
+        'Content-Type': 'application/x-www-form-urlencoded',
+      },
+      body: `grant_type=refresh_token&refresh_token=${encodeURIComponent(tokenEntry.refreshToken)}`,
+      signal: controller.signal,
+    })
+  } catch (error) {
+    if (error instanceof Error && error.name === 'AbortError') {
+      throw new Error(`Token refresh timed out after ${timeoutMs}ms`)
+    }
+    throw error
+  } finally {
+    clearTimeout(timeout)
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/database-integrations/src/federated-auth-tokens.ts` around lines 107
- 114, The token refresh fetch to tokenUrl should use an AbortController with a
timeout to avoid hanging: create an AbortController, pass controller.signal to
fetch, start a setTimeout (e.g., 10s) that calls controller.abort(), and clear
the timer when the response arrives; ensure fetch rejections (including
AbortError) are caught and the error is thrown/rejected up (do not suppress) so
callers see failures. Update the refresh logic that uses tokenEntry.refreshToken
and the fetch call to propagate errors instead of swallowing them so token
refresh failures are visible to callers.

Comment on lines +171 to +177
let finalToken = tokenEntry
if (isTokenExpired(tokenEntry)) {
try {
finalToken = await refreshAccessToken(tokenEntry, integration, tokensFilePath)
} catch {
return undefined
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Do not swallow refresh failures.

Returning undefined on refresh errors hides actionable auth failures and prevents callers from reporting them.

💡 Suggested fix
   let finalToken = tokenEntry
   if (isTokenExpired(tokenEntry)) {
-    try {
-      finalToken = await refreshAccessToken(tokenEntry, integration, tokensFilePath)
-    } catch {
-      return undefined
-    }
+    finalToken = await refreshAccessToken(tokenEntry, integration, tokensFilePath)
   }
📝 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
let finalToken = tokenEntry
if (isTokenExpired(tokenEntry)) {
try {
finalToken = await refreshAccessToken(tokenEntry, integration, tokensFilePath)
} catch {
return undefined
}
let finalToken = tokenEntry
if (isTokenExpired(tokenEntry)) {
finalToken = await refreshAccessToken(tokenEntry, integration, tokensFilePath)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/database-integrations/src/federated-auth-tokens.ts` around lines 171
- 177, The current try/catch around refreshAccessToken(tokenEntry, integration,
tokensFilePath) swallows failures and returns undefined; instead let refresh
errors propagate (or rethrow with context) so callers can handle/report them.
Remove the empty catch block (or change it to catch (err) { throw new
Error(`refreshAccessToken failed for ${integration}: ${err}`); }) and keep
finalToken assignment as before; keep references to tokenEntry, isTokenExpired,
refreshAccessToken, finalToken and tokensFilePath to locate the change.

tkislan added 2 commits March 2, 2026 15:07
- Updated `federatedAuthTokenEntrySchema` to enforce non-empty string fields.
- Added tests for invalid `expiresAt` format and empty required fields.
- Refactored `refreshAccessToken` to `refreshAccessTokenAndSave` for clarity and persistence.
- Introduced timeout handling for token refresh requests.
- Improved error handling for unsupported federated auth methods in environment variable retrieval.
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: 5

♻️ Duplicate comments (5)
packages/cli/src/federated-auth/oauth-local-server.ts (2)

116-117: ⚠️ Potential issue | 🟠 Major

Escape OAuth error text before embedding in HTML.

err.name and err.message are interpolated raw into HTML. Treat both as untrusted.

Suggested fix
+function escapeHtml(value: string): string {
+  return value
+    .replaceAll('&', '&amp;')
+    .replaceAll('<', '&lt;')
+    .replaceAll('>', '&gt;')
+    .replaceAll('"', '&quot;')
+    .replaceAll("'", '&#39;')
+}
+
 ...
-          const html = `<!DOCTYPE html><html><body><h2>${err.name}</h2><p>${err.message}</p></body></html>`
+          const html = `<!DOCTYPE html><html><body><h2>${escapeHtml(err.name)}</h2><p>${escapeHtml(err.message)}</p></body></html>`
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/federated-auth/oauth-local-server.ts` around lines 116 -
117, The HTML response embeds untrusted err.name and err.message directly;
escape both values before interpolation to prevent XSS. In oauth-local-server.ts
where html is constructed (the const html = `...` line inside the error handler
that uses err and res), replace the direct interpolation with escaped versions
(use an HTML-escape helper function or small utility that replaces &, <, >, ", '
with entities) and then send the escaped content via res.status(400).send(html);
ensure the helper is imported or defined near that handler and used for both
err.name and err.message.

127-136: ⚠️ Potential issue | 🟠 Major

Handle app.listen errors explicitly.

Port bind failures (e.g., EADDRINUSE) are not handled. Add a server error handler and reject the flow deterministically.

Suggested fix
     const server = app.listen(OAUTH_PORT, () => {
       log('Opening browser to authenticate...')
       log('If the browser does not open automatically, visit:')
       log(startURL)
       try {
         open(startURL).catch(() => {})
       } catch {
         // ignore
       }
     })
+
+    server.on('error', err => {
+      const error = err instanceof Error ? err : new Error(String(err))
+      rejectOnce(error)
+    })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/federated-auth/oauth-local-server.ts` around lines 127 -
136, The current app.listen call creates server without handling bind errors;
update the logic around server = app.listen(OAUTH_PORT, ...) to attach an
'error' handler on the returned server (server.on('error', ...)) that catches
failures like EADDRINUSE and rejects/throw a deterministic error for the OAuth
flow instead of letting it hang; ensure the same promise or control flow that
resolves on successful listen is rejected when server emits an error, and
include clear context (port/OAUTH_PORT and startURL) in the rejection path so
callers can handle the failure.
packages/cli/src/federated-auth/token-refresh.test.ts (1)

55-132: ⚠️ Potential issue | 🟠 Major

Expand refresh error-path coverage (still missing).

Suite currently misses invalid_grant, invalid_client/unauthorized_client, generic non-OK responses, invalid JSON, and save-failure assertions for refreshAccessTokenAndSave.

As per coding guidelines, "Create comprehensive tests for all new features using Vitest" and "Test edge cases, error handling, and special characters in test files".

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

In `@packages/cli/src/federated-auth/token-refresh.test.ts` around lines 55 - 132,
The test suite for refreshAccessTokenAndSave in token-refresh.test.ts lacks
coverage for error paths (invalid_grant, invalid_client/unauthorized_client,
non-OK HTTP responses, invalid JSON responses, and failures when saving tokens);
add tests that stub global fetch (mockFetch) to return: 1) a 200 with { error:
'invalid_grant' } and assert the function rejects with a clear error, 2) a
400/401 with body indicating invalid_client/unauthorized_client and assert
rejection, 3) a non-ok response (e.g., ok: false) and assert rejection, 4) a
resolved fetch whose json() throws (invalid JSON) and assert rejection, and 5)
simulate a successful token response but make the token-save path fail (mock
file write or the internal save function) and assert the save error is
propagated; reference refreshAccessTokenAndSave,
mockFetch/vi.stubGlobal('fetch'), and the test token fixtures to locate where to
insert each new it() case.
packages/database-integrations/src/federated-auth-tokens.ts (2)

207-214: ⚠️ Potential issue | 🟠 Major

Do not swallow refresh failures.

On expired tokens, any refresh/save failure currently returns undefined, which hides actionable auth/config errors and makes troubleshooting opaque.

Suggested fix
   let finalToken = tokenEntry
   if (isTokenExpired(tokenEntry)) {
-    try {
-      finalToken = await refreshAccessToken(tokenEntry, integration)
-      await saveTokenForIntegration(finalToken, tokensFilePath)
-    } catch {
-      return undefined
-    }
+    finalToken = await refreshAccessToken(tokenEntry, integration)
+    await saveTokenForIntegration(finalToken, tokensFilePath)
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/database-integrations/src/federated-auth-tokens.ts` around lines 207
- 214, The current block around isTokenExpired(tokenEntry) swallows any error
from refreshAccessToken(tokenEntry, integration) or
saveTokenForIntegration(finalToken, tokensFilePath) by returning undefined;
change the catch to propagate the error (include the caught error details)
instead of returning undefined so callers can handle or surface auth/config
failures — for example rethrow the caught error or throw a new Error that wraps
it; update the surrounding logic that expects undefined accordingly. Ensure this
change is applied to the tokens refresh flow involving finalToken,
refreshAccessToken, saveTokenForIntegration and tokenEntry.

137-152: ⚠️ Potential issue | 🟠 Major

Validate integration/token binding before refresh.

refreshAccessToken posts tokenEntry.refreshToken using integration metadata without asserting they belong to the same integration. A mismatched call can leak refresh tokens to the wrong token endpoint.

Suggested fix
 export async function refreshAccessToken(
   tokenEntry: FederatedAuthTokenEntry,
   integration: DatabaseIntegrationConfig
 ): Promise<FederatedAuthTokenEntry> {
+  if (integration.id !== tokenEntry.integrationId) {
+    throw new Error(
+      `Token entry integration mismatch: expected "${tokenEntry.integrationId}", got "${integration.id}"`
+    )
+  }
+
   const metadata = integration.metadata as {
     tokenUrl: string
     clientId: string
     clientSecret: string
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/database-integrations/src/federated-auth-tokens.ts` around lines 137
- 152, In refreshAccessToken, ensure the provided tokenEntry is bound to the
same integration before posting the refresh token: validate that tokenEntry's
integration identifier (e.g., tokenEntry.integrationId or equivalent field on
FederatedAuthTokenEntry) matches the integration passed in (integration.id or
another unique integration key), and if not throw an error; only call
fetchTokenWithTimeout(tokenUrl, clientId, clientSecret, tokenEntry.refreshToken)
after this check to prevent sending a refreshToken to the wrong tokenUrl.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/cli/src/commands/integrations/auth-integration.ts`:
- Around line 83-108: Replace the manual type-and-empty checks in
extractTrinoOAuthParams with a zod schema validation: define a
trinoOAuthParamsSchema using z.object({ authUrl: z.string().min(1, ...),
tokenUrl: z.string().min(1,...), clientId: z.string().min(1,...), clientSecret:
z.string().min(1,...) }), run safeParse(metadata) inside
extractTrinoOAuthParams, and on failure throw a single Error composed from
result.error.issues messages; on success return result.data (the typed authUrl,
tokenUrl, clientId, clientSecret). Ensure zod is imported.
- Around line 197-203: In the catch block handling errors in the auth
integration, stop execution after handling ExitPromptError by returning (or
using an else) right after the program.error(chalk.yellow('Cancelled.'), {
exitCode: ExitCode.Error }) call so the subsequent generic error handler
(program.error(chalk.red(...), { exitCode: ExitCode.Error })) is not executed
for ExitPromptError; update the catch in the function containing that try/catch
to either add a return after the ExitPromptError branch or convert it to an
if/else.

In `@packages/cli/src/federated-auth/oauth-local-server.ts`:
- Around line 52-58: The OAuth flow promise can hang if the browser callback
never arrives; add an overall timeout to reject the flow after a configurable
interval (e.g., 2 minutes) by wiring a setTimeout that calls rejectOnce (or
rejects the outer Promise) and clearing that timer on successful resolution;
implement this by racing the existing completion promise against a timeout
promise (or invoking rejectOnce from the timer) in the function that creates
resolveOnce/rejectOnce and do the same for the second identical block referenced
(the block around identifiers completion, resolveOnce, rejectOnce and the
Promise returned later at lines ~138-146) so the CLI never waits indefinitely.

In `@packages/database-integrations/src/database-integration-env-vars.ts`:
- Around line 79-96: The current branch treats a missing/undefined accessToken
the same as an unsupported federated_auth_method and pushes "Unsupported
federated auth method"; update the conditional in the block handling
integration.type === 'trino' && integration.federated_auth_method ===
'trino-oauth' to first check for a truthy accessToken and, if missing, push a
clear error describing the missing/expired token (include integration.id and
federated_auth_method for context) instead of falling through to the generic
unsupported-auth error; keep the existing path that calls
buildTrinoOAuthSqlAlchemyInput and pushes envVarsForThisIntegration when
accessToken is present, and leave errors.push(new Error(`Unsupported federated
auth method: ${integration.federated_auth_method}`)) only for truly unsupported
methods.

In `@packages/database-integrations/src/federated-auth-tokens.ts`:
- Around line 127-130: Token files are being created with default filesystem
permissions that can be group/world-readable; update the write path in the code
that builds and persists tokens (the block that computes dir, calls fs.mkdir,
builds content via stringify and calls fs.writeFile) to create the directory and
file with restrictive modes (e.g., mkdir with mode 0o700 and fs.writeFile with
mode 0o600) so only the owner can read/write the token file; ensure you pass the
mode option to fs.writeFile and to fs.mkdir (or perform a chmod after creation)
when writing the updatedTokens to filePath.

---

Duplicate comments:
In `@packages/cli/src/federated-auth/oauth-local-server.ts`:
- Around line 116-117: The HTML response embeds untrusted err.name and
err.message directly; escape both values before interpolation to prevent XSS. In
oauth-local-server.ts where html is constructed (the const html = `...` line
inside the error handler that uses err and res), replace the direct
interpolation with escaped versions (use an HTML-escape helper function or small
utility that replaces &, <, >, ", ' with entities) and then send the escaped
content via res.status(400).send(html); ensure the helper is imported or defined
near that handler and used for both err.name and err.message.
- Around line 127-136: The current app.listen call creates server without
handling bind errors; update the logic around server = app.listen(OAUTH_PORT,
...) to attach an 'error' handler on the returned server (server.on('error',
...)) that catches failures like EADDRINUSE and rejects/throw a deterministic
error for the OAuth flow instead of letting it hang; ensure the same promise or
control flow that resolves on successful listen is rejected when server emits an
error, and include clear context (port/OAUTH_PORT and startURL) in the rejection
path so callers can handle the failure.

In `@packages/cli/src/federated-auth/token-refresh.test.ts`:
- Around line 55-132: The test suite for refreshAccessTokenAndSave in
token-refresh.test.ts lacks coverage for error paths (invalid_grant,
invalid_client/unauthorized_client, non-OK HTTP responses, invalid JSON
responses, and failures when saving tokens); add tests that stub global fetch
(mockFetch) to return: 1) a 200 with { error: 'invalid_grant' } and assert the
function rejects with a clear error, 2) a 400/401 with body indicating
invalid_client/unauthorized_client and assert rejection, 3) a non-ok response
(e.g., ok: false) and assert rejection, 4) a resolved fetch whose json() throws
(invalid JSON) and assert rejection, and 5) simulate a successful token response
but make the token-save path fail (mock file write or the internal save
function) and assert the save error is propagated; reference
refreshAccessTokenAndSave, mockFetch/vi.stubGlobal('fetch'), and the test token
fixtures to locate where to insert each new it() case.

In `@packages/database-integrations/src/federated-auth-tokens.ts`:
- Around line 207-214: The current block around isTokenExpired(tokenEntry)
swallows any error from refreshAccessToken(tokenEntry, integration) or
saveTokenForIntegration(finalToken, tokensFilePath) by returning undefined;
change the catch to propagate the error (include the caught error details)
instead of returning undefined so callers can handle or surface auth/config
failures — for example rethrow the caught error or throw a new Error that wraps
it; update the surrounding logic that expects undefined accordingly. Ensure this
change is applied to the tokens refresh flow involving finalToken,
refreshAccessToken, saveTokenForIntegration and tokenEntry.
- Around line 137-152: In refreshAccessToken, ensure the provided tokenEntry is
bound to the same integration before posting the refresh token: validate that
tokenEntry's integration identifier (e.g., tokenEntry.integrationId or
equivalent field on FederatedAuthTokenEntry) matches the integration passed in
(integration.id or another unique integration key), and if not throw an error;
only call fetchTokenWithTimeout(tokenUrl, clientId, clientSecret,
tokenEntry.refreshToken) after this check to prevent sending a refreshToken to
the wrong tokenUrl.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 42c43e0 and 560b658.

📒 Files selected for processing (10)
  • packages/cli/src/commands/integrations/auth-integration.ts
  • packages/cli/src/federated-auth/federated-auth-tokens-schema.test.ts
  • packages/cli/src/federated-auth/federated-auth-tokens-schema.ts
  • packages/cli/src/federated-auth/federated-auth-tokens.test.ts
  • packages/cli/src/federated-auth/oauth-local-server.ts
  • packages/cli/src/federated-auth/token-refresh.test.ts
  • packages/cli/src/federated-auth/token-refresh.ts
  • packages/database-integrations/src/database-integration-env-vars.ts
  • packages/database-integrations/src/federated-auth-tokens.ts
  • packages/database-integrations/src/index.ts

Comment on lines +83 to +108
function extractTrinoOAuthParams(metadata: Record<string, unknown>): {
authUrl: string
tokenUrl: string
clientId: string
clientSecret: string
} {
const authUrl = metadata.authUrl
const tokenUrl = metadata.tokenUrl
const clientId = metadata.clientId
const clientSecret = metadata.clientSecret

if (typeof authUrl !== 'string' || !authUrl) {
throw new Error('Trino OAuth authUrl is required. Edit your integration and set authUrl in metadata.')
}
if (typeof tokenUrl !== 'string' || !tokenUrl) {
throw new Error('Trino OAuth tokenUrl is required. Edit your integration and set tokenUrl in metadata.')
}
if (typeof clientId !== 'string' || !clientId) {
throw new Error('Trino OAuth clientId is required. Edit your integration and set clientId in metadata.')
}
if (typeof clientSecret !== 'string' || !clientSecret) {
throw new Error('Trino OAuth clientSecret is required. Edit your integration and set clientSecret in metadata.')
}

return { authUrl, tokenUrl, clientId, clientSecret }
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider zod schema for param extraction.

Manual checks work but a zod schema would be more declarative and provide better error messages automatically.

Alternative with zod
const trinoOAuthParamsSchema = z.object({
  authUrl: z.string().min(1, 'Trino OAuth authUrl is required'),
  tokenUrl: z.string().min(1, 'Trino OAuth tokenUrl is required'),
  clientId: z.string().min(1, 'Trino OAuth clientId is required'),
  clientSecret: z.string().min(1, 'Trino OAuth clientSecret is required'),
})

function extractTrinoOAuthParams(metadata: Record<string, unknown>) {
  const result = trinoOAuthParamsSchema.safeParse(metadata)
  if (!result.success) {
    throw new Error(result.error.issues.map(i => i.message).join('; '))
  }
  return result.data
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/commands/integrations/auth-integration.ts` around lines 83 -
108, Replace the manual type-and-empty checks in extractTrinoOAuthParams with a
zod schema validation: define a trinoOAuthParamsSchema using z.object({ authUrl:
z.string().min(1, ...), tokenUrl: z.string().min(1,...), clientId:
z.string().min(1,...), clientSecret: z.string().min(1,...) }), run
safeParse(metadata) inside extractTrinoOAuthParams, and on failure throw a
single Error composed from result.error.issues messages; on success return
result.data (the typed authUrl, tokenUrl, clientId, clientSecret). Ensure zod is
imported.

Comment on lines +197 to +203
} catch (error) {
if (error instanceof Error && error.name === 'ExitPromptError') {
program.error(chalk.yellow('Cancelled.'), { exitCode: ExitCode.Error })
}
const message = error instanceof Error ? error.message : String(error)
program.error(chalk.red(message), { exitCode: ExitCode.Error })
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Missing return after ExitPromptError handling.

After calling program.error() on line 199, execution continues to line 201-202, calling program.error() twice.

Fix
       if (error instanceof Error && error.name === 'ExitPromptError') {
-        program.error(chalk.yellow('Cancelled.'), { exitCode: ExitCode.Error })
+        return program.error(chalk.yellow('Cancelled.'), { exitCode: ExitCode.Error })
       }
       const message = error instanceof Error ? error.message : String(error)
       program.error(chalk.red(message), { exitCode: ExitCode.Error })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/commands/integrations/auth-integration.ts` around lines 197
- 203, In the catch block handling errors in the auth integration, stop
execution after handling ExitPromptError by returning (or using an else) right
after the program.error(chalk.yellow('Cancelled.'), { exitCode: ExitCode.Error
}) call so the subsequent generic error handler (program.error(chalk.red(...), {
exitCode: ExitCode.Error })) is not executed for ExitPromptError; update the
catch in the function containing that try/catch to either add a return after the
ExitPromptError branch or convert it to an if/else.

Comment on lines +79 to +96
if (accessToken && integration.type === 'trino' && integration.federated_auth_method === 'trino-oauth') {
const metadata = integration.metadata as Extract<
DatabaseIntegrationMetadataByType['trino'],
{ authMethod: typeof TrinoAuthMethods.Oauth }
>
const sqlAlchemyInput = buildTrinoOAuthSqlAlchemyInput(
integration.id,
metadata,
accessToken,
params.projectRootDirectory
)
envVarsForThisIntegration.push({
name: getSqlEnvVarName(integration.id),
value: JSON.stringify({ integration_id: integration.id, ...sqlAlchemyInput }),
})
} else {
errors.push(new Error(`Unsupported federated auth method: ${integration.federated_auth_method}`))
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Misleading error when token is missing.

If accessToken is undefined (token not found/expired), code falls through to line 95 and throws "Unsupported federated auth method" — incorrect diagnosis.

Proposed fix
-          if (accessToken && integration.type === 'trino' && integration.federated_auth_method === 'trino-oauth') {
+          if (integration.type === 'trino' && integration.federated_auth_method === 'trino-oauth') {
+            if (!accessToken) {
+              // Token not available; skip SQL env var injection (user needs to authenticate)
+              continue
+            }
             const metadata = integration.metadata as Extract<
               DatabaseIntegrationMetadataByType['trino'],
               { authMethod: typeof TrinoAuthMethods.Oauth }
             >
             const sqlAlchemyInput = buildTrinoOAuthSqlAlchemyInput(
               integration.id,
               metadata,
               accessToken,
               params.projectRootDirectory
             )
             envVarsForThisIntegration.push({
               name: getSqlEnvVarName(integration.id),
               value: JSON.stringify({ integration_id: integration.id, ...sqlAlchemyInput }),
             })
           } else {
             errors.push(new Error(`Unsupported federated auth method: ${integration.federated_auth_method}`))
           }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/database-integrations/src/database-integration-env-vars.ts` around
lines 79 - 96, The current branch treats a missing/undefined accessToken the
same as an unsupported federated_auth_method and pushes "Unsupported federated
auth method"; update the conditional in the block handling integration.type ===
'trino' && integration.federated_auth_method === 'trino-oauth' to first check
for a truthy accessToken and, if missing, push a clear error describing the
missing/expired token (include integration.id and federated_auth_method for
context) instead of falling through to the generic unsupported-auth error; keep
the existing path that calls buildTrinoOAuthSqlAlchemyInput and pushes
envVarsForThisIntegration when accessToken is present, and leave errors.push(new
Error(`Unsupported federated auth method:
${integration.federated_auth_method}`)) only for truly unsupported methods.

Comment on lines +127 to +130
const dir = path.dirname(filePath)
await fs.mkdir(dir, { recursive: true })
const content = stringify({ tokens: updatedTokens }, { lineWidth: 0 })
await fs.writeFile(filePath, content, 'utf-8')
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Persist token files with restrictive permissions.

Refresh/access tokens are written with default filesystem modes. On common umask settings, this can make secrets group/world-readable.

Suggested fix
   const dir = path.dirname(filePath)
-  await fs.mkdir(dir, { recursive: true })
+  await fs.mkdir(dir, { recursive: true, mode: 0o700 })
   const content = stringify({ tokens: updatedTokens }, { lineWidth: 0 })
-  await fs.writeFile(filePath, content, 'utf-8')
+  await fs.writeFile(filePath, content, { encoding: 'utf-8', mode: 0o600 })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const dir = path.dirname(filePath)
await fs.mkdir(dir, { recursive: true })
const content = stringify({ tokens: updatedTokens }, { lineWidth: 0 })
await fs.writeFile(filePath, content, 'utf-8')
const dir = path.dirname(filePath)
await fs.mkdir(dir, { recursive: true, mode: 0o700 })
const content = stringify({ tokens: updatedTokens }, { lineWidth: 0 })
await fs.writeFile(filePath, content, { encoding: 'utf-8', mode: 0o600 })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/database-integrations/src/federated-auth-tokens.ts` around lines 127
- 130, Token files are being created with default filesystem permissions that
can be group/world-readable; update the write path in the code that builds and
persists tokens (the block that computes dir, calls fs.mkdir, builds content via
stringify and calls fs.writeFile) to create the directory and file with
restrictive modes (e.g., mkdir with mode 0o700 and fs.writeFile with mode 0o600)
so only the owner can read/write the token file; ensure you pass the mode option
to fs.writeFile and to fs.mkdir (or perform a chmod after creation) when writing
the updatedTokens to filePath.

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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/cli/src/commands/integrations/edit-integration.ts`:
- Around line 294-297: The catch block after calling resolveEnvVarRefsFromMap
sets debug using an interpolated unknown error; update the catch to explicitly
stringify the caught value before logging (e.g., use String(error) or check
error instanceof Error and use error.message/stack) so the debug call for
integrationRawJson resolution includes a safe, explicit error string; modify the
debug invocation that references resolveEnvVarRefsFromMap and integrationRawJson
to use that stringified error.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 560b658 and ccf516f.

📒 Files selected for processing (1)
  • packages/cli/src/commands/integrations/edit-integration.ts

Comment on lines 294 to 297
integrationRawJson = resolveEnvVarRefsFromMap(integrationRawJson, envVars)
} catch (error) {
debug('Failed to resolve env: refs in integration metadata:', error)
debug(`Failed to resolve env: refs in integration metadata: ${error}`)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider explicit error stringification.

error is unknown in catch blocks. Template interpolation works but explicit conversion is safer.

♻️ Suggested fix
   } catch (error) {
-    debug(`Failed to resolve env: refs in integration metadata: ${error}`)
+    debug(`Failed to resolve env: refs in integration metadata: ${error instanceof Error ? error.message : String(error)}`)
   }
📝 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
integrationRawJson = resolveEnvVarRefsFromMap(integrationRawJson, envVars)
} catch (error) {
debug('Failed to resolve env: refs in integration metadata:', error)
debug(`Failed to resolve env: refs in integration metadata: ${error}`)
}
integrationRawJson = resolveEnvVarRefsFromMap(integrationRawJson, envVars)
} catch (error) {
debug(`Failed to resolve env: refs in integration metadata: ${error instanceof Error ? error.message : String(error)}`)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/commands/integrations/edit-integration.ts` around lines 294
- 297, The catch block after calling resolveEnvVarRefsFromMap sets debug using
an interpolated unknown error; update the catch to explicitly stringify the
caught value before logging (e.g., use String(error) or check error instanceof
Error and use error.message/stack) so the debug call for integrationRawJson
resolution includes a safe, explicit error string; modify the debug invocation
that references resolveEnvVarRefsFromMap and integrationRawJson to use that
stringified error.

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

♻️ Duplicate comments (2)
packages/cli/src/federated-auth/federated-auth-tokens.ts (1)

174-184: ⚠️ Potential issue | 🟡 Minor

Upsert can silently drop existing invalid entries

Line 176 ignores parse issues; rewriting on Line 184 can discard malformed entries without warning.

Suggested fix
 export async function saveTokenForIntegration(entry: FederatedAuthTokenEntry, filePath?: string): Promise<void> {
   const resolvedPath = filePath ?? getDefaultTokensFilePath()
-  const { tokens } = await readTokensFile(resolvedPath)
+  const { tokens, issues } = await readTokensFile(resolvedPath)
+  if (issues.length > 0) {
+    throw new Error(`Cannot safely update tokens file at ${resolvedPath}: found ${issues.length} invalid entry issue(s).`)
+  }

   const existingIndex = tokens.findIndex(t => t.integrationId === entry.integrationId)
   const updatedTokens =
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/federated-auth/federated-auth-tokens.ts` around lines 174 -
184, The saveTokenForIntegration function currently ignores parse issues
returned by readTokensFile and may overwrite the tokens file, discarding
malformed entries; update saveTokenForIntegration to handle readTokensFile's
issues: call readTokensFile(resolvedPath) and inspect the returned issues array,
and if issues.length > 0 either (a) surface/log/throw a descriptive error
including those issues so the user can fix the file, or (b) preserve the
original raw/malformed entries when constructing updatedTokens by merging the
new/updated entry only into the valid tokens array while keeping the raw invalid
entries intact before calling writeTokensFile; reference the
saveTokenForIntegration function, the readTokensFile result (tokens and issues),
and the writeTokensFile call when implementing this change.
packages/database-integrations/src/database-integration-env-vars.ts (1)

78-101: ⚠️ Potential issue | 🟠 Major

Federated Trino branch still conflates valid cases

Lines 78-101 currently mix up:

  1. metadata-only Trino OAuth detection,
  2. missing/expired token, and
  3. truly unsupported auth method.

That causes misleading errors and can skip valid federated configs.

Suggested fix
-      const isFederated =
-        integration.federated_auth_method != null && isFederatedAuthMethod(integration.federated_auth_method)
+      const topLevelFederated =
+        integration.federated_auth_method != null && isFederatedAuthMethod(integration.federated_auth_method)
+      const trinoOauthFromMetadata =
+        integration.type === 'trino' &&
+        'authMethod' in integration.metadata &&
+        integration.metadata.authMethod === TrinoAuthMethods.Oauth
+      const isFederated = topLevelFederated || trinoOauthFromMetadata

       if (isFederated && params.federatedAuthTokenResolver) {
         try {
           const accessToken = await params.federatedAuthTokenResolver(integration)
-          if (accessToken && integration.type === 'trino' && integration.federated_auth_method === 'trino-oauth') {
+          if (integration.type === 'trino' && trinoOauthFromMetadata) {
+            if (!accessToken) {
+              errors.push(new Error(`Missing or expired federated token for integration: ${integration.id}`))
+              continue
+            }
             const metadata = integration.metadata as Extract<
               DatabaseIntegrationMetadataByType['trino'],
               { authMethod: typeof TrinoAuthMethods.Oauth }
             >
             const sqlAlchemyInput = buildTrinoOAuthSqlAlchemyInput(
               integration.id,
               metadata,
               accessToken,
               params.projectRootDirectory
             )
             envVarsForThisIntegration.push({
               name: getSqlEnvVarName(integration.id),
               value: JSON.stringify({ integration_id: integration.id, ...sqlAlchemyInput }),
             })
           } else {
             errors.push(new Error(`Unsupported federated auth method: ${integration.federated_auth_method}`))
           }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/database-integrations/src/database-integration-env-vars.ts` around
lines 78 - 101, The federated Trino branch conflates three cases (valid Trino
OAuth with metadata, missing/expired accessToken, and truly unsupported auth
method); update the logic in the block that calls
params.federatedAuthTokenResolver so that: if integration.type === 'trino' and
integration.federated_auth_method === 'trino-oauth' then validate
metadata.authMethod (the extracted metadata variable) and only when metadata
matches the expected OAuth shape call buildTrinoOAuthSqlAlchemyInput and push
the env var; if accessToken is falsy push a clear token-specific error (or skip
adding the unsupported-method error) instead of treating it as an unsupported
auth method; otherwise (federated_auth_method present but not 'trino-oauth')
push the unsupported-method error using integration.federated_auth_method.
Ensure you reference isFederated, params.federatedAuthTokenResolver,
accessToken, integration.federated_auth_method, integration.type, metadata,
buildTrinoOAuthSqlAlchemyInput, and errors.push when implementing these
branches.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/cli/src/federated-auth/federated-auth-tokens.ts`:
- Around line 171-173: Fix the cspell warning by editing the comment block above
the token upsert function: change the word "Upserts" to singular "Upsert" in the
sentence that reads "Upserts by integrationId - replaces existing or appends."
(Look for the comment immediately above the save/update token logic or the
function that handles token upsert for integrations to update the text.)

In `@packages/cli/src/utils/secure-file.ts`:
- Around line 10-11: The current write step using fs.writeFile(filePath,
content, { mode: 0o600 }) only applies mode on newly created files; update the
logic in secure-file.ts to always harden permissions by calling await
fs.chmod(filePath, 0o600) after the write (after the existing await
fs.writeFile(...) line), ensuring filePath is set to 0o600 regardless of prior
existence; keep the mkdir(...) and writeFile(...) calls as-is and just add the
chmod call (optionally wrap chmod in a try/catch if you need to surface a clear
error).

In `@packages/database-integrations/src/database-integration-env-vars.ts`:
- Around line 102-113: Replace the unsafe casts of unknown to Error in the catch
blocks by normalizing the thrown value before pushing to errors: when catching
in the branches that reference isFederated and getEnvVarForSqlCells (the block
that pushes into envVarsForThisIntegration), convert the caught unknown into a
proper Error instance (e.g., use instanceof Error to keep Error objects or
create a new Error(String(error)) for non-Errors) and then push that normalized
Error into the errors array so errors always contains Error instances without
using `error as Error`.

---

Duplicate comments:
In `@packages/cli/src/federated-auth/federated-auth-tokens.ts`:
- Around line 174-184: The saveTokenForIntegration function currently ignores
parse issues returned by readTokensFile and may overwrite the tokens file,
discarding malformed entries; update saveTokenForIntegration to handle
readTokensFile's issues: call readTokensFile(resolvedPath) and inspect the
returned issues array, and if issues.length > 0 either (a) surface/log/throw a
descriptive error including those issues so the user can fix the file, or (b)
preserve the original raw/malformed entries when constructing updatedTokens by
merging the new/updated entry only into the valid tokens array while keeping the
raw invalid entries intact before calling writeTokensFile; reference the
saveTokenForIntegration function, the readTokensFile result (tokens and issues),
and the writeTokensFile call when implementing this change.

In `@packages/database-integrations/src/database-integration-env-vars.ts`:
- Around line 78-101: The federated Trino branch conflates three cases (valid
Trino OAuth with metadata, missing/expired accessToken, and truly unsupported
auth method); update the logic in the block that calls
params.federatedAuthTokenResolver so that: if integration.type === 'trino' and
integration.federated_auth_method === 'trino-oauth' then validate
metadata.authMethod (the extracted metadata variable) and only when metadata
matches the expected OAuth shape call buildTrinoOAuthSqlAlchemyInput and push
the env var; if accessToken is falsy push a clear token-specific error (or skip
adding the unsupported-method error) instead of treating it as an unsupported
auth method; otherwise (federated_auth_method present but not 'trino-oauth')
push the unsupported-method error using integration.federated_auth_method.
Ensure you reference isFederated, params.federatedAuthTokenResolver,
accessToken, integration.federated_auth_method, integration.type, metadata,
buildTrinoOAuthSqlAlchemyInput, and errors.push when implementing these
branches.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between ccf516f and 3af7ee6.

📒 Files selected for processing (10)
  • packages/cli/src/cli.ts
  • packages/cli/src/commands/integrations/edit-integration.ts
  • packages/cli/src/commands/run.ts
  • packages/cli/src/constants.ts
  • packages/cli/src/federated-auth/federated-auth-tokens.ts
  • packages/cli/src/federated-auth/token-refresh.ts
  • packages/cli/src/integrations/inject-integration-env-vars.ts
  • packages/cli/src/utils/secure-file.ts
  • packages/database-integrations/src/database-integration-env-vars.ts
  • packages/database-integrations/src/index.ts

Comment on lines +171 to +173
* Save or update a token entry for an integration.
* Upserts by integrationId - replaces existing or appends.
*/
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix cspell warning in comment text

Line 172 uses “Upserts”, which is currently failing CSpell in CI. Rename to “Upsert” to clear the warning.

🧰 Tools
🪛 GitHub Actions: CI

[warning] 172-172: CSpell: Unknown word (Upserts).

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

In `@packages/cli/src/federated-auth/federated-auth-tokens.ts` around lines 171 -
173, Fix the cspell warning by editing the comment block above the token upsert
function: change the word "Upserts" to singular "Upsert" in the sentence that
reads "Upserts by integrationId - replaces existing or appends." (Look for the
comment immediately above the save/update token logic or the function that
handles token upsert for integrations to update the text.)

Comment on lines +10 to +11
await fs.mkdir(dir, { recursive: true })
await fs.writeFile(filePath, content, { encoding: 'utf-8', mode: 0o600 })
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Node.js fs/promises.writeFile mode option behavior existing files

💡 Result:

In fs/promises.writeFile(path, data, { mode }), the mode is only applied if the file is newly created; it does not change permissions on an existing file. To change an existing file’s permissions, call chmod/fs.promises.chmod separately. [1]

Also, writeFile with the default flag: 'w' replaces/truncates the file if it already exists; use a different flag (e.g. 'a' to append, 'wx' to fail if the path already exists) if you don’t want that behavior. [2]

Sources:
[1] Node.js File system docs — fsPromises.writeFile: “The mode option only affects the newly created file.” (nodejs.org)
[2] Node.js File system docs — fs.writeFile options (flag default 'w') and “replacing the file if it already exists” (nodejs.org)

Citations:


mode: 0o600 does not apply to existing files

On Line 11, writeFile(..., { mode: 0o600 }) only sets permissions on newly created files—it leaves existing files with their original permissions intact. Add fs.chmod() to ensure permissions are always hardened.

Suggested fix
 await fs.mkdir(dir, { recursive: true })
 await fs.writeFile(filePath, content, { encoding: 'utf-8', mode: 0o600 })
+await fs.chmod(filePath, 0o600)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/utils/secure-file.ts` around lines 10 - 11, The current
write step using fs.writeFile(filePath, content, { mode: 0o600 }) only applies
mode on newly created files; update the logic in secure-file.ts to always harden
permissions by calling await fs.chmod(filePath, 0o600) after the write (after
the existing await fs.writeFile(...) line), ensuring filePath is set to 0o600
regardless of prior existence; keep the mkdir(...) and writeFile(...) calls
as-is and just add the chmod call (optionally wrap chmod in a try/catch if you
need to surface a clear error).

Comment on lines +102 to 113
} catch (error) {
errors.push(error as Error)
}
} else if (!isFederated) {
try {
const envVar = getEnvVarForSqlCells(integration, params)
if (envVar) {
envVarsForThisIntegration.push(envVar)
}
} catch (error) {
errors.push(error as Error)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid unchecked unknownError casts

Lines 103 and 112 use error as Error. Normalize first so non-Error throws are preserved safely.

Suggested fix
-        } catch (error) {
-          errors.push(error as Error)
+        } catch (error) {
+          errors.push(error instanceof Error ? error : new Error(String(error)))
         }
       } else if (!isFederated) {
         try {
           const envVar = getEnvVarForSqlCells(integration, params)
           if (envVar) {
             envVarsForThisIntegration.push(envVar)
           }
         } catch (error) {
-          errors.push(error as Error)
+          errors.push(error instanceof Error ? error : new Error(String(error)))
         }
       }

As per coding guidelines: "Prefer type safety over convenience in TypeScript".

📝 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
} catch (error) {
errors.push(error as Error)
}
} else if (!isFederated) {
try {
const envVar = getEnvVarForSqlCells(integration, params)
if (envVar) {
envVarsForThisIntegration.push(envVar)
}
} catch (error) {
errors.push(error as Error)
}
} catch (error) {
errors.push(error instanceof Error ? error : new Error(String(error)))
}
} else if (!isFederated) {
try {
const envVar = getEnvVarForSqlCells(integration, params)
if (envVar) {
envVarsForThisIntegration.push(envVar)
}
} catch (error) {
errors.push(error instanceof Error ? error : new Error(String(error)))
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/database-integrations/src/database-integration-env-vars.ts` around
lines 102 - 113, Replace the unsafe casts of unknown to Error in the catch
blocks by normalizing the thrown value before pushing to errors: when catching
in the branches that reference isFederated and getEnvVarForSqlCells (the block
that pushes into envVarsForThisIntegration), convert the caught unknown into a
proper Error instance (e.g., use instanceof Error to keep Error objects or
create a new Error(String(error)) for non-Errors) and then push that normalized
Error into the errors array so errors always contains Error instances without
using `error as Error`.

tkislan added 4 commits March 4, 2026 09:40
- Introduced comprehensive tests for the `authIntegration` function, covering various scenarios including successful authentication, error handling for missing files, and validation of integration types.
- Added tests for the `runOAuthFlow` function in the local OAuth server, ensuring end-to-end functionality and error handling.
- Refactored the `auth-integration.ts` to improve type handling and integrate the new token saving functionality.
- Enhanced the local OAuth server to better manage server lifecycle and error responses.
- Updated the `runOAuthFlow` function to accept a port parameter, allowing for dynamic configuration of the local server port.
- Introduced `DEFAULT_OAUTH_PORT` constant for default port usage in the `authIntegration` function and local server tests.
- Modified tests to utilize the new port configuration, enhancing flexibility and maintainability.
- Added a new `extractOAuthCredentials` function to handle OAuth credential extraction specifically for Trino integrations.
- Updated the `authIntegration` function to utilize the new extraction method, improving code clarity and maintainability.
- Removed outdated checks for federated authentication methods, streamlining the integration process.
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: 3

♻️ Duplicate comments (3)
packages/cli/src/federated-auth/oauth-local-server.ts (1)

161-169: ⚠️ Potential issue | 🟠 Major

Handle app.listen bind errors explicitly.

Lines 161-169 do not attach a server 'error' handler, so port bind failures are abrupt and unclear.

🛠 Suggested fix
     server = app.listen(port, () => {
       log('Opening browser to authenticate...')
       log('If the browser does not open automatically, visit:')
       log(startURL)
       open(startURL).catch(err => {
         log('Error opening browser:')
         log(err instanceof Error ? err.message : String(err))
       })
     })
+    server.on('error', err => {
+      const listenError = err instanceof Error ? err : new Error(String(err))
+      reject(listenError)
+    })
#!/bin/bash
rg -n "app\\.listen|server\\.on\\('error'" packages/cli/src/federated-auth/oauth-local-server.ts
# Expected: app.listen plus a server.on('error', ...) handler.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/federated-auth/oauth-local-server.ts` around lines 161 -
169, The call to app.listen(...) assigns server but doesn’t attach an 'error'
handler, so bind failures (e.g., EADDRINUSE) crash unclearly; update the server
returned by app.listen (the server variable) to attach server.on('error', ...)
immediately after creation (or supply an error callback to app.listen) and
handle known errors by logging a clear message via log(...) and exiting/cleaning
up (handle error.code like 'EADDRINUSE' and other errors by logging
error.message/stack and process.exit(1) or rejecting the promise used by
oauth-local-server.ts); reference server and app.listen in your change.
packages/cli/src/federated-auth/federated-auth-tokens.ts (2)

171-173: ⚠️ Potential issue | 🟡 Minor

Rename “Upserts” to clear CI.

Line 172 still fails CSpell (Unknown word (Upserts)).

🛠 Suggested fix
- * Upserts by integrationId - replaces existing or appends.
+ * Upsert by integrationId - replaces existing or appends.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/federated-auth/federated-auth-tokens.ts` around lines 171 -
173, Replace the word "Upserts" in the comment block that starts with "Save or
update a token entry for an integration." with a CSpell-friendly phrase; for
example change "Upserts by integrationId - replaces existing or appends." to
"Performs an upsert by integrationId — replaces existing or appends." so the
comment above the token save/update function uses common words and passes the
spellchecker.

174-184: ⚠️ Potential issue | 🟠 Major

saveTokenForIntegration still drops malformed entries on write.

Line 176 reads only valid parsed entries; Line 184 rewrites the file from that subset, so malformed/raw entries are silently lost.

🛠 Minimal mitigation
 export async function saveTokenForIntegration(entry: FederatedAuthTokenEntry, filePath?: string): Promise<void> {
   const resolvedPath = filePath ?? getDefaultTokensFilePath()
-  const { tokens } = await readTokensFile(resolvedPath)
+  const { tokens, issues } = await readTokensFile(resolvedPath)
+  if (issues.length > 0) {
+    console.warn(`Warning: ${issues.length} invalid token entries in ${resolvedPath} will be omitted.`)
+  }

   const existingIndex = tokens.findIndex(t => t.integrationId === entry.integrationId)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/federated-auth/federated-auth-tokens.ts` around lines 174 -
184, The current saveTokenForIntegration (which calls readTokensFile and
writeTokensFile) overwrites the tokens file using only successfully parsed
entries and thus drops any malformed/raw entries; change the flow so
malformed/raw entries are preserved: update readTokensFile (or add a companion
reader) to return both parsed tokens and the unparsed/raw entries (or raw file
content), then in saveTokenForIntegration locate existingIndex within the parsed
tokens, replace or append the parsed entry as you do now, and finally write back
by merging the updated parsed tokens with the preserved raw/unparsed entries (so
writeTokensFile receives a combined list or implement a writer that accepts
parsed + raw) instead of discarding the raw entries. Use the symbols
saveTokenForIntegration, readTokensFile, writeTokensFile, tokens, and entry to
locate the code to change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/cli/src/commands/integrations/auth-integration.test.ts`:
- Around line 13-15: The test mock for '../../federated-auth/oauth-local-server'
is missing the DEFAULT_OAUTH_PORT export and the expectation for runOAuthFlow is
missing the port argument; update the vi.mock call to export both runOAuthFlow
and DEFAULT_OAUTH_PORT (matching the real module) and change the runOAuthFlow
expectation(s) (references to runOAuthFlow in the test) to include the port
parameter when comparing calls/args so the mocked shape and assertions match the
real call signature.

In `@packages/cli/src/federated-auth/oauth-local-server.ts`:
- Around line 74-83: The 5-minute flowTimeout is only cleared in one place,
leaving timers running on early errors; update the completion paths to always
clear the timer by calling clearTimeout(flowTimeout) wherever the flow ends: add
clearTimeout(flowTimeout) inside the reject function (alongside closeServer and
pReject) and also ensure any success path (where pResolve is called) and other
error handlers (the code around verify and the handlers referenced in the
comment) clearTimeout(flowTimeout) before closing the server or
resolving/rejecting so the timeout cannot leak.
- Around line 165-168: The error handler passed to open(startURL).catch uses an
invalid instanceof check (err instanceof err) which throws; update the catch
callback in oauth-local-server.ts to test the thrown value using "err instanceof
Error" and log err.message (or String(err) if not an Error) accordingly so the
handler for open(startURL) safely prints the error message.

---

Duplicate comments:
In `@packages/cli/src/federated-auth/federated-auth-tokens.ts`:
- Around line 171-173: Replace the word "Upserts" in the comment block that
starts with "Save or update a token entry for an integration." with a
CSpell-friendly phrase; for example change "Upserts by integrationId - replaces
existing or appends." to "Performs an upsert by integrationId — replaces
existing or appends." so the comment above the token save/update function uses
common words and passes the spellchecker.
- Around line 174-184: The current saveTokenForIntegration (which calls
readTokensFile and writeTokensFile) overwrites the tokens file using only
successfully parsed entries and thus drops any malformed/raw entries; change the
flow so malformed/raw entries are preserved: update readTokensFile (or add a
companion reader) to return both parsed tokens and the unparsed/raw entries (or
raw file content), then in saveTokenForIntegration locate existingIndex within
the parsed tokens, replace or append the parsed entry as you do now, and finally
write back by merging the updated parsed tokens with the preserved raw/unparsed
entries (so writeTokensFile receives a combined list or implement a writer that
accepts parsed + raw) instead of discarding the raw entries. Use the symbols
saveTokenForIntegration, readTokensFile, writeTokensFile, tokens, and entry to
locate the code to change.

In `@packages/cli/src/federated-auth/oauth-local-server.ts`:
- Around line 161-169: The call to app.listen(...) assigns server but doesn’t
attach an 'error' handler, so bind failures (e.g., EADDRINUSE) crash unclearly;
update the server returned by app.listen (the server variable) to attach
server.on('error', ...) immediately after creation (or supply an error callback
to app.listen) and handle known errors by logging a clear message via log(...)
and exiting/cleaning up (handle error.code like 'EADDRINUSE' and other errors by
logging error.message/stack and process.exit(1) or rejecting the promise used by
oauth-local-server.ts); reference server and app.listen in your change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: d7c149f0-83a7-4107-8c4f-11de79da5ebb

📥 Commits

Reviewing files that changed from the base of the PR and between 3af7ee6 and aaa9d88.

📒 Files selected for processing (5)
  • packages/cli/src/commands/integrations/auth-integration.test.ts
  • packages/cli/src/commands/integrations/auth-integration.ts
  • packages/cli/src/federated-auth/federated-auth-tokens.ts
  • packages/cli/src/federated-auth/oauth-local-server.test.ts
  • packages/cli/src/federated-auth/oauth-local-server.ts

Comment on lines +13 to +15
vi.mock('../../federated-auth/oauth-local-server', () => ({
runOAuthFlow: vi.fn(),
}))
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Fix oauth-local-server mock exports and expected runOAuthFlow args.

The mock at Lines 13-15 omits DEFAULT_OAUTH_PORT, which breaks module loading in CI. Also, Line 97 expectation should include port to match the real call.

🛠 Suggested fix
-vi.mock('../../federated-auth/oauth-local-server', () => ({
-  runOAuthFlow: vi.fn(),
-}))
+vi.mock('../../federated-auth/oauth-local-server', async importOriginal => {
+  const actual = await importOriginal<typeof import('../../federated-auth/oauth-local-server')>()
+  return {
+    ...actual,
+    runOAuthFlow: vi.fn(),
+  }
+})

-import { runOAuthFlow } from '../../federated-auth/oauth-local-server'
+import { DEFAULT_OAUTH_PORT, runOAuthFlow } from '../../federated-auth/oauth-local-server'

     expect(mockRunOAuthFlow).toHaveBeenCalledWith({
       integrationId: 'trino-prod',
       authUrl: 'https://idp.example.com/authorize',
       tokenUrl: 'https://idp.example.com/token',
       clientId: 'my-client-id',
       clientSecret: 'my-client-secret',
+      port: DEFAULT_OAUTH_PORT,
     })

Also applies to: 28-29, 97-103

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

In `@packages/cli/src/commands/integrations/auth-integration.test.ts` around lines
13 - 15, The test mock for '../../federated-auth/oauth-local-server' is missing
the DEFAULT_OAUTH_PORT export and the expectation for runOAuthFlow is missing
the port argument; update the vi.mock call to export both runOAuthFlow and
DEFAULT_OAUTH_PORT (matching the real module) and change the runOAuthFlow
expectation(s) (references to runOAuthFlow in the test) to include the port
parameter when comparing calls/args so the mocked shape and assertions match the
real call signature.

Comment on lines +74 to +83
const reject = (err: Error) => {
closeServer()
pReject(err)
}

const flowTimeoutMs = 5 * 60 * 1000
const flowTimeout = setTimeout(() => {
reject(new Error('OAuth flow timed out. Please try again.'))
}, flowTimeoutMs)

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Clear flow timeout on every completion path.

The timer is cleared at Line 102 only. Errors before verify still leave a live 5-minute timeout.

🛠 Suggested fix
   return new Promise<FederatedAuthTokenEntry>((pResolve, pReject) => {
     let server: Server | null = null
+    let flowTimeout: NodeJS.Timeout | null = null
+
+    const clearFlowTimeout = () => {
+      if (flowTimeout != null) {
+        clearTimeout(flowTimeout)
+        flowTimeout = null
+      }
+    }

     const closeServer = () => {
       if (server != null) {
         server.closeAllConnections()
         server.close()
         server = null
       }
     }

     const resolve = (entry: FederatedAuthTokenEntry) => {
+      clearFlowTimeout()
       closeServer()
       pResolve(entry)
     }

     const reject = (err: Error) => {
+      clearFlowTimeout()
       closeServer()
       pReject(err)
     }

     const flowTimeoutMs = 5 * 60 * 1000
-    const flowTimeout = setTimeout(() => {
+    flowTimeout = setTimeout(() => {
       reject(new Error('OAuth flow timed out. Please try again.'))
     }, flowTimeoutMs)

Also applies to: 95-103, 143-152

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

In `@packages/cli/src/federated-auth/oauth-local-server.ts` around lines 74 - 83,
The 5-minute flowTimeout is only cleared in one place, leaving timers running on
early errors; update the completion paths to always clear the timer by calling
clearTimeout(flowTimeout) wherever the flow ends: add clearTimeout(flowTimeout)
inside the reject function (alongside closeServer and pReject) and also ensure
any success path (where pResolve is called) and other error handlers (the code
around verify and the handlers referenced in the comment)
clearTimeout(flowTimeout) before closing the server or resolving/rejecting so
the timeout cannot leak.

Comment on lines +165 to +168
open(startURL).catch(err => {
log('Error opening browser:')
log(err instanceof err ? err.message : String(err))
})
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

cat -n packages/cli/src/federated-auth/oauth-local-server.ts | sed -n '165,168p'

Repository: deepnote/deepnote

Length of output: 231


🏁 Script executed:

rg -n "instanceof err" packages/cli/src/federated-auth/oauth-local-server.ts

Repository: deepnote/deepnote

Length of output: 124


Fix invalid instanceof check in error logging.

Line 167 uses err instanceof err, which throws TypeError at runtime since the right operand must be a constructor. Use err instanceof Error instead.

Fix
       open(startURL).catch(err => {
         log('Error opening browser:')
-        log(err instanceof err ? err.message : String(err))
+        log(err instanceof Error ? err.message : String(err))
       })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/federated-auth/oauth-local-server.ts` around lines 165 -
168, The error handler passed to open(startURL).catch uses an invalid instanceof
check (err instanceof err) which throws; update the catch callback in
oauth-local-server.ts to test the thrown value using "err instanceof Error" and
log err.message (or String(err) if not an Error) accordingly so the handler for
open(startURL) safely prints the error message.

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