@platformatic/kafka 1.2.0 → 1.3.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
@@ -137,6 +137,10 @@ await admin.deleteTopics({ topics: ['my-topic'] })
137
137
  await admin.close()
138
138
  ```
139
139
 
140
+ ## TLS and SASL
141
+
142
+ See the relevant sections in the the [Base Client](./docs/base.md) page.
143
+
140
144
  ## Serialisation/Deserialisation
141
145
 
142
146
  `@platformatic/kafka` supports customisation of serialisation out of the box.
@@ -1,4 +1,4 @@
1
- import { type Callback } from '../apis/definitions.ts';
1
+ import { type Callback } from './definitions.ts';
2
2
  export declare const kCallbackPromise: unique symbol;
3
3
  export declare const kNoopCallbackReturnValue: unique symbol;
4
4
  export declare const noopCallback: CallbackWithPromise<any>;
@@ -1,3 +1,5 @@
1
+ export declare const SASLMechanisms: readonly ["PLAIN", "SCRAM-SHA-256", "SCRAM-SHA-512"];
2
+ export type SASLMechanism = (typeof SASLMechanisms)[number];
1
3
  export declare const FindCoordinatorKeyTypes: {
2
4
  readonly GROUP: 0;
3
5
  readonly TRANSACTION: 1;
@@ -1,3 +1,5 @@
1
+ // SASL Authentication
2
+ export const SASLMechanisms = ['PLAIN', 'SCRAM-SHA-256', 'SCRAM-SHA-512'];
1
3
  // Metadata API
2
4
  // ./metadata/find-coordinator.ts
3
5
  export const FindCoordinatorKeyTypes = { GROUP: 0, TRANSACTION: 1, SHARE: 2 };
@@ -1,3 +1,4 @@
1
+ export * from './callbacks.ts';
1
2
  export * from './definitions.ts';
2
3
  export * from './enumerations.ts';
3
4
  export * from './admin/index.ts';
@@ -1,4 +1,5 @@
1
1
  // Generics
2
+ export * from "./callbacks.js";
2
3
  export * from "./definitions.js";
3
4
  export * from "./enumerations.js";
4
5
  // Low-level APIs
@@ -2,7 +2,7 @@ import { ResponseError } from "../../errors.js";
2
2
  import { Writer } from "../../protocol/writer.js";
3
3
  import { createAPI } from "../definitions.js";
4
4
  /*
5
- SaslHandshake Request (Version: 0) => mechanism
5
+ SaslHandshake Request (Version: 1) => mechanism
6
6
  mechanism => STRING
7
7
  */
8
8
  export function createRequest(mechanism) {
@@ -1,6 +1,6 @@
1
+ import { type CallbackWithPromise } from '../../apis/callbacks.ts';
1
2
  import { type Callback } from '../../apis/definitions.ts';
2
3
  import { Base } from '../base/base.ts';
3
- import { type CallbackWithPromise } from '../callbacks.ts';
4
4
  import { type AdminOptions, type CreatedTopic, type CreateTopicsOptions, type DeleteGroupsOptions, type DeleteTopicsOptions, type DescribeGroupsOptions, type Group, type GroupBase, type ListGroupsOptions } from './types.ts';
5
5
  export declare class Admin extends Base<AdminOptions> {
6
6
  #private;
@@ -1,8 +1,8 @@
1
+ import { createPromisifiedCallback, kCallbackPromise, runConcurrentCallbacks } from "../../apis/callbacks.js";
1
2
  import { FindCoordinatorKeyTypes } from "../../apis/enumerations.js";
2
3
  import { adminGroupsChannel, adminTopicsChannel, createDiagnosticContext } from "../../diagnostic.js";
3
4
  import { Reader } from "../../protocol/reader.js";
4
- import { Base, kAfterCreate, kBootstrapBrokers, kCheckNotClosed, kConnections, kGetApi, kMetadata, kOptions, kPerformDeduplicated, kPerformWithRetry, kValidateOptions } from "../base/base.js";
5
- import { createPromisifiedCallback, kCallbackPromise, runConcurrentCallbacks } from "../callbacks.js";
5
+ import { Base, kAfterCreate, kCheckNotClosed, kGetApi, kGetBootstrapConnection, kGetConnection, kMetadata, kOptions, kPerformDeduplicated, kPerformWithRetry, kValidateOptions } from "../base/base.js";
6
6
  import { createTopicsOptionsValidator, deleteGroupsOptionsValidator, deleteTopicsOptionsValidator, describeGroupsOptionsValidator, listGroupsOptionsValidator } from "./options.js";
7
7
  export class Admin extends Base {
8
8
  constructor(options) {
@@ -107,7 +107,7 @@ export class Admin extends Base {
107
107
  }
108
108
  this[kPerformDeduplicated]('createTopics', deduplicateCallback => {
109
109
  this[kPerformWithRetry]('createTopics', retryCallback => {
110
- this[kConnections].getFirstAvailable(this[kBootstrapBrokers], (error, connection) => {
110
+ this[kGetBootstrapConnection]((error, connection) => {
111
111
  if (error) {
112
112
  retryCallback(error, undefined);
113
113
  return;
@@ -140,7 +140,7 @@ export class Admin extends Base {
140
140
  #deleteTopics(options, callback) {
141
141
  this[kPerformDeduplicated]('deleteTopics', deduplicateCallback => {
142
142
  this[kPerformWithRetry]('deleteTopics', retryCallback => {
143
- this[kConnections].getFirstAvailable(this[kBootstrapBrokers], (error, connection) => {
143
+ this[kGetBootstrapConnection]((error, connection) => {
144
144
  if (error) {
145
145
  retryCallback(error, undefined);
146
146
  return;
@@ -168,7 +168,7 @@ export class Admin extends Base {
168
168
  return;
169
169
  }
170
170
  runConcurrentCallbacks('Listing groups failed.', metadata.brokers, ([, broker], concurrentCallback) => {
171
- this[kConnections].get(broker, (error, connection) => {
171
+ this[kGetConnection](broker, (error, connection) => {
172
172
  if (error) {
173
173
  concurrentCallback(error, undefined);
174
174
  return;
@@ -231,7 +231,7 @@ export class Admin extends Base {
231
231
  coordinator.push(group);
232
232
  }
233
233
  runConcurrentCallbacks('Describing groups failed.', coordinators, ([node, groups], concurrentCallback) => {
234
- this[kConnections].get(metadata.brokers.get(node), (error, connection) => {
234
+ this[kGetConnection](metadata.brokers.get(node), (error, connection) => {
235
235
  if (error) {
236
236
  concurrentCallback(error, undefined);
237
237
  return;
@@ -313,7 +313,7 @@ export class Admin extends Base {
313
313
  coordinator.push(group);
314
314
  }
315
315
  runConcurrentCallbacks('Deleting groups failed.', coordinators, ([node, groups], concurrentCallback) => {
316
- this[kConnections].get(metadata.brokers.get(node), (error, connection) => {
316
+ this[kGetConnection](metadata.brokers.get(node), (error, connection) => {
317
317
  if (error) {
318
318
  concurrentCallback(error, undefined);
319
319
  return;
@@ -334,7 +334,7 @@ export class Admin extends Base {
334
334
  }
335
335
  #findGroupCoordinator(groups, callback) {
336
336
  this[kPerformWithRetry]('findGroupCoordinator', retryCallback => {
337
- this[kConnections].getFirstAvailable(this[kBootstrapBrokers], (error, connection) => {
337
+ this[kGetBootstrapConnection]((error, connection) => {
338
338
  if (error) {
339
339
  retryCallback(error, undefined);
340
340
  return;
@@ -1,18 +1,20 @@
1
1
  import { type ValidateFunction } from 'ajv';
2
2
  import { EventEmitter } from 'node:events';
3
+ import { type CallbackWithPromise } from '../../apis/callbacks.ts';
3
4
  import { type API, type Callback } from '../../apis/definitions.ts';
4
5
  import { type ApiVersionsResponseApi } from '../../apis/metadata/api-versions-v3.ts';
5
6
  import { type ClientType } from '../../diagnostic.ts';
6
7
  import { ConnectionPool } from '../../network/connection-pool.ts';
7
- import { type Broker } from '../../network/connection.ts';
8
+ import { type Broker, type Connection } from '../../network/connection.ts';
8
9
  import { kInstance } from '../../symbols.ts';
9
- import { type CallbackWithPromise } from '../callbacks.ts';
10
10
  import { type Metrics } from '../metrics.ts';
11
11
  import { type BaseOptions, type ClusterMetadata, type MetadataOptions } from './types.ts';
12
12
  export declare const kClientId: unique symbol;
13
13
  export declare const kBootstrapBrokers: unique symbol;
14
14
  export declare const kApis: unique symbol;
15
15
  export declare const kGetApi: unique symbol;
16
+ export declare const kGetConnection: unique symbol;
17
+ export declare const kGetBootstrapConnection: unique symbol;
16
18
  export declare const kOptions: unique symbol;
17
19
  export declare const kConnections: unique symbol;
18
20
  export declare const kFetchConnections: unique symbol;
@@ -63,6 +65,8 @@ export declare class Base<OptionsType extends BaseOptions = BaseOptions> extends
63
65
  [kPerformWithRetry]<ReturnType>(operationId: string, operation: (callback: Callback<ReturnType>) => void, callback: CallbackWithPromise<ReturnType>, attempt?: number, errors?: Error[], shouldSkipRetry?: (e: Error) => boolean): void | Promise<ReturnType>;
64
66
  [kPerformDeduplicated]<ReturnType>(operationId: string, operation: (callback: CallbackWithPromise<ReturnType>) => void, callback: CallbackWithPromise<ReturnType>): void | Promise<ReturnType>;
65
67
  [kGetApi]<RequestArguments extends Array<unknown>, ResponseType>(name: string, callback: Callback<API<RequestArguments, ResponseType>>): void;
68
+ [kGetConnection](broker: Broker, callback: Callback<Connection>): void;
69
+ [kGetBootstrapConnection](callback: Callback<Connection>): void;
66
70
  [kValidateOptions](target: unknown, validator: ValidateFunction<unknown>, targetName: string, throwOnErrors?: boolean): Error | null;
67
71
  [kInspect](...args: unknown[]): void;
68
72
  [kFormatValidationErrors](validator: ValidateFunction<unknown>, targetName: string): string;
@@ -1,4 +1,5 @@
1
1
  import { EventEmitter } from 'node:events';
2
+ import { createPromisifiedCallback, kCallbackPromise } from "../../apis/callbacks.js";
2
3
  import * as apis from "../../apis/index.js";
3
4
  import { api as apiVersionsV3 } from "../../apis/metadata/api-versions-v3.js";
4
5
  import { baseApisChannel, baseMetadataChannel, createDiagnosticContext, notifyCreation } from "../../diagnostic.js";
@@ -6,12 +7,13 @@ import { MultipleErrors, NetworkError, UnsupportedApiError, UserError } from "..
6
7
  import { ConnectionPool } from "../../network/connection-pool.js";
7
8
  import { kInstance } from "../../symbols.js";
8
9
  import { ajv, debugDump, loggers } from "../../utils.js";
9
- import { createPromisifiedCallback, kCallbackPromise } from "../callbacks.js";
10
10
  import { baseOptionsValidator, clientSoftwareName, clientSoftwareVersion, defaultBaseOptions, defaultPort, metadataOptionsValidator } from "./options.js";
11
11
  export const kClientId = Symbol('plt.kafka.base.clientId');
12
12
  export const kBootstrapBrokers = Symbol('plt.kafka.base.bootstrapBrokers');
13
13
  export const kApis = Symbol('plt.kafka.base.apis');
14
14
  export const kGetApi = Symbol('plt.kafka.base.getApi');
15
+ export const kGetConnection = Symbol('plt.kafka.base.getConnection');
16
+ export const kGetBootstrapConnection = Symbol('plt.kafka.base.getBootstrapConnection');
15
17
  export const kOptions = Symbol('plt.kafka.base.options');
16
18
  export const kConnections = Symbol('plt.kafka.base.connections');
17
19
  export const kFetchConnections = Symbol('plt.kafka.base.fetchCnnections');
@@ -119,15 +121,13 @@ export class Base extends EventEmitter {
119
121
  ownerId: this[kInstance],
120
122
  ...this[kOptions]
121
123
  });
122
- for (const event of ['connect', 'disconnect', 'failed', 'drain']) {
123
- pool.on(event, payload => this.emitWithDebug('client', `broker:${event}`, payload));
124
- }
124
+ this.#forwardEvents(pool, ['connect', 'disconnect', 'failed', 'drain', 'sasl:handshake', 'sasl:authentication']);
125
125
  return pool;
126
126
  }
127
127
  [kListApis](callback) {
128
128
  this[kPerformDeduplicated]('listApis', deduplicateCallback => {
129
129
  this[kPerformWithRetry]('listApis', retryCallback => {
130
- this[kConnections].getFirstAvailable(this[kBootstrapBrokers], (error, connection) => {
130
+ this[kGetBootstrapConnection]((error, connection) => {
131
131
  if (error) {
132
132
  retryCallback(error, undefined);
133
133
  return;
@@ -157,7 +157,7 @@ export class Base extends EventEmitter {
157
157
  const autocreateTopics = options.autocreateTopics ?? this[kOptions].autocreateTopics;
158
158
  this[kPerformDeduplicated]('metadata', deduplicateCallback => {
159
159
  this[kPerformWithRetry]('metadata', retryCallback => {
160
- this[kConnections].getFirstAvailable(this[kBootstrapBrokers], (error, connection) => {
160
+ this[kGetBootstrapConnection]((error, connection) => {
161
161
  if (error) {
162
162
  retryCallback(error, undefined);
163
163
  return;
@@ -305,6 +305,12 @@ export class Base extends EventEmitter {
305
305
  }
306
306
  callback(new UnsupportedApiError(`No usable implementation found for API ${name}.`, { minVersion, maxVersion }), undefined);
307
307
  }
308
+ [kGetConnection](broker, callback) {
309
+ this[kConnections].get(broker, callback);
310
+ }
311
+ [kGetBootstrapConnection](callback) {
312
+ this[kConnections].getFirstAvailable(this[kBootstrapBrokers], callback);
313
+ }
308
314
  [kValidateOptions](target, validator, targetName, throwOnErrors = true) {
309
315
  if (!this[kOptions].strict) {
310
316
  return null;
@@ -330,4 +336,11 @@ export class Base extends EventEmitter {
330
336
  this[kClientType] = type;
331
337
  notifyCreation(type, this);
332
338
  }
339
+ #forwardEvents(source, events) {
340
+ for (const event of events) {
341
+ source.on(event, (...args) => {
342
+ this.emitWithDebug('client', `broker:${event}`, ...args);
343
+ });
344
+ }
345
+ }
333
346
  }
@@ -69,6 +69,27 @@ export declare const baseOptionsSchema: {
69
69
  type: string;
70
70
  minimum: number;
71
71
  };
72
+ tls: {
73
+ type: string;
74
+ additionalProperties: boolean;
75
+ };
76
+ sasl: {
77
+ type: string;
78
+ properties: {
79
+ mechanism: {
80
+ type: string;
81
+ enum: readonly ["PLAIN", "SCRAM-SHA-256", "SCRAM-SHA-512"];
82
+ };
83
+ username: {
84
+ type: string;
85
+ };
86
+ password: {
87
+ type: string;
88
+ };
89
+ };
90
+ required: string[];
91
+ additionalProperties: boolean;
92
+ };
72
93
  metadataMaxAge: {
73
94
  type: string;
74
95
  minimum: number;
@@ -1,4 +1,5 @@
1
1
  import { readFileSync } from 'node:fs';
2
+ import { SASLMechanisms } from "../../apis/enumerations.js";
2
3
  import { ajv } from "../../utils.js";
3
4
  const packageJson = JSON.parse(readFileSync(new URL('../../../package.json', import.meta.url), 'utf-8'));
4
5
  // Note: clientSoftwareName can only contain alphanumeric characters, hyphens and dots
@@ -31,6 +32,17 @@ export const baseOptionsSchema = {
31
32
  retries: { type: 'number', minimum: 0 },
32
33
  retryDelay: { type: 'number', minimum: 0 },
33
34
  maxInflights: { type: 'number', minimum: 0 },
35
+ tls: { type: 'object', additionalProperties: true }, // No validation as they come from Node.js
36
+ sasl: {
37
+ type: 'object',
38
+ properties: {
39
+ mechanism: { type: 'string', enum: SASLMechanisms },
40
+ username: { type: 'string' },
41
+ password: { type: 'string' }
42
+ },
43
+ required: ['mechanism', 'username', 'password'],
44
+ additionalProperties: false
45
+ },
34
46
  metadataMaxAge: { type: 'number', minimum: 0 },
35
47
  autocreateTopics: { type: 'boolean' },
36
48
  strict: { type: 'boolean' },
@@ -1,7 +1,7 @@
1
+ import { type CallbackWithPromise } from '../../apis/callbacks.ts';
1
2
  import { type FetchResponse } from '../../apis/consumer/fetch-v17.ts';
2
3
  import { type ConnectionPool } from '../../network/connection-pool.ts';
3
4
  import { Base, kFetchConnections } from '../base/base.ts';
4
- import { type CallbackWithPromise } from '../callbacks.ts';
5
5
  import { MessagesStream } from './messages-stream.ts';
6
6
  import { TopicsMap } from './topics-map.ts';
7
7
  import { type CommitOptions, type ConsumeOptions, type ConsumerOptions, type FetchOptions, type GroupAssignment, type GroupOptions, type ListCommitsOptions, type ListOffsetsOptions, type Offsets } from './types.ts';
@@ -1,11 +1,11 @@
1
+ import { createPromisifiedCallback, kCallbackPromise, runConcurrentCallbacks } from "../../apis/callbacks.js";
1
2
  import { FetchIsolationLevels, FindCoordinatorKeyTypes } from "../../apis/enumerations.js";
2
3
  import { consumerCommitsChannel, consumerConsumesChannel, consumerFetchesChannel, consumerGroupChannel, consumerHeartbeatChannel, consumerOffsetsChannel, createDiagnosticContext } from "../../diagnostic.js";
3
4
  import { UserError } from "../../errors.js";
4
5
  import { Reader } from "../../protocol/reader.js";
5
6
  import { Writer } from "../../protocol/writer.js";
6
- import { Base, kAfterCreate, kBootstrapBrokers, kCheckNotClosed, kClosed, kConnections, kCreateConnectionPool, kFetchConnections, kFormatValidationErrors, kGetApi, kMetadata, kOptions, kPerformDeduplicated, kPerformWithRetry, kPrometheus, kValidateOptions } from "../base/base.js";
7
+ import { Base, kAfterCreate, kCheckNotClosed, kClosed, kCreateConnectionPool, kFetchConnections, kFormatValidationErrors, kGetApi, kGetBootstrapConnection, kGetConnection, kMetadata, kOptions, kPerformDeduplicated, kPerformWithRetry, kPrometheus, kValidateOptions } from "../base/base.js";
7
8
  import { defaultBaseOptions } from "../base/options.js";
8
- import { createPromisifiedCallback, kCallbackPromise, runConcurrentCallbacks } from "../callbacks.js";
9
9
  import { ensureMetric } from "../metrics.js";
10
10
  import { MessagesStream } from "./messages-stream.js";
11
11
  import { commitOptionsValidator, consumeOptionsValidator, consumerOptionsValidator, defaultConsumerOptions, fetchOptionsValidator, groupIdAndOptionsValidator, groupOptionsValidator, listCommitsOptionsValidator, listOffsetsOptionsValidator } from "./options.js";
@@ -340,7 +340,7 @@ export class Consumer extends Base {
340
340
  }
341
341
  runConcurrentCallbacks('Listing offsets failed.', requests, ([leader, requests], concurrentCallback) => {
342
342
  this[kPerformWithRetry]('listOffsets', retryCallback => {
343
- this[kConnections].get(metadata.brokers.get(leader), (error, connection) => {
343
+ this[kGetConnection](metadata.brokers.get(leader), (error, connection) => {
344
344
  if (error) {
345
345
  retryCallback(error, undefined);
346
346
  return;
@@ -508,7 +508,7 @@ export class Consumer extends Base {
508
508
  #performFindGroupCoordinator(callback) {
509
509
  this[kPerformDeduplicated]('findGroupCoordinator', deduplicateCallback => {
510
510
  this[kPerformWithRetry]('findGroupCoordinator', retryCallback => {
511
- this[kConnections].getFirstAvailable(this[kBootstrapBrokers], (error, connection) => {
511
+ this[kGetBootstrapConnection]((error, connection) => {
512
512
  if (error) {
513
513
  retryCallback(error, undefined);
514
514
  return;
@@ -738,7 +738,7 @@ export class Consumer extends Base {
738
738
  return;
739
739
  }
740
740
  this[kPerformWithRetry](operationId, retryCallback => {
741
- this[kConnections].get(metadata.brokers.get(coordinatorId), (error, connection) => {
741
+ this[kGetConnection](metadata.brokers.get(coordinatorId), (error, connection) => {
742
742
  if (error) {
743
743
  retryCallback(error, undefined);
744
744
  return;
@@ -1,7 +1,7 @@
1
1
  import { Readable } from 'node:stream';
2
+ import { type CallbackWithPromise } from '../../apis/callbacks.ts';
2
3
  import { type Message } from '../../protocol/records.ts';
3
4
  import { kInspect } from '../base/base.ts';
4
- import { type CallbackWithPromise } from '../callbacks.ts';
5
5
  import { type Consumer } from './consumer.ts';
6
6
  import { type CommitOptionsPartition, type ConsumeOptions } from './types.ts';
7
7
  export declare function noopDeserializer(data?: Buffer): Buffer | undefined;
@@ -1,9 +1,9 @@
1
1
  import { Readable } from 'node:stream';
2
+ import { createPromisifiedCallback, kCallbackPromise, noopCallback } from "../../apis/callbacks.js";
2
3
  import { ListOffsetTimestamps } from "../../apis/enumerations.js";
3
4
  import { consumerReceivesChannel, createDiagnosticContext, notifyCreation } from "../../diagnostic.js";
4
5
  import { UserError } from "../../errors.js";
5
6
  import { kInspect, kPrometheus } from "../base/base.js";
6
- import { createPromisifiedCallback, kCallbackPromise, noopCallback } from "../callbacks.js";
7
7
  import { ensureMetric } from "../metrics.js";
8
8
  import { defaultConsumerOptions } from "./options.js";
9
9
  import { MessagesStreamFallbackModes, MessagesStreamModes } from "./types.js";
@@ -1,4 +1,3 @@
1
- export * from './callbacks.ts';
2
1
  export * from './serde.ts';
3
2
  export * from './admin/index.ts';
4
3
  export * from './base/index.ts';
@@ -1,4 +1,3 @@
1
- export * from "./callbacks.js";
2
1
  export * from "./serde.js";
3
2
  export * from "./admin/index.js";
4
3
  export * from "./base/index.js";
@@ -1,5 +1,5 @@
1
+ import { type CallbackWithPromise } from '../../apis/callbacks.ts';
1
2
  import { Base } from '../base/base.ts';
2
- import { type CallbackWithPromise } from '../callbacks.ts';
3
3
  import { type ProduceOptions, type ProduceResult, type ProducerInfo, type ProducerOptions, type SendOptions } from './types.ts';
4
4
  export declare class Producer<Key = Buffer, Value = Buffer, HeaderKey = Buffer, HeaderValue = Buffer> extends Base<ProducerOptions<Key, Value, HeaderKey, HeaderValue>> {
5
5
  #private;
@@ -1,10 +1,10 @@
1
+ import { createPromisifiedCallback, kCallbackPromise, runConcurrentCallbacks } from "../../apis/callbacks.js";
1
2
  import { ProduceAcks } from "../../apis/enumerations.js";
2
3
  import { createDiagnosticContext, producerInitIdempotentChannel, producerSendsChannel } from "../../diagnostic.js";
3
4
  import { UserError } from "../../errors.js";
4
5
  import { murmur2 } from "../../protocol/murmur2.js";
5
6
  import { NumericMap } from "../../utils.js";
6
- import { Base, kAfterCreate, kBootstrapBrokers, kCheckNotClosed, kClearMetadata, kClosed, kConnections, kGetApi, kMetadata, kOptions, kPerformDeduplicated, kPerformWithRetry, kPrometheus, kValidateOptions } from "../base/base.js";
7
- import { createPromisifiedCallback, kCallbackPromise, runConcurrentCallbacks } from "../callbacks.js";
7
+ import { Base, kAfterCreate, kCheckNotClosed, kClearMetadata, kClosed, kGetApi, kGetBootstrapConnection, kGetConnection, kMetadata, kOptions, kPerformDeduplicated, kPerformWithRetry, kPrometheus, kValidateOptions } from "../base/base.js";
8
8
  import { ensureMetric } from "../metrics.js";
9
9
  import { produceOptionsValidator, producerOptionsValidator, sendOptionsValidator } from "./options.js";
10
10
  // Don't move this function as being in the same file will enable V8 to remove.
@@ -108,7 +108,7 @@ export class Producer extends Base {
108
108
  #initIdempotentProducer(options, callback) {
109
109
  this[kPerformDeduplicated]('initProducerId', deduplicateCallback => {
110
110
  this[kPerformWithRetry]('initProducerId', retryCallback => {
111
- this[kConnections].getFirstAvailable(this[kBootstrapBrokers], (error, connection) => {
111
+ this[kGetBootstrapConnection]((error, connection) => {
112
112
  if (error) {
113
113
  retryCallback(error, undefined);
114
114
  return;
@@ -292,7 +292,7 @@ export class Producer extends Base {
292
292
  const { topic, partition } = messages[0];
293
293
  const leader = metadata.topics.get(topic).partitions[partition].leader;
294
294
  this[kPerformWithRetry]('produce', retryCallback => {
295
- this[kConnections].get(metadata.brokers.get(leader), (error, connection) => {
295
+ this[kGetConnection](metadata.brokers.get(leader), (error, connection) => {
296
296
  if (error) {
297
297
  retryCallback(error, undefined);
298
298
  return;
@@ -19,13 +19,13 @@ export type ClientDiagnosticEvent<InstanceType extends Base = Base, Attributes =
19
19
  export type TracingChannelWithName<EventType extends object> = TracingChannel<string, EventType> & {
20
20
  name: string;
21
21
  };
22
- export type DiagnosticContext<BaseContext> = BaseContext & {
22
+ export type DiagnosticContext<BaseContext = {}> = BaseContext & {
23
23
  operationId: bigint;
24
24
  result?: unknown;
25
25
  error?: unknown;
26
26
  };
27
27
  export declare const channelsNamespace: "plt:kafka";
28
- export declare function createDiagnosticContext<BaseContext>(context: BaseContext): DiagnosticContext<BaseContext>;
28
+ export declare function createDiagnosticContext<BaseContext = {}>(context: BaseContext): DiagnosticContext<BaseContext>;
29
29
  export declare function notifyCreation<InstanceType>(type: ClientType | 'connection' | 'connection-pool' | 'messages-stream', instance: InstanceType): void;
30
30
  export declare function createTracingChannel<DiagnosticEvent extends object>(name: string): TracingChannelWithName<DiagnosticEvent>;
31
31
  export declare const instancesChannel: import("diagnostics_channel").Channel<unknown, unknown>;
package/dist/errors.js CHANGED
@@ -87,7 +87,7 @@ export * from "./protocol/errors.js";
87
87
  export class AuthenticationError extends GenericError {
88
88
  static code = 'PLT_KFK_AUTHENTICATION';
89
89
  constructor(message, properties = {}) {
90
- super(AuthenticationError.code, message, properties);
90
+ super(AuthenticationError.code, message, { canRetry: false, ...properties });
91
91
  }
92
92
  }
93
93
  export class NetworkError extends GenericError {
@@ -1,5 +1,5 @@
1
1
  import EventEmitter from 'node:events';
2
- import { type CallbackWithPromise } from '../clients/callbacks.ts';
2
+ import { type CallbackWithPromise } from '../apis/callbacks.ts';
3
3
  import { Connection, type Broker, type ConnectionOptions } from './connection.ts';
4
4
  export declare class ConnectionPool extends EventEmitter {
5
5
  #private;
@@ -7,6 +7,8 @@ export declare class ConnectionPool extends EventEmitter {
7
7
  get instanceId(): number;
8
8
  get(broker: Broker, callback: CallbackWithPromise<Connection>): void;
9
9
  get(broker: Broker): Promise<Connection>;
10
- getFirstAvailable(brokers: Broker[], callback?: CallbackWithPromise<Connection>): void | Promise<Connection>;
11
- close(callback?: CallbackWithPromise<void>): void | Promise<void>;
10
+ getFirstAvailable(brokers: Broker[], callback: CallbackWithPromise<Connection>): void;
11
+ getFirstAvailable(brokers: Broker[]): Promise<Connection>;
12
+ close(callback: CallbackWithPromise<void>): void;
13
+ close(): Promise<void>;
12
14
  }
@@ -1,5 +1,5 @@
1
1
  import EventEmitter from 'node:events';
2
- import { createPromisifiedCallback, kCallbackPromise, runConcurrentCallbacks } from "../clients/callbacks.js";
2
+ import { createPromisifiedCallback, kCallbackPromise, runConcurrentCallbacks } from "../apis/callbacks.js";
3
3
  import { connectionsPoolGetsChannel, createDiagnosticContext, notifyCreation } from "../diagnostic.js";
4
4
  import { MultipleErrors } from "../errors.js";
5
5
  import { Connection, ConnectionStatuses } from "./connection.js";
@@ -83,6 +83,12 @@ export class ConnectionPool extends EventEmitter {
83
83
  this.emit('connect', eventPayload);
84
84
  callback(null, connection);
85
85
  });
86
+ connection.on('sasl:handshake', mechanisms => {
87
+ this.emit('sasl:handshake', { ...eventPayload, mechanisms });
88
+ });
89
+ connection.on('sasl:authentication', authentication => {
90
+ this.emit('sasl:authentication', { ...eventPayload, authentication });
91
+ });
86
92
  // Remove stale connections from the pool
87
93
  connection.once('close', () => {
88
94
  this.emit('disconnect', eventPayload);
@@ -1,17 +1,24 @@
1
1
  import EventEmitter from 'node:events';
2
2
  import { type Socket } from 'node:net';
3
3
  import { type ConnectionOptions as TLSConnectionOptions } from 'node:tls';
4
+ import { type CallbackWithPromise } from '../apis/callbacks.ts';
4
5
  import { type Callback, type ResponseParser } from '../apis/definitions.ts';
5
- import { type CallbackWithPromise } from '../clients/callbacks.ts';
6
+ import { type SASLMechanism } from '../apis/enumerations.ts';
6
7
  import { Writer } from '../protocol/writer.ts';
7
8
  export interface Broker {
8
9
  host: string;
9
10
  port: number;
10
11
  }
12
+ export interface SASLOptions {
13
+ mechanism: SASLMechanism;
14
+ username: string;
15
+ password: string;
16
+ }
11
17
  export interface ConnectionOptions {
12
18
  connectTimeout?: number;
13
19
  maxInflights?: number;
14
20
  tls?: TLSConnectionOptions;
21
+ sasl?: SASLOptions;
15
22
  ownerId?: number;
16
23
  }
17
24
  export interface Request {
@@ -28,6 +35,7 @@ export interface Request {
28
35
  export declare const ConnectionStatuses: {
29
36
  readonly NONE: "none";
30
37
  readonly CONNECTING: "connecting";
38
+ readonly AUTHENTICATING: "authenticating";
31
39
  readonly CONNECTED: "connected";
32
40
  readonly CLOSED: "closed";
33
41
  readonly CLOSING: "closing";
@@ -43,7 +51,9 @@ export declare class Connection extends EventEmitter {
43
51
  get status(): ConnectionStatusValue;
44
52
  get socket(): Socket;
45
53
  connect(host: string, port: number, callback?: CallbackWithPromise<void>): void | Promise<void>;
46
- ready(callback?: CallbackWithPromise<void>): void | Promise<void>;
47
- close(callback?: CallbackWithPromise<void>): void | Promise<void>;
54
+ ready(callback: CallbackWithPromise<void>): void;
55
+ ready(): Promise<void>;
56
+ close(callback: CallbackWithPromise<void>): void;
57
+ close(): Promise<void>;
48
58
  send<ReturnType>(apiKey: number, apiVersion: number, payload: () => Writer, responseParser: ResponseParser<ReturnType>, hasRequestHeaderTaggedFields: boolean, hasResponseHeaderTaggedFields: boolean, callback: Callback<ReturnType>): void;
49
59
  }
@@ -2,18 +2,23 @@ import fastq from 'fastq';
2
2
  import EventEmitter from 'node:events';
3
3
  import { createConnection } from 'node:net';
4
4
  import { connect as createTLSConnection } from 'node:tls';
5
- import { createPromisifiedCallback, kCallbackPromise } from "../clients/callbacks.js";
5
+ import { createPromisifiedCallback, kCallbackPromise } from "../apis/callbacks.js";
6
+ import { SASLMechanisms } from "../apis/enumerations.js";
7
+ import { saslAuthenticateV2, saslHandshakeV1 } from "../apis/index.js";
6
8
  import { connectionsApiChannel, connectionsConnectsChannel, createDiagnosticContext, notifyCreation } from "../diagnostic.js";
7
- import { NetworkError, TimeoutError, UnexpectedCorrelationIdError } from "../errors.js";
9
+ import { AuthenticationError, NetworkError, TimeoutError, UnexpectedCorrelationIdError, UserError } from "../errors.js";
8
10
  import { protocolAPIsById } from "../protocol/apis.js";
9
11
  import { EMPTY_OR_SINGLE_COMPACT_LENGTH_SIZE, INT32_SIZE } from "../protocol/definitions.js";
10
12
  import { DynamicBuffer } from "../protocol/dynamic-buffer.js";
13
+ import { saslPlain, saslScramSha } from "../protocol/index.js";
11
14
  import { Reader } from "../protocol/reader.js";
15
+ import { defaultCrypto } from "../protocol/sasl/scram-sha.js";
12
16
  import { Writer } from "../protocol/writer.js";
13
17
  import { loggers } from "../utils.js";
14
18
  export const ConnectionStatuses = {
15
19
  NONE: 'none',
16
20
  CONNECTING: 'connecting',
21
+ AUTHENTICATING: 'authenticating',
17
22
  CONNECTED: 'connected',
18
23
  CLOSED: 'closed',
19
24
  CLOSING: 'closing',
@@ -97,14 +102,8 @@ export class Connection extends EventEmitter {
97
102
  this.emit('error', error);
98
103
  connectionsConnectsChannel.asyncEnd.publish(diagnosticContext);
99
104
  };
100
- const connectionErrorHandler = (e) => {
101
- const error = new NetworkError(`Connection to ${host}:${port} failed.`, { cause: e });
102
- diagnosticContext.error = error;
103
- this.#status = ConnectionStatuses.ERROR;
104
- connectionsConnectsChannel.error.publish(diagnosticContext);
105
- connectionsConnectsChannel.asyncStart.publish(diagnosticContext);
106
- this.emit('error', error);
107
- connectionsConnectsChannel.asyncEnd.publish(diagnosticContext);
105
+ const connectionErrorHandler = (error) => {
106
+ this.#onConnectionError(host, port, diagnosticContext, error);
108
107
  };
109
108
  this.emit('connecting');
110
109
  /* c8 ignore next 3 - TLS connection is not tested but we rely on Node.js tests */
@@ -120,10 +119,12 @@ export class Connection extends EventEmitter {
120
119
  this.#socket.on('drain', this.#onDrain.bind(this));
121
120
  this.#socket.on('close', this.#onClose.bind(this));
122
121
  this.#socket.setTimeout(0);
123
- this.#status = ConnectionStatuses.CONNECTED;
124
- connectionsConnectsChannel.asyncStart.publish(diagnosticContext);
125
- this.emit('connect');
126
- connectionsConnectsChannel.asyncEnd.publish(diagnosticContext);
122
+ if (this.#options.sasl) {
123
+ this.#authenticate(host, port, diagnosticContext);
124
+ }
125
+ else {
126
+ this.#onConnectionSucceed(diagnosticContext);
127
+ }
127
128
  });
128
129
  this.#socket.once('timeout', connectionTimeoutHandler);
129
130
  this.#socket.once('error', connectionErrorHandler);
@@ -212,6 +213,27 @@ export class Connection extends EventEmitter {
212
213
  return this.#sendRequest(request);
213
214
  }, callback);
214
215
  }
216
+ #authenticate(host, port, diagnosticContext) {
217
+ this.#status = ConnectionStatuses.AUTHENTICATING;
218
+ const { mechanism, username, password } = this.#options.sasl;
219
+ if (!SASLMechanisms.includes(mechanism)) {
220
+ this.#onConnectionError(host, port, diagnosticContext, new UserError(`SASL mechanism ${mechanism} not supported.`));
221
+ return;
222
+ }
223
+ saslHandshakeV1.api(this, mechanism, (error, response) => {
224
+ if (error) {
225
+ this.#onConnectionError(host, port, diagnosticContext, new AuthenticationError('Cannot find a suitable SASL mechanism.', { cause: error }));
226
+ return;
227
+ }
228
+ this.emit('sasl:handshake', response.mechanisms);
229
+ if (mechanism === 'PLAIN') {
230
+ saslPlain.authenticate(saslAuthenticateV2.api, this, username, password, this.#onSaslAuthenticate.bind(this, host, port, diagnosticContext));
231
+ }
232
+ else {
233
+ saslScramSha.authenticate(saslAuthenticateV2.api, this, mechanism.substring(6), username, password, defaultCrypto, this.#onSaslAuthenticate.bind(this, host, port, diagnosticContext));
234
+ }
235
+ });
236
+ }
215
237
  /*
216
238
  Request => Size [Request Header v2] [payload]
217
239
  Request Header v2 => request_api_key request_api_version correlation_id client_id TAG_BUFFER
@@ -223,7 +245,7 @@ export class Connection extends EventEmitter {
223
245
  #sendRequest(request) {
224
246
  connectionsApiChannel.start.publish(request.diagnostic);
225
247
  try {
226
- if (this.#status !== ConnectionStatuses.CONNECTED) {
248
+ if (this.#status !== ConnectionStatuses.CONNECTED && this.#status !== ConnectionStatuses.AUTHENTICATING) {
227
249
  request.callback(new NetworkError('Connection closed'), undefined);
228
250
  return false;
229
251
  }
@@ -245,7 +267,7 @@ export class Connection extends EventEmitter {
245
267
  if (!payload.context.noResponse) {
246
268
  this.#inflightRequests.set(correlationId, request);
247
269
  }
248
- loggers.protocol({ apiKey: protocolAPIsById[apiKey], correlationId, request }, 'Sending request.');
270
+ loggers.protocol('Sending request.', { apiKey: protocolAPIsById[apiKey], correlationId, request });
249
271
  for (const buf of writer.buffers) {
250
272
  if (!this.#socket.write(buf)) {
251
273
  canWrite = false;
@@ -272,6 +294,34 @@ export class Connection extends EventEmitter {
272
294
  connectionsApiChannel.end.publish(request.diagnostic);
273
295
  }
274
296
  }
297
+ #onConnectionSucceed(diagnosticContext) {
298
+ this.#status = ConnectionStatuses.CONNECTED;
299
+ connectionsConnectsChannel.asyncStart.publish(diagnosticContext);
300
+ this.emit('connect');
301
+ connectionsConnectsChannel.asyncEnd.publish(diagnosticContext);
302
+ }
303
+ #onConnectionError(host, port, diagnosticContext, cause) {
304
+ const error = new NetworkError(`Connection to ${host}:${port} failed.`, { cause });
305
+ this.#status = ConnectionStatuses.ERROR;
306
+ diagnosticContext.error = error;
307
+ connectionsConnectsChannel.error.publish(diagnosticContext);
308
+ connectionsConnectsChannel.asyncStart.publish(diagnosticContext);
309
+ this.emit('error', error);
310
+ connectionsConnectsChannel.asyncEnd.publish(diagnosticContext);
311
+ this.#socket.end();
312
+ }
313
+ #onSaslAuthenticate(host, port, diagnosticContext, error, response) {
314
+ if (error) {
315
+ const protocolError = error.errors[0];
316
+ if (protocolError.apiId === 'SASL_AUTHENTICATION_FAILED') {
317
+ error = new AuthenticationError('SASL authentication failed.', { cause: error });
318
+ }
319
+ this.#onConnectionError(host, port, diagnosticContext, error);
320
+ return;
321
+ }
322
+ this.emit('sasl:authentication', response.authBytes);
323
+ this.#onConnectionSucceed(diagnosticContext);
324
+ }
275
325
  /*
276
326
  Response Header v1 => correlation_id TAG_BUFFER
277
327
  correlation_id => INT32
@@ -327,7 +377,7 @@ export class Connection extends EventEmitter {
327
377
  // apiKey: protocolAPIsById[apiKey],
328
378
  // correlationId
329
379
  // })
330
- loggers.protocol({ apiKey: protocolAPIsById[apiKey], correlationId, request }, 'Received response.');
380
+ loggers.protocol('Received response.', { apiKey: protocolAPIsById[apiKey], correlationId, request, deserialized });
331
381
  if (responseError) {
332
382
  request.diagnostic.error = responseError;
333
383
  connectionsApiChannel.error.publish(request.diagnostic);
@@ -1,3 +1,5 @@
1
+ import { type CallbackWithPromise } from '../../apis/callbacks.ts';
1
2
  import { type SASLAuthenticationAPI, type SaslAuthenticateResponse } from '../../apis/security/sasl-authenticate-v2.ts';
2
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;
3
5
  export declare function authenticate(authenticateAPI: SASLAuthenticationAPI, connection: Connection, username: string, password: string): Promise<SaslAuthenticateResponse>;
@@ -1,3 +1,8 @@
1
- export function authenticate(authenticateAPI, connection, username, password) {
2
- return authenticateAPI.async(connection, Buffer.from(['', username, password].join('\0')));
1
+ import { createPromisifiedCallback, kCallbackPromise } from "../../apis/callbacks.js";
2
+ export function authenticate(authenticateAPI, connection, username, password, callback) {
3
+ if (!callback) {
4
+ callback = createPromisifiedCallback();
5
+ }
6
+ authenticateAPI(connection, Buffer.from(['', username, password].join('\0')), callback);
7
+ return callback[kCallbackPromise];
3
8
  }
@@ -1,3 +1,4 @@
1
+ import { type CallbackWithPromise } from '../../apis/callbacks.ts';
1
2
  import { type SASLAuthenticationAPI, type SaslAuthenticateResponse } from '../../apis/security/sasl-authenticate-v2.ts';
2
3
  import { type Connection } from '../../network/connection.ts';
3
4
  export interface ScramAlgorithmDefinition {
@@ -32,4 +33,5 @@ export declare function hi(definition: ScramAlgorithmDefinition, password: strin
32
33
  export declare function hmac(definition: ScramAlgorithmDefinition, key: Buffer, data: string | Buffer): Buffer;
33
34
  export declare function xor(a: Buffer, b: Buffer): Buffer;
34
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;
35
37
  export declare function authenticate(authenticateAPI: SASLAuthenticationAPI, connection: Connection, algorithm: ScramAlgorithm, username: string, password: string, crypto?: ScramCryptoModule): Promise<SaslAuthenticateResponse>;
@@ -1,4 +1,5 @@
1
1
  import { createHash, createHmac, pbkdf2Sync, randomBytes } from 'node:crypto';
2
+ import { createPromisifiedCallback, kCallbackPromise } from "../../apis/callbacks.js";
2
3
  import { AuthenticationError } from "../../errors.js";
3
4
  const GS2_HEADER = 'n,,';
4
5
  const GS2_HEADER_BASE64 = Buffer.from(GS2_HEADER).toString('base64');
@@ -56,8 +57,10 @@ export const defaultCrypto = {
56
57
  hmac,
57
58
  xor
58
59
  };
59
- // Implements https://datatracker.ietf.org/doc/html/rfc5802#section-9
60
- export async function authenticate(authenticateAPI, connection, algorithm, username, password, crypto = defaultCrypto) {
60
+ export function authenticate(authenticateAPI, connection, algorithm, username, password, crypto = defaultCrypto, callback) {
61
+ if (!callback) {
62
+ callback = createPromisifiedCallback();
63
+ }
61
64
  const { h, hi, hmac, xor } = crypto;
62
65
  const definition = ScramAlgorithms[algorithm];
63
66
  if (!definition) {
@@ -66,45 +69,60 @@ export async function authenticate(authenticateAPI, connection, algorithm, usern
66
69
  const clientNonce = createNonce();
67
70
  const clientFirstMessageBare = `n=${sanitizeString(username)},r=${clientNonce}`;
68
71
  // First of all, send the first message
69
- const firstResponse = await authenticateAPI.async(connection, Buffer.from(`${GS2_HEADER}${clientFirstMessageBare}`));
70
- const firstData = parseParameters(firstResponse.authBytes);
71
- // Extract some parameters
72
- const salt = Buffer.from(firstData.s, 'base64');
73
- const iterations = parseInt(firstData.i, 10);
74
- const serverNonce = firstData.r;
75
- const serverFirstMessage = firstData.__original;
76
- // Validate response
77
- if (!serverNonce.startsWith(clientNonce)) {
78
- throw new AuthenticationError('Server nonce does not start with client nonce.');
79
- }
80
- if (definition.minIterations > iterations) {
81
- throw new AuthenticationError(`Algorithm ${algorithm} requires at least ${definition.minIterations} iterations, while ${iterations} were requested.`);
82
- }
83
- // SaltedPassword := Hi(Normalize(password), salt, i)
84
- // ClientKey := HMAC(SaltedPassword, "Client Key")
85
- // StoredKey := H(ClientKey)
86
- // AuthMessage := ClientFirstMessageBare + "," ServerFirstMessage + "," + ClientFinalMessageWithoutProof
87
- // ClientSignature := HMAC(StoredKey, AuthMessage)
88
- // ClientProof := ClientKey XOR ClientSignature
89
- // ServerKey := HMAC(SaltedPassword, "Server Key")
90
- // ServerSignature := HMAC(ServerKey, AuthMessage)
91
- const saltedPassword = hi(definition, password, salt, iterations);
92
- const clientKey = hmac(definition, saltedPassword, HMAC_CLIENT_KEY);
93
- const storedKey = h(definition, clientKey);
94
- const clientFinalMessageWithoutProof = `c=${GS2_HEADER_BASE64},r=${serverNonce}`;
95
- const authMessage = `${clientFirstMessageBare},${serverFirstMessage},${clientFinalMessageWithoutProof}`;
96
- const clientSignature = hmac(definition, storedKey, authMessage);
97
- const clientProof = xor(clientKey, clientSignature);
98
- const serverKey = hmac(definition, saltedPassword, HMAC_SERVER_KEY);
99
- const serverSignature = hmac(definition, serverKey, authMessage);
100
- // Send the last message to the server
101
- const lastResponse = await authenticateAPI.async(connection, Buffer.from(`${clientFinalMessageWithoutProof},p=${clientProof.toString('base64')}`));
102
- const lastData = parseParameters(lastResponse.authBytes);
103
- if (lastData.e) {
104
- throw new AuthenticationError(lastData.e);
105
- }
106
- else if (lastData.v !== serverSignature.toString('base64')) {
107
- throw new AuthenticationError('Invalid server signature.');
108
- }
109
- return lastResponse;
72
+ authenticateAPI(connection, Buffer.from(`${GS2_HEADER}${clientFirstMessageBare}`), (error, firstResponse) => {
73
+ if (error) {
74
+ callback(new AuthenticationError('Authentication failed.', { cause: error }), undefined);
75
+ return;
76
+ }
77
+ const firstData = parseParameters(firstResponse.authBytes);
78
+ // Extract some parameters
79
+ const salt = Buffer.from(firstData.s, 'base64');
80
+ const iterations = parseInt(firstData.i, 10);
81
+ const serverNonce = firstData.r;
82
+ const serverFirstMessage = firstData.__original;
83
+ // Validate response
84
+ if (!serverNonce.startsWith(clientNonce)) {
85
+ callback(new AuthenticationError('Server nonce does not start with client nonce.'), undefined);
86
+ return;
87
+ }
88
+ else if (definition.minIterations > iterations) {
89
+ callback(new AuthenticationError(`Algorithm ${algorithm} requires at least ${definition.minIterations} iterations, while ${iterations} were requested.`), undefined);
90
+ return;
91
+ }
92
+ // SaltedPassword := Hi(Normalize(password), salt, i)
93
+ // ClientKey := HMAC(SaltedPassword, "Client Key")
94
+ // StoredKey := H(ClientKey)
95
+ // AuthMessage := ClientFirstMessageBare + "," ServerFirstMessage + "," + ClientFinalMessageWithoutProof
96
+ // ClientSignature := HMAC(StoredKey, AuthMessage)
97
+ // ClientProof := ClientKey XOR ClientSignature
98
+ // ServerKey := HMAC(SaltedPassword, "Server Key")
99
+ // ServerSignature := HMAC(ServerKey, AuthMessage)
100
+ const saltedPassword = hi(definition, password, salt, iterations);
101
+ const clientKey = hmac(definition, saltedPassword, HMAC_CLIENT_KEY);
102
+ const storedKey = h(definition, clientKey);
103
+ const clientFinalMessageWithoutProof = `c=${GS2_HEADER_BASE64},r=${serverNonce}`;
104
+ const authMessage = `${clientFirstMessageBare},${serverFirstMessage},${clientFinalMessageWithoutProof}`;
105
+ const clientSignature = hmac(definition, storedKey, authMessage);
106
+ const clientProof = xor(clientKey, clientSignature);
107
+ const serverKey = hmac(definition, saltedPassword, HMAC_SERVER_KEY);
108
+ const serverSignature = hmac(definition, serverKey, authMessage);
109
+ authenticateAPI(connection, Buffer.from(`${clientFinalMessageWithoutProof},p=${clientProof.toString('base64')}`), (error, lastResponse) => {
110
+ if (error) {
111
+ callback(new AuthenticationError('Authentication failed.', { cause: error }), undefined);
112
+ return;
113
+ }
114
+ // Send the last message to the server
115
+ const lastData = parseParameters(lastResponse.authBytes);
116
+ if (lastData.e) {
117
+ callback(new AuthenticationError(lastData.e), undefined);
118
+ return;
119
+ }
120
+ else if (lastData.v !== serverSignature.toString('base64')) {
121
+ callback(new AuthenticationError('Invalid server signature.'), undefined);
122
+ return;
123
+ }
124
+ callback(null, lastResponse);
125
+ });
126
+ });
127
+ return callback[kCallbackPromise];
110
128
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/kafka",
3
- "version": "1.2.0",
3
+ "version": "1.3.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)",
File without changes