feat(integrations): Add support for federated auth integrations, starting with BigQuery#399
feat(integrations): Add support for federated auth integrations, starting with BigQuery#399tkislan wants to merge 12 commits into
Conversation
Lay the foundation for BigQuery Google OAuth integration: DI symbols, shared type contracts, command/localization keys, and vendored @deepnote/blocks helpers required to generate federated SQL cell code without persisting access tokens. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Boot the federated-auth runtime: encrypted refresh-token storage with metadata-fingerprint stale detection, a Google OAuth strategy that owns its own verify callback, a session-less PKCE store, and a loopback flow that mints a refresh token via the user's browser. No access token is persisted — refresh is per-execution. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…xecution (M3) Each federated SQL cell now runs through a code generator that mints a fresh access token from Google before execution, defines it in a kernel-side variable via a silent prelude (store_history disabled), and references that variable from the cell's main Python — so the access token never lands in Jupyter's In[] history. Plumbs the generator through KernelProvider → NotebookKernelExecution → CellExecutionFactory → CellExecution as an optional dep so the web build degrades gracefully. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…d (M4) Wire the federated auth flow into reachable surfaces: a "Google OAuth" option on the BigQuery integration form, an Authenticate button with status pill in the integration list, a command that drives the loopback flow on desktop (and a clear "not supported" toast on web/remote), and all the localization plumbing. Stale tokens are cleared automatically when OAuth metadata changes, and federated integrations skip the kernel-startup env-var batch so the per-cell refresh path owns credential delivery. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add the two activation services that keep federated-auth state coherent across the extension host: a bridge that restarts kernels when a token changes (so the next cell starts with a clean os.environ) and an orphan cleaner that drops stored tokens for integrations that no longer exist. Also distinguish the misconfigured-OAuth-client error from the not-authenticated error in cell execution, so the user gets a clear "verify your credentials in integration settings" message rather than a generic re-auth prompt. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The federated-auth code generator persists rotated refresh tokens during
cell preparation. That save was firing onDidChangeTokens, which the
kernel-restart bridge handled by queuing a kernel.restart() — landing
microseconds later while the very SQL cell that triggered the rotation
was still executing.
The new refresh token doesn't change anything the kernel sees: the
extension mints a fresh access token via the per-cell pre-execute on
every SQL block run, so there's no stale state to clear. Add a
{ silent: true } option to FederatedAuthTokenStorage.save() and use it
on the rotation path. Initial-auth and metadata-change saves keep
firing the event as before.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
📝 WalkthroughWalkthroughThis PR implements federated authentication for BigQuery integrations in the VS Code Deepnote extension. It introduces OAuth-based token management with refresh logic, a loopback browser flow for acquiring credentials, SQL code generation that embeds fresh access tokens, and extension services that manage token lifecycle. The webview now displays token status and supports re-authentication. Cell execution conditionally runs a silent prelude to set connection context before executing federated SQL blocks. Kernel restarts are triggered when tokens change, and stale tokens are pruned when integrations are removed. Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #399 +/- ##
===========================
===========================
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Actionable comments posted: 9
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/kernels/execution/cellExecution.federatedAuth.unit.test.ts`:
- Around line 217-225: Replace the multiple individual property assertions on
the first call with a single assert.deepStrictEqual comparing the full request
payload: build an expected object for the prelude (e.g., { code: prelude,
silent: true, store_history: false, allow_stdin: false, stop_on_error: true })
and call assert.deepStrictEqual(preludeContent, expectedPrelude); keep the
existing boolean check for preludeDispose if desired or include it in a second
deepStrictEqual against the full args tuple; do the same refactor for the second
call (calls[1] / its requestArgs/requestContent) referenced in the comment so
each payload comparison uses a single assert.deepStrictEqual rather than many
assert.strictEqual calls.
In `@src/kernels/execution/cellExecution.ts`:
- Around line 486-500: The current await of
kernelConnection.requestExecute(...).done ignores execute_reply content.status
so errors inside the federated prelude don't throw; change the call to capture
the reply (e.g., const reply = await
kernelConnection.requestExecute({...}).done), then inspect reply.content?.status
and if it equals "error" call logger.error with context including
federated.prelude and the reply content, and return
this.completedWithErrors(reply) (or an Error created from the reply) to stop
main execution; keep existing try/catch for transport exceptions around the same
kernelConnection.requestExecute call and use federated.prelude,
kernelConnection.requestExecute, this.completedWithErrors, and logger.error to
locate where to implement the check.
In
`@src/notebooks/deepnote/integrations/federatedAuth/federatedAuthInvariants.unit.test.ts`:
- Around line 10-13: The current conditional type distributes over the union
'accessToken' | 'expiresAt' causing a false negative; update the invariant to
test the union as a whole by using a non-distributive or aggregate check (for
example, use Extract<keyof FederatedAuthTokenEntry, Forbidden> extends never ?
true : never or wrap Forbidden in a tuple like [Forbidden] extends [keyof
FederatedAuthTokenEntry] ?) so that the test fails if any forbidden key is
present; change the AssertEntryOmitsForbidden definition that references
FederatedAuthTokenEntry and Forbidden accordingly and keep the rest of the
assertion (_entryShapeCheck and void usage) intact.
In
`@src/notebooks/deepnote/integrations/federatedAuth/federatedAuthKernelRestartBridge.node.unit.test.ts`:
- Line 83: Replace the repeated magic-number waits by extracting a named
constant and helper: add a top-of-module constant (e.g., const TEST_WAIT_MS =
10) and a small async helper function (e.g., async function waitMs(ms =
TEST_WAIT_MS) { return new Promise(resolve => setTimeout(resolve, ms)); }) and
then replace every occurrence of await new Promise((resolve) =>
setTimeout(resolve, 10)); in the tests with await waitMs(); so fixtures and
tests (references in federatedAuthKernelRestartBridge.node.unit.test.ts) use the
shared constant and helper to avoid duplication.
In
`@src/notebooks/deepnote/integrations/federatedAuth/federatedAuthOrphanedTokenCleaner.node.ts`:
- Around line 31-33: In FederatedAuthOrphanedTokenCleaner, the activate() method
is currently a no-op so any orphaned tokens present at startup are never
cleaned; update activate() to invoke the existing orphaned-token cleanup routine
used for integration-change events (call the exact method name in this class —
e.g., cleanOrphanedTokens(), runOrphanedTokenCleanup(), or similar) so cleanup
runs once on activation, await or handle the returned Promise if the cleanup is
async, and catch/log any errors so activation doesn't crash.
In `@src/notebooks/deepnote/integrations/integrationWebview.ts`:
- Around line 665-666: When deleting an integration, token cleanup
(tokenStorage.delete(integrationId)) must not be allowed to abort the overall
delete/reset flow if it fails; wrap the tokenStorage.delete(integrationId) calls
(the ones at/around where integrationStorage.delete(...) is called in
integrationWebview.ts) in a try/catch, log the error (or call process/project
logger) inside the catch, and do not rethrow so the in-memory and project state
transitions (the integrationStorage.delete and subsequent state updates) always
complete; apply the same pattern to both occurrences (the calls at the two
locations noted).
- Around line 50-52: The event callback passed to tokenStorage.onDidChangeTokens
calls this.updateWebview() without handling rejections, which can produce
unhandled promise rejections; change the callback so it consumes errors from
updateWebview (for example replace void this.updateWebview() with
this.updateWebview().catch(err => { /* handle or log the error */ }) or wrap the
call in an async IIFE with try/catch) so any rejection from updateWebview is
caught; reference tokenStorage.onDidChangeTokens and the updateWebview method
when making the change.
In
`@src/platform/notebooks/deepnote/sqlIntegrationEnvironmentVariablesProvider.ts`:
- Around line 110-116: The Promise.all call loading integration configs will
abort all work if any getIntegrationConfig rejects; change to per-item error
isolation by using Promise.allSettled over projectIntegrations.map(...) (or wrap
each await in try/catch inside the map) to collect only fulfilled results into
allConfigs, filter out rejected/undefined entries, and log the individual errors
(including integration.id) so one failing getIntegrationConfig call in
integrationStorage.getIntegrationConfig does not prevent returning other env
vars (including DuckDB).
In `@src/webviews/webview-side/integrations/BigQueryForm.tsx`:
- Around line 44-55: buildInitialConfig currently returns a blind clone of
existingConfig which can carry an unsupported authMethod into the form; change
it to clone the object (structuredClone(existingConfig)) then validate
existingConfig.authMethod against the supported set (e.g., 'service-account' and
'google-oauth') and if it's not supported, normalize it to a safe default (for
example 'service-account') and clear or reset any auth-specific fields (e.g.,
serviceAccountKey, oauthToken, oauthClientId/secrets) so the form doesn't start
in an invalid state; use the function buildInitialConfig and
createEmptyBigQueryConfig as anchors for where to perform this normalization.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 36bbea8e-d36e-4a6d-b50b-3de8abb8e3e5
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (41)
package.jsonpackage.nls.jsonsrc/kernels/execution/cellExecution.federatedAuth.unit.test.tssrc/kernels/execution/cellExecution.tssrc/kernels/kernelExecution.tssrc/kernels/kernelProvider.node.tssrc/messageTypes.tssrc/notebooks/deepnote/integrations/federatedAuth/federatedAuthCommandHandler.node.tssrc/notebooks/deepnote/integrations/federatedAuth/federatedAuthCommandHandler.node.unit.test.tssrc/notebooks/deepnote/integrations/federatedAuth/federatedAuthCommandHandler.web.tssrc/notebooks/deepnote/integrations/federatedAuth/federatedAuthCommandHandler.web.unit.test.tssrc/notebooks/deepnote/integrations/federatedAuth/federatedAuthInvariants.unit.test.tssrc/notebooks/deepnote/integrations/federatedAuth/federatedAuthKernelRestartBridge.node.tssrc/notebooks/deepnote/integrations/federatedAuth/federatedAuthKernelRestartBridge.node.unit.test.tssrc/notebooks/deepnote/integrations/federatedAuth/federatedAuthOrphanedTokenCleaner.node.tssrc/notebooks/deepnote/integrations/federatedAuth/federatedAuthOrphanedTokenCleaner.node.unit.test.tssrc/notebooks/deepnote/integrations/federatedAuth/federatedAuthSqlBlockCodeGenerator.node.tssrc/notebooks/deepnote/integrations/federatedAuth/federatedAuthSqlBlockCodeGenerator.node.unit.test.tssrc/notebooks/deepnote/integrations/federatedAuth/federatedAuthTokenStorage.node.tssrc/notebooks/deepnote/integrations/federatedAuth/federatedAuthTokenStorage.node.unit.test.tssrc/notebooks/deepnote/integrations/federatedAuth/googleOAuthProvider.node.tssrc/notebooks/deepnote/integrations/federatedAuth/googleOAuthProvider.node.unit.test.tssrc/notebooks/deepnote/integrations/federatedAuth/oauthLoopbackFlow.node.tssrc/notebooks/deepnote/integrations/federatedAuth/oauthLoopbackFlow.node.unit.test.tssrc/notebooks/deepnote/integrations/federatedAuth/vendoredBlocksHelpers.tssrc/notebooks/deepnote/integrations/federatedAuth/vendoredBlocksHelpers.unit.test.tssrc/notebooks/deepnote/integrations/integrationWebview.tssrc/notebooks/deepnote/integrations/integrationWebview.unit.test.tssrc/notebooks/deepnote/integrations/types.tssrc/notebooks/serviceRegistry.node.tssrc/notebooks/serviceRegistry.web.tssrc/platform/common/constants.tssrc/platform/common/utils/localize.tssrc/platform/notebooks/deepnote/integrationTypes.tssrc/platform/notebooks/deepnote/sqlIntegrationEnvironmentVariablesProvider.tssrc/platform/notebooks/deepnote/sqlIntegrationEnvironmentVariablesProvider.unit.test.tssrc/webviews/webview-side/integrations/BigQueryForm.tsxsrc/webviews/webview-side/integrations/IntegrationItem.tsxsrc/webviews/webview-side/integrations/IntegrationList.tsxsrc/webviews/webview-side/integrations/IntegrationPanel.tsxsrc/webviews/webview-side/integrations/types.ts
| // First call: silent prelude. | ||
| const [preludeArgs, preludeDispose] = calls[0].args; | ||
| const preludeContent = preludeArgs as KernelMessage.IExecuteRequestMsg['content']; | ||
| assert.strictEqual(preludeContent.code, prelude); | ||
| assert.strictEqual(preludeContent.silent, true); | ||
| assert.strictEqual(preludeContent.store_history, false); | ||
| assert.strictEqual(preludeContent.allow_stdin, false); | ||
| assert.strictEqual(preludeContent.stop_on_error, true); | ||
| assert.strictEqual(preludeDispose, true); |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Prefer deepStrictEqual for request payload object comparisons.
These assertions are object-shape checks and should use one assert.deepStrictEqual(...) per payload to match the unit-test guideline and reduce drift.
Suggested refactor
- assert.strictEqual(preludeContent.code, prelude);
- assert.strictEqual(preludeContent.silent, true);
- assert.strictEqual(preludeContent.store_history, false);
- assert.strictEqual(preludeContent.allow_stdin, false);
- assert.strictEqual(preludeContent.stop_on_error, true);
+ assert.deepStrictEqual(preludeContent, {
+ code: prelude,
+ silent: true,
+ store_history: false,
+ allow_stdin: false,
+ stop_on_error: true
+ });
...
- assert.strictEqual(mainContent.code, cellCode);
- assert.strictEqual(mainContent.silent, false);
- assert.strictEqual(mainContent.store_history, true);
+ assert.deepStrictEqual(mainContent, {
+ code: cellCode,
+ silent: false,
+ stop_on_error: false,
+ allow_stdin: true,
+ store_history: true
+ });Also applies to: 227-233
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/kernels/execution/cellExecution.federatedAuth.unit.test.ts` around lines
217 - 225, Replace the multiple individual property assertions on the first call
with a single assert.deepStrictEqual comparing the full request payload: build
an expected object for the prelude (e.g., { code: prelude, silent: true,
store_history: false, allow_stdin: false, stop_on_error: true }) and call
assert.deepStrictEqual(preludeContent, expectedPrelude); keep the existing
boolean check for preludeDispose if desired or include it in a second
deepStrictEqual against the full args tuple; do the same refactor for the second
call (calls[1] / its requestArgs/requestContent) referenced in the comment so
each payload comparison uses a single assert.deepStrictEqual rather than many
assert.strictEqual calls.
| await kernelConnection.requestExecute( | ||
| { | ||
| code: federated.prelude, | ||
| silent: true, | ||
| store_history: false, | ||
| allow_stdin: false, | ||
| stop_on_error: true | ||
| }, | ||
| /* dispose: */ true, | ||
| /* metadata: */ undefined | ||
| ).done; | ||
| } catch (ex) { | ||
| logger.error(`Federated pre-execute failed for cell Index ${this.cell.index}`, ex); | ||
| return this.completedWithErrors(ex); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
In @jupyterlab/servicesv7.x, doeskernel.requestExecute(...).donereject onexecute_replywithstatus: "error", or resolve with an error-status reply?
💡 Result:
In @jupyterlab/services v7.x, the.done promise of a kernel future (returned by kernel.requestExecute) resolves with the execute_reply message, even when the kernel returns an execute_reply with status: "error" [1][2][3]. The.done promise is designed to resolve once the kernel has processed the request and sent the final idle status message [1][2]. It does not automatically reject if the code execution results in a kernel-side error [1][2]. If your application needs to handle errors during execution, you must inspect the content of the resolved execute_reply message to check if its status field is "error" [4]. Key points regarding this behavior: - The.done promise fulfills when the shell reply is received, validated, and the idle iopub status is received [1][3]. - The Jupyter messaging protocol distinguishes between protocol-level errors (like message format issues) and execution-level errors, where an execute_reply with status: "error" indicates the code failed to execute in the kernel [4]. - Because.done resolves regardless of whether the execution was successful or resulted in an error, relying on it to catch execution failures will result in "success" states for failed code execution [1][2]. Always check the status of the reply object [4].
Citations:
- 1: https://jupyterlab.readthedocs.io/en/stable/api/classes/services.KernelConnection.html
- 2: https://jupyterlab.readthedocs.io/en/4.0.x/api/interfaces/services.Kernel.IKernelConnection-1.html
- 3: https://github.com/jupyterlab/jupyterlab/blob/master/packages/services/src/kernel/kernel.ts
- 4: https://jupyter-client.readthedocs.io/en/latest/messaging.html
Handle federated prelude execution errors via execute_reply.content.status
.done resolves even when the kernel returns an execute_reply with content.status: "error", so the current try/catch won’t stop main execution on a failed federated prelude.
Suggested fix
- await kernelConnection.requestExecute(
+ const preludeReply = await kernelConnection.requestExecute(
{
code: federated.prelude,
silent: true,
store_history: false,
allow_stdin: false,
stop_on_error: true
},
/* dispose: */ true,
/* metadata: */ undefined
).done;
+ if (preludeReply.content.status === 'error') {
+ return this.completedWithErrors(new KernelError(preludeReply.content), undefined, false);
+ }
} catch (ex) {
logger.error(`Federated pre-execute failed for cell Index ${this.cell.index}`, ex);
return this.completedWithErrors(ex);
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/kernels/execution/cellExecution.ts` around lines 486 - 500, The current
await of kernelConnection.requestExecute(...).done ignores execute_reply
content.status so errors inside the federated prelude don't throw; change the
call to capture the reply (e.g., const reply = await
kernelConnection.requestExecute({...}).done), then inspect reply.content?.status
and if it equals "error" call logger.error with context including
federated.prelude and the reply content, and return
this.completedWithErrors(reply) (or an Error created from the reply) to stop
main execution; keep existing try/catch for transport exceptions around the same
kernelConnection.requestExecute call and use federated.prelude,
kernelConnection.requestExecute, this.completedWithErrors, and logger.error to
locate where to implement the check.
| type Forbidden = 'accessToken' | 'expiresAt'; | ||
| type AssertEntryOmitsForbidden = Forbidden extends keyof FederatedAuthTokenEntry ? never : true; | ||
| const _entryShapeCheck: AssertEntryOmitsForbidden = true; | ||
| void _entryShapeCheck; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="src/notebooks/deepnote/integrations/federatedAuth/federatedAuthInvariants.unit.test.ts"
echo "== File exists? =="
ls -l "$FILE"
echo
echo "== Lines 1-80 =="
nl -ba "$FILE" | sed -n '1,120p'Repository: deepnote/vscode-deepnote
Length of output: 303
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="src/notebooks/deepnote/integrations/federatedAuth/federatedAuthInvariants.unit.test.ts"
echo "== Lines 1-80 (with line numbers) =="
cat -n "$FILE" | sed -n '1,120p'
echo
echo "== Search for FederatedAuthTokenEntry and forbidden invariant =="
rg -n "FederatedAuthTokenEntry|Forbidden|AssertEntryOmitsForbidden|_entryShapeCheck" "$FILE"Repository: deepnote/vscode-deepnote
Length of output: 1956
Fix forbidden-key type invariant in federatedAuthInvariants.unit.test.ts (lines 10-13).
Forbidden extends keyof FederatedAuthTokenEntry ? ... distributes over the 'accessToken' | 'expiresAt' union, so the check only fails when both forbidden keys are present—if only one is added, it still passes.
Proposed fix
type Forbidden = 'accessToken' | 'expiresAt';
-type AssertEntryOmitsForbidden = Forbidden extends keyof FederatedAuthTokenEntry ? never : true;
+type PresentForbidden = Extract<keyof FederatedAuthTokenEntry, Forbidden>;
+type AssertEntryOmitsForbidden = PresentForbidden extends never ? true : never;
const _entryShapeCheck: AssertEntryOmitsForbidden = true;📝 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.
| type Forbidden = 'accessToken' | 'expiresAt'; | |
| type AssertEntryOmitsForbidden = Forbidden extends keyof FederatedAuthTokenEntry ? never : true; | |
| const _entryShapeCheck: AssertEntryOmitsForbidden = true; | |
| void _entryShapeCheck; | |
| type Forbidden = 'accessToken' | 'expiresAt'; | |
| type PresentForbidden = Extract<keyof FederatedAuthTokenEntry, Forbidden>; | |
| type AssertEntryOmitsForbidden = PresentForbidden extends never ? true : never; | |
| const _entryShapeCheck: AssertEntryOmitsForbidden = true; | |
| void _entryShapeCheck; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@src/notebooks/deepnote/integrations/federatedAuth/federatedAuthInvariants.unit.test.ts`
around lines 10 - 13, The current conditional type distributes over the union
'accessToken' | 'expiresAt' causing a false negative; update the invariant to
test the union as a whole by using a non-distributive or aggregate check (for
example, use Extract<keyof FederatedAuthTokenEntry, Forbidden> extends never ?
true : never or wrap Forbidden in a tuple like [Forbidden] extends [keyof
FederatedAuthTokenEntry] ?) so that the test fails if any forbidden key is
present; change the AssertEntryOmitsForbidden definition that references
FederatedAuthTokenEntry and Forbidden accordingly and keep the rest of the
assertion (_entryShapeCheck and void usage) intact.
|
|
||
| onDidChangeTokens.fire('orphan-integration'); | ||
|
|
||
| await new Promise((resolve) => setTimeout(resolve, 10)); |
There was a problem hiding this comment.
Replace repeated timed waits with a shared helper and named constant.
These repeated setTimeout(..., 10) waits are a magic number and duplicate logic across tests.
Proposed refactor
suite('FederatedAuthKernelRestartBridge', () => {
+ const BRIDGE_ASYNC_SETTLE_MS = 10;
+ const waitForBridgeAsyncSettle = () =>
+ new Promise((resolve) => setTimeout(resolve, BRIDGE_ASYNC_SETTLE_MS));
+
let bridge: FederatedAuthKernelRestartBridge;
@@
- await new Promise((resolve) => setTimeout(resolve, 10));
+ await waitForBridgeAsyncSettle();
@@
- await new Promise((resolve) => setTimeout(resolve, 10));
+ await waitForBridgeAsyncSettle();
@@
- await new Promise((resolve) => setTimeout(resolve, 10));
+ await waitForBridgeAsyncSettle();
@@
- await new Promise((resolve) => setTimeout(resolve, 10));
+ await waitForBridgeAsyncSettle();
@@
- await new Promise((resolve) => setTimeout(resolve, 10));
+ await waitForBridgeAsyncSettle();
@@
- await new Promise((resolve) => setTimeout(resolve, 10));
+ await waitForBridgeAsyncSettle();
@@
- await new Promise((resolve) => setTimeout(resolve, 10));
+ await waitForBridgeAsyncSettle();
@@
- await new Promise((resolve) => setTimeout(resolve, 10));
+ await waitForBridgeAsyncSettle();As per coding guidelines, “Extract magic numbers (retry counts, delays, timeouts) as named constants near the top of the module” and “Extract duplicate logic into helper methods to prevent drift following DRY principle.”
Also applies to: 107-107, 131-131, 149-149, 166-166, 189-189, 206-206, 232-232
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@src/notebooks/deepnote/integrations/federatedAuth/federatedAuthKernelRestartBridge.node.unit.test.ts`
at line 83, Replace the repeated magic-number waits by extracting a named
constant and helper: add a top-of-module constant (e.g., const TEST_WAIT_MS =
10) and a small async helper function (e.g., async function waitMs(ms =
TEST_WAIT_MS) { return new Promise(resolve => setTimeout(resolve, ms)); }) and
then replace every occurrence of await new Promise((resolve) =>
setTimeout(resolve, 10)); in the tests with await waitMs(); so fixtures and
tests (references in federatedAuthKernelRestartBridge.node.unit.test.ts) use the
shared constant and helper to avoid duplication.
| public activate(): void { | ||
| // Service is activated via constructor. | ||
| } |
There was a problem hiding this comment.
Run orphaned-token cleanup once on activation.
Right now cleanup runs only after future integration-change events. Existing orphaned tokens at startup are left behind indefinitely if no changes occur.
Proposed fix
public activate(): void {
- // Service is activated via constructor.
+ void this.cleanupOrphanedTokens().catch((err) =>
+ logger.error('FederatedAuthOrphanedTokenCleaner: Initial orphaned token cleanup failed', err)
+ );
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@src/notebooks/deepnote/integrations/federatedAuth/federatedAuthOrphanedTokenCleaner.node.ts`
around lines 31 - 33, In FederatedAuthOrphanedTokenCleaner, the activate()
method is currently a no-op so any orphaned tokens present at startup are never
cleaned; update activate() to invoke the existing orphaned-token cleanup routine
used for integration-change events (call the exact method name in this class —
e.g., cleanOrphanedTokens(), runOrphanedTokenCleanup(), or similar) so cleanup
runs once on activation, await or handle the returned Promise if the cleanup is
async, and catch/log any errors so activation doesn't crash.
| this.tokenStorage.onDidChangeTokens(() => { | ||
| void this.updateWebview(); | ||
| }) |
There was a problem hiding this comment.
Handle token-change refresh failures to avoid unhandled promise rejections.
At Line 50, void this.updateWebview() can reject and bubble as an unhandled rejection from the event callback.
Proposed fix
this.tokenStorageDisposables.push(
this.tokenStorage.onDidChangeTokens(() => {
- void this.updateWebview();
+ void this.updateWebview().catch((err) => {
+ logger.error('IntegrationWebviewProvider: failed to refresh webview after token change', err);
+ });
})
);📝 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.
| this.tokenStorage.onDidChangeTokens(() => { | |
| void this.updateWebview(); | |
| }) | |
| this.tokenStorage.onDidChangeTokens(() => { | |
| void this.updateWebview().catch((err) => { | |
| logger.error('IntegrationWebviewProvider: failed to refresh webview after token change', err); | |
| }); | |
| }) |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/notebooks/deepnote/integrations/integrationWebview.ts` around lines 50 -
52, The event callback passed to tokenStorage.onDidChangeTokens calls
this.updateWebview() without handling rejections, which can produce unhandled
promise rejections; change the callback so it consumes errors from updateWebview
(for example replace void this.updateWebview() with
this.updateWebview().catch(err => { /* handle or log the error */ }) or wrap the
call in an async IIFE with try/catch) so any rejection from updateWebview is
caught; reference tokenStorage.onDidChangeTokens and the updateWebview method
when making the change.
| await this.tokenStorage?.delete(integrationId); | ||
|
|
There was a problem hiding this comment.
Don’t let token cleanup failure break reset/delete state transitions.
At Line 665 and Line 701, token deletion is a secondary cleanup step, but if it throws after integrationStorage.delete() succeeds, the method aborts and leaves in-memory/project state stale.
Proposed fix
+ private async deleteFederatedTokenBestEffort(integrationId: string): Promise<void> {
+ if (!this.tokenStorage) {
+ return;
+ }
+ try {
+ await this.tokenStorage.delete(integrationId);
+ } catch (error) {
+ logger.warn(`IntegrationWebviewProvider: failed to delete federated token for ${integrationId}`, error);
+ }
+ }
private async resetConfiguration(integrationId: string): Promise<void> {
try {
await this.integrationStorage.delete(integrationId);
- await this.tokenStorage?.delete(integrationId);
+ await this.deleteFederatedTokenBestEffort(integrationId);
// ...
} catch (error) {
// ...
}
}
private async deleteConfiguration(integrationId: string): Promise<void> {
try {
await this.integrationStorage.delete(integrationId);
- await this.tokenStorage?.delete(integrationId);
+ await this.deleteFederatedTokenBestEffort(integrationId);
// ...
} catch (error) {
// ...
}
}Also applies to: 701-702
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/notebooks/deepnote/integrations/integrationWebview.ts` around lines 665 -
666, When deleting an integration, token cleanup
(tokenStorage.delete(integrationId)) must not be allowed to abort the overall
delete/reset flow if it fails; wrap the tokenStorage.delete(integrationId) calls
(the ones at/around where integrationStorage.delete(...) is called in
integrationWebview.ts) in a try/catch, log the error (or call process/project
logger) inside the catch, and do not rethrow so the in-memory and project state
transitions (the integrationStorage.delete and subsequent state updates) always
complete; apply the same pattern to both occurrences (the calls at the two
locations noted).
| const allConfigs: Array<DatabaseIntegrationConfig> = ( | ||
| await Promise.all( | ||
| projectIntegrations.map((integration) => { | ||
| return this.integrationStorage.getIntegrationConfig(integration.id); | ||
| }) | ||
| ) | ||
| ).filter((config) => config != null); |
There was a problem hiding this comment.
Harden integration-config loading so one failure doesn’t abort all env vars.
If one getIntegrationConfig() rejects, Promise.all rejects and you return no vars at all (including valid integrations + DuckDB). Use per-item error isolation (allSettled or try/catch in map).
Suggested fix
- const allConfigs: Array<DatabaseIntegrationConfig> = (
- await Promise.all(
- projectIntegrations.map((integration) => {
- return this.integrationStorage.getIntegrationConfig(integration.id);
- })
- )
- ).filter((config) => config != null);
+ const configResults = await Promise.allSettled(
+ projectIntegrations.map((integration) => this.integrationStorage.getIntegrationConfig(integration.id))
+ );
+ const allConfigs: Array<DatabaseIntegrationConfig> = configResults.flatMap((result, index) => {
+ if (result.status === 'fulfilled') {
+ return result.value ? [result.value] : [];
+ }
+ logger.error(
+ `SqlIntegrationEnvironmentVariablesProvider: Failed to load integration config ${projectIntegrations[index].id}`,
+ result.reason
+ );
+ return [];
+ });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@src/platform/notebooks/deepnote/sqlIntegrationEnvironmentVariablesProvider.ts`
around lines 110 - 116, The Promise.all call loading integration configs will
abort all work if any getIntegrationConfig rejects; change to per-item error
isolation by using Promise.allSettled over projectIntegrations.map(...) (or wrap
each await in try/catch inside the map) to collect only fulfilled results into
allConfigs, filter out rejected/undefined entries, and log the individual errors
(including integration.id) so one failing getIntegrationConfig call in
integrationStorage.getIntegrationConfig does not prevent returning other env
vars (including DuckDB).
| function buildInitialConfig( | ||
| existingConfig: BigQueryConfig | null, | ||
| integrationId: string, | ||
| defaultName?: string | ||
| ): BigQueryConfig { | ||
| if (!existingConfig) { | ||
| return createEmptyBigQueryConfig({ id: integrationId, name: defaultName }); | ||
| } | ||
| // Preserve existing config when its auth method is supported. Both | ||
| // service-account and google-oauth are editable in this milestone. | ||
| return structuredClone(existingConfig); | ||
| } |
There was a problem hiding this comment.
Normalize unsupported persisted auth methods in initial state.
At Line 54, cloning existingConfig blindly can trap the form in an invalid state if backend data contains an unexpected authMethod.
Proposed fix
function buildInitialConfig(
existingConfig: BigQueryConfig | null,
integrationId: string,
defaultName?: string
): BigQueryConfig {
if (!existingConfig) {
return createEmptyBigQueryConfig({ id: integrationId, name: defaultName });
}
- // Preserve existing config when its auth method is supported. Both
- // service-account and google-oauth are editable in this milestone.
- return structuredClone(existingConfig);
+ if (!isBigQueryAuthMethod(existingConfig.metadata.authMethod)) {
+ return createEmptyBigQueryConfig({
+ id: integrationId,
+ name: existingConfig.name || defaultName,
+ authMethod: BigQueryAuthMethods.ServiceAccount
+ });
+ }
+ return structuredClone(existingConfig);
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/webviews/webview-side/integrations/BigQueryForm.tsx` around lines 44 -
55, buildInitialConfig currently returns a blind clone of existingConfig which
can carry an unsupported authMethod into the form; change it to clone the object
(structuredClone(existingConfig)) then validate existingConfig.authMethod
against the supported set (e.g., 'service-account' and 'google-oauth') and if
it's not supported, normalize it to a safe default (for example
'service-account') and clear or reset any auth-specific fields (e.g.,
serviceAccountKey, oauthToken, oauthClientId/secrets) so the form doesn't start
in an invalid state; use the function buildInitialConfig and
createEmptyBigQueryConfig as anchors for where to perform this normalization.
@coderabbitai ignore
Summary by CodeRabbit
Release Notes