@itentialopensource/adapter-kafkav2 1.0.5 → 1.0.7

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/CHANGELOG.md CHANGED
@@ -1,4 +1,20 @@
1
1
 
2
+ ## 1.0.7 [07-17-2026]
3
+
4
+ * Add SASL OAUTHBEARER support
5
+
6
+ See merge request itentialopensource/adapters/adapter-kafkav2!80
7
+
8
+ ---
9
+
10
+ ## 1.0.6 [07-13-2026]
11
+
12
+ * Upgrade kafka-lz4-lite
13
+
14
+ See merge request itentialopensource/adapters/adapter-kafkav2!82
15
+
16
+ ---
17
+
2
18
  ## 1.0.5 [07-01-2026]
3
19
 
4
20
  * Changes made at 2026.07.01_14:21PM
package/adapter.js CHANGED
@@ -29,6 +29,152 @@ const DEFAULT_SOCKET_TIMEOUT = 30000; // 30s
29
29
 
30
30
  const axios = require('axios');
31
31
 
32
+ /**
33
+ * Builds an oauthBearerProvider function for KafkaJS using the OAuth 2.0
34
+ * client_credentials grant. Called by resolveSaslConfig when mechanism is 'oauthbearer'.
35
+ *
36
+ * Token lifecycle:
37
+ * - Caches the access token and only fetches a new one when within REFRESH_BUFFER_MS
38
+ * of expiry, avoiding unnecessary round-trips on every KafkaJS (re)authentication.
39
+ * - If the token endpoint returns a refresh_token, it is stored and used on subsequent
40
+ * refreshes via grant_type=refresh_token. This avoids re-sending client credentials on every cycle.
41
+ * - If the refresh_token grant fails (token expired, rotated, or revoked) the stored
42
+ * refresh_token is cleared and the provider automatically falls back to a fresh
43
+ * client_credentials grant, so the adapter self-heals without operator intervention.
44
+ *
45
+ * @param {object} oauthConfig - The sasl.oauth config block from adapter properties.
46
+ * @param {string} oauthConfig.tokenEndpoint - Token endpoint URL.
47
+ * @param {string} oauthConfig.clientId - OAuth client ID.
48
+ * @param {string} oauthConfig.clientSecret - OAuth client secret.
49
+ * @param {string} [oauthConfig.scope] - Optional OAuth scope.
50
+ * @param {number} [oauthConfig.tokenRefreshBuffer=30000] - Milliseconds before token expiry
51
+ * to proactively refresh. Defaults to 30000 (30s).
52
+ * @returns {Function} Async provider function returning { value }.
53
+ */
54
+ function buildOAuthBearerProvider(oauthConfig) {
55
+ const {
56
+ tokenEndpoint, clientId, clientSecret, scope, tokenRefreshBuffer
57
+ } = oauthConfig;
58
+
59
+ const REFRESH_BUFFER_MS = Number.isInteger(tokenRefreshBuffer) && tokenRefreshBuffer > 0
60
+ ? tokenRefreshBuffer
61
+ : 30000;
62
+ let cachedToken = null;
63
+ let tokenExpiresAt = 0;
64
+ let storedRefreshToken = null;
65
+ // Single in-flight refresh promise (promise coalescing / single-flight pattern).
66
+ // Concurrent callers that arrive while a refresh is in-flight await this same
67
+ // promise instead of each issuing their own HTTP request to the token endpoint.
68
+ let refreshPromise = null;
69
+
70
+ /**
71
+ * POSTs to the token endpoint and returns response data.
72
+ * Validates that access_token is present, stores any issued refresh_token.
73
+ */
74
+ async function postToTokenEndpoint(params) {
75
+ const grantType = params.get('grant_type');
76
+ log.debug(`Kafka OAuth: requesting token via grant_type=${grantType}`);
77
+ const response = await axios.post(tokenEndpoint, params.toString(), {
78
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
79
+ });
80
+ const { data } = response;
81
+ if (!data || !data.access_token) {
82
+ throw new Error(`Kafka adapter: token endpoint returned an invalid response: ${JSON.stringify(data)}`);
83
+ }
84
+ if (data.refresh_token) {
85
+ log.debug('Kafka OAuth: refresh_token received and stored');
86
+ storedRefreshToken = data.refresh_token;
87
+ }
88
+ return data;
89
+ }
90
+
91
+ /**
92
+ * Fetches a new token from the token endpoint. Tries grant_type=refresh_token first
93
+ * if a refresh token is available, falling back to client_credentials on failure.
94
+ */
95
+ async function fetchToken() {
96
+ if (storedRefreshToken) {
97
+ try {
98
+ const data = await postToTokenEndpoint(new URLSearchParams({
99
+ grant_type: 'refresh_token',
100
+ refresh_token: storedRefreshToken,
101
+ client_id: clientId,
102
+ client_secret: clientSecret
103
+ }));
104
+ return data;
105
+ } catch (_refreshErr) {
106
+ log.warn(`Kafka adapter: refresh_token grant failed, falling back to client_credentials. Reason: ${_refreshErr.message}`);
107
+ storedRefreshToken = null;
108
+ }
109
+ }
110
+
111
+ const data = await postToTokenEndpoint(new URLSearchParams({
112
+ grant_type: 'client_credentials',
113
+ client_id: clientId,
114
+ client_secret: clientSecret,
115
+ ...(scope ? { scope } : {})
116
+ }));
117
+ return data;
118
+ }
119
+
120
+ function hasValidCachedToken() {
121
+ return cachedToken !== null && Date.now() < tokenExpiresAt - REFRESH_BUFFER_MS;
122
+ }
123
+
124
+ return async () => {
125
+ if (hasValidCachedToken()) {
126
+ log.debug('Kafka OAuth: returning cached token');
127
+ return cachedToken;
128
+ }
129
+
130
+ if (!refreshPromise) {
131
+ refreshPromise = (async () => {
132
+ try {
133
+ // eslint-disable-next-line camelcase
134
+ const { access_token: accessToken, expires_in: expiresIn } = await fetchToken();
135
+
136
+ cachedToken = { value: accessToken };
137
+
138
+ tokenExpiresAt = Date.now() + (expiresIn ?? 3600) * 1000;
139
+ log.info(`Kafka OAuth: new token cached, expires in ${expiresIn ?? 3600}s`);
140
+ return cachedToken;
141
+ } finally {
142
+ refreshPromise = null;
143
+ }
144
+ })();
145
+ }
146
+
147
+ return refreshPromise;
148
+ };
149
+ }
150
+
151
+ /**
152
+ * Resolves a SASL config object for KafkaJS. For non-oauthbearer mechanisms the
153
+ * config is returned unchanged. For oauthbearer it builds the oauthBearerProvider
154
+ * function from the sasl.oauth sub-object.
155
+ *
156
+ * @param {object|null} saslConfig - SASL config from adapter properties.
157
+ * @returns {object|null} KafkaJS-compatible SASL config.
158
+ */
159
+ function resolveSaslConfig(saslConfig) {
160
+ if (!saslConfig) return saslConfig;
161
+
162
+ const mechanism = (saslConfig.mechanism || '').toLowerCase();
163
+ if (mechanism !== 'oauthbearer') return saslConfig;
164
+
165
+ if (!saslConfig.oauth) {
166
+ throw new Error(
167
+ 'Kafka adapter: SASL mechanism is "oauthbearer" but sasl.oauth config is missing. '
168
+ + 'Provide tokenEndpoint, clientId, and clientSecret under sasl.oauth.'
169
+ );
170
+ }
171
+
172
+ return {
173
+ mechanism: 'oauthbearer',
174
+ oauthBearerProvider: buildOAuthBearerProvider(saslConfig.oauth)
175
+ };
176
+ }
177
+
32
178
  let needRestart = false;
