@platformatic/kafka 1.11.0 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -281,7 +281,7 @@ Many of the methods accept the same options as the client's constructors. The co
281
281
 
282
282
  ## Requirements
283
283
 
284
- Node.js LTS versions:
284
+ Node.js LTS versions:
285
285
 
286
286
  - `20.19.4` or above
287
287
  - `22.18.0` or above
@@ -301,7 +301,7 @@ Edit .env file as needed, enabling or not `JAVA_OPTIONS`
301
301
  Start Kafka cluster locally
302
302
 
303
303
  ```bash
304
- docker compose -f compose-local.yml up -d
304
+ docker compose -f compose.yml up -d
305
305
  ```
306
306
 
307
307
  ## License
@@ -24,7 +24,6 @@ export declare const kListApis: unique symbol;
24
24
  export declare const kMetadata: unique symbol;
25
25
  export declare const kCheckNotClosed: unique symbol;
26
26
  export declare const kClearMetadata: unique symbol;
27
- export declare const kParseBroker: unique symbol;
28
27
  export declare const kPerformWithRetry: unique symbol;
29
28
  export declare const kPerformDeduplicated: unique symbol;
30
29
  export declare const kValidateOptions: unique symbol;
@@ -65,7 +64,6 @@ export declare class Base<OptionsType extends BaseOptions = BaseOptions> extends
65
64
  [kMetadata](options: MetadataOptions, callback: CallbackWithPromise<ClusterMetadata>): void;
66
65
  [kCheckNotClosed](callback: CallbackWithPromise<any>): boolean;
67
66
  [kClearMetadata](): void;
68
- [kParseBroker](broker: Broker | string): Broker;
69
67
  [kPerformWithRetry]<ReturnType>(operationId: string, operation: (callback: Callback<ReturnType>) => void, callback: CallbackWithPromise<ReturnType>, attempt?: number, errors?: Error[], shouldSkipRetry?: (e: Error) => boolean): void | Promise<ReturnType>;
70
68
  [kPerformDeduplicated]<ReturnType>(operationId: string, operation: (callback: CallbackWithPromise<ReturnType>) => void, callback: CallbackWithPromise<ReturnType>): void | Promise<ReturnType>;
71
69
  [kGetApi]<RequestArguments extends Array<unknown>, ResponseType>(name: string, callback: Callback<API<RequestArguments, ResponseType>>): void;
@@ -5,6 +5,7 @@ import { api as apiVersionsV3 } from "../../apis/metadata/api-versions-v3.js";
5
5
  import { baseApisChannel, baseMetadataChannel, createDiagnosticContext, notifyCreation } from "../../diagnostic.js";
6
6
  import { MultipleErrors, NetworkError, UnsupportedApiError, UserError } from "../../errors.js";
7
7
  import { ConnectionPool } from "../../network/connection-pool.js";
8
+ import { parseBroker } from "../../network/utils.js";
8
9
  import { kInstance } from "../../symbols.js";
9
10
  import { ajv, debugDump, loggers } from "../../utils.js";
10
11
  import { baseOptionsValidator, clientSoftwareName, clientSoftwareVersion, defaultBaseOptions, defaultPort, metadataOptionsValidator } from "./options.js";
@@ -23,7 +24,6 @@ export const kListApis = Symbol('plt.kafka.base.listApis');
23
24
  export const kMetadata = Symbol('plt.kafka.base.metadata');
24
25
  export const kCheckNotClosed = Symbol('plt.kafka.base.checkNotClosed');
25
26
  export const kClearMetadata = Symbol('plt.kafka.base.clearMetadata');
26
- export const kParseBroker = Symbol('plt.kafka.base.parseBroker');
27
27
  export const kPerformWithRetry = Symbol('plt.kafka.base.performWithRetry');
28
28
  export const kPerformDeduplicated = Symbol('plt.kafka.base.performDeduplicated');
29
29
  export const kValidateOptions = Symbol('plt.kafka.base.validateOptions');
@@ -63,7 +63,7 @@ export class Base extends EventEmitter {
63
63
  // Initialize bootstrap brokers
64
64
  this[kBootstrapBrokers] = [];
65
65
  for (const broker of options.bootstrapBrokers) {
66
- this[kBootstrapBrokers].push(this[kParseBroker](broker));
66
+ this[kBootstrapBrokers].push(parseBroker(broker, defaultPort));
67
67
  }
68
68
  // Initialize main connection pool
69
69
  this[kConnections] = this[kCreateConnectionPool]();
@@ -177,7 +177,15 @@ export class Base extends EventEmitter {
177
177
  ownerId: this[kInstance],
178
178
  ...this[kOptions]
179
179
  });
180
- this.#forwardEvents(pool, ['connect', 'disconnect', 'failed', 'drain', 'sasl:handshake', 'sasl:authentication']);
180
+ this.#forwardEvents(pool, [
181
+ 'connect',
182
+ 'disconnect',
183
+ 'failed',
184
+ 'drain',
185
+ 'sasl:handshake',
186
+ 'sasl:authentication',
187
+ 'sasl:authentication:extended'
188
+ ]);
181
189
  return pool;
182
190
  }
183
191
  [kListApis](callback) {
@@ -216,7 +224,7 @@ export class Base extends EventEmitter {
216
224
  }
217
225
  }
218
226
  // All topics are already up-to-date, simply return them
