youeduc-sdk-messaging 0.0.0 → 0.5.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.
Files changed (81) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +218 -0
  3. package/dist/admin.d.ts +61 -0
  4. package/dist/admin.js +143 -0
  5. package/dist/admin.js.map +1 -0
  6. package/dist/config.d.ts +74 -0
  7. package/dist/config.js +128 -0
  8. package/dist/config.js.map +1 -0
  9. package/dist/consumer-client.d.ts +155 -0
  10. package/dist/consumer-client.js +299 -0
  11. package/dist/consumer-client.js.map +1 -0
  12. package/dist/consumer.d.ts +156 -0
  13. package/dist/consumer.js +1228 -0
  14. package/dist/consumer.js.map +1 -0
  15. package/dist/contracts/index.d.ts +16 -0
  16. package/dist/contracts/index.js +42 -0
  17. package/dist/contracts/index.js.map +1 -0
  18. package/dist/contracts/schemas.d.ts +570 -0
  19. package/dist/contracts/schemas.js +902 -0
  20. package/dist/contracts/schemas.js.map +1 -0
  21. package/dist/contracts/usuarios.d.ts +163 -0
  22. package/dist/contracts/usuarios.js +5 -0
  23. package/dist/contracts/usuarios.js.map +1 -0
  24. package/dist/dedup.d.ts +29 -0
  25. package/dist/dedup.js +65 -0
  26. package/dist/dedup.js.map +1 -0
  27. package/dist/dlq.d.ts +161 -0
  28. package/dist/dlq.js +341 -0
  29. package/dist/dlq.js.map +1 -0
  30. package/dist/errors.d.ts +72 -0
  31. package/dist/errors.js +95 -0
  32. package/dist/errors.js.map +1 -0
  33. package/dist/headers.d.ts +92 -0
  34. package/dist/headers.js +144 -0
  35. package/dist/headers.js.map +1 -0
  36. package/dist/index.d.ts +30 -0
  37. package/dist/index.js +32 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/integrations/redis.d.ts +118 -0
  40. package/dist/integrations/redis.js +272 -0
  41. package/dist/integrations/redis.js.map +1 -0
  42. package/dist/integrations/runtime.d.ts +232 -0
  43. package/dist/integrations/runtime.js +554 -0
  44. package/dist/integrations/runtime.js.map +1 -0
  45. package/dist/logger.d.ts +37 -0
  46. package/dist/logger.js +49 -0
  47. package/dist/logger.js.map +1 -0
  48. package/dist/messaging.d.ts +145 -0
  49. package/dist/messaging.js +164 -0
  50. package/dist/messaging.js.map +1 -0
  51. package/dist/naming.d.ts +64 -0
  52. package/dist/naming.js +167 -0
  53. package/dist/naming.js.map +1 -0
  54. package/dist/payload.d.ts +8 -0
  55. package/dist/payload.js +32 -0
  56. package/dist/payload.js.map +1 -0
  57. package/dist/publisher.d.ts +50 -0
  58. package/dist/publisher.js +199 -0
  59. package/dist/publisher.js.map +1 -0
  60. package/dist/retry.d.ts +24 -0
  61. package/dist/retry.js +42 -0
  62. package/dist/retry.js.map +1 -0
  63. package/dist/schema.d.ts +83 -0
  64. package/dist/schema.js +676 -0
  65. package/dist/schema.js.map +1 -0
  66. package/dist/telemetry.d.ts +52 -0
  67. package/dist/telemetry.js +221 -0
  68. package/dist/telemetry.js.map +1 -0
  69. package/dist/transport.d.ts +130 -0
  70. package/dist/transport.js +283 -0
  71. package/dist/transport.js.map +1 -0
  72. package/dist/types.d.ts +148 -0
  73. package/dist/types.js +35 -0
  74. package/dist/types.js.map +1 -0
  75. package/dist/util.d.ts +38 -0
  76. package/dist/util.js +112 -0
  77. package/dist/util.js.map +1 -0
  78. package/dist/w3c.d.ts +16 -0
  79. package/dist/w3c.js +119 -0
  80. package/dist/w3c.js.map +1 -0
  81. package/package.json +74 -1
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 YouEduc
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,218 @@
1
+ # youeduc-sdk-messaging
2
+
3
+ SDK Node.js de mensageria assíncrona orientada a eventos sobre **Apache Kafka**, construída sobre
4
+ `@confluentinc/kafka-javascript` (librdkafka). Publica eventos validados por JSON Schema e os consome com retry por
5
+ tópicos, DLQ, deduplicação, commit manual de offsets e observabilidade (OpenTelemetry + logs estruturados com `pino`).
6
+
7
+ - Pacote: `youeduc-sdk-messaging`; TypeScript (tipos incluídos), **ESM only**, Node >= 22, Kafka >= 3.x.
8
+ - Existe uma SDK Python equivalente, publicada no PyPI com o mesmo nome, que segue o mesmo protocolo (tópicos,
9
+ headers, retry/DLQ e telemetria): um serviço Node publica e um serviço Python consome, e o contrário. As duas saem
10
+ sempre com a mesma versão.
11
+
12
+ ## Instalação
13
+
14
+ Fixe sempre uma versão exata:
15
+
16
+ ```bash
17
+ npm install --save-exact youeduc-sdk-messaging@X.Y.Z
18
+ ```
19
+
20
+ Conferir a versão instalada:
21
+
22
+ ```bash
23
+ node --input-type=module -e "import { version } from 'youeduc-sdk-messaging'; console.log(version)"
24
+ ```
25
+
26
+ - **ESM only**: em TypeScript use `"module": "NodeNext"` (ou `"moduleResolution": "Bundler"`) para os `exports` do
27
+ pacote (inclusive o subpath `/redis`) resolverem. Projetos CommonJS (NestJS, por exemplo) podem usar
28
+ `require('youeduc-sdk-messaging')` a partir do **Node 22.12** (`require(esm)` sem flag), com `"module": "NodeNext"`
29
+ no `tsconfig.json`; no Node 22.0–22.11 use `await import('youeduc-sdk-messaging')`.
30
+ - Dependências de runtime (fixadas): `@confluentinc/kafka-javascript`, `@opentelemetry/api`, `ajv`, `ajv-formats` e
31
+ `pino`. O `redis` é *peer dependency* opcional, só para o subpath `youeduc-sdk-messaging/redis`. SDK e exporters
32
+ do OpenTelemetry ficam a cargo da aplicação.
33
+
34
+ ### Módulo nativo (librdkafka)
35
+
36
+ `@confluentinc/kafka-javascript` é um módulo nativo: no `npm install`/`npm ci`, o script de instalação dele baixa um
37
+ binário pré-compilado (com o librdkafka embutido) para a versão do Node, o sistema, a libc e a arquitetura.
38
+
39
+ - Não instale com `--ignore-scripts` (ou rode `npm rebuild @confluentinc/kafka-javascript` em seguida): sem o binário,
40
+ o erro só aparece em runtime, ao conectar.
41
+ - O build precisa alcançar `github.com` (de onde vem o binário), além do registry npm.
42
+ - Rode o `npm ci` com a **mesma imagem base** do runtime: `node_modules` instalado em glibc (Debian) não funciona em
43
+ musl (Alpine), e vice-versa. Recomendado: `node:22-bookworm-slim`; Alpine 3.20+ também tem binário.
44
+ - Importar a SDK não carrega o librdkafka: ele só é carregado no primeiro uso do cliente Kafka (`start()`,
45
+ `ensureTopics`, `replayDlq`).
46
+
47
+ ## Publicar
48
+
49
+ ```ts
50
+ import { FileSystemSchemaLoader, KafkaConfig, Messaging } from 'youeduc-sdk-messaging';
51
+
52
+ const messaging = new Messaging(KafkaConfig.fromEnv(), { // KAFKA_BOOTSTRAP_SERVERS etc.
53
+ service: 'enrollment-api', // kebab-case; vai no header `producer`
54
+ environment: 'production',
55
+ schemaLoader: new FileSystemSchemaLoader('schemas'),
56
+ });
57
+
58
+ await messaging.start();
59
+ try {
60
+ const result = await messaging.publish(
61
+ 'enrollment', 'student', 'enrolled', 'v1', // domain, aggregate, event, version
62
+ { student_id: 's-123', course_id: 'c-9' },
63
+ {
64
+ key: 's-123', // define a partição (ordem por chave)
65
+ idempotencyKey: 'enrollment-987', // opcional; chave de dedup preferida
66
+ },
67
+ );
68
+ console.log(result.messageId, result.topic, result.partition, result.offset);
69
+ } finally {
70
+ await messaging.stop(); // um Messaging por processo: pare só no shutdown
71
+ }
72
+ ```
73
+
74
+ O evento vai para o tópico `enrollment.events.v1` com `event_type = "student.enrolled"` e é validado contra
75
+ `schemas/enrollment/student-enrolled/v1.json` (JSON Schema Draft 2020-12) antes de sair. `publish` aguarda o ack do
76
+ broker; payload inválido rejeita com `SchemaValidationError` sem enviar nada, e erro do Kafka rejeita com
77
+ `PublishError`.
78
+
79
+ ## Consumir
80
+
81
+ ```ts
82
+ import {
83
+ type ConsumedMessage,
84
+ FileSystemSchemaLoader,
85
+ KafkaConfig,
86
+ Messaging,
87
+ PermanentProcessingError,
88
+ } from 'youeduc-sdk-messaging';
89
+
90
+ async function onEnrolled(message: ConsumedMessage, signal: AbortSignal): Promise<void> {
91
+ if (!('course_id' in message.payload)) {
92
+ throw new PermanentProcessingError('course_id ausente'); // vai direto para a DLQ
93
+ }
94
+ // Resolver = sucesso; qualquer outra rejeição = retry. Repasse o signal ao I/O cancelável:
95
+ // ele é abortado no timeout de processamento ou no prazo de drenagem do shutdown.
96
+ await fetch('https://seats.internal/reserve', { method: 'POST', body: JSON.stringify(message.payload), signal });
97
+ }
98
+
99
+ const messaging = new Messaging(KafkaConfig.fromEnv(), {
100
+ service: 'learning-worker', // também é o consumer group padrão
101
+ environment: 'production',
102
+ schemaLoader: new FileSystemSchemaLoader('schemas'),
103
+ });
104
+ await messaging.start();
105
+
106
+ const consumer = messaging.consumer({
107
+ domain: 'enrollment',
108
+ version: 'v1',
109
+ handlers: { 'student.enrolled': onEnrolled }, // event_type -> handler
110
+ processingTimeoutMs: 60_000,
111
+ });
112
+ await consumer.start();
113
+
114
+ for (const signal of ['SIGINT', 'SIGTERM'] as const) {
115
+ // Para os consumers (o registro em voo termina e o progresso é commitado) e só depois fecha o producer.
116
+ process.once(signal, () => void messaging.stop({ drainTimeoutMs: 30_000 }));
117
+ }
118
+ try {
119
+ await consumer.wait(); // rejeita com o erro fatal, se houve
120
+ } finally {
121
+ await messaging.stop(); // idempotente
122
+ }
123
+ ```
124
+
125
+ Eventos do domínio sem handler são ignorados e o offset avança. Os tópicos (principal, retry e DLQ do grupo) precisam
126
+ existir antes do consumer iniciar: crie-os por IaC ou, em desenvolvimento, com `messaging.ensureTopics(...)`.
127
+
128
+ ## Garantias
129
+
130
+ | Garantia | Como |
131
+ |---|---|
132
+ | **At-least-once** no consumo | O offset só é commitado depois do desfecho final (sucesso, DLQ, ignorada) ou do envio **confirmado** para retry/DLQ. Um handler pode rodar mais de uma vez para a mesma mensagem: **handlers devem ser idempotentes** ou usar um `DuplicateChecker`. |
133
+ | **Ordem por chave** | Mensagens com a mesma `key` vão para a mesma partição e são processadas em sequência. Uma mensagem que vai para retry sai da ordem. |
134
+ | **Producer idempotente** | Um producer por processo, `acks=all` e idempotência ligada, compartilhado por publish, retry e DLQ. |
135
+ | **Schema obrigatório** | Todo payload é validado no publish e no consumo. |
136
+ | **Sem PII em logs** | Logs, spans, métricas e erros de schema nunca incluem valores do payload nem a `key`. |
137
+
138
+ Não há exactly-once nem transações Kafka.
139
+
140
+ ## Retry e DLQ
141
+
142
+ A política padrão (`new RetryPolicy()`) reprocessa uma falha transitória após **30 s, 5 min, 1 h e 12 h**; a quinta
143
+ falha vai para a DLQ. Cada atraso tem um tópico de retry por consumer group (`{tópico}.{grupo}.retry.30s` ...
144
+ `.retry.12h`) e a DLQ é `{tópico}.{grupo}.dlq`. A mensagem de retry é relida só pelo próprio grupo e nunca volta ao
145
+ tópico principal; enquanto espera, a partição de retry fica pausada sem bloquear as demais.
146
+
147
+ - `PermanentProcessingError` (ou subclasse), payload inválido e falha de schema vão direto para a DLQ.
148
+ - Qualquer outra rejeição, inclusive timeout de processamento, segue a política de retry.
149
+ - Política própria: `new RetryPolicy({ delaysMs: [10_000, 60_000, 300_000] })` (segundos inteiros); `delaysMs: []`
150
+ desativa o retry.
151
+ - A DLQ nunca marca a deduplicação, então `replayDlq(...)` pode reprocessar as mensagens.
152
+
153
+ ## Prefixo de ambiente
154
+
155
+ Para homologação e produção no mesmo cluster, `resourcePrefix` (kebab-case, como `hmg` ou `prd`) vira o primeiro
156
+ segmento do tópico principal e do consumer group, e por consequência de retry, DLQ e replay:
157
+
158
+ ```ts
159
+ new Messaging(kafka, { service: 'catalogo-api', environment: 'homologacao', schemaLoader: loader, resourcePrefix: 'hmg' });
160
+ // tópico: hmg.usuarios.events.v1 grupo: hmg.catalogo-api
161
+ // retry: hmg.usuarios.events.v1.catalogo-api.retry.30s DLQ: hmg.usuarios.events.v1.catalogo-api.dlq
162
+ ```
163
+
164
+ Prefixar também o grupo impede que réplicas dos dois ambientes dividam as partições do mesmo `group.id`. Sem prefixo,
165
+ os nomes não mudam.
166
+
167
+ ## Deduplicação
168
+
169
+ `InMemoryDuplicateChecker` serve a um processo só. Para várias réplicas, use o store Redis do subpath
170
+ `youeduc-sdk-messaging/redis` (instale `redis`, o cliente oficial node-redis), com um `namespace` por sistema e
171
+ ambiente:
172
+
173
+ ```bash
174
+ npm install --save-exact redis
175
+ ```
176
+
177
+ ```ts
178
+ import { RedisDedupStore } from 'youeduc-sdk-messaging/redis';
179
+
180
+ const store = RedisDedupStore.fromUrl('redis://redis:6379/0', { namespace: 'hmgcatalogo_api' });
181
+ const consumer = messaging.consumer({
182
+ domain: 'enrollment',
183
+ version: 'v1',
184
+ handlers: { 'student.enrolled': onEnrolled },
185
+ duplicateChecker: store.checker('learning-worker'), // o mesmo nome do consumer group
186
+ });
187
+ // no shutdown, depois de messaging.stop():
188
+ await store.close();
189
+ ```
190
+
191
+ A chave é marcada só depois do sucesso do handler; retry e DLQ não marcam. O formato da chave é o mesmo da SDK
192
+ Python: consumers das duas linguagens podem compartilhar o mesmo Redis.
193
+
194
+ ## Contratos embutidos
195
+
196
+ Os schemas e tipos dos eventos corporativos vêm no pacote, na mesma versão da SDK:
197
+
198
+ ```ts
199
+ import { contracts } from 'youeduc-sdk-messaging';
200
+
201
+ const loader = contracts.schemaLoader(); // InMemorySchemaLoader com todos os schemas embutidos
202
+ const payload = { versao: 3, ocorrido_em: '2026-10-07T09:00:00-03:00', login: 'maria.silva' } satisfies contracts.usuarios.UsuarioDesativado;
203
+ ```
204
+
205
+ Para combinar com schemas próprios do serviço:
206
+ `new ChainSchemaLoader(new FileSystemSchemaLoader('schemas'), contracts.schemaLoader())`.
207
+
208
+ ## Configuração
209
+
210
+ `KafkaConfig.fromEnv()` lê `KAFKA_BOOTSTRAP_SERVERS` (obrigatória), `KAFKA_CLIENT_ID`, `KAFKA_SECURITY_PROTOCOL`
211
+ (`PLAINTEXT`, `SSL`, `SASL_PLAINTEXT`, `SASL_SSL`), `KAFKA_SASL_MECHANISM`, `KAFKA_SASL_USERNAME` e
212
+ `KAFKA_SASL_PASSWORD`. Express, NestJS, Next.js, AdonisJS e serviços sem framework também podem montar tudo a partir
213
+ de um mapa `SDK_MESSAGING` (`SERVICE`, `ENVIRONMENT`, `KAFKA`, `REDIS`, `RESOURCE_PREFIX`...) com
214
+ `MessagingSettings.fromMapping`, e rodar os consumers num processo próprio com `serveConsumers`.
215
+
216
+ ## Licença
217
+
218
+ MIT.
@@ -0,0 +1,61 @@
1
+ /** Topic provisioning (bootstrap/dev use; in production prefer IaC). */
2
+ import type { ClientConfig, KafkaConfig } from './config.js';
3
+ import { type Logger } from './logger.js';
4
+ /** Topic definition in the KafkaJS admin API. */
5
+ export interface AdminTopicConfig {
6
+ topic: string;
7
+ numPartitions: number;
8
+ replicationFactor: number;
9
+ configEntries: Array<{
10
+ name: string;
11
+ value: string;
12
+ }>;
13
+ }
14
+ /** Subset of the KafkaJS `Admin` used here (the confluent admin client satisfies it). */
15
+ export interface KafkaAdminLike {
16
+ connect(): Promise<void>;
17
+ /** Resolves `true` when created, `false` when it already existed; per-topic errors reject. */
18
+ createTopics(options: {
19
+ topics: AdminTopicConfig[];
20
+ timeout?: number;
21
+ }): Promise<boolean>;
22
+ disconnect(): Promise<void>;
23
+ }
24
+ export type AdminFactory = (config: ClientConfig) => KafkaAdminLike | Promise<KafkaAdminLike>;
25
+ /** Default factory: `new KafkaJS.Kafka().admin(config)` (native module loaded on first use). */
26
+ export declare const createConfluentAdmin: AdminFactory;
27
+ export interface EnsureTopicsOptions {
28
+ /** Default 3. */
29
+ numPartitions?: number;
30
+ /** Default 1. */
31
+ replicationFactor?: number;
32
+ /** Topic configs applied to every created topic (e.g. `{"retention.ms": "604800000"}`). */
33
+ topicConfigs?: Readonly<Record<string, string>>;
34
+ /** Timeout of each create request. Default 30 000 ms. */
35
+ timeoutMs?: number;
36
+ /** Admin factory (tests inject fakes). Default {@link createConfluentAdmin}. */
37
+ adminFactory?: AdminFactory;
38
+ /** Default: the SDK default logger. */
39
+ logger?: Logger;
40
+ }
41
+ /** One topic to create: name, partitions and configs (spec §2.6, per topic kind). */
42
+ export interface TopicSpec {
43
+ topic: string;
44
+ numPartitions: number;
45
+ topicConfigs?: Readonly<Record<string, string>> | undefined;
46
+ }
47
+ export type CreateTopicsOptions = Omit<EnsureTopicsOptions, 'numPartitions' | 'topicConfigs'>;
48
+ /**
49
+ * Creates the topics that do not exist yet and returns the names created.
50
+ *
51
+ * An existing topic is not an error. Any other per-topic error throws `MessagingError` listing
52
+ * each topic and its error; request-level errors (connection, timeout) propagate as thrown by
53
+ * the client. Each topic is created by its own request, so "created" vs "already existed" is
54
+ * known per topic (the KafkaJS API only reports it per request).
55
+ */
56
+ export declare function ensureTopics(config: KafkaConfig, topics: Iterable<string>, options?: EnsureTopicsOptions): Promise<string[]>;
57
+ /**
58
+ * Like {@link ensureTopics}, with partitions and configs per topic. Repeated names: the first
59
+ * definition wins.
60
+ */
61
+ export declare function createTopics(config: KafkaConfig, specs: Iterable<TopicSpec>, options?: CreateTopicsOptions): Promise<string[]>;
package/dist/admin.js ADDED
@@ -0,0 +1,143 @@
1
+ /** Topic provisioning (bootstrap/dev use; in production prefer IaC). */
2
+ import { MessagingError } from './errors.js';
3
+ import { defaultLogger } from './logger.js';
4
+ import { KafkaErrorCodes } from './transport.js';
5
+ import { errorFields, pyRepr } from './util.js';
6
+ /** Default factory: `new KafkaJS.Kafka().admin(config)` (native module loaded on first use). */
7
+ export const createConfluentAdmin = async (config) => {
8
+ const { KafkaJS } = (await import('@confluentinc/kafka-javascript')).default;
9
+ return new KafkaJS.Kafka({}).admin(config);
10
+ };
11
+ function describeFailure(topic, failure) {
12
+ const code = typeof failure.code === 'number' ? failure.code : undefined;
13
+ const kind = typeof failure.type === 'string' && failure.type !== ''
14
+ ? failure.type
15
+ : typeof failure.name === 'string'
16
+ ? failure.name
17
+ : 'Error';
18
+ let detail = code === undefined ? kind : `${kind} (error code ${code})`;
19
+ const message = typeof failure.message === 'string' ? failure.message : '';
20
+ if (message)
21
+ detail += `: ${message}`;
22
+ return `${pyRepr(topic)}: ${detail}`;
23
+ }
24
+ /** Per-topic failures carried by a `KafkaJSAggregateError` (each with `topic`), else `null`. */
25
+ function topicFailures(error) {
26
+ const errors = error?.errors;
27
+ if (!Array.isArray(errors) || errors.length === 0)
28
+ return null;
29
+ if (!errors.every((item) => typeof item === 'object' && item !== null && 'topic' in item))
30
+ return null;
31
+ return errors;
32
+ }
33
+ /**
34
+ * Creates the topics that do not exist yet and returns the names created.
35
+ *
36
+ * An existing topic is not an error. Any other per-topic error throws `MessagingError` listing
37
+ * each topic and its error; request-level errors (connection, timeout) propagate as thrown by
38
+ * the client. Each topic is created by its own request, so "created" vs "already existed" is
39
+ * known per topic (the KafkaJS API only reports it per request).
40
+ */
41
+ export async function ensureTopics(config, topics, options = {}) {
42
+ if (typeof topics === 'string') {
43
+ throw new TypeError("'topics' must be an iterable of topic names, not a string");
44
+ }
45
+ const names = [...new Set(topics)];
46
+ if (names.length === 0)
47
+ return [];
48
+ for (const name of names)
49
+ requireTopicName(name);
50
+ const numPartitions = options.numPartitions ?? 3;
51
+ requireCount('num_partitions', numPartitions);
52
+ const { numPartitions: _n, topicConfigs, ...rest } = options;
53
+ return createTopics(config, names.map((topic) => ({ topic, numPartitions, topicConfigs })), rest);
54
+ }
55
+ function requireTopicName(name) {
56
+ if (typeof name !== 'string' || name === '') {
57
+ throw new TypeError(`topic names must be non-empty strings; got ${typeof name === 'string' ? pyRepr(name) : String(name)}`);
58
+ }
59
+ }
60
+ function requireCount(name, value) {
61
+ if (typeof value !== 'number' || !Number.isInteger(value) || value < 1) {
62
+ throw new RangeError(`'${name}' must be >= 1; got ${String(value)}`);
63
+ }
64
+ }
65
+ /**
66
+ * Like {@link ensureTopics}, with partitions and configs per topic. Repeated names: the first
67
+ * definition wins.
68
+ */
69
+ export async function createTopics(config, specs, options = {}) {
70
+ const unique = new Map();
71
+ for (const spec of specs) {
72
+ requireTopicName(spec.topic);
73
+ requireCount('num_partitions', spec.numPartitions);
74
+ if (!unique.has(spec.topic))
75
+ unique.set(spec.topic, spec);
76
+ }
77
+ if (unique.size === 0)
78
+ return [];
79
+ const replicationFactor = options.replicationFactor ?? 1;
80
+ requireCount('replication_factor', replicationFactor);
81
+ const names = [...unique.keys()];
82
+ const timeout = options.timeoutMs ?? 30_000;
83
+ const logger = options.logger ?? defaultLogger();
84
+ const factory = options.adminFactory ?? createConfluentAdmin;
85
+ const admin = await factory(config.clientConfig());
86
+ const created = [];
87
+ const failures = [];
88
+ try {
89
+ await admin.connect();
90
+ const results = await Promise.allSettled([...unique.values()].map((spec) => admin.createTopics({
91
+ topics: [
92
+ {
93
+ topic: spec.topic,
94
+ numPartitions: spec.numPartitions,
95
+ replicationFactor,
96
+ configEntries: Object.entries(spec.topicConfigs ?? {}).map(([name, value]) => ({
97
+ name,
98
+ value,
99
+ })),
100
+ },
101
+ ],
102
+ timeout,
103
+ })));
104
+ let requestError;
105
+ results.forEach((result, index) => {
106
+ const topic = names[index];
107
+ if (result.status === 'fulfilled') {
108
+ if (result.value)
109
+ created.push(topic);
110
+ return;
111
+ }
112
+ const perTopic = topicFailures(result.reason);
113
+ if (perTopic === null) {
114
+ requestError ??= result.reason;
115
+ return;
116
+ }
117
+ for (const failure of perTopic) {
118
+ if (failure.code === KafkaErrorCodes.ERR_TOPIC_ALREADY_EXISTS)
119
+ continue;
120
+ failures.push(describeFailure(typeof failure.topic === 'string' ? failure.topic : topic, failure));
121
+ }
122
+ });
123
+ if (requestError !== undefined)
124
+ throw requestError;
125
+ }
126
+ finally {
127
+ await closeQuietly(admin, logger);
128
+ }
129
+ if (created.length > 0)
130
+ logger.info('kafka_topics_created', { messaging: { topics: created } });
131
+ if (failures.length > 0)
132
+ throw new MessagingError(`failed to create topics: ${failures.join('; ')}`);
133
+ return created;
134
+ }
135
+ async function closeQuietly(admin, logger) {
136
+ try {
137
+ await admin.disconnect();
138
+ }
139
+ catch (error) {
140
+ logger.warn('kafka_admin_close_failed', { error: errorFields(error) });
141
+ }
142
+ }
143
+ //# sourceMappingURL=admin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"admin.js","sourceRoot":"","sources":["../src/admin.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Connection parameters shared by producer, consumer and admin (Python `KafkaConfig`).
3
+ *
4
+ * `clientConfig()` converts them to the librdkafka configuration accepted by
5
+ * `@confluentinc/kafka-javascript` (`new KafkaJS.Kafka().producer(config)` etc.).
6
+ */
7
+ import { inspect } from 'node:util';
8
+ export declare const SECURITY_PROTOCOLS: readonly ['PLAINTEXT', 'SSL', 'SASL_PLAINTEXT', 'SASL_SSL'];
9
+ export type SecurityProtocol = (typeof SECURITY_PROTOCOLS)[number];
10
+ /**
11
+ * TLS material (the Node equivalent of Python's `ssl_context`). Without it, `SSL`/`SASL_SSL`
12
+ * still verify the broker certificate against the system CAs and check the hostname.
13
+ */
14
+ export interface KafkaSslOptions {
15
+ /** File/dir with CA certificates (`ssl.ca.location`). Default: probe the system CAs. */
16
+ caLocation?: string;
17
+ /** CA certificates in PEM (`ssl.ca.pem`). */
18
+ caPem?: string;
19
+ /** Client certificate for mTLS (`ssl.certificate.location` / `ssl.certificate.pem`). */
20
+ certificateLocation?: string;
21
+ certificatePem?: string;
22
+ /** Client key for mTLS (`ssl.key.location` / `ssl.key.pem`) and its password. */
23
+ keyLocation?: string;
24
+ keyPem?: string;
25
+ keyPassword?: string;
26
+ }
27
+ export interface KafkaConfigInit {
28
+ bootstrapServers: string;
29
+ clientId?: string | null | undefined;
30
+ securityProtocol?: string | undefined;
31
+ /** Required with `SASL_*` (e.g. `PLAIN`, `SCRAM-SHA-512`). */
32
+ saslMechanism?: string | null | undefined;
33
+ saslUsername?: string | null | undefined;
34
+ saslPassword?: string | null | undefined;
35
+ ssl?: KafkaSslOptions | null | undefined;
36
+ }
37
+ /** librdkafka configuration (string keys, scalar values). */
38
+ export type ClientConfig = Record<string, string | number | boolean>;
39
+ /** Environment-like mapping (`process.env`). */
40
+ export type Environment = Readonly<Record<string, string | undefined>>;
41
+ /**
42
+ * Validated, immutable connection settings. The SASL password and the TLS material are kept in
43
+ * private fields: they never appear in `JSON.stringify`, `console.log` or `util.inspect`.
44
+ */
45
+ export declare class KafkaConfig {
46
+ #private;
47
+ readonly bootstrapServers: string;
48
+ readonly clientId: string | null;
49
+ readonly securityProtocol: SecurityProtocol;
50
+ readonly saslMechanism: string | null;
51
+ readonly saslUsername: string | null;
52
+ constructor(init: KafkaConfigInit);
53
+ get saslPassword(): string | null;
54
+ get ssl(): Readonly<KafkaSslOptions> | null;
55
+ /**
56
+ * Reads `{prefix}BOOTSTRAP_SERVERS` (required), `CLIENT_ID`, `SECURITY_PROTOCOL`,
57
+ * `SASL_MECHANISM`, `SASL_USERNAME` and `SASL_PASSWORD`. TLS material is not read from the
58
+ * environment (build `ssl` in code); without it, TLS uses the system CAs.
59
+ */
60
+ static fromEnv(options?: {
61
+ prefix?: string;
62
+ env?: Environment;
63
+ }): KafkaConfig;
64
+ /**
65
+ * Common librdkafka configuration for producer, consumer and admin (Python `client_kwargs()`).
66
+ *
67
+ * With TLS (`SSL`/`SASL_SSL`) the broker certificate is always verified and the hostname
68
+ * checked; without explicit CA material the system CAs are probed (`ssl.ca.location=probe`).
69
+ */
70
+ clientConfig(): ClientConfig;
71
+ /** Safe representation (no password, no TLS material). */
72
+ toJSON(): Record<string, unknown>;
73
+ [inspect.custom](): string;
74
+ }
package/dist/config.js ADDED
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Connection parameters shared by producer, consumer and admin (Python `KafkaConfig`).
3
+ *
4
+ * `clientConfig()` converts them to the librdkafka configuration accepted by
5
+ * `@confluentinc/kafka-javascript` (`new KafkaJS.Kafka().producer(config)` etc.).
6
+ */
7
+ import { inspect } from 'node:util';
8
+ import { pyRepr } from './util.js';
9
+ export const SECURITY_PROTOCOLS = ['PLAINTEXT', 'SSL', 'SASL_PLAINTEXT', 'SASL_SSL'];
10
+ const TLS_PROTOCOLS = ['SSL', 'SASL_SSL'];
11
+ /**
12
+ * Validated, immutable connection settings. The SASL password and the TLS material are kept in
13
+ * private fields: they never appear in `JSON.stringify`, `console.log` or `util.inspect`.
14
+ */
15
+ export class KafkaConfig {
16
+ bootstrapServers;
17
+ clientId;
18
+ securityProtocol;
19
+ saslMechanism;
20
+ saslUsername;
21
+ #saslPassword;
22
+ #ssl;
23
+ constructor(init) {
24
+ if (typeof init.bootstrapServers !== 'string' || init.bootstrapServers.trim() === '') {
25
+ throw new TypeError("'bootstrap_servers' must not be empty");
26
+ }
27
+ const protocol = init.securityProtocol ?? 'PLAINTEXT';
28
+ if (!SECURITY_PROTOCOLS.includes(protocol)) {
29
+ throw new TypeError(`'security_protocol' must be one of (${SECURITY_PROTOCOLS.map(pyRepr).join(', ')}); got ${pyRepr(String(protocol))}`);
30
+ }
31
+ const mechanism = init.saslMechanism ?? null;
32
+ if (protocol.startsWith('SASL') && mechanism === null) {
33
+ throw new TypeError("'sasl_mechanism' is required for SASL security protocols");
34
+ }
35
+ this.bootstrapServers = init.bootstrapServers;
36
+ this.clientId = init.clientId ?? null;
37
+ this.securityProtocol = protocol;
38
+ this.saslMechanism = mechanism;
39
+ this.saslUsername = init.saslUsername ?? null;
40
+ this.#saslPassword = init.saslPassword ?? null;
41
+ this.#ssl = init.ssl ? Object.freeze({ ...init.ssl }) : null;
42
+ Object.freeze(this);
43
+ }
44
+ get saslPassword() {
45
+ return this.#saslPassword;
46
+ }
47
+ get ssl() {
48
+ return this.#ssl;
49
+ }
50
+ /**
51
+ * Reads `{prefix}BOOTSTRAP_SERVERS` (required), `CLIENT_ID`, `SECURITY_PROTOCOL`,
52
+ * `SASL_MECHANISM`, `SASL_USERNAME` and `SASL_PASSWORD`. TLS material is not read from the
53
+ * environment (build `ssl` in code); without it, TLS uses the system CAs.
54
+ */
55
+ static fromEnv(options = {}) {
56
+ const prefix = options.prefix ?? 'KAFKA_';
57
+ const env = options.env ?? process.env;
58
+ const servers = env[`${prefix}BOOTSTRAP_SERVERS`];
59
+ if (servers === undefined) {
60
+ throw new TypeError(`environment variable ${prefix}BOOTSTRAP_SERVERS is required`);
61
+ }
62
+ return new KafkaConfig({
63
+ bootstrapServers: servers,
64
+ clientId: env[`${prefix}CLIENT_ID`],
65
+ securityProtocol: env[`${prefix}SECURITY_PROTOCOL`] ?? 'PLAINTEXT',
66
+ saslMechanism: env[`${prefix}SASL_MECHANISM`],
67
+ saslUsername: env[`${prefix}SASL_USERNAME`],
68
+ saslPassword: env[`${prefix}SASL_PASSWORD`],
69
+ });
70
+ }
71
+ /**
72
+ * Common librdkafka configuration for producer, consumer and admin (Python `client_kwargs()`).
73
+ *
74
+ * With TLS (`SSL`/`SASL_SSL`) the broker certificate is always verified and the hostname
75
+ * checked; without explicit CA material the system CAs are probed (`ssl.ca.location=probe`).
76
+ */
77
+ clientConfig() {
78
+ const config = {
79
+ 'bootstrap.servers': this.bootstrapServers,
80
+ 'security.protocol': this.securityProtocol.toLowerCase(),
81
+ };
82
+ if (this.clientId !== null)
83
+ config['client.id'] = this.clientId;
84
+ if (this.saslMechanism !== null) {
85
+ config['sasl.mechanisms'] = this.saslMechanism;
86
+ if (this.saslUsername !== null)
87
+ config['sasl.username'] = this.saslUsername;
88
+ if (this.#saslPassword !== null)
89
+ config['sasl.password'] = this.#saslPassword;
90
+ }
91
+ if (TLS_PROTOCOLS.includes(this.securityProtocol)) {
92
+ const ssl = this.#ssl ?? {};
93
+ config['enable.ssl.certificate.verification'] = true;
94
+ config['ssl.endpoint.identification.algorithm'] = 'https';
95
+ if (ssl.caLocation !== undefined)
96
+ config['ssl.ca.location'] = ssl.caLocation;
97
+ if (ssl.caPem !== undefined)
98
+ config['ssl.ca.pem'] = ssl.caPem;
99
+ if (ssl.caLocation === undefined && ssl.caPem === undefined)
100
+ config['ssl.ca.location'] = 'probe';
101
+ if (ssl.certificateLocation !== undefined)
102
+ config['ssl.certificate.location'] = ssl.certificateLocation;
103
+ if (ssl.certificatePem !== undefined)
104
+ config['ssl.certificate.pem'] = ssl.certificatePem;
105
+ if (ssl.keyLocation !== undefined)
106
+ config['ssl.key.location'] = ssl.keyLocation;
107
+ if (ssl.keyPem !== undefined)
108
+ config['ssl.key.pem'] = ssl.keyPem;
109
+ if (ssl.keyPassword !== undefined)
110
+ config['ssl.key.password'] = ssl.keyPassword;
111
+ }
112
+ return config;
113
+ }
114
+ /** Safe representation (no password, no TLS material). */
115
+ toJSON() {
116
+ return {
117
+ bootstrapServers: this.bootstrapServers,
118
+ clientId: this.clientId,
119
+ securityProtocol: this.securityProtocol,
120
+ saslMechanism: this.saslMechanism,
121
+ saslUsername: this.saslUsername,
122
+ };
123
+ }
124
+ [inspect.custom]() {
125
+ return `KafkaConfig ${inspect(this.toJSON())}`;
126
+ }
127
+ }
128
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}