@loadstrike/loadstrike-sdk 1.0.30201 → 1.0.30401

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.
@@ -596,9 +596,13 @@ class WebSocketEndpointDefinitionModel extends TrafficEndpointDefinitionModel {
596
596
  parsed = new URL(url);
597
597
  }
598
598
  catch {
599
+ // Diagnostic text only; no connection is opened here.
600
+ // nosemgrep: vulnerability-tools.semgrep-rules.javascript.lang.security.detect-insecure-websocket
599
601
  throw new Error("Url must be an absolute ws:// or wss:// URI.");
600
602
  }
601
603
  if (parsed.protocol !== "ws:" && parsed.protocol !== "wss:") {
604
+ // Diagnostic text only; no connection is opened here.
605
+ // nosemgrep: vulnerability-tools.semgrep-rules.javascript.lang.security.detect-insecure-websocket
602
606
  throw new Error("Url must be an absolute ws:// or wss:// URI.");
603
607
  }
604
608
  if (this.ConnectTimeoutSeconds <= 0) {
@@ -1233,11 +1237,13 @@ function validateHttpAuthOptionsModel(target) {
1233
1237
  }
1234
1238
  function validateKafkaSaslOptionsModel(target) {
1235
1239
  switch (normalizeToken(target.Mechanism)) {
1236
- case "oauthbearer":
1237
- if (!String(target.OAuthBearerTokenEndpointUrl ?? "").trim()) {
1238
- throw new Error("OAuthBearerTokenEndpointUrl must be provided when SASL mechanism is OAuthBearer.");
1239
- }
1240
+ case "oauthbearer": {
1241
+ const additionalSettings = target.AdditionalSettings;
1242
+ validateKafkaOAuthBearerCredentials(String(target.AccessToken ?? "").trim() || String(target.OAuthBearerToken ?? "").trim(), String(target.OAuthBearerTokenEndpointUrl ?? "").trim()
1243
+ || String(target.TokenEndpoint ?? "").trim()
1244
+ || optionString(additionalSettings, "TokenEndpoint", "tokenEndpoint"), optionString(additionalSettings, "ClientId", "clientId"), optionString(additionalSettings, "ClientSecret", "clientSecret"));
1240
1245
  return;
1246
+ }
1241
1247
  default:
1242
1248
  if (!String(target.Username ?? "").trim()) {
1243
1249
  throw new Error("Username must be provided for SASL authentication.");
@@ -1786,21 +1792,12 @@ class KafkaEndpointAdapter extends CallbackAdapter {
1786
1792
  if (!this.consumerStartPromise) {
1787
1793
  this.consumerStartPromise = (async () => {
1788
1794
  const options = this.endpoint.kafka ?? {};
1789
- const useConfluentClient = shouldUseConfluentKafkaClient(this.endpoint);
1790
1795
  const kafka = await createKafkaClient(this.endpoint);
1791
1796
  const consumer = kafka.consumer(buildKafkaConsumerOptions(this.endpoint));
1792
1797
  await consumer.connect();
1793
- if (useConfluentClient) {
1794
- await consumer.subscribe({
1795
- topics: [optionString(options, "Topic", "topic")]
1796
- });
1797
- }
1798
- else {
1799
- await consumer.subscribe({
1800
- topic: optionString(options, "Topic", "topic"),
1801
- fromBeginning: optionBoolean(options, true, "StartFromEarliest", "startFromEarliest")
1802
- });
1803
- }
1798
+ await consumer.subscribe({
1799
+ topics: [optionString(options, "Topic", "topic")]
1800
+ });
1804
1801
  await consumer.run({
1805
1802
  eachMessage: async ({ message }) => {
1806
1803
  this.queue.push(createBrokerPayload(fromKafkaHeaders(message.headers), bufferToUint8Array(message.value), this.endpoint, headerValue(message.headers, "content-type")));
@@ -3135,9 +3132,13 @@ function validateWebSocketEndpoint(endpoint, mode) {
3135
3132
  parsed = new URL(url);
3136
3133
  }
3137
3134
  catch {
3135
+ // Diagnostic text only; no connection is opened here.
3136
+ // nosemgrep: vulnerability-tools.semgrep-rules.javascript.lang.security.detect-insecure-websocket
3138
3137
  throw new Error("Url must be an absolute ws:// or wss:// URI.");
3139
3138
  }
3140
3139
  if (parsed.protocol !== "ws:" && parsed.protocol !== "wss:") {
3140
+ // Diagnostic text only; no connection is opened here.
3141
+ // nosemgrep: vulnerability-tools.semgrep-rules.javascript.lang.security.detect-insecure-websocket
3141
3142
  throw new Error("Url must be an absolute ws:// or wss:// URI.");
3142
3143
  }
3143
3144
  const connectMs = optionNumber(options, "ConnectTimeoutMs", "connectTimeoutMs");
@@ -3213,7 +3214,8 @@ function validateKafkaEndpoint(endpoint, mode, hasModeDelegate) {
3213
3214
  function validateKafkaSaslOptions(options) {
3214
3215
  const mechanism = optionString(options, "Mechanism", "mechanism") || "Plain";
3215
3216
  if (mechanism.toLowerCase() === "oauthbearer") {
3216
- requireNonEmptyString(optionString(options, "OAuthBearerTokenEndpointUrl", "oauthBearerTokenEndpointUrl", "TokenEndpoint", "tokenEndpoint") || optionString(asRecordOrEmpty(pickProtocolValue(options, "AdditionalSettings", "additionalSettings")), "TokenEndpoint", "tokenEndpoint"), "OAuthBearerTokenEndpointUrl must be provided when SASL mechanism is OAuthBearer.");
3217
+ const additionalSettings = asRecordOrEmpty(pickProtocolValue(options, "AdditionalSettings", "additionalSettings"));
3218
+ validateKafkaOAuthBearerCredentials(optionString(options, "AccessToken", "accessToken", "OAuthBearerToken", "oauthBearerToken"), optionString(options, "OAuthBearerTokenEndpointUrl", "oauthBearerTokenEndpointUrl", "TokenEndpoint", "tokenEndpoint") || optionString(additionalSettings, "TokenEndpoint", "tokenEndpoint"), optionString(additionalSettings, "ClientId", "clientId"), optionString(additionalSettings, "ClientSecret", "clientSecret"));
3217
3219
  return;
3218
3220
  }
3219
3221
  requireNonEmptyString(optionString(options, "Username", "username"), "Username must be provided for SASL authentication.");
@@ -4206,74 +4208,31 @@ function pickProtocolValue(options, ...keys) {
4206
4208
  }
4207
4209
  return undefined;
4208
4210
  }
4209
- function buildKafkaClientOptions(endpoint) {
4210
- const options = endpoint.kafka ?? {};
4211
- const securityProtocol = (optionString(options, "SecurityProtocol", "securityProtocol") || "Plaintext").toLowerCase();
4212
- const bootstrapServers = optionString(options, "BootstrapServers", "bootstrapServers")
4213
- .split(",")
4214
- .map((value) => value.trim())
4215
- .filter((value) => value.length > 0);
4216
- const clientOptions = {
4217
- clientId: `${endpoint.name}-${randomUUID().slice(0, 8)}`,
4218
- brokers: bootstrapServers,
4219
- ssl: securityProtocol === "ssl" || securityProtocol === "saslssl"
4220
- };
4221
- if (securityProtocol === "saslplaintext" || securityProtocol === "saslssl") {
4222
- const saslOptions = asRecordOrEmpty(pickProtocolValue(options, "Sasl", "sasl"));
4223
- const mechanism = (optionString(saslOptions, "Mechanism", "mechanism") || "Plain").toLowerCase();
4224
- if (mechanism === "oauthbearer") {
4225
- clientOptions.sasl = {
4226
- mechanism: "oauthbearer",
4227
- oauthBearerProvider: async () => ({
4228
- value: await resolveKafkaOAuthBearerToken(saslOptions)
4229
- })
4230
- };
4231
- }
4232
- else {
4233
- clientOptions.sasl = {
4234
- mechanism: mapKafkaSaslMechanism(mechanism),
4235
- username: optionString(saslOptions, "Username", "username"),
4236
- password: optionString(saslOptions, "Password", "password")
4237
- };
4238
- }
4211
+ let kafkaClientFactoryForTests = null;
4212
+ async function createKafkaClient(endpoint) {
4213
+ if (kafkaClientFactoryForTests) {
4214
+ return await kafkaClientFactoryForTests(endpoint);
4239
4215
  }
4240
- return clientOptions;
4216
+ const { KafkaJS } = await import("@confluentinc/kafka-javascript");
4217
+ return new KafkaJS.Kafka(buildConfluentKafkaClientOptions(endpoint));
4241
4218
  }
4242
- async function createKafkaClient(endpoint) {
4243
- if (shouldUseConfluentKafkaClient(endpoint)) {
4244
- const { KafkaJS } = await import("@confluentinc/kafka-javascript");
4245
- return new KafkaJS.Kafka(buildConfluentKafkaClientOptions(endpoint));
4219
+ function validateKafkaOAuthBearerCredentials(directToken, tokenEndpoint, clientId, clientSecret) {
4220
+ if (directToken.trim()) {
4221
+ return;
4222
+ }
4223
+ if (!tokenEndpoint.trim() || !clientId.trim() || !clientSecret.trim()) {
4224
+ throw new Error("Kafka OAuthBearer requires a direct access token or token endpoint with client credentials.");
4246
4225
  }
4247
- const { Kafka } = await import("kafkajs");
4248
- return new Kafka(buildKafkaClientOptions(endpoint));
4249
4226
  }
4250
4227
  function buildKafkaConsumerOptions(endpoint) {
4251
4228
  const options = endpoint.kafka ?? {};
4252
4229
  const groupId = optionString(options, "ConsumerGroupId", "consumerGroupId");
4253
4230
  const fromBeginning = optionBoolean(options, true, "StartFromEarliest", "startFromEarliest");
4254
- if (shouldUseConfluentKafkaClient(endpoint)) {
4255
- return {
4256
- "group.id": groupId,
4257
- "auto.offset.reset": fromBeginning ? "earliest" : "latest",
4258
- "enable.auto.commit": true
4259
- };
4260
- }
4261
- return { groupId };
4262
- }
4263
- function shouldUseConfluentKafkaClient(endpoint) {
4264
- const options = endpoint.kafka ?? {};
4265
- const securityProtocol = (optionString(options, "SecurityProtocol", "securityProtocol") || "Plaintext").toLowerCase();
4266
- if (securityProtocol !== "saslplaintext" && securityProtocol !== "saslssl") {
4267
- return Object.keys(toStringRecord(pickProtocolValue(options, "ConfluentSettings", "confluentSettings"))).length > 0;
4268
- }
4269
- const saslOptions = asRecordOrEmpty(pickProtocolValue(options, "Sasl", "sasl"));
4270
- const mechanism = (optionString(saslOptions, "Mechanism", "mechanism") || "Plain").toLowerCase();
4271
- const confluentSettings = toStringRecord(pickProtocolValue(options, "ConfluentSettings", "confluentSettings"));
4272
- const additionalSettings = toStringRecord(pickProtocolValue(saslOptions, "AdditionalSettings", "additionalSettings"));
4273
- const hasDirectOAuthToken = mechanism === "oauthbearer" && Boolean(optionString(saslOptions, "AccessToken", "accessToken", "OAuthBearerToken", "oauthBearerToken"));
4274
- return mechanism === "gssapi"
4275
- || Object.keys(confluentSettings).length > 0
4276
- || (Object.keys(additionalSettings).length > 0 && !hasDirectOAuthToken);
4231
+ return {
4232
+ "group.id": groupId,
4233
+ "auto.offset.reset": fromBeginning ? "earliest" : "latest",
4234
+ "enable.auto.commit": true
4235
+ };
4277
4236
  }
4278
4237
  function buildConfluentKafkaClientOptions(endpoint) {
4279
4238
  const options = endpoint.kafka ?? {};
@@ -4289,30 +4248,41 @@ function buildConfluentKafkaClientOptions(endpoint) {
4289
4248
  const additionalSettings = toStringRecord(pickProtocolValue(saslOptions, "AdditionalSettings", "additionalSettings"));
4290
4249
  base["sasl.mechanism"] = mapConfluentKafkaSaslMechanism(mechanism);
4291
4250
  if (mechanism.toLowerCase() === "oauthbearer") {
4251
+ const directToken = optionString(saslOptions, "AccessToken", "accessToken", "OAuthBearerToken", "oauthBearerToken");
4292
4252
  const tokenEndpoint = optionString(saslOptions, "OAuthBearerTokenEndpointUrl", "oauthBearerTokenEndpointUrl", "TokenEndpoint", "tokenEndpoint") || optionString(additionalSettings, "TokenEndpoint", "tokenEndpoint");
4293
- if (tokenEndpoint) {
4294
- base["sasl.oauthbearer.method"] = "oidc";
4295
- base["sasl.oauthbearer.token.endpoint.url"] = tokenEndpoint;
4296
- }
4297
4253
  const clientId = optionString(additionalSettings, "ClientId", "clientId");
4298
- if (clientId) {
4299
- base["sasl.oauthbearer.client.id"] = clientId;
4300
- }
4301
4254
  const clientSecret = optionString(additionalSettings, "ClientSecret", "clientSecret");
4302
- if (clientSecret) {
4303
- base["sasl.oauthbearer.client.secret"] = clientSecret;
4304
- }
4305
4255
  const scope = optionString(additionalSettings, "Scope", "scope");
4306
- if (scope) {
4307
- base["sasl.oauthbearer.scope"] = scope;
4308
- }
4309
4256
  const grantType = optionString(additionalSettings, "GrantType", "grantType");
4310
- if (grantType) {
4311
- base["sasl.oauthbearer.grant.type"] = grantType;
4312
- }
4313
4257
  const extensions = optionString(additionalSettings, "Extensions", "extensions");
4314
- if (extensions) {
4315
- base["sasl.oauthbearer.extensions"] = extensions;
4258
+ if (directToken) {
4259
+ const principal = optionString(additionalSettings, "Principal", "principal")
4260
+ || optionString(saslOptions, "Username", "username")
4261
+ || "loadstrike";
4262
+ base.oauthbearer_token_refresh_cb = async () => ({
4263
+ tokenValue: directToken,
4264
+ lifetime: Date.now() + 60 * 60 * 1000,
4265
+ principal,
4266
+ extensions: parseKafkaOAuthExtensions(extensions)
4267
+ });
4268
+ }
4269
+ else {
4270
+ if (!tokenEndpoint || !clientId || !clientSecret) {
4271
+ throw new Error("Kafka OAuthBearer requires a direct access token or token endpoint with client credentials.");
4272
+ }
4273
+ base["sasl.oauthbearer.method"] = "oidc";
4274
+ base["sasl.oauthbearer.token.endpoint.url"] = tokenEndpoint;
4275
+ base["sasl.oauthbearer.client.id"] = clientId;
4276
+ base["sasl.oauthbearer.client.secret"] = clientSecret;
4277
+ if (scope) {
4278
+ base["sasl.oauthbearer.scope"] = scope;
4279
+ }
4280
+ if (grantType) {
4281
+ base["sasl.oauthbearer.grant.type"] = grantType;
4282
+ }
4283
+ if (extensions) {
4284
+ base["sasl.oauthbearer.extensions"] = extensions;
4285
+ }
4316
4286
  }
4317
4287
  }
4318
4288
  else {
@@ -4331,7 +4301,9 @@ function buildConfluentKafkaClientOptions(endpoint) {
4331
4301
  || key === "Extensions"
4332
4302
  || key === "extensions"
4333
4303
  || key === "TokenEndpoint"
4334
- || key === "tokenEndpoint") {
4304
+ || key === "tokenEndpoint"
4305
+ || key === "Principal"
4306
+ || key === "principal") {
4335
4307
  continue;
4336
4308
  }
4337
4309
  base[key] = value;
@@ -4342,53 +4314,20 @@ function buildConfluentKafkaClientOptions(endpoint) {
4342
4314
  }
4343
4315
  return base;
4344
4316
  }
4345
- async function resolveKafkaOAuthBearerToken(options) {
4346
- const directToken = optionString(options, "AccessToken", "accessToken", "OAuthBearerToken", "oauthBearerToken");
4347
- if (directToken) {
4348
- return directToken;
4349
- }
4350
- const additional = asRecordOrEmpty(pickProtocolValue(options, "AdditionalSettings", "additionalSettings"));
4351
- const tokenEndpoint = optionString(options, "OAuthBearerTokenEndpointUrl", "oauthBearerTokenEndpointUrl", "TokenEndpoint", "tokenEndpoint") || optionString(additional, "TokenEndpoint", "tokenEndpoint");
4352
- const clientId = optionString(additional, "ClientId", "clientId");
4353
- const clientSecret = optionString(additional, "ClientSecret", "clientSecret");
4354
- if (!tokenEndpoint || !clientId || !clientSecret) {
4355
- throw new Error("Kafka OAuthBearer requires a direct access token or token endpoint with client credentials.");
4356
- }
4357
- const form = new URLSearchParams();
4358
- form.set("grant_type", "client_credentials");
4359
- form.set("client_id", clientId);
4360
- form.set("client_secret", clientSecret);
4361
- const scope = optionString(additional, "Scope", "scope");
4362
- if (scope) {
4363
- form.set("scope", scope);
4364
- }
4365
- const response = await fetch(tokenEndpoint, {
4366
- method: "POST",
4367
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
4368
- body: form.toString()
4369
- });
4370
- if (!response.ok) {
4371
- throw new Error(`Kafka OAuthBearer token request failed with status ${response.status}.`);
4372
- }
4373
- const payload = parseMaybeJson(await response.text());
4374
- if (!isRecord(payload) || !payload.access_token) {
4375
- throw new Error("Kafka OAuthBearer token response did not contain access_token.");
4376
- }
4377
- return String(payload.access_token);
4378
- }
4379
- function mapKafkaSaslMechanism(mechanism) {
4380
- switch (mechanism) {
4381
- case "plain":
4382
- return "plain";
4383
- case "scramsha256":
4384
- case "scram-sha-256":
4385
- return "scram-sha-256";
4386
- case "scramsha512":
4387
- case "scram-sha-512":
4388
- return "scram-sha-512";
4389
- default:
4390
- throw new Error(`Unsupported Kafka SASL mechanism: ${mechanism}.`);
4317
+ function parseKafkaOAuthExtensions(value) {
4318
+ const parsed = {};
4319
+ for (const entry of value.split(",")) {
4320
+ const separatorIndex = entry.indexOf("=");
4321
+ if (separatorIndex <= 0) {
4322
+ continue;
4323
+ }
4324
+ const key = entry.slice(0, separatorIndex).trim();
4325
+ if (!key) {
4326
+ continue;
4327
+ }
4328
+ parsed[key] = entry.slice(separatorIndex + 1).trim();
4391
4329
  }
4330
+ return parsed;
4392
4331
  }
4393
4332
  function mapConfluentKafkaSecurityProtocol(protocol) {
4394
4333
  switch (protocol.toLowerCase()) {
@@ -4672,7 +4611,6 @@ export const __loadstrikeTestExports = {
4672
4611
  applyHttpAuthHeaders,
4673
4612
  buildHttpRequestBody,
4674
4613
  buildConfluentKafkaClientOptions,
4675
- buildKafkaClientOptions,
4676
4614
  canonicalizeHttpResponseSource,
4677
4615
  createDelegateRequestEndpointView,
4678
4616
  createNatsHeaders,
@@ -4688,7 +4626,6 @@ export const __loadstrikeTestExports = {
4688
4626
  injectTrackingValue,
4689
4627
  mapConfluentKafkaSaslMechanism,
4690
4628
  mapConfluentKafkaSecurityProtocol,
4691
- mapKafkaSaslMechanism,
4692
4629
  parseBodyObject,
4693
4630
  partitionFromKey,
4694
4631
  payloadBodyAsUtf8,
@@ -4696,10 +4633,8 @@ export const __loadstrikeTestExports = {
4696
4633
  protocolBus: protocolBus,
4697
4634
  readFirstRedisStreamEntry,
4698
4635
  resolveConnectionMetadata,
4699
- resolveKafkaOAuthBearerToken,
4700
4636
  serializePayloadBody,
4701
4637
  setJsonBodyValue,
4702
- shouldUseConfluentKafkaClient,
4703
4638
  toHeaderRecord,
4704
4639
  toKafkaHeadersWithContentType,
4705
4640
  toSqsMessageAttributes,
@@ -4707,6 +4642,9 @@ export const __loadstrikeTestExports = {
4707
4642
  setSqsClientFactoryForTests: (factory) => {
4708
4643
  sqsClientFactoryForTests = factory;
4709
4644
  },
4645
+ setKafkaClientFactoryForTests: (factory) => {
4646
+ kafkaClientFactoryForTests = factory;
4647
+ },
4710
4648
  validateHttpEndpoint,
4711
4649
  validateTrackingSelectorPath
4712
4650
  };