219
- if (this.#metadata && !topicsToFetch.length) {
227
+ if (this.#metadata && !topicsToFetch.length && !options.forceUpdate) {
220
228
  callback(null, {
221
229
  ...this.#metadata,
222
230
  topics: new Map(options.topics.map(topic => [topic, this.#metadata.topics.get(topic)]))
@@ -306,18 +314,6 @@ export class Base extends EventEmitter {
306
314
  [kClearMetadata]() {
307
315
  this.#metadata = undefined;
308
316
  }
309
- [kParseBroker](broker) {
310
- if (typeof broker === 'string') {
311
- if (broker.includes(':')) {
312
- const [host, port] = broker.split(':');
313
- return { host, port: Number(port) };
314
- }
315
- else {
316
- return { host: broker, port: defaultPort };
317
- }
318
- }
319
- return broker;
320
- }
321
317
  [kPerformWithRetry](operationId, operation, callback, attempt = 0, errors = [], shouldSkipRetry) {
322
318
  const retries = this[kOptions].retries;
323
319
  this.emitWithDebug('client', 'performWithRetry', operationId, attempt, retries);
@@ -1,3 +1,3 @@
1
- export { Base, kCheckNotClosed, kClearMetadata, kGetApi, kGetBootstrapConnection, kGetConnection, kListApis, kMetadata, kOptions, kParseBroker, kPerformDeduplicated, kPerformWithRetry, kValidateOptions } from './base.ts';
1
+ export { Base, kCheckNotClosed, kClearMetadata, kGetApi, kGetBootstrapConnection, kGetConnection, kListApis, kMetadata, kOptions, kPerformDeduplicated, kPerformWithRetry, kValidateOptions } from './base.ts';
2
2
  export * from './options.ts';
3
3
  export * from './types.ts';
@@ -1,3 +1,3 @@
1
- export { Base, kCheckNotClosed, kClearMetadata, kGetApi, kGetBootstrapConnection, kGetConnection, kListApis, kMetadata, kOptions, kParseBroker, kPerformDeduplicated, kPerformWithRetry, kValidateOptions } from "./base.js";
1
+ export { Base, kCheckNotClosed, kClearMetadata, kGetApi, kGetBootstrapConnection, kGetConnection, kListApis, kMetadata, kOptions, kPerformDeduplicated, kPerformWithRetry, kValidateOptions } from "./base.js";
2
2
  export * from "./options.js";
3
3
  export * from "./types.js";
@@ -91,13 +91,34 @@ export declare const baseOptionsSchema: {
91
91
  enum: readonly ["PLAIN", "SCRAM-SHA-256", "SCRAM-SHA-512", "OAUTHBEARER"];
92
92
  };
93
93
  username: {
94
- type: string;
94
+ oneOf: ({
95
+ type: string;
96
+ function?: undefined;
97
+ } | {
98
+ function: boolean;
99
+ type?: undefined;
100
+ })[];
95
101
  };
96
102
  password: {
97
- type: string;
103
+ oneOf: ({
104
+ type: string;
105
+ function?: undefined;
106
+ } | {
107
+ function: boolean;
108
+ type?: undefined;
109
+ })[];
98
110
  };
99
111
  token: {
100
- type: string;
112
+ oneOf: ({
113
+ type: string;
114
+ function?: undefined;
115
+ } | {
116
+ function: boolean;
117
+ type?: undefined;
118
+ })[];
119
+ };
120
+ authBytesValidator: {
121
+ function: boolean;
101
122
  };
102
123
  };
103
124
  required: string[];
@@ -37,9 +37,10 @@ export const baseOptionsSchema = {
37
37
  type: 'object',
38
38
  properties: {
39
39
  mechanism: { type: 'string', enum: SASLMechanisms },
40
- username: { type: 'string' },
41
- password: { type: 'string' },
42
- token: { type: 'string' }
40
+ username: { oneOf: [{ type: 'string' }, { function: true }] },
41
+ password: { oneOf: [{ type: 'string' }, { function: true }] },
42
+ token: { oneOf: [{ type: 'string' }, { function: true }] },
43
+ authBytesValidator: { function: true }
43
44
  },
44
45
  required: ['mechanism'],
45
46
  additionalProperties: false
@@ -1,4 +1,4 @@
1
- import { type CompressionAlgorithms } from '../../protocol/compression.ts';
1
+ import { type CompressionAlgorithmValue } from '../../protocol/compression.ts';
2
2
  import { type MessageToProduce } from '../../protocol/records.ts';
3
3
  import { type BaseOptions, type TopicWithPartitionAndOffset } from '../base/types.ts';
4
4
  import { type Serializers } from '../serde.ts';
@@ -16,7 +16,7 @@ export interface ProduceOptions<Key, Value, HeaderKey, HeaderValue> {
16
16
  producerEpoch?: number;
17
17
  idempotent?: boolean;
18
18
  acks?: number;
19
- compression?: CompressionAlgorithms;
19
+ compression?: CompressionAlgorithmValue;
20
20
  partitioner?: Partitioner<Key, Value, HeaderKey, HeaderValue>;
21
21
  autocreateTopics?: boolean;
22
22
  repeatOnStaleMetadata?: boolean;
@@ -114,6 +114,9 @@ export class ConnectionPool extends EventEmitter {
114
114
  connection.on('sasl:authentication', authentication => {
115
115
  this.emit('sasl:authentication', { ...eventPayload, authentication });
116
116
  });
117
+ connection.on('sasl:authentication:extended', authentication => {
118
+ this.emit('sasl:authentication:extended', { ...eventPayload, authentication });
119
+ });
117
120
  // Remove stale connections from the pool
118
121
  connection.once('close', () => {
119
122
  this.emit('disconnect', eventPayload);
@@ -5,15 +5,17 @@ import { type CallbackWithPromise } from '../apis/callbacks.ts';
5
5
  import { type Callback, type ResponseParser } from '../apis/definitions.ts';
6
6
  import { type SASLMechanism } from '../apis/enumerations.ts';
7
7
  import { Writer } from '../protocol/writer.ts';
8
+ export type SASLCredentialProvider = () => string | Promise<string>;
8
9
  export interface Broker {
9
10
  host: string;
10
11
  port: number;
11
12
  }
12
13
  export interface SASLOptions {
13
14
  mechanism: SASLMechanism;
14
- username?: string;
15
- password?: string;
16
- token?: string;
15
+ username?: string | SASLCredentialProvider;
16
+ password?: string | SASLCredentialProvider;
17
+ token?: string | SASLCredentialProvider;
18
+ authBytesValidator?: (authBytes: Buffer, callback: CallbackWithPromise<Buffer>) => void;
17
19
  }
18
20
  export interface ConnectionOptions {
19
21
  connectTimeout?: number;
@@ -47,6 +47,7 @@ export class Connection extends EventEmitter {
47
47
  #responseReader;
48
48
  #socket;
49
49
  #socketMustBeDrained;
50
+ #reauthenticationTimeout;
50
51
  constructor(clientId, options = {}) {
51
52
  super();
52
53
  this.setMaxListeners(0);
@@ -178,6 +179,7 @@ export class Connection extends EventEmitter {
178
179
  if (!callback) {
179
180
  callback = createPromisifiedCallback();
180
181
  }
182
+ clearInterval(this.#reauthenticationTimeout);
181
183
  if (this.#status === ConnectionStatuses.CLOSED ||
182
184
  this.#status === ConnectionStatuses.ERROR ||
183
185
  this.#status === ConnectionStatuses.NONE) {
@@ -231,7 +233,9 @@ export class Connection extends EventEmitter {
231
233
  }, callback);
232
234
  }
233
235
  #authenticate(host, port, diagnosticContext) {
234
- this.#status = ConnectionStatuses.AUTHENTICATING;
236
+ if (this.#status === ConnectionStatuses.CONNECTING) {
237
+ this.#status = ConnectionStatuses.AUTHENTICATING;
238
+ }
235
239
  const { mechanism, username, password, token } = this.#options.sasl;
236
240
  if (!SASLMechanisms.includes(mechanism)) {
237
241
  this.#onConnectionError(host, port, diagnosticContext, new UserError(`SASL mechanism ${mechanism} not supported.`));
@@ -243,14 +247,15 @@ export class Connection extends EventEmitter {
243
247
  return;
244
248
  }
245
249
  this.emit('sasl:handshake', response.mechanisms);
250
+ const callback = this.#onSaslAuthenticate.bind(this, host, port, diagnosticContext);
246
251
  if (mechanism === 'PLAIN') {
247
- saslPlain.authenticate(saslAuthenticateV2.api, this, username, password, this.#onSaslAuthenticate.bind(this, host, port, diagnosticContext));
252
+ saslPlain.authenticate(saslAuthenticateV2.api, this, username, password, callback);
248
253
  }
249
254
  else if (mechanism === 'OAUTHBEARER') {
250
- saslOAuthBearer.authenticate(saslAuthenticateV2.api, this, token, this.#onSaslAuthenticate.bind(this, host, port, diagnosticContext));
255
+ saslOAuthBearer.authenticate(saslAuthenticateV2.api, this, token, callback);
251
256
  }
252
257
  else {
253
- saslScramSha.authenticate(saslAuthenticateV2.api, this, mechanism.substring(6), username, password, defaultCrypto, this.#onSaslAuthenticate.bind(this, host, port, diagnosticContext));
258
+ saslScramSha.authenticate(saslAuthenticateV2.api, this, mechanism.substring(6), username, password, defaultCrypto, callback);
254
259
  }
255
260
  });
256
261
  }
@@ -299,6 +304,7 @@ export class Connection extends EventEmitter {
299
304
  request.diagnostic.error = err;
300
305
  connectionsApiChannel.error.publish(request.diagnostic);
301
306
  throw err;
307
+ /* c8 ignore next 3 - Hard to test */
302
308
  }
303
309
  finally {
304
310
  connectionsApiChannel.end.publish(request.diagnostic);
@@ -313,6 +319,7 @@ export class Connection extends EventEmitter {
313
319
  #onConnectionError(host, port, diagnosticContext, cause) {
314
320
  const error = new NetworkError(`Connection to ${host}:${port} failed.`, { cause });
315
321
  this.#status = ConnectionStatuses.ERROR;
322
+ clearTimeout(this.#reauthenticationTimeout);
316
323
  diagnosticContext.error = error;
317
324
  connectionsConnectsChannel.error.publish(diagnosticContext);
318
325
  connectionsConnectsChannel.asyncStart.publish(diagnosticContext);
@@ -322,15 +329,43 @@ export class Connection extends EventEmitter {
322
329
  }
323
330
  #onSaslAuthenticate(host, port, diagnosticContext, error, response) {
324
331
  if (error) {
325
- const protocolError = error.errors[0];
326
- if (protocolError.apiId === 'SASL_AUTHENTICATION_FAILED') {
332
+ const protocolError = error.errors?.[0];
333
+ if (protocolError?.apiId === 'SASL_AUTHENTICATION_FAILED') {
327
334
  error = new AuthenticationError('SASL authentication failed.', { cause: error });
328
335
  }
329
336
  this.#onConnectionError(host, port, diagnosticContext, error);
330
337
  return;
331
338
  }
332
- this.emit('sasl:authentication', response.authBytes);
333
- this.#onConnectionSucceed(diagnosticContext);
339
+ if (this.#options.sasl.authBytesValidator) {
340
+ this.#options.sasl.authBytesValidator(response.authBytes, this.#onSaslAuthenticationValidation.bind(this, host, port, diagnosticContext, response.sessionLifetimeMs));
341
+ }
342
+ else {
343
+ this.#onSaslAuthenticationValidation(host, port, diagnosticContext, response.sessionLifetimeMs, null, response.authBytes);
344
+ }
345
+ }
346
+ #onSaslAuthenticationValidation(host, port, diagnosticContext, sessionLifetimeMs, error, authBytes) {
347
+ if (error) {
348
+ this.#onConnectionError(host, port, diagnosticContext, error);
349
+ return;
350
+ }
351
+ if (sessionLifetimeMs > 0) {
352
+ this.#reauthenticationTimeout = setTimeout(() => {
353
+ const diagnosticContext = createDiagnosticContext({
354
+ connection: this,
355
+ operation: 'reauthenticate',
356
+ host,
357
+ port
358
+ });
359
+ this.#authenticate(host, port, diagnosticContext);
360
+ }, Number(sessionLifetimeMs) * 0.8);
361
+ }
362
+ if (this.#status === ConnectionStatuses.CONNECTED) {
363
+ this.emit('sasl:authentication:extended', authBytes);
364
+ }
365
+ else {
366
+ this.emit('sasl:authentication', authBytes);
367
+ this.#onConnectionSucceed(diagnosticContext);
368
+ }
334
369
  }
335
370
  /*
336
371
  Response Header v1 => correlation_id TAG_BUFFER
@@ -428,6 +463,7 @@ export class Connection extends EventEmitter {
428
463
  }
429
464
  }
430
465
  #onError(error) {
466
+ clearTimeout(this.#reauthenticationTimeout);
431
467
  this.emit('error', new NetworkError('Connection error', { cause: error }));
432
468
  }
433
469
  }
@@ -1,2 +1,3 @@
1
1
  export * from './connection-pool.ts';
2
2
  export * from './connection.ts';
3
+ export * from './utils.ts';
@@ -1,2 +1,3 @@
1
1
  export * from "./connection-pool.js";
2
2
  export * from "./connection.js";
3
+ export * from "./utils.js";
@@ -0,0 +1,2 @@
1
+ import { type Broker } from './connection.ts';
2
+ export declare function parseBroker(broker: Broker | string, defaultPort?: number): Broker;
@@ -0,0 +1,12 @@
1
+ export function parseBroker(broker, defaultPort = 9092) {
2
+ if (typeof broker === 'string') {
3
+ if (broker.includes(':')) {
4
+ const [host, port] = broker.split(':');
5
+ return { host, port: Number(port) };
6
+ }
7
+ else {
8
+ return { host: broker, port: defaultPort };
9
+ }
10
+ }
11
+ return broker;
12
+ }
@@ -1,12 +1,21 @@
1
1
  import { DynamicBuffer } from './dynamic-buffer.ts';
2
2
  export type SyncCompressionPhase = (data: Buffer | DynamicBuffer) => Buffer;
3
3
  export type CompressionOperation = (data: Buffer) => Buffer;
4
- export interface CompressionAlgorithm {
4
+ export interface CompressionAlgorithmSpecification {
5
5
  compressSync: SyncCompressionPhase;
6
6
  decompressSync: SyncCompressionPhase;
7
7
  bitmask: number;
8
8
  available?: boolean;
9
9
  }
10
+ export declare const CompressionAlgorithms: {
11
+ readonly NONE: "none";
12
+ readonly GZIP: "gzip";
13
+ readonly SNAPPY: "snappy";
14
+ readonly LZ4: "lz4";
15
+ readonly ZSTD: "zstd";
16
+ };
17
+ export type CompressionAlgorithm = keyof typeof CompressionAlgorithms;
18
+ export type CompressionAlgorithmValue = (typeof CompressionAlgorithms)[keyof typeof CompressionAlgorithms];
10
19
  export declare const compressionsAlgorithms: {
11
20
  readonly none: {
12
21
  readonly compressSync: (data: Buffer | DynamicBuffer) => Buffer;
@@ -67,4 +76,3 @@ export declare const compressionsAlgorithmsByBitmask: {
67
76
  readonly available: boolean;
68
77
  };
69
78
  };
70
- export type CompressionAlgorithms = keyof typeof compressionsAlgorithms;
@@ -4,6 +4,13 @@ import { UnsupportedCompressionError } from "../errors.js";
4
4
  import { DynamicBuffer } from "./dynamic-buffer.js";
5
5
  const require = createRequire(import.meta.url);
6
6
  const { zstdCompressSync, zstdDecompressSync, gzipSync, gunzipSync } = zlib;
7
+ export const CompressionAlgorithms = {
8
+ NONE: 'none',
9
+ GZIP: 'gzip',
10
+ SNAPPY: 'snappy',
11
+ LZ4: 'lz4',
12
+ ZSTD: 'zstd'
13
+ };
7
14
  function ensureBuffer(data) {
8
15
  return DynamicBuffer.isDynamicBuffer(data) ? data.slice() : data;
9
16
  }
@@ -489,7 +489,7 @@ export const protocolErrors = {
489
489
  id: 'SASL_AUTHENTICATION_FAILED',
490
490
  code: 58,
491
491
  canRetry: false,
492
- message: 'SASL Authentication failed.'
492
+ message: 'SASL authentication failed.'
493
493
  },
494
494
  UNKNOWN_PRODUCER_ID: {
495
495
  id: 'UNKNOWN_PRODUCER_ID',
@@ -1,5 +1,5 @@
1
1
  import { type NumericMap } from '../utils.ts';
2
- import { type CompressionAlgorithms } from './compression.ts';
2
+ import { type CompressionAlgorithmValue } from './compression.ts';
3
3
  import { type NullableString } from './definitions.ts';
4
4
  import { Reader } from './reader.ts';
5
5
  import { Writer } from './writer.ts';
@@ -28,7 +28,7 @@ export interface MessageRecord {
28
28
  }
29
29
  export interface CreateRecordsBatchOptions {
30
30
  transactionalId?: NullableString;
31
- compression: CompressionAlgorithms;
31
+ compression: CompressionAlgorithmValue;
32
32
  firstSequence?: number;
33
33
  producerId: bigint;
34
34
  producerEpoch: number;
@@ -65,8 +65,10 @@ export function createRecordsBatch(messages, options = {}) {
65
65
  let buffer = new DynamicBuffer();
66
66
  for (let i = 0; i < messages.length; i++) {
67
67
  let ts = messages[i].timestamp ?? now;
68
- if (typeof ts === 'number')
68
+ /* c8 ignore next 3 - Hard to test */
69
+ if (typeof ts === 'number') {
69
70
  ts = BigInt(ts);
71
+ }
70
72
  messages[i].timestamp = ts;
71
73
  if (ts > maxTimestamp)
72
74
  maxTimestamp = ts;
@@ -138,6 +140,9 @@ export function readRecordsBatch(reader) {
138
140
  throw new UnsupportedCompressionError(`Unsupported compression algorithm with bitmask ${compression}`);
139
141
  }
140
142
  const buffer = algorithm.decompressSync(reader.buffer.slice(reader.position, reader.buffer.length));
143
+ // Move the original reader to the end
144
+ reader.skip(reader.buffer.length - reader.position);
145
+ // Replace the reader with the decompressed buffer
141
146
  reader = Reader.from(buffer);
142
147
  }
143
148
  for (let i = 0; i < recordsLength; i++) {
@@ -0,0 +1,3 @@
1
+ import { type Callback } from '../../apis/index.ts';
2
+ import { type SASLCredentialProvider } from '../../network/connection.ts';
3
+ export declare function getCredential(label: string, credentialOrProvider: string | SASLCredentialProvider, callback: Callback<string>): void;
@@ -0,0 +1,36 @@
1
+ import { AuthenticationError } from "../../errors.js";
2
+ export function getCredential(label, credentialOrProvider, callback) {
3
+ if (typeof credentialOrProvider === 'string') {
4
+ callback(null, credentialOrProvider);
5
+ return;
6
+ }
7
+ else if (typeof credentialOrProvider !== 'function') {
8
+ callback(new AuthenticationError(`The ${label} should be a string or a function.`), undefined);
9
+ return;
10
+ }
11
+ try {
12
+ const credential = credentialOrProvider();
13
+ if (typeof credential === 'string') {
14
+ callback(null, credential);
15
+ return;
16
+ }
17
+ else if (typeof credential?.then !== 'function') {
18
+ callback(new AuthenticationError(`The ${label} provider should return a string or a promise that resolves to a string.`), undefined);
19
+ return;
20
+ }
21
+ credential
22
+ .then(token => {
23
+ if (typeof token !== 'string') {
24
+ process.nextTick(callback, new AuthenticationError(`The ${label} provider should resolve to a string.`), undefined);
25
+ return;
26
+ }
27
+ process.nextTick(callback, null, token);
28
+ })
29
+ .catch(error => {
30
+ process.nextTick(callback, new AuthenticationError(`The ${label} provider threw an error.`, { cause: error }));
31
+ });
32
+ }
33
+ catch (error) {
34
+ callback(new AuthenticationError(`The ${label} provider threw an error.`, { cause: error }), undefined);
35
+ }
36
+ }
@@ -1,5 +1,6 @@
1
1
  import { type CallbackWithPromise } from '../../apis/callbacks.ts';
2
2
  import { type SASLAuthenticationAPI, type SaslAuthenticateResponse } from '../../apis/security/sasl-authenticate-v2.ts';
3
- import { type Connection } from '../../network/connection.ts';
4
- export declare function authenticate(authenticateAPI: SASLAuthenticationAPI, connection: Connection, token: string, callback: CallbackWithPromise<SaslAuthenticateResponse>): void;
5
- export declare function authenticate(authenticateAPI: SASLAuthenticationAPI, connection: Connection, token: string): Promise<SaslAuthenticateResponse>;
3
+ import { type Connection, type SASLCredentialProvider } from '../../network/connection.ts';
4
+ export declare function jwtValidateAuthenticationBytes(authBytes: Buffer, callback: CallbackWithPromise<Buffer>): void;
5
+ export declare function authenticate(authenticateAPI: SASLAuthenticationAPI, connection: Connection, tokenOrProvider: string | SASLCredentialProvider, callback: CallbackWithPromise<SaslAuthenticateResponse>): void;
6
+ export declare function authenticate(authenticateAPI: SASLAuthenticationAPI, connection: Connection, tokenOrProvider: string | SASLCredentialProvider): Promise<SaslAuthenticateResponse>;
@@ -1,8 +1,29 @@
1
1
  import { createPromisifiedCallback, kCallbackPromise } from "../../apis/callbacks.js";
2
- export function authenticate(authenticateAPI, connection, token, callback) {
2
+ import { AuthenticationError } from "../../errors.js";
3
+ import { getCredential } from "./credential-provider.js";
4
+ export function jwtValidateAuthenticationBytes(authBytes, callback) {
5
+ let authData;
6
+ try {
7
+ authData = authBytes.length > 0 ? JSON.parse(authBytes.toString('utf-8')) : {};
8
+ }
9
+ catch (e) {
10
+ callback(new AuthenticationError('Invalid authBytes in SASL/OAUTHBEARER response', { authBytes }), undefined);
11
+ return;
12
+ }
13
+ if (authData.status === 'invalid_token') {
14
+ callback(new AuthenticationError('Invalid SASL/OAUTHBEARER token.', { authData }), undefined);
15
+ }
16
+ callback(null, authBytes);
17
+ }
18
+ export function authenticate(authenticateAPI, connection, tokenOrProvider, callback) {
3
19
  if (!callback) {
4
20
  callback = createPromisifiedCallback();
5
21
  }
6
- authenticateAPI(connection, Buffer.from(`n,,\x01auth=Bearer ${token}\x01\x01`), callback);
22
+ getCredential('SASL/OAUTHBEARER token', tokenOrProvider, (error, token) => {
23
+ if (error) {
24
+ return callback(error, undefined);
25
+ }
26
+ authenticateAPI(connection, Buffer.from(`n,,\x01auth=Bearer ${token}\x01\x01`), callback);
27
+ });
7
28
  return callback[kCallbackPromise];
8
29
  }
@@ -1,5 +1,5 @@
1
1
  import { type CallbackWithPromise } from '../../apis/callbacks.ts';
2
2
  import { type SASLAuthenticationAPI, type SaslAuthenticateResponse } from '../../apis/security/sasl-authenticate-v2.ts';
3
- import { type Connection } from '../../network/connection.ts';
4
- export declare function authenticate(authenticateAPI: SASLAuthenticationAPI, connection: Connection, username: string, password: string, callback: CallbackWithPromise<SaslAuthenticateResponse>): void;
5
- export declare function authenticate(authenticateAPI: SASLAuthenticationAPI, connection: Connection, username: string, password: string): Promise<SaslAuthenticateResponse>;
3
+ import { type Connection, type SASLCredentialProvider } from '../../network/connection.ts';
4
+ export declare function authenticate(authenticateAPI: SASLAuthenticationAPI, connection: Connection, usernameProvider: string | SASLCredentialProvider, passwordProvider: string | SASLCredentialProvider, callback: CallbackWithPromise<SaslAuthenticateResponse>): void;
5
+ export declare function authenticate(authenticateAPI: SASLAuthenticationAPI, connection: Connection, usernameProvider: string | SASLCredentialProvider, passwordProvider: string | SASLCredentialProvider): Promise<SaslAuthenticateResponse>;
@@ -1,8 +1,19 @@
1
1
  import { createPromisifiedCallback, kCallbackPromise } from "../../apis/callbacks.js";
2
- export function authenticate(authenticateAPI, connection, username, password, callback) {
2
+ import { getCredential } from "./credential-provider.js";
3
+ export function authenticate(authenticateAPI, connection, usernameProvider, passwordProvider, callback) {
3
4
  if (!callback) {
4
5
  callback = createPromisifiedCallback();
5
6
  }
6
- authenticateAPI(connection, Buffer.from(['', username, password].join('\0')), callback);
7
+ getCredential('SASL/PLAIN username', usernameProvider, (error, username) => {
8
+ if (error) {
9
+ return callback(error, undefined);
10
+ }
11
+ getCredential('SASL/PLAIN password', passwordProvider, (error, password) => {
12
+ if (error) {
13
+ return callback(error, undefined);
14
+ }
15
+ authenticateAPI(connection, Buffer.from(['', username, password].join('\0')), callback);
16
+ });
17
+ });
7
18
  return callback[kCallbackPromise];
8
19
  }
@@ -1,6 +1,6 @@
1
1
  import { type CallbackWithPromise } from '../../apis/callbacks.ts';
2
2
  import { type SASLAuthenticationAPI, type SaslAuthenticateResponse } from '../../apis/security/sasl-authenticate-v2.ts';
3
- import { type Connection } from '../../network/connection.ts';
3
+ import { type Connection, type SASLCredentialProvider } from '../../network/connection.ts';
4
4
  export interface ScramAlgorithmDefinition {
5
5
  keyLength: number;
6
6
  algorithm: string;
@@ -33,5 +33,5 @@ export declare function hi(definition: ScramAlgorithmDefinition, password: strin
33
33
  export declare function hmac(definition: ScramAlgorithmDefinition, key: Buffer, data: string | Buffer): Buffer;
34
34
  export declare function xor(a: Buffer, b: Buffer): Buffer;
35
35
  export declare const defaultCrypto: ScramCryptoModule;
36
- export declare function authenticate(authenticateAPI: SASLAuthenticationAPI, connection: Connection, algorithm: ScramAlgorithm, username: string, password: string, crypto: ScramCryptoModule, callback: CallbackWithPromise<SaslAuthenticateResponse>): void;
37
- export declare function authenticate(authenticateAPI: SASLAuthenticationAPI, connection: Connection, algorithm: ScramAlgorithm, username: string, password: string, crypto?: ScramCryptoModule): Promise<SaslAuthenticateResponse>;
36
+ export declare function authenticate(authenticateAPI: SASLAuthenticationAPI, connection: Connection, algorithm: ScramAlgorithm, usernameProvider: string | SASLCredentialProvider, passwordProvider: string | SASLCredentialProvider, crypto: ScramCryptoModule, callback: CallbackWithPromise<SaslAuthenticateResponse>): void;
37
+ export declare function authenticate(authenticateAPI: SASLAuthenticationAPI, connection: Connection, algorithm: ScramAlgorithm, usernameProvider: string | SASLCredentialProvider, passwordProvider: string | SASLCredentialProvider, crypto?: ScramCryptoModule): Promise<SaslAuthenticateResponse>;
@@ -1,6 +1,7 @@
1
1
  import { createHash, createHmac, pbkdf2Sync, randomBytes } from 'node:crypto';
2
2
  import { createPromisifiedCallback, kCallbackPromise } from "../../apis/callbacks.js";
3
3
  import { AuthenticationError } from "../../errors.js";
4
+ import { getCredential } from "./credential-provider.js";
4
5
  const GS2_HEADER = 'n,,';
5
6
  const GS2_HEADER_BASE64 = Buffer.from(GS2_HEADER).toString('base64');
6
7
  const HMAC_CLIENT_KEY = 'Client Key';
@@ -57,21 +58,14 @@ export const defaultCrypto = {
57
58
  hmac,
58
59
  xor
59
60
  };
60
- export function authenticate(authenticateAPI, connection, algorithm, username, password, crypto = defaultCrypto, callback) {
61
- if (!callback) {
62
- callback = createPromisifiedCallback();
63
- }
61
+ function performAuthentication(connection, algorithm, definition, authenticateAPI, crypto, username, password, callback) {
64
62
  const { h, hi, hmac, xor } = crypto;
65
- const definition = ScramAlgorithms[algorithm];
66
- if (!definition) {
67
- throw new AuthenticationError(`Unsupported SCRAM algorithm ${algorithm}`);
68
- }
69
63
  const clientNonce = createNonce();
70
64
  const clientFirstMessageBare = `n=${sanitizeString(username)},r=${clientNonce}`;
71
65
  // First of all, send the first message
72
66
  authenticateAPI(connection, Buffer.from(`${GS2_HEADER}${clientFirstMessageBare}`), (error, firstResponse) => {
73
67
  if (error) {
74
- callback(new AuthenticationError('Authentication failed.', { cause: error }), undefined);
68
+ callback(new AuthenticationError('SASL authentication failed.', { cause: error }), undefined);
75
69
  return;
76
70
  }
77
71
  const firstData = parseParameters(firstResponse.authBytes);
@@ -108,7 +102,7 @@ export function authenticate(authenticateAPI, connection, algorithm, username, p
108
102
  const serverSignature = hmac(definition, serverKey, authMessage);
109
103
  authenticateAPI(connection, Buffer.from(`${clientFinalMessageWithoutProof},p=${clientProof.toString('base64')}`), (error, lastResponse) => {
110
104
  if (error) {
111
- callback(new AuthenticationError('Authentication failed.', { cause: error }), undefined);
105
+ callback(new AuthenticationError('SASL authentication failed.', { cause: error }), undefined);
112
106
  return;
113
107
  }
114
108
  // Send the last message to the server
@@ -124,5 +118,25 @@ export function authenticate(authenticateAPI, connection, algorithm, username, p
124
118
  callback(null, lastResponse);
125
119
  });
126
120
  });
121
+ }
122
+ export function authenticate(authenticateAPI, connection, algorithm, usernameProvider, passwordProvider, crypto = defaultCrypto, callback) {
123
+ if (!callback) {
124
+ callback = createPromisifiedCallback();
125
+ }
126
+ const definition = ScramAlgorithms[algorithm];
127
+ if (!definition) {
128
+ throw new AuthenticationError(`Unsupported SCRAM algorithm ${algorithm}`);
129
+ }
130
+ getCredential(`SASL/SCRAM-${algorithm} username`, usernameProvider, (error, username) => {
131
+ if (error) {
132
+ return callback(error, undefined);
133
+ }
134
+ getCredential(`SASL/SCRAM-${algorithm} password`, passwordProvider, (error, password) => {
135
+ if (error) {
136
+ return callback(error, undefined);
137
+ }
138
+ performAuthentication(connection, algorithm, definition, authenticateAPI, crypto, username, password, callback);
139
+ });
140
+ });
127
141
  return callback[kCallbackPromise];
128
142
  }
package/dist/utils.js CHANGED
@@ -2,6 +2,7 @@ import { Ajv2020 } from 'ajv/dist/2020.js';
2
2
  import debug from 'debug';
3
3
  import { inspect } from 'node:util';
4
4
  export { setTimeout as sleep } from 'node:timers/promises';
5
+ /* c8 ignore start - Hard to test */
5
6
  function PromiseWithResolversPolyfill() {
6
7
  let resolve;
7
8
  let reject;
@@ -12,7 +13,10 @@ function PromiseWithResolversPolyfill() {
12
13
  // @ts-expect-error - resolve and reject are assigned in the promise constructor
13
14
  return { promise, resolve, reject };
14
15
  }
15
- export const PromiseWithResolvers = Promise.withResolvers ? Promise.withResolvers.bind(Promise) : PromiseWithResolversPolyfill;
16
+ export const PromiseWithResolvers = Promise.withResolvers
17
+ ? Promise.withResolvers.bind(Promise)
18
+ : PromiseWithResolversPolyfill;
19
+ /* c8 ignore end */
16
20
  export const ajv = new Ajv2020({ allErrors: true, coerceTypes: false, strict: true });
17
21
  export const loggers = {
18
22
  protocol: debug('plt:kafka:protocol'),
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  export const name = "@platformatic/kafka";
2
- export const version = "1.11.0";
2
+ export const version = "1.13.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/kafka",
3
- "version": "1.11.0",
3
+ "version": "1.13.0",
4
4
  "description": "Modern and performant client for Apache Kafka",
5
5
  "homepage": "https://github.com/platformatic/kafka",
6
6
  "author": "Platformatic Inc. <oss@platformatic.dev> (https://platformatic.dev)",
@@ -26,39 +26,39 @@
26
26
  "types": "./dist/index.d.ts",
27
27
  "dependencies": {
28
28
  "ajv": "^8.17.1",
29
- "debug": "^4.4.0",
29
+ "debug": "^4.4.3",
30
30
  "fastq": "^1.19.1",
31
31
  "mnemonist": "^0.40.3",
32
32
  "scule": "^1.3.0"
33
33
  },
34
34
  "optionalDependencies": {
35
- "lz4-napi": "^2.8.0",
36
- "snappy": "^7.2.2"
35
+ "lz4-napi": "^2.9.0",
36
+ "snappy": "^7.3.3"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@platformatic/rdkafka": "^4.0.0",
40
40
  "@types/debug": "^4.1.12",
41
- "@types/node": "^22.17.2",
42
- "@types/semver": "^7.7.0",
41
+ "@types/node": "^22.18.5",
42
+ "@types/semver": "^7.7.1",
43
43
  "@watchable/unpromise": "^1.0.2",
44
- "avsc": "^5.7.7",
44
+ "avsc": "^5.7.9",
45
45
  "c8": "^10.1.3",
46
46
  "cleaner-spec-reporter": "^0.5.0",
47
47
  "cronometro": "^5.3.0",
48
- "eslint": "^9.21.0",
48
+ "eslint": "^9.35.0",
49
49
  "fast-jwt": "^6.0.2",
50
50
  "hwp": "^0.4.1",
51
51
  "json5": "^2.2.3",
52
52
  "kafkajs": "^2.2.4",
53
- "neostandard": "^0.12.1",
54
- "node-rdkafka": "^3.3.1",
55
- "parse5": "^7.2.1",
56
- "prettier": "^3.5.3",
53
+ "neostandard": "^0.12.2",
54
+ "node-rdkafka": "^3.5.0",
55
+ "parse5": "^7.3.0",
56
+ "prettier": "^3.6.2",
57
57
  "prettier-plugin-space-before-function-paren": "^0.0.8",
58
58
  "prom-client": "^15.1.3",
59
- "semver": "^7.7.1",
59
+ "semver": "^7.7.2",
60
60
  "table": "^6.9.0",
61
- "typescript": "^5.7.3"
61
+ "typescript": "^5.9.2"
62
62
  },
63
63
  "engines": {
64
64
  "node": ">= 20.19.4 || >= 22.18.0 || >= 24.6.0"
@@ -74,12 +74,9 @@
74
74
  "test:docker:up": "node scripts/docker.ts up -d --wait",
75
75
  "test:docker:down": "node scripts/docker.ts down",
76
76
  "ci": "npm run build && npm run lint && npm run test:ci",
77
+ "ci:v20": "npm run lint && rm -rf dist && tsc -p tsconfig.json && node dist/scripts/postbuild.js && cd dist && c8 -c ../test/config/c8-ci.json node --env-file=../test/config/env --no-warnings --test --test-timeout=300000",
77
78
  "generate:apis": "node scripts/generate-apis.ts",
78
79
  "generate:errors": "node scripts/generate-errors.ts",
79
- "create:api": "node scripts/create-api.ts",
80
- "build:v20": "rm -rf dist && tsc -p tsconfig.json",
81
- "postbuild:v20": "cp package.json dist/package.json && node dist/scripts/postbuild.js",
82
- "test:v20": "npm run build:v20 && cp -R test/fixtures dist/test/fixtures && node --env-file=test/config/env --no-warnings --test --test-timeout=300000 dist/test/**/*.test.js",
83
- "test:ci:v20": "npm run build:v20 && cp -R test/fixtures dist/test/fixtures && node --env-file=test/config/env --no-warnings --test --test-timeout=300000 dist/test/**/*.test.js"
80
+ "create:api": "node scripts/create-api.ts"
84
81
  }
85
82
  }