33
179
 
34
180
  function consoleLoggerProvider(name) {
@@ -733,10 +879,13 @@ class Kafkav2 extends AdapterBaseCl {
733
879
  if (this.props.client.ssl && this.props.client.ssl.key) {
734
880
  combinedProps.ssl.key = fs.readFileSync(this.props.client.ssl.key, 'utf-8');
735
881
  }
882
+ if (combinedProps.sasl) {
883
+ combinedProps.sasl = resolveSaslConfig(combinedProps.sasl);
884
+ }
736
885
  this.KafkaClient = new Kafka(combinedProps);
737
886
 
738
887
  if (this.props.client && this.props.client.producersasl) {
739
- combinedProps.sasl = this.props.client.producersasl;
888
+ combinedProps.sasl = resolveSaslConfig(this.props.client.producersasl);
740
889
  this.KafkaProducerClient = new Kafka(combinedProps);
741
890
  this.producer = this.KafkaProducerClient.producer(this.props.producer || {});
742
891
  } else {
@@ -750,7 +899,7 @@ class Kafkav2 extends AdapterBaseCl {
750
899
  }
751
900
 
752
901
  if (this.props.client && this.props.client.consumersasl) {
753
- combinedProps.sasl = this.props.client.consumersasl;
902
+ combinedProps.sasl = resolveSaslConfig(this.props.client.consumersasl);
754
903
  this.KafkaConsumerClient = new Kafka(combinedProps);
755
904
  this.consumer = this.KafkaConsumerClient.consumer(consumerConfig);
756
905
  } else {
@@ -1029,7 +1178,7 @@ class Kafkav2 extends AdapterBaseCl {
1029
1178
  });
1030
1179
  } catch (ex) {
1031
1180
  const errorObj = formatErrorObject(origin, 'Caught Exception', null, null, null, ex);
1032
- log.error(JSON.stringify(ex));
1181
+ log.error(ex instanceof Error ? ex.message : JSON.stringify(ex));
1033
1182
  log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
1034
1183
  return (null, errorObj);
1035
1184
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@itentialopensource/adapter-kafkav2",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "Itential adapter to connect to kafka",
5
5
  "main": "adapter.js",
6
6
  "scripts": {
@@ -44,11 +44,11 @@
44
44
  "fs-extra": "11.3.6",
45
45
  "json-query": "2.2.2",
46
46
  "kafkajs": "2.2.4",
47
- "kafka-lz4-lite": "1.0.5",
47
+ "kafka-lz4-lite": "2.0.1",
48
48
  "kafkajs-snappy": "1.1.0",
49
49
  "mongodb": "4.17.2",
50
50
  "readline-sync": "1.4.10",
51
- "uuid": "3.0.1",
51
+ "uuid": "11.1.1",
52
52
  "mocha": "12.0.0-rc.1",
53
53
  "winston": "3.17.0"
54
54
  },
@@ -80,7 +80,38 @@
80
80
  "type": "object"
81
81
  },
82
82
  "sasl": {
83
- "type": "object"
83
+ "type": "object",
84
+ "description": "SASL authentication. Supports plain, scram-sha-256, scram-sha-512, and oauthbearer. For oauthbearer, omit username/password and provide an 'oauth' sub-object instead.",
85
+ "properties": {
86
+ "mechanism": {
87
+ "type": "string",
88
+ "description": "SASL mechanism: plain | scram-sha-256 | scram-sha-512 | oauthbearer"
89
+ },
90
+ "username": { "type": "string" },
91
+ "password": { "type": "string" },
92
+ "oauth": {
93
+ "type": "object",
94
+ "description": "OAuth 2.0 client_credentials config. Required when mechanism is oauthbearer.",
95
+ "properties": {
96
+ "tokenEndpoint": {
97
+ "type": "string",
98
+ "description": "Token endpoint URL (e.g. https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token)."
99
+ },
100
+ "clientId": { "type": "string" },
101
+ "clientSecret": { "type": "string" },
102
+ "scope": {
103
+ "type": "string",
104
+ "description": "Optional OAuth scope (e.g. https://eventhubs.azure.net/.default)."
105
+ },
106
+ "tokenRefreshBuffer": {
107
+ "type": "integer",
108
+ "description": "Milliseconds before token expiry to proactively refresh. Must be a positive integer. Defaults to 30000 (30s).",
109
+ "minimum": 1
110
+ }
111
+ },
112
+ "required": ["tokenEndpoint", "clientId", "clientSecret"]
113
+ }
114
+ }
84
115
  },
85
116
  "socketOptions": {
86
117
  "type": "object",
package/utils/dbUtil.js CHANGED
@@ -5,7 +5,7 @@
5
5
  /* eslint no-unused-vars:warn */
6
6
 
7
7
  const fs = require('fs-extra');
8
- const uuid = require('uuid');
8
+ const { v4: uuidv4 } = require('uuid');
9
9
 
10
10
  /* Fetch in the other needed components for the this Adaptor */
11
11
  const {
@@ -983,7 +983,7 @@ class DBUtil {
983
983
  // create the unique identifier (should we let mongo do this?)
984
984
  const dataInfo = data;
985
985
  if (!{}.hasOwnProperty.call(dataInfo, '_id')) {
986
- dataInfo._id = uuid.v4();
986
+ dataInfo._id = uuidv4();
987
987
  }
988
988
 
989
989
  let res;