@eventferry/kafka 3.0.0 → 3.1.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/dist/index.d.cts CHANGED
@@ -20,17 +20,108 @@ interface KafkaDriver {
20
20
  */
21
21
  readonly transactional: boolean;
22
22
  }
23
+ /**
24
+ * TLS configuration for client connections. Pass a full {@link TlsConfig}
25
+ * when the cluster requires CA pinning, mutual TLS (client cert + key), or a
26
+ * specific SNI host. Plain `ssl: true` keeps the previous behavior (one-way
27
+ * TLS using the driver's default trust store).
28
+ *
29
+ * `rejectUnauthorized` is intentionally NOT a knob here — TLS verification is
30
+ * non-negotiable. Dev clusters with self-signed certs pass their CA via `ca`.
31
+ */
32
+ interface TlsConfig {
33
+ /** PEM-encoded CA bundle. Buffers and strings both accepted. */
34
+ ca?: string | Buffer | Array<string | Buffer>;
35
+ /** PEM-encoded client certificate (required for mTLS). */
36
+ cert?: string | Buffer;
37
+ /** PEM-encoded private key for the client certificate (required for mTLS). */
38
+ key?: string | Buffer;
39
+ /** Passphrase for an encrypted private key. */
40
+ passphrase?: string;
41
+ /** SNI host. Useful when broker address doesn't match the cert SAN. */
42
+ servername?: string;
43
+ }
44
+ /**
45
+ * Username + password SASL: PLAIN and SCRAM-SHA-256/512. The conventional
46
+ * "API key + secret" shape used by Confluent Cloud, Aiven, on-prem SCRAM.
47
+ */
48
+ interface SaslPasswordConfig {
49
+ mechanism: "plain" | "scram-sha-256" | "scram-sha-512";
50
+ username: string;
51
+ password: string;
52
+ }
53
+ /**
54
+ * Token returned by an OAUTHBEARER provider.
55
+ *
56
+ * Driver asymmetry (verified against `kafkajs/types/index.d.ts` and
57
+ * `@confluentinc/kafka-javascript/types/kafkajs.d.ts`):
58
+ *
59
+ * - `kafkajs` reads only `value`. Other fields are silently ignored.
60
+ * - `@confluentinc/kafka-javascript` REQUIRES `value` + `principal` + `lifetime`
61
+ * and accepts an optional `extensions` map. Passing only `{ value }` throws.
62
+ *
63
+ * Cross-driver portable providers MUST populate all four. eventferry treats
64
+ * `principal` / `lifetime` / `extensions` as optional in the type to support
65
+ * kafkajs-only setups; supplying them is a no-op there.
66
+ */
67
+ interface OauthBearerToken {
68
+ /** The bearer token string (JWT, opaque, …). */
69
+ value: string;
70
+ /** Principal name. REQUIRED on the confluent driver. */
71
+ principal?: string;
72
+ /** Lifetime in MILLISECONDS. REQUIRED on the confluent driver. */
73
+ lifetime?: number;
74
+ /** SASL extensions to send alongside the token (e.g. for OIDC scopes). */
75
+ extensions?: Record<string, string>;
76
+ }
77
+ /**
78
+ * SASL/OAUTHBEARER: bring-your-own token provider. The function is invoked
79
+ * by the underlying client on demand (NOT on a fixed timer); cache the
80
+ * token in your provider if you want to amortise issuance cost.
81
+ *
82
+ * Required for Azure Event Hubs, Confluent Cloud with OAuth/SSO, and any
83
+ * OIDC-fronted Kafka. For AWS MSK IAM, wrap the AWS SigV4 signer in this
84
+ * callback.
85
+ */
86
+ interface SaslOauthbearerConfig {
87
+ mechanism: "oauthbearer";
88
+ oauthBearerProvider: () => Promise<OauthBearerToken>;
89
+ }
90
+ /**
91
+ * Discriminated union over the SASL mechanisms eventferry supports today.
92
+ * Add new mechanisms by extending this union and mapping them in each driver.
93
+ */
94
+ type SaslConfig = SaslPasswordConfig | SaslOauthbearerConfig;
23
95
  /** Shared connection config accepted by both drivers. */
