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..b8cdc1e2 100644 --- a/src/connectors/sqlserver/index.ts +++ b/src/connectors/sqlserver/index.ts @@ -11,7 +11,7 @@ import { ExecuteOptions, ConnectorConfig, } from "../interface.js"; -import { DefaultAzureCredential } from "@azure/identity"; +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"; @@ -94,6 +94,17 @@ 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 { + ({ 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/"); 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..519e99cc --- /dev/null +++ b/src/utils/__tests__/aws-rds-signer-missing.test.ts @@ -0,0 +1,22 @@ +import { describe, it, expect } from 'vitest'; +import { isDriverNotInstalled } from '../module-loader.js'; + +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" + ); + (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 a8165e28..6e114785 100644 --- a/src/utils/aws-rds-signer.ts +++ b/src/utils/aws-rds-signer.ts @@ -1,4 +1,4 @@ -import { Signer } from "@aws-sdk/rds-signer"; +import { isDriverNotInstalled } from "./module-loader.js"; export interface RdsAuthTokenParams { hostname: string; @@ -13,6 +13,18 @@ export interface RdsAuthTokenParams { * (AWS CLI profile, env vars, instance role, etc.). */ 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 (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({ hostname: params.hostname, port: params.port, diff --git a/tsup.config.ts b/tsup.config.ts index 6a3aa719..e13bedaa 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -8,10 +8,13 @@ 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"). - external: ['pg', 'mysql2', 'mariadb', 'mssql', 'better-sqlite3'], + // Optional runtime-loaded dependencies (database drivers and cloud auth + // 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() { // Create target directory