@ecdt/server-common 1.6.1 → 1.7.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/README.md CHANGED
@@ -196,6 +196,51 @@ consiga dar `await` e acoplar a latência do request ao motor.
196
196
 
197
197
  ---
198
198
 
199
+ ### `publicarEventoDaRequisicao(req, options)`
200
+
201
+ O mesmo publish, montado a partir da requisição que gerou o evento. Resolve o token pelo header
202
+ `Authorization` e o identificador anônimo pelo corpo (`id_visitante`) ou pelo cookie, nessa ordem.
203
+
204
+ ```js
205
+ const { publicarEventoDaRequisicao } = require('@ecdt/server-common');
206
+
207
+ publicarEventoDaRequisicao(req, {
208
+ tipo: 'meu_evento',
209
+ payload: { campo: 'valor' },
210
+ });
211
+ ```
212
+
213
+ | Opção | Default | Descrição |
214
+ |---|---|---|
215
+ | `tipo`, `payload`, `url`, `timeoutMs`, `aoFalhar` | — | Idênticos ao `publicarEvento` |
216
+ | `cookieVisitante` | `process.env.MOTOR_EVENTOS_COOKIE_VISITANTE` | Nome do cookie que guarda o identificador anônimo |
217
+
218
+ Sem `cookieVisitante` configurado, só o corpo alimenta o `id_visitante`. Também não lança em
219
+ nenhuma hipótese, inclusive com `req` sem `headers` nem `body`.
220
+
221
+ ---
222
+
223
+ ### `lerCookie`, `idDoCookie` e `sanitizarId`
224
+
225
+ Leitura do header `Cookie` no servidor, sem dependência de framework.
226
+
227
+ ```js
228
+ const { idDoCookie } = require('@ecdt/server-common');
229
+
230
+ const idVisitante = idDoCookie(req.headers.cookie, 'nome_do_cookie');
231
+ ```
232
+
233
+ | Função | Devolve |
234
+ |---|---|
235
+ | `lerCookie(cabecalho, nome)` | Valor cru do cookie, ou `null` |
236
+ | `idDoCookie(cabecalho, nome, { maxLen })` | O identificador: aceita o cookie como string simples ou como JSON com campo `id`, decodifica o valor e corta em `maxLen` (100 por padrão) |
237
+ | `sanitizarId(valor, maxLen)` | O mesmo corte e validação, para identificador que veio de outro lugar |
238
+
239
+ O match do nome é exato, então `nome_antigo` não casa com `nome`. Nenhuma das três lança: entrada
240
+ que não é string vira `null`.
241
+
242
+ ---
243
+
199
244
  ## Instalação
200
245
 
201
246
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecdt/server-common",
3
- "version": "1.6.1",
3
+ "version": "1.7.0",
4
4
  "description": "Conjunto de ferramentas e configurações comuns nos servidores da Econodata",
5
5
  "main": "src/index.js",
6
6
  "types": "src/index.d.ts",
package/src/index.d.ts CHANGED
@@ -147,3 +147,27 @@ export interface PublicarEventoOptions {
147
147
  }
148
148
 
149
149
  export declare function publicarEvento(options?: PublicarEventoOptions): void;
150
+
151
+ export interface PublicarEventoDaRequisicaoOptions
152
+ extends Omit<PublicarEventoOptions, 'autorizacao' | 'idVisitante'> {
153
+ cookieVisitante?: string;
154
+ }
155
+
156
+ export declare function publicarEventoDaRequisicao(
157
+ req: unknown,
158
+ options?: PublicarEventoDaRequisicaoOptions
159
+ ): void;
160
+
161
+ export interface IdDoCookieOptions {
162
+ maxLen?: number;
163
+ }
164
+
165
+ export declare function lerCookie(cabecalho: unknown, nome: string): string | null;
166
+
167
+ export declare function idDoCookie(
168
+ cabecalho: unknown,
169
+ nome: string,
170
+ options?: IdDoCookieOptions
171
+ ): string | null;
172
+
173
+ export declare function sanitizarId(valor: unknown, maxLen?: number): string | null;
package/src/index.js CHANGED
@@ -3,7 +3,8 @@ const { devMktTokenSanitaze } = require("./services/tokenService.js");
3
3
  const { expressCors } = require("./services/corsService.js");
4
4
  const { setupGracefulShutdown } = require("./services/shutdownService.js");
5
5
  const { setupRedMetrics, initRedMetrics, redMetricsMiddleware, metricsHandler, renderMetrics } = require("./services/redMetricsService.js");