24
96
  interface KafkaConnectionConfig {
25
97
  brokers: string[];
26
98
  clientId?: string;
27
- ssl?: boolean;
28
- sasl?: {
29
- mechanism: "plain" | "scram-sha-256" | "scram-sha-512";
30
- username: string;
31
- password: string;
32
- };
99
+ /**
100
+ * TLS configuration. `true` enables one-way TLS using the driver's default
101
+ * trust store; a {@link TlsConfig} object lets you supply a custom CA,
102
+ * client cert (for mTLS), and SNI host.
103
+ */
104
+ ssl?: boolean | TlsConfig;
105
+ sasl?: SaslConfig;
33
106
  }
107
+ /**
108
+ * Choice of partitioner. Only honored by the kafkajs driver — the confluent
109
+ * driver uses librdkafka's `consistent_random` (key-aware sticky) and
110
+ * partitioner override is out of scope for this release.
111
+ *
112
+ * - `"java-compatible"` (recommended for greenfield): kafkajs's
113
+ * `Partitioners.JavaCompatiblePartitioner`. Matches the Java client's
114
+ * murmur2-based hash so producers across language boundaries land on the
115
+ * same partition for the same key.
116
+ * - `"legacy"`: kafkajs's pre-v2 partitioner. Use when migrating an existing
117
+ * topic where hash continuity matters.
118
+ * - `"default"`: kafkajs's current default. Equivalent to legacy in v2 but
119
+ * may change with major kafkajs releases.
120
+ *
121
+ * Setting this also silences the noisy `KafkaJSPartitionerNotSpecified`
122
+ * warning kafkajs emits when no partitioner choice is made explicitly.
123
+ */
124
+ type KafkaJsPartitionerChoice = "default" | "legacy" | "java-compatible";
34
125
  interface ProducerBehaviorConfig {
35
126
  /** Enable idempotent producer (dedup + ordering). Default true. */
36
127
  idempotent?: boolean;
@@ -45,6 +136,46 @@ interface ProducerBehaviorConfig {
45
136
  acks?: number;
46
137
  /** Compression codec. Driver maps to its native enum. */
47
138
  compression?: "none" | "gzip" | "snappy" | "lz4" | "zstd";
139
+ /**
140
+ * (confluent only) How long the producer waits to accumulate records before
141
+ * flushing a partition batch. Default 0 (ship-immediately). Increase to
142
+ * 10–50ms for higher throughput at the cost of latency.
143
+ */
144
+ lingerMs?: number;
145
+ /** (confluent only) Maximum bytes per partition batch before forced flush. */
146
+ batchSize?: number;
147
+ /**
148
+ * Max concurrent unacknowledged producer requests. MUST be ≤5 when
149
+ * `idempotent: true`. Higher = throughput; lower = stricter ordering on
150
+ * non-idempotent producers (no other path preserves order on retry).
151
+ */
152
+ maxInFlightRequests?: number;
153
+ /** Per-request broker-ack timeout. Default 30 s. */
154
+ requestTimeoutMs?: number;
155
+ /**
156
+ * (confluent only) End-to-end timeout for a record from produce() call to
157
+ * terminal success / failure (includes retries). Defaults to 120 s.
158
+ * If this exceeds the relay's `claimTimeoutMs`, the reaper may double-
159
+ * publish a slow record — set both coherently.
160
+ */
161
+ deliveryTimeoutMs?: number;
162
+ /**
163
+ * (confluent only) Max bytes of a single record (after compression).
164
+ * MUST be ≤ broker's `message.max.bytes`. Defaults to 1 MB.
165
+ */
166
+ maxRequestSize?: number;
167
+ /**
168
+ * Broker-side ceiling on how long a transaction can stay open before
169
+ * auto-abort. Maps to `transaction.timeout.ms`. Default 60 s; capped by
170
+ * the broker's `transaction.max.timeout.ms`.
171
+ */
172
+ transactionTimeoutMs?: number;
173
+ /**
174
+ * (kafkajs only) Choice of partitioner. See
175
+ * {@link KafkaJsPartitionerChoice} for the options. Setting any value
176
+ * silences kafkajs's `KafkaJSPartitionerNotSpecified` warning.
177
+ */
178
+ partitioner?: KafkaJsPartitionerChoice;
48
179
  }
49
180
  type DriverKind = "kafkajs" | "confluent";
50
181
 
@@ -78,6 +209,8 @@ declare class KafkaJsDriver implements KafkaDriver {
78
209
  disconnect(): Promise<void>;
79
210
  sendBatch(messages: PublishableMessage[]): Promise<PublishResult[]>;
80
211
  }
212
+ /** Internal — used by tests. Resets the dedup so warnings can be observed in isolation. */
213
+ declare function _resetKafkajsWarnDedup(): void;
81
214
 
82
215
  /**
83
216
  * Classify a kafkajs producer error into a {@link PublishErrorKind} so the
@@ -144,6 +277,28 @@ declare class ConfluentDriver implements KafkaDriver {
144
277
  */
145
278
  declare function classifyConfluentError(err: unknown): PublishErrorKind;
146
279
 
280
+ /**
281
+ * Translate eventferry's normalized `KafkaConnectionConfig` into the shape
282
+ * expected by `@confluentinc/kafka-javascript`'s `KafkaJS.Kafka` constructor.
283
+ *
284
+ * Returns an object with two parts:
285
+ * - `kafkaJS`: the kafkajs-compatible config layer (clientId, brokers, and
286
+ * simple ssl/sasl when no advanced TLS is needed).
287
+ * - top-level keys: librdkafka-style config (e.g. `ssl.ca.pem`,
288
+ * `security.protocol`) used when the user supplies a {@link TlsConfig}.
289
+ *
290
+ * Why a separate translator: the kafkajs-compat layer accepts the simple
291
+ * `ssl: true` boolean but the verified path for mTLS (CA + cert + key) is
292
+ * librdkafka's `ssl.*.pem` keys. The translator picks the right surface
293
+ * based on what the caller supplied. Buffer inputs are coerced to strings —
294
+ * librdkafka accepts PEM strings, NOT Buffers.
295
+ */
296
+ interface ConfluentClientConfig {
297
+ kafkaJS: Record<string, unknown>;
298
+ librdkafka: Record<string, unknown>;
299
+ }
300
+ declare function buildConfluentClientConfig(opts: KafkaConnectionConfig & ProducerBehaviorConfig): ConfluentClientConfig;
301
+
147
302
  interface KafkaPublisherOptions extends KafkaConnectionConfig, ProducerBehaviorConfig {
148
303
  /** Which underlying client to use. Default "kafkajs". */
149
304
  driver?: DriverKind;
@@ -173,4 +328,4 @@ declare class KafkaPublisher implements Publisher {
173
328
  get transactional(): boolean;
174
329
  }
175
330
 
176
- export { ConfluentDriver, type ConfluentDriverOptions, type DriverKind, type KafkaConnectionConfig, type KafkaDriver, KafkaJsDriver, type KafkaJsDriverOptions, KafkaPublisher, type KafkaPublisherOptions, type ProducerBehaviorConfig, classifyConfluentError, classifyKafkajsError };
331
+ export { type ConfluentClientConfig, ConfluentDriver, type ConfluentDriverOptions, type DriverKind, type KafkaConnectionConfig, type KafkaDriver, KafkaJsDriver, type KafkaJsDriverOptions, type KafkaJsPartitionerChoice, KafkaPublisher, type KafkaPublisherOptions, type OauthBearerToken, type ProducerBehaviorConfig, type SaslConfig, type SaslOauthbearerConfig, type SaslPasswordConfig, type TlsConfig, _resetKafkajsWarnDedup, buildConfluentClientConfig, classifyConfluentError, classifyKafkajsError };
package/dist/index.d.ts CHANGED
@@ -20,17 +20,108 @@ interface KafkaDriver {
20
20
  */
21
21
  readonly transactional: boolean;
22
22
  }
23
+ /**
24
+ * TLS configuration for client connections. Pass a full {@link TlsConfig}
25
+ * when the cluster requires CA pinning, mutual TLS (client cert + key), or a
26
+ * specific SNI host. Plain `ssl: true` keeps the previous behavior (one-way
27
+ * TLS using the driver's default trust store).
28
+ *
29
+ * `rejectUnauthorized` is intentionally NOT a knob here — TLS verification is
30
+ * non-negotiable. Dev clusters with self-signed certs pass their CA via `ca`.
31
+ */
32
+ interface TlsConfig {
33
+ /** PEM-encoded CA bundle. Buffers and strings both accepted. */
34
+ ca?: string | Buffer | Array<string | Buffer>;
35
+ /** PEM-encoded client certificate (required for mTLS). */
36
+ cert?: string | Buffer;
37
+ /** PEM-encoded private key for the client certificate (required for mTLS). */
38
+ key?: string | Buffer;
39
+ /** Passphrase for an encrypted private key. */
40
+ passphrase?: string;
41
+ /** SNI host. Useful when broker address doesn't match the cert SAN. */
42
+ servername?: string;
43
+ }
44
+ /**
45
+ * Username + password SASL: PLAIN and SCRAM-SHA-256/512. The conventional
46
+ * "API key + secret" shape used by Confluent Cloud, Aiven, on-prem SCRAM.
47
+ */
48
+ interface SaslPasswordConfig {
49
+ mechanism: "plain" | "scram-sha-256" | "scram-sha-512";
50
+ username: string;
51
+ password: string;
52
+ }
53
+ /**
54
+ * Token returned by an OAUTHBEARER provider.
55
+ *
56
+ * Driver asymmetry (verified against `kafkajs/types/index.d.ts` and
57
+ * `@confluentinc/kafka-javascript/types/kafkajs.d.ts`):
58
+ *
59
+ * - `kafkajs` reads only `value`. Other fields are silently ignored.
60
+ * - `@confluentinc/kafka-javascript` REQUIRES `value` + `principal` + `lifetime`
61
+ * and accepts an optional `extensions` map. Passing only `{ value }` throws.
62
+ *
63
+ * Cross-driver portable providers MUST populate all four. eventferry treats
64
+ * `principal` / `lifetime` / `extensions` as optional in the type to support
65
+ * kafkajs-only setups; supplying them is a no-op there.
66
+ */
67
+ interface OauthBearerToken {
68
+ /** The bearer token string (JWT, opaque, …). */
69
+ value: string;
70
+ /** Principal name. REQUIRED on the confluent driver. */
71
+ principal?: string;
72
+ /** Lifetime in MILLISECONDS. REQUIRED on the confluent driver. */
73
+ lifetime?: number;
74
+ /** SASL extensions to send alongside the token (e.g. for OIDC scopes). */
75
+ extensions?: Record<string, string>;
76
+ }
77
+ /**
78
+ * SASL/OAUTHBEARER: bring-your-own token provider. The function is invoked
79
+ * by the underlying client on demand (NOT on a fixed timer); cache the
80
+ * token in your provider if you want to amortise issuance cost.
81
+ *
82
+ * Required for Azure Event Hubs, Confluent Cloud with OAuth/SSO, and any
83
+ * OIDC-fronted Kafka. For AWS MSK IAM, wrap the AWS SigV4 signer in this
84
+ * callback.
85
+ */
86
+ interface SaslOauthbearerConfig {
87
+ mechanism: "oauthbearer";
88
+ oauthBearerProvider: () => Promise<OauthBearerToken>;
89
+ }
90
+ /**
91
+ * Discriminated union over the SASL mechanisms eventferry supports today.
92
+ * Add new mechanisms by extending this union and mapping them in each driver.
93
+ */
94
+ type SaslConfig = SaslPasswordConfig | SaslOauthbearerConfig;
23
95
  /** Shared connection config accepted by both drivers. */
24
96
  interface KafkaConnectionConfig {
25
97
  brokers: string[];
26
98
  clientId?: string;
27
- ssl?: boolean;
28
- sasl?: {
29
- mechanism: "plain" | "scram-sha-256" | "scram-sha-512";
30
- username: string;
31
- password: string;
32
- };
99
+ /**
100
+ * TLS configuration. `true` enables one-way TLS using the driver's default
101
+ * trust store; a {@link TlsConfig} object lets you supply a custom CA,
102
+ * client cert (for mTLS), and SNI host.
103
+ */
104
+ ssl?: boolean | TlsConfig;
105
+ sasl?: SaslConfig;
33
106
  }
107
+ /**
108
+ * Choice of partitioner. Only honored by the kafkajs driver — the confluent
109
+ * driver uses librdkafka's `consistent_random` (key-aware sticky) and
110
+ * partitioner override is out of scope for this release.
111
+ *
112
+ * - `"java-compatible"` (recommended for greenfield): kafkajs's
113
+ * `Partitioners.JavaCompatiblePartitioner`. Matches the Java client's
114
+ * murmur2-based hash so producers across language boundaries land on the
115
+ * same partition for the same key.
116
+ * - `"legacy"`: kafkajs's pre-v2 partitioner. Use when migrating an existing
117
+ * topic where hash continuity matters.
118
+ * - `"default"`: kafkajs's current default. Equivalent to legacy in v2 but
119
+ * may change with major kafkajs releases.
120
+ *
121
+ * Setting this also silences the noisy `KafkaJSPartitionerNotSpecified`
122
+ * warning kafkajs emits when no partitioner choice is made explicitly.
123
+ */
124
+ type KafkaJsPartitionerChoice = "default" | "legacy" | "java-compatible";
34
125
  interface ProducerBehaviorConfig {
35
126
  /** Enable idempotent producer (dedup + ordering). Default true. */
36
127
  idempotent?: boolean;
@@ -45,6 +136,46 @@ interface ProducerBehaviorConfig {
45
136
  acks?: number;
46
137
  /** Compression codec. Driver maps to its native enum. */
47
138
  compression?: "none" | "gzip" | "snappy" | "lz4" | "zstd";
139
+ /**
140
+ * (confluent only) How long the producer waits to accumulate records before
141
+ * flushing a partition batch. Default 0 (ship-immediately). Increase to
142
+ * 10–50ms for higher throughput at the cost of latency.
143
+ */
144
+ lingerMs?: number;
145
+ /** (confluent only) Maximum bytes per partition batch before forced flush. */
146
+ batchSize?: number;
147
+ /**
148
+ * Max concurrent unacknowledged producer requests. MUST be ≤5 when
149
+ * `idempotent: true`. Higher = throughput; lower = stricter ordering on
150
+ * non-idempotent producers (no other path preserves order on retry).
151
+ */
152
+ maxInFlightRequests?: number;
153
+ /** Per-request broker-ack timeout. Default 30 s. */
154
+ requestTimeoutMs?: number;
155
+ /**
156
+ * (confluent only) End-to-end timeout for a record from produce() call to
157
+ * terminal success / failure (includes retries). Defaults to 120 s.
158
+ * If this exceeds the relay's `claimTimeoutMs`, the reaper may double-
159
+ * publish a slow record — set both coherently.
160
+ */
161
+ deliveryTimeoutMs?: number;
162
+ /**
163
+ * (confluent only) Max bytes of a single record (after compression).
164
+ * MUST be ≤ broker's `message.max.bytes`. Defaults to 1 MB.
165
+ */
166
+ maxRequestSize?: number;
167
+ /**
168
+ * Broker-side ceiling on how long a transaction can stay open before
169
+ * auto-abort. Maps to `transaction.timeout.ms`. Default 60 s; capped by
170
+ * the broker's `transaction.max.timeout.ms`.
171
+ */
172
+ transactionTimeoutMs?: number;
173
+ /**
174
+ * (kafkajs only) Choice of partitioner. See
175
+ * {@link KafkaJsPartitionerChoice} for the options. Setting any value
176
+ * silences kafkajs's `KafkaJSPartitionerNotSpecified` warning.
177
+ */
178
+ partitioner?: KafkaJsPartitionerChoice;
48
179
  }
49
180
  type DriverKind = "kafkajs" | "confluent";
50
181
 
@@ -78,6 +209,8 @@ declare class KafkaJsDriver implements KafkaDriver {
78
209
  disconnect(): Promise<void>;
79
210
  sendBatch(messages: PublishableMessage[]): Promise<PublishResult[]>;
80
211
  }
212
+ /** Internal — used by tests. Resets the dedup so warnings can be observed in isolation. */
213
+ declare function _resetKafkajsWarnDedup(): void;
81
214
 
82
215
  /**
83
216
  * Classify a kafkajs producer error into a {@link PublishErrorKind} so the
@@ -144,6 +277,28 @@ declare class ConfluentDriver implements KafkaDriver {
144
277
  */
145
278
  declare function classifyConfluentError(err: unknown): PublishErrorKind;
146
279
 
280
+ /**
281
+ * Translate eventferry's normalized `KafkaConnectionConfig` into the shape
282
+ * expected by `@confluentinc/kafka-javascript`'s `KafkaJS.Kafka` constructor.
283
+ *
284
+ * Returns an object with two parts:
285
+ * - `kafkaJS`: the kafkajs-compatible config layer (clientId, brokers, and
286
+ * simple ssl/sasl when no advanced TLS is needed).
287
+ * - top-level keys: librdkafka-style config (e.g. `ssl.ca.pem`,
288
+ * `security.protocol`) used when the user supplies a {@link TlsConfig}.
289
+ *
290
+ * Why a separate translator: the kafkajs-compat layer accepts the simple
291
+ * `ssl: true` boolean but the verified path for mTLS (CA + cert + key) is
292
+ * librdkafka's `ssl.*.pem` keys. The translator picks the right surface
293
+ * based on what the caller supplied. Buffer inputs are coerced to strings —
294
+ * librdkafka accepts PEM strings, NOT Buffers.
295
+ */
296
+ interface ConfluentClientConfig {
297
+ kafkaJS: Record<string, unknown>;
298
+ librdkafka: Record<string, unknown>;
299
+ }
300
+ declare function buildConfluentClientConfig(opts: KafkaConnectionConfig & ProducerBehaviorConfig): ConfluentClientConfig;
301
+
147
302
  interface KafkaPublisherOptions extends KafkaConnectionConfig, ProducerBehaviorConfig {
148
303
  /** Which underlying client to use. Default "kafkajs". */
149
304
  driver?: DriverKind;
@@ -173,4 +328,4 @@ declare class KafkaPublisher implements Publisher {
173
328
  get transactional(): boolean;
174
329
  }
175
330
 
176
- export { ConfluentDriver, type ConfluentDriverOptions, type DriverKind, type KafkaConnectionConfig, type KafkaDriver, KafkaJsDriver, type KafkaJsDriverOptions, KafkaPublisher, type KafkaPublisherOptions, type ProducerBehaviorConfig, classifyConfluentError, classifyKafkajsError };
331
+ export { type ConfluentClientConfig, ConfluentDriver, type ConfluentDriverOptions, type DriverKind, type KafkaConnectionConfig, type KafkaDriver, KafkaJsDriver, type KafkaJsDriverOptions, type KafkaJsPartitionerChoice, KafkaPublisher, type KafkaPublisherOptions, type OauthBearerToken, type ProducerBehaviorConfig, type SaslConfig, type SaslOauthbearerConfig, type SaslPasswordConfig, type TlsConfig, _resetKafkajsWarnDedup, buildConfluentClientConfig, classifyConfluentError, classifyKafkajsError };
package/dist/index.js CHANGED
@@ -86,6 +86,12 @@ var CODE_TO_KIND = /* @__PURE__ */ new Map([
86
86
  ]);
87
87
 
88
88
  // src/kafkajs-driver.ts
89
+ var UNSUPPORTED_BY_KAFKAJS = [
90
+ "lingerMs",
91
+ "batchSize",
92
+ "deliveryTimeoutMs",
93
+ "maxRequestSize"
94
+ ];
89
95
  var KafkaJsDriver = class {
90
96
  transactional;
91
97
  producer = null;
@@ -98,6 +104,7 @@ var KafkaJsDriver = class {
98
104
  "KafkaJsDriver: transactionalId is required when transactional=true"
99
105
  );
100
106
  }
107
+ warnUnsupportedKafkajsOptions(opts);
101
108
  }
102
109
  async connect() {
103
110
  this.producer = await this.createProducer();
@@ -112,13 +119,36 @@ var KafkaJsDriver = class {
112
119
  const kafka = new mod.Kafka({
113
120
  clientId: this.opts.clientId ?? "eventferry",
114
121
  brokers: this.opts.brokers,
122
+ // kafkajs accepts `ssl: tls.ConnectionOptions` directly — Buffer + PEM
123
+ // string both supported. Our TlsConfig is a structural subset of that
124
+ // (`rejectUnauthorized` intentionally omitted; the cluster CA goes via
125
+ // `ca`). No translation needed.
115
126
  ssl: this.opts.ssl,
127
+ // SASL: PLAIN / SCRAM-SHA-256 / SCRAM-SHA-512 / OAUTHBEARER. kafkajs's
128
+ // shape matches ours; for OAUTHBEARER kafkajs reads only `value` from
129
+ // the provider's returned token (other fields are ignored).
116
130
  sasl: this.opts.sasl
117
131
  });
132
+ const createPartitioner = resolveCreatePartitioner(
133
+ mod.Partitioners,
134
+ this.opts.partitioner,
135
+ this.transactional
136
+ );
118
137
  return kafka.producer({
119
138
  idempotent: this.opts.idempotent ?? true,
120
- maxInFlightRequests: this.transactional ? 1 : void 0,
121
- transactionalId: this.transactional ? this.opts.transactionalId : void 0
139
+ // Idempotent / transactional producers cap maxInFlight at 5. When the
140
+ // user picks transactional we force 1 to keep strict ordering across
141
+ // retries on classic (non-idempotent) clusters that haven't migrated
142
+ // to the broker-side fence.
143
+ maxInFlightRequests: this.transactional ? 1 : this.opts.maxInFlightRequests,
144
+ transactionalId: this.transactional ? this.opts.transactionalId : void 0,
145
+ // kafkajs accepts these directly when set; undefined falls through to
146
+ // the kafkajs default.
147
+ requestTimeout: this.opts.requestTimeoutMs,
148
+ transactionTimeout: this.opts.transactionTimeoutMs,
149
+ // Setting any partitioner choice silences kafkajs's
150
+ // KafkaJSPartitionerNotSpecified warning.
151
+ createPartitioner
122
152
  });
123
153
  }
124
154
  async disconnect() {
@@ -164,7 +194,12 @@ function groupByTopic(messages, compression) {
164
194
  arr.push({
165
195
  key: m.key,
166
196
  value: m.value,
167
- headers: m.headers
197
+ headers: m.headers,
198
+ // Per-message partition override. When set, kafkajs routes the record
199
+ // to this exact partition; when undefined, the configured partitioner
200
+ // chooses. We keep the key here too because compacted topics need it
201
+ // even when partition is pinned.
202
+ ...m.partition !== void 0 ? { partition: m.partition } : {}
168
203
  });
169
204
  byTopic.set(m.topic, arr);
170
205
  }
@@ -174,6 +209,32 @@ function groupByTopic(messages, compression) {
174
209
  ...compression && compression !== "none" ? { compression } : {}
175
210
  }));
176
211
  }
212
+ function resolveCreatePartitioner(partitioners, choice, transactional) {
213
+ if (!partitioners) return void 0;
214
+ const effective = choice ?? (transactional ? "default" : "java-compatible");
215
+ switch (effective) {
216
+ case "java-compatible":
217
+ return partitioners.JavaCompatiblePartitioner;
218
+ case "legacy":
219
+ return partitioners.LegacyPartitioner;
220
+ case "default":
221
+ return partitioners.DefaultPartitioner;
222
+ }
223
+ }
224
+ var warnedKafkajsKeys = /* @__PURE__ */ new Set();
225
+ function warnUnsupportedKafkajsOptions(opts) {
226
+ for (const key of UNSUPPORTED_BY_KAFKAJS) {
227
+ if (opts[key] === void 0) continue;
228
+ if (warnedKafkajsKeys.has(key)) continue;
229
+ warnedKafkajsKeys.add(key);
230
+ console.warn(
231
+ `[@eventferry/kafka] '${key}' is not configurable on the kafkajs driver and was ignored. Switch to the confluent driver (driver: "confluent") for fine-grained tuning, or remove the option to silence this warning.`
232
+ );
233
+ }
234
+ }
235
+ function _resetKafkajsWarnDedup() {
236
+ warnedKafkajsKeys.clear();
237
+ }
177
238
  async function importKafkaJs() {
178
239
  try {
179
240
  return await import("kafkajs");
@@ -282,6 +343,71 @@ var NAME_TO_KIND = /* @__PURE__ */ new Map([
282
343
  ["ERR_THROTTLING_QUOTA_EXCEEDED", "quota"]
283
344
  ]);
284
345
 
346
+ // src/confluent-config.ts
347
+ function buildConfluentClientConfig(opts) {
348
+ const kafkaJS = {
349
+ clientId: opts.clientId ?? "eventferry",
350
+ brokers: opts.brokers
351
+ };
352
+ const librdkafka = {};
353
+ if (opts.lingerMs !== void 0) librdkafka["linger.ms"] = opts.lingerMs;
354
+ if (opts.batchSize !== void 0) librdkafka["batch.size"] = opts.batchSize;
355
+ if (opts.maxInFlightRequests !== void 0) {
356
+ librdkafka["max.in.flight.requests.per.connection"] = opts.maxInFlightRequests;
357
+ }
358
+ if (opts.requestTimeoutMs !== void 0) {
359
+ librdkafka["request.timeout.ms"] = opts.requestTimeoutMs;
360
+ }
361
+ if (opts.deliveryTimeoutMs !== void 0) {
362
+ librdkafka["delivery.timeout.ms"] = opts.deliveryTimeoutMs;
363
+ }
364
+ if (opts.maxRequestSize !== void 0) {
365
+ librdkafka["message.max.bytes"] = opts.maxRequestSize;
366
+ }
367
+ if (opts.transactionTimeoutMs !== void 0) {
368
+ librdkafka["transaction.timeout.ms"] = opts.transactionTimeoutMs;
369
+ }
370
+ const tlsRequested = opts.ssl === true || isTlsConfig(opts.ssl);
371
+ const saslRequested = !!opts.sasl;
372
+ if (saslRequested && tlsRequested) {
373
+ librdkafka["security.protocol"] = "sasl_ssl";
374
+ } else if (tlsRequested) {
375
+ librdkafka["security.protocol"] = "ssl";
376
+ } else if (saslRequested) {
377
+ librdkafka["security.protocol"] = "sasl_plaintext";
378
+ }
379
+ if (isTlsConfig(opts.ssl)) {
380
+ const tls = opts.ssl;
381
+ if (tls.ca !== void 0) {
382
+ librdkafka["ssl.ca.pem"] = stringifyPem(tls.ca);
383
+ }
384
+ if (tls.cert !== void 0) {
385
+ librdkafka["ssl.certificate.pem"] = stringifyPem(tls.cert);
386
+ }
387
+ if (tls.key !== void 0) {
388
+ librdkafka["ssl.key.pem"] = stringifyPem(tls.key);
389
+ }
390
+ if (tls.passphrase !== void 0) {
391
+ librdkafka["ssl.key.password"] = tls.passphrase;
392
+ }
393
+ } else if (opts.ssl === true) {
394
+ kafkaJS["ssl"] = true;
395
+ }
396
+ if (opts.sasl) {
397
+ kafkaJS["sasl"] = opts.sasl;
398
+ }
399
+ return { kafkaJS, librdkafka };
400
+ }
401
+ function isTlsConfig(v) {
402
+ return typeof v === "object" && v !== null;
403
+ }
404
+ function stringifyPem(input) {
405
+ if (Array.isArray(input)) {
406
+ return input.map((x) => typeof x === "string" ? x : x.toString("utf8")).join("\n");
407
+ }
408
+ return typeof input === "string" ? input : input.toString("utf8");
409
+ }
410
+
285
411
  // src/confluent-driver.ts
286
412
  var ConfluentDriver = class {
287
413
  transactional;
@@ -306,13 +432,10 @@ var ConfluentDriver = class {
306
432
  */
307
433
  async createProducer() {
308
434
  const mod = await importConfluent();
435
+ const { kafkaJS, librdkafka } = buildConfluentClientConfig(this.opts);
309
436
  const kafka = new mod.KafkaJS.Kafka({
310
- kafkaJS: {
311
- clientId: this.opts.clientId ?? "eventferry",
312
- brokers: this.opts.brokers,
313
- ssl: this.opts.ssl,
314
- sasl: this.opts.sasl
315
- }
437
+ kafkaJS,
438
+ ...librdkafka
316
439
  });
317
440
  return kafka.producer({
318
441
  kafkaJS: {
@@ -373,7 +496,14 @@ function groupByTopic2(messages) {
373
496
  const byTopic = /* @__PURE__ */ new Map();
374
497
  for (const m of messages) {
375
498
  const arr = byTopic.get(m.topic) ?? [];
376
- arr.push({ key: m.key, value: m.value, headers: m.headers });
499
+ arr.push({
500
+ key: m.key,
501
+ value: m.value,
502
+ headers: m.headers,
503
+ // Per-message partition override. librdkafka honors an explicit
504
+ // partition value; undefined leaves the default partitioner in charge.
505
+ ...m.partition !== void 0 ? { partition: m.partition } : {}
506
+ });
377
507
  byTopic.set(m.topic, arr);
378
508
  }
379
509
  return [...byTopic.entries()].map(([topic, msgs]) => ({
@@ -445,6 +575,8 @@ export {
445
575
  ConfluentDriver,
446
576
  KafkaJsDriver,
447
577
  KafkaPublisher,
578
+ _resetKafkajsWarnDedup,
579
+ buildConfluentClientConfig,
448
580
  classifyConfluentError,
449
581
  classifyKafkajsError
450
582
  };