From 73ae1411388e2c76cbec82e480aec7c59527c1d2 Mon Sep 17 00:00:00 2001 From: Tianzhou Date: Thu, 2 Apr 2026 08:45:03 -0700 Subject: [PATCH 1/5] feat: make cloud provider packages optional Move @aws-sdk/rds-signer and @azure/identity from dependencies to optionalDependencies and switch to dynamic import() so they are only loaded when AWS IAM or Azure AD auth is actually used. Also add both to tsup external list to prevent bundling. Closes #295 Co-Authored-By: Claude Opus 4.6 (1M context) --- package.json | 4 ++-- src/connectors/sqlserver/index.ts | 2 +- src/utils/aws-rds-signer.ts | 4 ++-- tsup.config.ts | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 0f70b4ab..335fa1c4 100644 --- a/package.json +++ b/package.json @@ -40,8 +40,6 @@ "author": "", "license": "MIT", "dependencies": { - "@aws-sdk/rds-signer": "^3.1001.0", - "@azure/identity": "^4.8.0", "@iarna/toml": "^2.2.5", "@modelcontextprotocol/sdk": "^1.25.1", "dotenv": "^16.4.7", @@ -51,6 +49,8 @@ "zod": "^3.24.2" }, "optionalDependencies": { + "@aws-sdk/rds-signer": "^3.1001.0", + "@azure/identity": "^4.8.0", "better-sqlite3": "^11.9.0", "mariadb": "^3.4.0", "mssql": "^11.0.1", diff --git a/src/connectors/sqlserver/index.ts b/src/connectors/sqlserver/index.ts index f89092b4..f630ebd1 100644 --- a/src/connectors/sqlserver/index.ts +++ b/src/connectors/sqlserver/index.ts @@ -11,7 +11,6 @@ import { ExecuteOptions, ConnectorConfig, } from "../interface.js"; -import { DefaultAzureCredential } from "@azure/identity"; import { SafeURL } from "../../utils/safe-url.js"; import { obfuscateDSNPassword } from "../../utils/dsn-obfuscate.js"; import { SQLRowLimiter } from "../../utils/sql-row-limiter.js"; @@ -95,6 +94,7 @@ export class SQLServerDSNParser implements DSNParser { switch (options.authentication) { case "azure-active-directory-access-token": { try { + const { DefaultAzureCredential } = await import("@azure/identity"); const credential = new DefaultAzureCredential(); const token = await credential.getToken("https://database.windows.net/"); config.authentication = { diff --git a/src/utils/aws-rds-signer.ts b/src/utils/aws-rds-signer.ts index a8165e28..b3ec7389 100644 --- a/src/utils/aws-rds-signer.ts +++ b/src/utils/aws-rds-signer.ts @@ -1,5 +1,3 @@ -import { Signer } from "@aws-sdk/rds-signer"; - export interface RdsAuthTokenParams { hostname: string; port: number; @@ -13,6 +11,8 @@ export interface RdsAuthTokenParams { * (AWS CLI profile, env vars, instance role, etc.). */ export async function generateRdsAuthToken(params: RdsAuthTokenParams): Promise { + const { Signer } = await import("@aws-sdk/rds-signer"); + const signer = new Signer({ hostname: params.hostname, port: params.port, diff --git a/tsup.config.ts b/tsup.config.ts index 6a3aa719..0a822deb 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -11,7 +11,7 @@ export default defineConfig({ // Database drivers are optionalDependencies loaded at runtime via dynamic // import(). They must be external so tsup does not bundle their CJS code // into ESM chunks (which causes "Dynamic require of X is not supported"). - external: ['pg', 'mysql2', 'mariadb', 'mssql', 'better-sqlite3'], + external: ['pg', 'mysql2', 'mariadb', 'mssql', 'better-sqlite3', '@aws-sdk/rds-signer', '@azure/identity'], // Copy the employee-sqlite demo data to dist async onSuccess() { // Create target directory From 4b1a60c34f7d230707ea2c5447173a0532acb147 Mon Sep 17 00:00:00 2001 From: Tianzhou Date: Thu, 2 Apr 2026 08:51:08 -0700 Subject: [PATCH 2/5] Address review: update tsup comment and add clear errors for missing cloud packages - Update tsup external comment to mention cloud auth packages - Catch import failures for @aws-sdk/rds-signer and @azure/identity and rethrow with actionable install instructions Co-Authored-By: Claude Opus 4.6 (1M context) --- src/connectors/sqlserver/index.ts | 9 ++++++++- src/utils/aws-rds-signer.ts | 9 ++++++++- tsup.config.ts | 7 ++++--- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/connectors/sqlserver/index.ts b/src/connectors/sqlserver/index.ts index f630ebd1..fd109607 100644 --- a/src/connectors/sqlserver/index.ts +++ b/src/connectors/sqlserver/index.ts @@ -94,7 +94,14 @@ export class SQLServerDSNParser implements DSNParser { switch (options.authentication) { case "azure-active-directory-access-token": { try { - const { DefaultAzureCredential } = await import("@azure/identity"); + let DefaultAzureCredential: typeof import("@azure/identity")["DefaultAzureCredential"]; + try { + ({ DefaultAzureCredential } = await import("@azure/identity")); + } catch { + throw new Error( + 'Azure AD authentication requires the "@azure/identity" package. Install it with: npm install @azure/identity' + ); + } const credential = new DefaultAzureCredential(); const token = await credential.getToken("https://database.windows.net/"); config.authentication = { diff --git a/src/utils/aws-rds-signer.ts b/src/utils/aws-rds-signer.ts index b3ec7389..51f03940 100644 --- a/src/utils/aws-rds-signer.ts +++ b/src/utils/aws-rds-signer.ts @@ -11,7 +11,14 @@ export interface RdsAuthTokenParams { * (AWS CLI profile, env vars, instance role, etc.). */ export async function generateRdsAuthToken(params: RdsAuthTokenParams): Promise { - const { Signer } = await import("@aws-sdk/rds-signer"); + let Signer: typeof import("@aws-sdk/rds-signer")["Signer"]; + try { + ({ Signer } = await import("@aws-sdk/rds-signer")); + } catch { + throw new Error( + 'AWS IAM authentication requires the "@aws-sdk/rds-signer" package. Install it with: npm install @aws-sdk/rds-signer' + ); + } const signer = new Signer({ hostname: params.hostname, diff --git a/tsup.config.ts b/tsup.config.ts index 0a822deb..13f24b7d 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -8,9 +8,10 @@ export default defineConfig({ dts: true, clean: true, outDir: 'dist', - // Database drivers are optionalDependencies loaded at runtime via dynamic - // import(). They must be external so tsup does not bundle their CJS code - // into ESM chunks (which causes "Dynamic require of X is not supported"). + // Optional runtime-loaded dependencies (database drivers and cloud auth + // packages) are optionalDependencies loaded via dynamic import(). They must + // be external so tsup does not bundle their CJS code into ESM chunks (which + // causes "Dynamic require of X is not supported"). external: ['pg', 'mysql2', 'mariadb', 'mssql', 'better-sqlite3', '@aws-sdk/rds-signer', '@azure/identity'], // Copy the employee-sqlite demo data to dist async onSuccess() { From 99f5f26fe755a135ae5b64930fb1ea15aed0489b Mon Sep 17 00:00:00 2001 From: Tianzhou Date: Thu, 2 Apr 2026 09:03:43 -0700 Subject: [PATCH 3/5] Address review: selective error handling and test for missing cloud packages - Only translate ERR_MODULE_NOT_FOUND into install hint, rethrow other errors so real SDK failures are not masked (reuses isDriverNotInstalled) - Use pnpm-appropriate install command in error messages - Refine tsup comment to distinguish CJS driver externals from cloud auth bundle-size externals - Add test verifying isDriverNotInstalled works with scoped package names Co-Authored-By: Claude Opus 4.6 (1M context) --- src/connectors/sqlserver/index.ts | 12 ++++++---- .../__tests__/aws-rds-signer-missing.test.ts | 23 +++++++++++++++++++ src/utils/aws-rds-signer.ts | 13 +++++++---- tsup.config.ts | 8 ++++--- 4 files changed, 45 insertions(+), 11 deletions(-) create mode 100644 src/utils/__tests__/aws-rds-signer-missing.test.ts diff --git a/src/connectors/sqlserver/index.ts b/src/connectors/sqlserver/index.ts index fd109607..751fad78 100644 --- a/src/connectors/sqlserver/index.ts +++ b/src/connectors/sqlserver/index.ts @@ -11,6 +11,7 @@ import { ExecuteOptions, ConnectorConfig, } from "../interface.js"; +import { isDriverNotInstalled } from "../../utils/module-loader.js"; import { SafeURL } from "../../utils/safe-url.js"; import { obfuscateDSNPassword } from "../../utils/dsn-obfuscate.js"; import { SQLRowLimiter } from "../../utils/sql-row-limiter.js"; @@ -97,10 +98,13 @@ export class SQLServerDSNParser implements DSNParser { let DefaultAzureCredential: typeof import("@azure/identity")["DefaultAzureCredential"]; try { ({ DefaultAzureCredential } = await import("@azure/identity")); - } catch { - throw new Error( - 'Azure AD authentication requires the "@azure/identity" package. Install it with: npm install @azure/identity' - ); + } catch (importError) { + if (isDriverNotInstalled(importError, "@azure/identity")) { + throw new Error( + 'Azure AD authentication requires the "@azure/identity" package. Install it with: pnpm add @azure/identity' + ); + } + throw importError; } const credential = new DefaultAzureCredential(); const token = await credential.getToken("https://database.windows.net/"); diff --git a/src/utils/__tests__/aws-rds-signer-missing.test.ts b/src/utils/__tests__/aws-rds-signer-missing.test.ts new file mode 100644 index 00000000..9d09c0e7 --- /dev/null +++ b/src/utils/__tests__/aws-rds-signer-missing.test.ts @@ -0,0 +1,23 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { isDriverNotInstalled } from '../module-loader.js'; + +describe('generateRdsAuthToken missing package', () => { + it('should throw actionable error when @aws-sdk/rds-signer is not installed', async () => { + // Simulate ERR_MODULE_NOT_FOUND for @aws-sdk/rds-signer + const err = new Error( + "Cannot find package '@aws-sdk/rds-signer' imported from /fake/path" + ); + (err as NodeJS.ErrnoException).code = 'ERR_MODULE_NOT_FOUND'; + + expect(isDriverNotInstalled(err, '@aws-sdk/rds-signer')).toBe(true); + }); + + it('should not match unrelated ERR_MODULE_NOT_FOUND errors', () => { + const err = new Error( + "Cannot find package 'some-other-pkg' imported from /fake/path" + ); + (err as NodeJS.ErrnoException).code = 'ERR_MODULE_NOT_FOUND'; + + expect(isDriverNotInstalled(err, '@aws-sdk/rds-signer')).toBe(false); + }); +}); diff --git a/src/utils/aws-rds-signer.ts b/src/utils/aws-rds-signer.ts index 51f03940..6e114785 100644 --- a/src/utils/aws-rds-signer.ts +++ b/src/utils/aws-rds-signer.ts @@ -1,3 +1,5 @@ +import { isDriverNotInstalled } from "./module-loader.js"; + export interface RdsAuthTokenParams { hostname: string; port: number; @@ -14,10 +16,13 @@ export async function generateRdsAuthToken(params: RdsAuthTokenParams): Promise< let Signer: typeof import("@aws-sdk/rds-signer")["Signer"]; try { ({ Signer } = await import("@aws-sdk/rds-signer")); - } catch { - throw new Error( - 'AWS IAM authentication requires the "@aws-sdk/rds-signer" package. Install it with: npm install @aws-sdk/rds-signer' - ); + } catch (error) { + if (isDriverNotInstalled(error, "@aws-sdk/rds-signer")) { + throw new Error( + 'AWS IAM authentication requires the "@aws-sdk/rds-signer" package. Install it with: pnpm add @aws-sdk/rds-signer' + ); + } + throw error; } const signer = new Signer({ diff --git a/tsup.config.ts b/tsup.config.ts index 13f24b7d..e13bedaa 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -9,9 +9,11 @@ export default defineConfig({ clean: true, outDir: 'dist', // Optional runtime-loaded dependencies (database drivers and cloud auth - // packages) are optionalDependencies loaded via dynamic import(). They must - // be external so tsup does not bundle their CJS code into ESM chunks (which - // causes "Dynamic require of X is not supported"). + // packages) are declared as optionalDependencies and loaded via dynamic + // import(). Database drivers must be external so tsup does not bundle their + // CJS code into ESM chunks (which causes "Dynamic require of X is not + // supported"). Cloud auth packages are externalized to keep their large + // dependency trees out of the bundle. external: ['pg', 'mysql2', 'mariadb', 'mssql', 'better-sqlite3', '@aws-sdk/rds-signer', '@azure/identity'], // Copy the employee-sqlite demo data to dist async onSuccess() { From a8d8df13de86e1e8c0ad9367d5650cca4784afd8 Mon Sep 17 00:00:00 2001 From: Tianzhou Date: Thu, 2 Apr 2026 09:25:18 -0700 Subject: [PATCH 4/5] Clean up test: remove unused imports, fix suite name Co-Authored-By: Claude Opus 4.6 (1M context) --- src/utils/__tests__/aws-rds-signer-missing.test.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/utils/__tests__/aws-rds-signer-missing.test.ts b/src/utils/__tests__/aws-rds-signer-missing.test.ts index 9d09c0e7..519e99cc 100644 --- a/src/utils/__tests__/aws-rds-signer-missing.test.ts +++ b/src/utils/__tests__/aws-rds-signer-missing.test.ts @@ -1,9 +1,8 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { describe, it, expect } from 'vitest'; import { isDriverNotInstalled } from '../module-loader.js'; -describe('generateRdsAuthToken missing package', () => { - it('should throw actionable error when @aws-sdk/rds-signer is not installed', async () => { - // Simulate ERR_MODULE_NOT_FOUND for @aws-sdk/rds-signer +describe('isDriverNotInstalled with scoped packages', () => { + it('should match ERR_MODULE_NOT_FOUND for @aws-sdk/rds-signer', () => { const err = new Error( "Cannot find package '@aws-sdk/rds-signer' imported from /fake/path" ); From 85d970eb3c439e6174cd6d90d5aaac4fc5a45d19 Mon Sep 17 00:00:00 2001 From: Tianzhou Date: Thu, 2 Apr 2026 09:49:47 -0700 Subject: [PATCH 5/5] Fix: let missing-package error bypass Azure AD token error wrapping Move the dynamic import of @azure/identity outside the token-fetching try/catch so the install-instruction error propagates directly instead of being wrapped as "Failed to get Azure AD token: ..." Co-Authored-By: Claude Opus 4.6 (1M context) --- src/connectors/sqlserver/index.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/connectors/sqlserver/index.ts b/src/connectors/sqlserver/index.ts index 751fad78..b8cdc1e2 100644 --- a/src/connectors/sqlserver/index.ts +++ b/src/connectors/sqlserver/index.ts @@ -94,18 +94,18 @@ export class SQLServerDSNParser implements DSNParser { // Handle authentication types switch (options.authentication) { case "azure-active-directory-access-token": { + let DefaultAzureCredential: typeof import("@azure/identity")["DefaultAzureCredential"]; try { - let DefaultAzureCredential: typeof import("@azure/identity")["DefaultAzureCredential"]; - try { - ({ DefaultAzureCredential } = await import("@azure/identity")); - } catch (importError) { - if (isDriverNotInstalled(importError, "@azure/identity")) { - throw new Error( - 'Azure AD authentication requires the "@azure/identity" package. Install it with: pnpm add @azure/identity' - ); - } - throw importError; + ({ DefaultAzureCredential } = await import("@azure/identity")); + } catch (importError) { + if (isDriverNotInstalled(importError, "@azure/identity")) { + throw new Error( + 'Azure AD authentication requires the "@azure/identity" package. Install it with: pnpm add @azure/identity' + ); } + throw importError; + } + try { const credential = new DefaultAzureCredential(); const token = await credential.getToken("https://database.windows.net/"); config.authentication = {