@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 +16 -0
- package/adapter.js +152 -3
- package/package.json +3 -3
- package/propertiesSchema.json +32 -1
- package/utils/dbUtil.js +2 -2
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.
|
|
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": "
|
|
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": "
|
|
51
|
+
"uuid": "11.1.1",
|
|
52
52
|
"mocha": "12.0.0-rc.1",
|
|
53
53
|
"winston": "3.17.0"
|
|
54
54
|
},
|
package/propertiesSchema.json
CHANGED
|
@@ -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
|
|
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 =
|
|
986
|
+
dataInfo._id = uuidv4();
|
|
987
987
|
}
|
|
988
988
|
|
|
989
989
|
let res;
|