6
- const { publicarEvento } = require("./services/motorEventosService.js");
6
+ const { publicarEvento, publicarEventoDaRequisicao } = require("./services/motorEventosService.js");
7
+ const { lerCookie, idDoCookie, sanitizarId } = require("./services/cookieService.js");
7
8
 
8
9
  function expressCommonMiddlewares({ cookieName } = {}){
9
10
  return [bodyParser.text(), bodyParser.json(), bodyParser.urlencoded({extended: false}), devMktTokenSanitaze({ cookieName })];
@@ -12,4 +13,4 @@ function expressCommonMiddlewares({ cookieName } = {}){
12
13
 
13
14
 
14
15
 
15
- module.exports = { expressCommonMiddlewares, expressCors, setupGracefulShutdown, setupRedMetrics, initRedMetrics, redMetricsMiddleware, metricsHandler, renderMetrics, publicarEvento }
16
+ module.exports = { expressCommonMiddlewares, expressCors, setupGracefulShutdown, setupRedMetrics, initRedMetrics, redMetricsMiddleware, metricsHandler, renderMetrics, publicarEvento, publicarEventoDaRequisicao, lerCookie, idDoCookie, sanitizarId }
@@ -0,0 +1,43 @@
1
+ const TAMANHO_MAXIMO_PADRAO = 100;
2
+
3
+ function sanitizarId(valor, maxLen = TAMANHO_MAXIMO_PADRAO) {
4
+ if (typeof valor !== "string") return null;
5
+
6
+ const limpo = valor.trim();
7
+ return limpo ? limpo.slice(0, maxLen) : null;
8
+ }
9
+
10
+ function lerCookie(cabecalho, nome) {
11
+ if (typeof cabecalho !== "string" || typeof nome !== "string") return null;
12
+
13
+ for (const parte of cabecalho.split(";")) {
14
+ const separador = parte.indexOf("=");
15
+ if (separador === -1) continue;
16
+ if (parte.slice(0, separador).trim() !== nome) continue;
17
+ return parte.slice(separador + 1).trim();
18
+ }
19
+
20
+ return null;
21
+ }
22
+
23
+ function idDoCookie(cabecalho, nome, { maxLen = TAMANHO_MAXIMO_PADRAO } = {}) {
24
+ const bruto = lerCookie(cabecalho, nome);
25
+ if (!bruto) return null;
26
+
27
+ let valor = bruto;
28
+ try {
29
+ valor = decodeURIComponent(bruto);
30
+ } catch (_erroDeEncoding) {
31
+ valor = bruto;
32
+ }
33
+
34
+ try {
35
+ const conteudo = JSON.parse(valor);
36
+ const id = conteudo && typeof conteudo === "object" ? conteudo.id : conteudo;
37
+ return sanitizarId(typeof id === "string" ? id : String(id ?? ""), maxLen);
38
+ } catch (_naoEhJson) {
39
+ return sanitizarId(valor, maxLen);
40
+ }
41
+ }
42
+
43
+ module.exports = { lerCookie, idDoCookie, sanitizarId };
@@ -1,3 +1,5 @@
1
+ const { idDoCookie, sanitizarId } = require("./cookieService.js");
2
+
1
3
  const TIMEOUT_PADRAO_MS = 2000;
2
4
 
3
5
  function urlConfigurada(url) {
@@ -69,4 +71,33 @@ function publicarEvento({
69
71
  }
70
72
  }
71
73
 
72
- module.exports = { publicarEvento };
74
+ /**
75
+ * Publica o evento a partir da requisicao que o gerou: tira o token do header
76
+ * Authorization e o identificador anonimo do corpo ou do cookie, nessa ordem.
77
+ * Nunca lanca, nem quando `req` nao tem headers nem corpo — o evento e
78
+ * secundario ao fluxo, e boa parte das chamadas acontece depois de uma escrita
79
+ * ja efetivada.
80
+ */
81
+ function publicarEventoDaRequisicao(req, {
82
+ tipo,
83
+ payload,
84
+ cookieVisitante = process.env.MOTOR_EVENTOS_COOKIE_VISITANTE,
85
+ ...resto
86
+ } = {}) {
87
+ try {
88
+ const doCorpo = sanitizarId(req?.body?.id_visitante);
89
+ const doCookie = cookieVisitante ? idDoCookie(req?.headers?.cookie, cookieVisitante) : null;
90
+
91
+ publicarEvento({
92
+ tipo,
93
+ payload,
94
+ autorizacao: req?.headers?.authorization,
95
+ idVisitante: doCorpo ?? doCookie,
96
+ ...resto,
97
+ });
98
+ } catch (erro) {
99
+ console.error(`motorEventos: ${tipo} nao publicado (${erro?.message})`);
100
+ }
101
+ }
102
+
103
+ module.exports = { publicarEvento, publicarEventoDaRequisicao };
@@ -0,0 +1,59 @@
1
+ const { test, describe } = require('node:test');
2
+ const assert = require('node:assert/strict');
3
+
4
+ const { lerCookie, idDoCookie, sanitizarId } = require('../src/services/cookieService.js');
5
+
6
+ const comCookie = (nome, valor) => `_ga=GA1.1.9; ${nome}=${encodeURIComponent(valor)}; outro=x`;
7
+
8
+ describe('sanitizarId', () => {
9
+ test('devolve null para vazio, espaco em branco ou valor que nao e string', () => {
10
+ assert.equal(sanitizarId(''), null);
11
+ assert.equal(sanitizarId(' '), null);
12
+ assert.equal(sanitizarId(undefined), null);
13
+ assert.equal(sanitizarId(42), null);
14
+ });
15
+
16
+ test('corta no tamanho maximo', () => {
17
+ assert.equal(sanitizarId('a'.repeat(300)).length, 100);
18
+ assert.equal(sanitizarId('a'.repeat(300), 10).length, 10);
19
+ });
20
+ });
21
+
22
+ describe('lerCookie', () => {
23
+ test('acha o cookie no meio do header', () => {
24
+ assert.equal(lerCookie('a=1; alvo=valor; b=2', 'alvo'), 'valor');
25
+ });
26
+
27
+ test('nao casa com cookie de nome parecido', () => {
28
+ assert.equal(lerCookie('alvo_antigo=valor', 'alvo'), null);
29
+ });
30
+
31
+ test('devolve null sem header, sem o cookie ou com header que nao e string', () => {
32
+ assert.equal(lerCookie(undefined, 'alvo'), null);
33
+ assert.equal(lerCookie('a=1', 'alvo'), null);
34
+ assert.equal(lerCookie({}, 'alvo'), null);
35
+ assert.equal(lerCookie('a=1', undefined), null);
36
+ });
37
+ });
38
+
39
+ describe('idDoCookie', () => {
40
+ test('extrai o id quando o cookie guarda JSON', () => {
41
+ assert.equal(idDoCookie(comCookie('trk', '{"id":"visitante-1"}'), 'trk'), 'visitante-1');
42
+ });
43
+
44
+ test('aceita o cookie como string simples', () => {
45
+ assert.equal(idDoCookie(comCookie('trk', 'visitante-2'), 'trk'), 'visitante-2');
46
+ });
47
+
48
+ test('devolve null quando o JSON nao tem id', () => {
49
+ assert.equal(idDoCookie(comCookie('trk', '{"outro":"x"}'), 'trk'), null);
50
+ });
51
+
52
+ test('devolve null quando o cookie nao existe', () => {
53
+ assert.equal(idDoCookie('a=1', 'trk'), null);
54
+ });
55
+
56
+ test('respeita o maxLen', () => {
57
+ assert.equal(idDoCookie(comCookie('trk', 'a'.repeat(300)), 'trk', { maxLen: 5 }).length, 5);
58
+ });
59
+ });
@@ -1,7 +1,10 @@
1
1
  const { test, describe, beforeEach, afterEach } = require('node:test');
2
2
  const assert = require('node:assert/strict');
3
3
 
4
- const { publicarEvento } = require('../src/services/motorEventosService.js');
4
+ const {
5
+ publicarEvento,
6
+ publicarEventoDaRequisicao,
7
+ } = require('../src/services/motorEventosService.js');
5
8
 
6
9
  const URL = 'http://coletor.interno/eventos';
7
10
 
@@ -194,3 +197,114 @@ describe('publicarEvento', () => {
194
197
  assert.equal(retorno, undefined);
195
198
  });
196
199
  });
200
+
201
+ describe('publicarEventoDaRequisicao', () => {
202
+ let fetchOriginal;
203
+ let warnOriginal;
204
+ let errorOriginal;
205
+ let urlOriginal;
206
+ let cookieOriginal;
207
+ let chamadas;
208
+
209
+ beforeEach(() => {
210
+ fetchOriginal = global.fetch;
211
+ warnOriginal = console.warn;
212
+ errorOriginal = console.error;
213
+ urlOriginal = process.env.MOTOR_EVENTOS_URL;
214
+ cookieOriginal = process.env.MOTOR_EVENTOS_COOKIE_VISITANTE;
215
+ chamadas = [];
216
+ console.warn = () => {};
217
+ console.error = () => {};
218
+ process.env.MOTOR_EVENTOS_URL = URL;
219
+ global.fetch = (url, opcoes) => {
220
+ chamadas.push({ url, opcoes });
221
+ return Promise.resolve({ ok: true, status: 201 });
222
+ };
223
+ });
224
+
225
+ afterEach(() => {
226
+ global.fetch = fetchOriginal;
227
+ console.warn = warnOriginal;
228
+ console.error = errorOriginal;
229
+ for (const [chave, valor] of [
230
+ ['MOTOR_EVENTOS_URL', urlOriginal],
231
+ ['MOTOR_EVENTOS_COOKIE_VISITANTE', cookieOriginal],
232
+ ]) {
233
+ if (valor === undefined) delete process.env[chave];
234
+ else process.env[chave] = valor;
235
+ }
236
+ });
237
+
238
+ const corpoEnviado = () => JSON.parse(chamadas[0].opcoes.body);
239
+
240
+ test('tira o token do header da requisicao', async () => {
241
+ publicarEventoDaRequisicao(
242
+ { headers: { authorization: 'Bearer jwt' }, body: {} },
243
+ { tipo: 'meu_evento', payload: { campo: 'valor' } }
244
+ );
245
+ await esperarMicrotasks();
246
+
247
+ assert.equal(chamadas[0].opcoes.headers.Authorization, 'Bearer jwt');
248
+ assert.deepEqual(corpoEnviado().payload, { campo: 'valor' });
249
+ });
250
+
251
+ test('tira o visitante do cookie quando nao ha token', async () => {
252
+ publicarEventoDaRequisicao(
253
+ { headers: { cookie: 'trk=%7B%22id%22%3A%22visitante-1%22%7D' }, body: {} },
254
+ { tipo: 'meu_evento', cookieVisitante: 'trk' }
255
+ );
256
+ await esperarMicrotasks();
257
+
258
+ assert.equal(corpoEnviado().id_visitante, 'visitante-1');
259
+ assert.equal(chamadas[0].opcoes.headers.Authorization, undefined);
260
+ });
261
+
262
+ test('o corpo tem precedencia sobre o cookie', async () => {
263
+ publicarEventoDaRequisicao(
264
+ {
265
+ headers: { cookie: 'trk=%7B%22id%22%3A%22do-cookie%22%7D' },
266
+ body: { id_visitante: 'do-corpo' },
267
+ },
268
+ { tipo: 'meu_evento', cookieVisitante: 'trk' }
269
+ );
270
+ await esperarMicrotasks();
271
+
272
+ assert.equal(corpoEnviado().id_visitante, 'do-corpo');
273
+ });
274
+
275
+ test('cai na MOTOR_EVENTOS_COOKIE_VISITANTE quando o nome nao e passado', async () => {
276
+ process.env.MOTOR_EVENTOS_COOKIE_VISITANTE = 'trk';
277
+
278
+ publicarEventoDaRequisicao(
279
+ { headers: { cookie: 'trk=visitante-2' }, body: {} },
280
+ { tipo: 'meu_evento' }
281
+ );
282
+ await esperarMicrotasks();
283
+
284
+ assert.equal(corpoEnviado().id_visitante, 'visitante-2');
285
+ });
286
+
287
+ test('nao publica quando a requisicao nao traz identidade nenhuma', async () => {
288
+ delete process.env.MOTOR_EVENTOS_COOKIE_VISITANTE;
289
+
290
+ publicarEventoDaRequisicao({ headers: {}, body: {} }, { tipo: 'meu_evento' });
291
+ await esperarMicrotasks();
292
+
293
+ assert.equal(chamadas.length, 0);
294
+ });
295
+
296
+ test('nao quebra com requisicao sem headers, sem corpo, ou sem argumento nenhum', () => {
297
+ assert.doesNotThrow(() => publicarEventoDaRequisicao({}, { tipo: 'meu_evento' }));
298
+ assert.doesNotThrow(() => publicarEventoDaRequisicao());
299
+ });
300
+
301
+ test('repassa as demais opcoes para o publicarEvento', async () => {
302
+ publicarEventoDaRequisicao(
303
+ { headers: { authorization: 'Bearer jwt' }, body: {} },
304
+ { tipo: 'meu_evento', url: 'http://outro.interno/eventos' }
305
+ );
306
+ await esperarMicrotasks();
307
+
308
+ assert.equal(chamadas[0].url, 'http://outro.interno/eventos');
309
+ });
310
+ });