diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 868a5cc..0904eda 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -337,7 +337,10 @@ jobs: - uses: actions/checkout@v4 - name: setup utils - run: brew tap wix/brew && brew install applesimutils + run: | + brew tap wix/brew + brew trust --formula wix/brew/applesimutils + brew install applesimutils env: HOMEBREW_NO_AUTO_UPDATE: 1 HOMEBREW_NO_INSTALL_CLEANUP: 1 @@ -463,7 +466,10 @@ jobs: - uses: actions/checkout@v4 - name: setup utils - run: brew tap wix/brew && brew install applesimutils + run: | + brew tap wix/brew + brew trust --formula wix/brew/applesimutils + brew install applesimutils env: HOMEBREW_NO_AUTO_UPDATE: 1 HOMEBREW_NO_INSTALL_CLEANUP: 1 diff --git a/src/drivers/connection-tls.ts b/src/drivers/connection-tls.ts index 6c2d798..6245681 100644 --- a/src/drivers/connection-tls.ts +++ b/src/drivers/connection-tls.ts @@ -93,6 +93,13 @@ export class SQLiteCloudTlsConnection extends SQLiteCloudConnection { if (this.config.verbose) { console.debug(`SQLiteCloudTlsConnection - connected to ${this.config.host}, authorized: ${this.socket?.authorized}`) } + if (!initializationCommands) { + if (this.config.verbose) { + console.debug(`SQLiteCloudTlsConnection - initialized connection`) + } + callback?.call(this, null) + return + } this.transportCommands(initializationCommands, error => { if (this.config.verbose) { console.debug(`SQLiteCloudTlsConnection - initialized connection`) diff --git a/src/drivers/connection-ws.ts b/src/drivers/connection-ws.ts index 917118d..40fac06 100644 --- a/src/drivers/connection-ws.ts +++ b/src/drivers/connection-ws.ts @@ -99,8 +99,11 @@ export class SQLiteCloudWebsocketConnection extends SQLiteCloudConnection { // (→ crvheg7dhk.g4.gateway.sqlite.cloud). For local development, pass a `gatewayurl` // containing `localhost` — the driver routes TCP to it and injects the tenant Host header // (with the gateway marker label) so the gateway still tenant-routes correctly. - const authToken = this.config.apikey || this.config.token - const ioOpts: Record = { auth: { token: authToken }, parser: createSocketIOParser(websocketMaxAttachments) } + const authToken = this.config.unauthenticated ? undefined : this.config.apikey || this.config.token + const ioOpts: Record = { parser: createSocketIOParser(websocketMaxAttachments) } + if (authToken) { + ioOpts.auth = { token: authToken } + } let gatewayUrl: string if (this.config.gatewayurl?.includes('localhost')) { const raw = this.config.gatewayurl diff --git a/src/drivers/types.ts b/src/drivers/types.ts index 99689d9..6235e27 100644 --- a/src/drivers/types.ts +++ b/src/drivers/types.ts @@ -58,6 +58,8 @@ export interface SQLiteCloudConfig { apikey?: string /** Access Token provided in place of API Key or username/password */ token?: string + /** Connect without sending AUTH. Only commands explicitly allowed pre-auth can run. */ + unauthenticated?: boolean /** Host name is required unless connectionstring is provided, eg: xxx.sqlitecloud.io */ host?: string diff --git a/src/drivers/utilities.ts b/src/drivers/utilities.ts index fd1b6cd..162f20c 100644 --- a/src/drivers/utilities.ts +++ b/src/drivers/utilities.ts @@ -61,12 +61,14 @@ export function getInitializationCommands(config: SQLiteCloudConfig): string { let commands = 'SET CLIENT KEY NONLINEARIZABLE TO 1;' // first user authentication, then all other commands - if (config.apikey) { - commands += `AUTH APIKEY ${config.apikey};` - } else if (config.token) { - commands += `AUTH TOKEN ${config.token};` - } else { - commands += `AUTH USER ${config.username || ''} ${config.password_hashed ? 'HASH' : 'PASSWORD'} ${config.password || ''};` + if (!config.unauthenticated) { + if (config.apikey) { + commands += `AUTH APIKEY ${config.apikey};` + } else if (config.token) { + commands += `AUTH TOKEN ${config.token};` + } else { + commands += `AUTH USER ${config.username || ''} ${config.password_hashed ? 'HASH' : 'PASSWORD'} ${config.password || ''};` + } } if (config.compression) { @@ -94,7 +96,7 @@ export function getInitializationCommands(config: SQLiteCloudConfig): string { commands += 'SET CLIENT KEY NONLINEARIZABLE TO 0;' } - if (config.database) { + if (!config.unauthenticated && config.database) { if (config.create && !config.memory) { commands += `CREATE DATABASE ${config.database} IF NOT EXISTS;` } @@ -198,14 +200,15 @@ export function validateConfiguration(config: SQLiteCloudConfig): SQLiteCloudCon config.create = parseBoolean(config.create) config.non_linearizable = parseBoolean(config.non_linearizable) config.insecure = parseBoolean(config.insecure) + config.unauthenticated = parseBoolean(config.unauthenticated) const hasCredentials = (config.username && config.password) || config.apikey || config.token - if (!config.host || !hasCredentials) { + if (!config.host || (!config.unauthenticated && !hasCredentials)) { console.error('SQLiteCloudConnection.validateConfiguration - missing arguments', config) throw new SQLiteCloudError('The user, password and host arguments, the ?apikey= or the ?token= must be specified.', { errorCode: 'ERR_MISSING_ARGS' }) } - if (!config.connectionstring) { + if (!config.connectionstring && !config.unauthenticated) { // build connection string from configuration, values are already validated config.connectionstring = `sqlitecloud://${config.host}:${config.port}/${config.database || ''}` if (config.apikey) { @@ -258,6 +261,7 @@ export function parseconnectionstring(connectionstring: string): SQLiteCloudConf memory: options.memory ? parseBoolean(options.memory) : undefined, compression: options.compression ? parseBoolean(options.compression) : undefined, non_linearizable: options.non_linearizable ? parseBoolean(options.non_linearizable) : undefined, + unauthenticated: options.unauthenticated ? parseBoolean(options.unauthenticated) : undefined, noblob: options.noblob ? parseBoolean(options.noblob) : undefined, maxdata: options.maxdata ? parseInt(options.maxdata) : undefined, maxrows: options.maxrows ? parseInt(options.maxrows) : undefined, diff --git a/test/utilities.test.ts b/test/utilities.test.ts index 81feb5f..29c38b0 100644 --- a/test/utilities.test.ts +++ b/test/utilities.test.ts @@ -263,6 +263,26 @@ describe('validateConfiguration()', () => { expect(config.websocketBlobFormat).toBe('base64-blobs-v1') expect(config.websocketMaxAttachments).toBe(100000) }) + + it('should allow unauthenticated configuration without credentials', () => { + const config = validateConfiguration({ + host: 'host', + unauthenticated: true + }) + + expect(config.host).toBe('host') + expect(config.unauthenticated).toBe(true) + expect(config.connectionstring).toBeUndefined() + }) + + it('should parse unauthenticated from connection string params', () => { + const config = validateConfiguration({ + connectionstring: 'sqlitecloud://host:1234?unauthenticated=1' + }) + + expect(config.host).toBe('host') + expect(config.unauthenticated).toBe(true) + }) }) describe('safe integer marker utilities', () => { @@ -326,4 +346,41 @@ describe('getInitializationCommands()', () => { expect(result).toContain('AUTH TOKEN mytoken;') expect(result).not.toContain('AUTH APIKEY') }) + + it('should keep existing authenticated initialization behavior', () => { + const config = { + username: 'admin', + password: 'secret', + database: 'mydb', + compression: true, + non_linearizable: true + } + + const result = getInitializationCommands(config) + + expect(result).toBe('SET CLIENT KEY NONLINEARIZABLE TO 1;AUTH USER admin PASSWORD secret;SET CLIENT KEY COMPRESSION TO 1;USE DATABASE mydb;') + }) + + it('should omit auth and database commands for unauthenticated initialization', () => { + const config = { + host: 'host', + database: 'mydb', + unauthenticated: true, + compression: true, + non_linearizable: true, + noblob: true, + maxdata: 128, + maxrows: 256, + maxrowset: 512 + } + + const result = getInitializationCommands(config) + + expect(result).toBe( + 'SET CLIENT KEY NONLINEARIZABLE TO 1;SET CLIENT KEY COMPRESSION TO 1;SET CLIENT KEY NOBLOB TO 1;SET CLIENT KEY MAXDATA TO 128;SET CLIENT KEY MAXROWS TO 256;SET CLIENT KEY MAXROWSET TO 512;' + ) + expect(result).not.toContain('AUTH ') + expect(result).not.toContain('USE DATABASE') + expect(result).not.toContain('CREATE DATABASE') + }) })