@lexnode/legal-data-sdk 1.0.0 → 1.24.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.
@@ -0,0 +1,330 @@
1
+ /**
2
+ * Verificação e tipagem das entregas assinadas do motor.
3
+ *
4
+ * O motor assina cada entrega como `x-lexnode-signature: t=<unix>,v1=<hex>`,
5
+ * onde o HMAC-SHA256 cobre `${t}.${corpoCru}` com o segredo do endpoint. A
6
+ * conferência é sobre os BYTES CRUS recebidos: reserializar o JSON muda a
7
+ * ordem das chaves e o digest deixa de bater.
8
+ */
9
+ export declare const LEXNODE_WEBHOOK_HEADERS: {
10
+ readonly event: "x-lexnode-event";
11
+ readonly timestamp: "x-lexnode-timestamp";
12
+ readonly signature: "x-lexnode-signature";
13
+ readonly deliveryAttempt: "x-lexnode-delivery-attempt";
14
+ };
15
+ /** Janela de replay do motor. */
16
+ export declare const DEFAULT_WEBHOOK_TOLERANCE_SECONDS = 300;
17
+ /**
18
+ * Os BYTES como chegaram. Reserializar o JSON reordena as chaves e o digest
19
+ * deixa de bater, então um corpo já parseado é recusado em vez de conferido.
20
+ */
21
+ export type WebhookRawBody = string | Uint8Array;
22
+ export type WebhookRejection = "MISSING_SECRET" | "RAW_BODY_REQUIRED" | "MISSING_SIGNATURE" | "MALFORMED_SIGNATURE" | "TIMESTAMP_MISMATCH" | "TIMESTAMP_OUT_OF_TOLERANCE" | "SIGNATURE_MISMATCH";
23
+ export type WebhookVerification = {
24
+ ok: true;
25
+ timestamp: number;
26
+ } | {
27
+ ok: false;
28
+ reason: WebhookRejection;
29
+ };
30
+ export type WebhookParseRejection = WebhookRejection | "INVALID_JSON" | "INVALID_ENVELOPE";
31
+ /**
32
+ * O desfecho de `parseWebhookEvent`.
33
+ *
34
+ * `known` é o discriminador que separa o catálogo conhecido do resto. Ele mora
35
+ * no RESULTADO, e não no evento, por uma razão de tipo: um ramo `type: string`
36
+ * dentro da própria união apagaria o estreitamento por `type` de todos os
37
+ * outros — `payload` viraria `unknown` mesmo no caso reconhecido.
38
+ */
39
+ export type WebhookParseResult = {
40
+ ok: true;
41
+ known: true;
42
+ event: WebhookEvent;
43
+ } | {
44
+ ok: true;
45
+ known: false;
46
+ event: UnknownWebhookEvent;
47
+ } | {
48
+ ok: false;
49
+ reason: WebhookParseRejection;
50
+ };
51
+ /** Cabeçalhos como um `Headers` do fetch ou como o objeto cru de um framework. */
52
+ export type WebhookHeaders = Headers | Record<string, string | string[] | undefined>;
53
+ export interface VerifyWebhookOptions {
54
+ /** Janela de replay, em segundos. Padrão: 300, como o motor. */
55
+ toleranceSeconds?: number;
56
+ /** Relógio, em SEGUNDOS da época Unix — o mesmo eixo do `t` do cabeçalho. */
57
+ now?: number;
58
+ }
59
+ /**
60
+ * Confere a assinatura de uma entrega e diz POR QUE recusou.
61
+ *
62
+ * O veredito é motivado de propósito: um `false` nu manda o receptor devolver
63
+ * o mesmo erro para segredo trocado, relógio fora de hora e corpo adulterado —
64
+ * três incidentes com conduta diferente.
65
+ */
66
+ export declare function verifyWebhookSignature(rawBody: WebhookRawBody, headers: WebhookHeaders, secret: string, options?: VerifyWebhookOptions): WebhookVerification;
67
+ /** O envelope comum a toda entrega. */
68
+ export interface WebhookEventEnvelope {
69
+ id: string;
70
+ occurredAt: string;
71
+ projectId: string | null;
72
+ monitorId: string | null;
73
+ }
74
+ /** Sinal compacto de suspensão de prazo — nível, termos e trecho. */
75
+ export interface SuspensionSignal {
76
+ confidence: "HIGH" | "MEDIUM" | "LOW";
77
+ keywords: string[];
78
+ excerpt: string;
79
+ }
80
+ /** A data-limite do índice da fonte que qualifica um `total: 0`. */
81
+ export interface ProcessSourceIndex {
82
+ tribunalAlias: string;
83
+ updatedThrough: string | null;
84
+ checkedAt: string;
85
+ }
86
+ export interface ProcessMovementCreatedPayload {
87
+ canonicalProcessId: string;
88
+ tribunalAlias: string;
89
+ processNumber: string;
90
+ /** O ponteiro que o consumidor informou ao assinar o processo. */
91
+ externalRef: string | null;
92
+ newMovementCount: number;
93
+ latestMovementKey: string;
94
+ /**
95
+ * O que a movimentação mais recente É: o código da Tabela Processual
96
+ * Unificada e o nome do ato como a fonte o escreve. Nulos quando a fonte não
97
+ * os declara. É por eles que um aviso diz o que aconteceu sem reler a
98
+ * listagem de movimentações a cada evento.
99
+ */
100
+ latestMovementCode?: number | null;
101
+ latestMovementLabel?: string | null;
102
+ /**
103
+ * Presente quando o processo não é compartilhável: o aviso diz que ele andou
104
+ * e não diz o quê, porque ali o nome do ato é teor.
105
+ */
106
+ contentWithheld?: true;
107
+ }
108
+ export interface PublicationCreatedPayload {
109
+ monitorId: string;
110
+ officeId: string | null;
111
+ publicationKey: string;
112
+ processNumber: string;
113
+ formattedProcessNumber: string;
114
+ /** `null` quando o diário devolveu sigla que o catálogo do CNJ não cobre. */
115
+ tribunalAlias: string | null;
116
+ tribunalCode: string | null;
117
+ availableAt: string | null;
118
+ sourceUrl: string | null;
119
+ type: string;
120
+ /** Prévia do teor; `null` quando o gate reteve. */
121
+ excerpt: string | null;
122
+ suspensionSignal: SuspensionSignal | null;
123
+ /** Presente só quando o teor foi retido no instante do envio. */
124
+ contentWithheld?: true;
125
+ /** Só na vigília por termo. */
126
+ term?: string;
127
+ /** Só na vigília por nome de parte. */
128
+ partyName?: string;
129
+ partyRole?: string | null;
130
+ /** Só na vigília por CNPJ: o literal vigiado — o documento pontuado
131
+ * inteiro, ou a raiz `NN.NNN.NNN` quando `includeBranches`. */
132
+ partyDocument?: string;
133
+ includeBranches?: boolean;
134
+ /** `document`: o CNPJ consta no teor — a empresa foi citada no ato, seja
135
+ * ou não parte. */
136
+ matchedBy?: "document";
137
+ }
138
+ export interface MonitorSyncSucceededPayload {
139
+ monitorId: string;
140
+ officeId: string | null;
141
+ checkedAt: string;
142
+ newEvents: number;
143
+ /** `false` quando a passada não cobriu a janela inteira. */
144
+ complete: boolean;
145
+ /** Vigília de processo: se algo mudou desde a passada anterior. */
146
+ changed?: boolean;
147
+ processNumber?: string | null;
148
+ formattedProcessNumber?: string;
149
+ tribunalAlias?: string;
150
+ tribunalCode?: string;
151
+ snapshotHash?: string;
152
+ sourceUpdatedAt?: string | null;
153
+ latestMovementAt?: string | null;
154
+ /** O alvo que a fonte não indexava passou a existir nesta passada. */
155
+ firstAppearance?: true;
156
+ /** A vigília de aparição segue esperando o alvo entrar no índice. */
157
+ awaitingAppearance?: true;
158
+ indexUpdatedThrough?: string | null;
159
+ expectedFrom?: string | null;
160
+ /**
161
+ * Varredura de diário: os EXTREMOS do que a passada abriu de fato. `null`
162
+ * quando o ciclo não abriu fatia nenhuma — anunciar uma janela nesse caso
163
+ * seria dizer que leu o que não leu.
164
+ */
165
+ availableFrom?: string | null;
166
+ availableTo?: string | null;
167
+ daysSwept?: number;
168
+ pagesFetched?: number;
169
+ publicationsSeen?: number;
170
+ coveredThrough?: string;
171
+ /** Onde a próxima passada retoma: o dia e a página. `null` quando fechou. */
172
+ resumesAt?: {
173
+ day: string;
174
+ page: number;
175
+ } | null;
176
+ pendingDays?: number;
177
+ /**
178
+ * Fatias encerradas sem leitura completa, como chaves `AAAA-MM-DD#SIGLA`
179
+ * (sem a sigla no dia não dividido). `volumeCapped` é o dia grande demais,
180
+ * com cauda por ler; `refusedDays` é o que a fonte recusa em definitivo.
181
+ * Separá-los do dia lido por inteiro é a razão de os campos existirem.
182
+ */
183
+ volumeCapped?: string[];
184
+ refusedDays?: string[];
185
+ term?: string;
186
+ partyName?: string;
187
+ partyDocument?: string;
188
+ includeBranches?: boolean;
189
+ publicationsMatched?: number;
190
+ }
191
+ export interface MonitorSyncFailedPayload {
192
+ monitorId: string;
193
+ officeId: string | null;
194
+ error: string;
195
+ code: string;
196
+ class: "RETRYABLE" | "TERMINAL";
197
+ retryable: boolean;
198
+ /** O que o ciclo alcançou antes da recusa, quando houve ciclo. */
199
+ coverage?: Record<string, unknown>;
200
+ }
201
+ export interface SearchJobSucceededPayload {
202
+ jobId: string;
203
+ status: "SUCCEEDED";
204
+ kind: string;
205
+ officeId: string | null;
206
+ traceId: string | null;
207
+ total: number;
208
+ relation: "eq" | "gte";
209
+ sourceIndex?: ProcessSourceIndex;
210
+ processNumbers: string[];
211
+ pollUrl: string;
212
+ }
213
+ export interface SearchJobFailedPayload {
214
+ jobId: string;
215
+ status: "FAILED";
216
+ kind: string;
217
+ officeId: string | null;
218
+ traceId: string | null;
219
+ error: {
220
+ class: "RETRYABLE" | "TERMINAL";
221
+ code: string;
222
+ message: string;
223
+ retryable: boolean;
224
+ upstreamStatus?: number;
225
+ };
226
+ pollUrl: string;
227
+ }
228
+ export interface PublicationTextAlertMatch {
229
+ source: string;
230
+ publicationKey: string;
231
+ tribunalSigla: string | null;
232
+ publicationDate: string | null;
233
+ headline: string | null;
234
+ /** Presente só quando o teor deste item foi retido no envio. */
235
+ contentWithheld?: true;
236
+ }
237
+ export interface PublicationTextAlertMatchedPayload {
238
+ alertId: string;
239
+ externalRef: string | null;
240
+ queryHash: string;
241
+ /** O total real; `matches` é uma amostra. */
242
+ totalNew: number;
243
+ matches: PublicationTextAlertMatch[];
244
+ }
245
+ export interface PublicationHarvestCompletedPayload {
246
+ sliceId: string;
247
+ tribunalSigla: string;
248
+ harvestDate: string;
249
+ seedTerm: string | null;
250
+ itemsIndexable: number;
251
+ }
252
+ export interface QuotaLowPayload {
253
+ usedCredits: number;
254
+ limitCredits: number;
255
+ includedMonthlyCredits: number;
256
+ topupCredits: number;
257
+ remainingCredits: number;
258
+ thresholdPercent: number;
259
+ periodStart: string;
260
+ periodEnd: string;
261
+ }
262
+ export interface PeriodEndingPayload {
263
+ planCode: string;
264
+ periodEnd: string;
265
+ daysRemaining: number;
266
+ }
267
+ export interface SubscriptionPastDuePayload {
268
+ status: string;
269
+ planCode: string;
270
+ currentPeriodEnd: string | null;
271
+ }
272
+ /**
273
+ * A entrega sintética do teste de conectividade do endpoint. O motor a manda a
274
+ * pedido, com a mesma assinatura de um evento real.
275
+ */
276
+ export interface WebhookTestPayload {
277
+ message: string;
278
+ }
279
+ type Typed<T extends string, P> = WebhookEventEnvelope & {
280
+ type: T;
281
+ payload: P;
282
+ };
283
+ /** Os dez tipos do catálogo, discriminados por `type`. */
284
+ export type WebhookEvent = Typed<"PROCESS_MOVEMENT_CREATED", ProcessMovementCreatedPayload> | Typed<"PUBLICATION_CREATED", PublicationCreatedPayload> | Typed<"MONITOR_SYNC_SUCCEEDED", MonitorSyncSucceededPayload> | Typed<"MONITOR_SYNC_FAILED", MonitorSyncFailedPayload> | Typed<"SEARCH_JOB_COMPLETED", SearchJobSucceededPayload | SearchJobFailedPayload> | Typed<"PUBLICATION_TEXT_ALERT_MATCHED", PublicationTextAlertMatchedPayload> | Typed<"PUBLICATION_HARVEST_COMPLETED", PublicationHarvestCompletedPayload> | Typed<"QUOTA_LOW", QuotaLowPayload> | Typed<"PERIOD_ENDING", PeriodEndingPayload> | Typed<"SUBSCRIPTION_PAST_DUE", SubscriptionPastDuePayload> | Typed<"WEBHOOK_TEST", WebhookTestPayload>;
285
+ /**
286
+ * O evento que o catálogo ainda não descreve.
287
+ *
288
+ * Tipo novo é coisa a ignorar, nunca exceção: o receptor precisa responder 2xx
289
+ * para o motor não entrar na curva de reentrega por uma novidade inofensiva.
290
+ */
291
+ export type UnknownWebhookEvent = WebhookEventEnvelope & {
292
+ type: string;
293
+ payload: unknown;
294
+ };
295
+ export type WebhookEventType = WebhookEvent["type"];
296
+ /**
297
+ * A entrega de teste, que o endpoint recebe sem assinar.
298
+ *
299
+ * Ela é o PRIMEIRO corpo que um receptor novo vê — o botão de testar
300
+ * conectividade existe para ser apertado antes do primeiro evento real. Por
301
+ * isso está na união (quem faz `switch (event.type)` precisa enxergá-la) e
302
+ * fora do catálogo assinável (não se pede, não se recusa, chega a qualquer
303
+ * endpoint).
304
+ */
305
+ export declare const WEBHOOK_TEST_EVENT_TYPE: "WEBHOOK_TEST";
306
+ /** Os tipos que se pode ASSINAR ao cadastrar um endpoint. */
307
+ export declare const WEBHOOK_EVENT_TYPES: readonly ["PROCESS_MOVEMENT_CREATED", "PUBLICATION_CREATED", "MONITOR_SYNC_SUCCEEDED", "MONITOR_SYNC_FAILED", "SEARCH_JOB_COMPLETED", "PUBLICATION_TEXT_ALERT_MATCHED", "PUBLICATION_HARVEST_COMPLETED", "QUOTA_LOW", "PERIOD_ENDING", "SUBSCRIPTION_PAST_DUE"];
308
+ /**
309
+ * O que entra em `eventTypes` ao cadastrar um endpoint. É subconjunto próprio
310
+ * de `WebhookEventType`: mandar a entrega de teste nessa lista é recusa.
311
+ */
312
+ export type SubscribableWebhookEventType = (typeof WEBHOOK_EVENT_TYPES)[number];
313
+ /** Se o tipo está no catálogo que este pacote descreve. */
314
+ export declare function isKnownWebhookEventType(type: string): type is WebhookEventType;
315
+ /** Se este evento é a entrega de teste, e não um fato do acervo. */
316
+ export declare function isWebhookTest(event: WebhookEvent): event is Extract<WebhookEvent, {
317
+ type: "WEBHOOK_TEST";
318
+ }>;
319
+ export declare function webhookRejectionStatus(reason: WebhookParseRejection): number;
320
+ /** Se a recusa é de responsabilidade do receptor, e o remetente deve voltar. */
321
+ export declare function isReceiverFault(reason: WebhookParseRejection): boolean;
322
+ /**
323
+ * Confere a assinatura e devolve o evento tipado.
324
+ *
325
+ * A assinatura prova a ORIGEM; a conferência de forma que vem depois prova que
326
+ * o corpo é um envelope de evento — sem ela, um corpo assinado mas malformado
327
+ * viraria um `as` silencioso dentro do consumidor.
328
+ */
329
+ export declare function parseWebhookEvent(rawBody: WebhookRawBody, headers: WebhookHeaders, secret: string, options?: VerifyWebhookOptions): WebhookParseResult;
330
+ export {};
@@ -0,0 +1,223 @@
1
+ import { createHmac, timingSafeEqual } from "node:crypto";
2
+ /**
3
+ * Verificação e tipagem das entregas assinadas do motor.
4
+ *
5
+ * O motor assina cada entrega como `x-lexnode-signature: t=<unix>,v1=<hex>`,
6
+ * onde o HMAC-SHA256 cobre `${t}.${corpoCru}` com o segredo do endpoint. A
7
+ * conferência é sobre os BYTES CRUS recebidos: reserializar o JSON muda a
8
+ * ordem das chaves e o digest deixa de bater.
9
+ */
10
+ export const LEXNODE_WEBHOOK_HEADERS = {
11
+ event: "x-lexnode-event",
12
+ timestamp: "x-lexnode-timestamp",
13
+ signature: "x-lexnode-signature",
14
+ deliveryAttempt: "x-lexnode-delivery-attempt",
15
+ };
16
+ /** Janela de replay do motor. */
17
+ export const DEFAULT_WEBHOOK_TOLERANCE_SECONDS = 300;
18
+ /**
19
+ * A forma exata do digest que o motor emite: 64 dígitos hex minúsculos.
20
+ *
21
+ * Conferir a forma ANTES de decodificar é o que barra o cabeçalho maleável:
22
+ * `Buffer.from(v1, "hex")` para no primeiro caractere inválido, então um
23
+ * digest válido com lixo colado no fim decodifica nos mesmos bytes e passaria
24
+ * pela guarda de tamanho — a mesma entrega verificaria quantas vezes o
25
+ * remetente quisesse, com cabeçalhos diferentes a cada vez.
26
+ */
27
+ const V1_DIGEST = /^[0-9a-f]{64}$/u;
28
+ function isRawBody(value) {
29
+ return typeof value === "string" || value instanceof Uint8Array;
30
+ }
31
+ function readHeader(headers, name) {
32
+ if (typeof headers.get === "function") {
33
+ return headers.get(name);
34
+ }
35
+ const bag = headers;
36
+ const alvo = name.toLowerCase();
37
+ const encontrado = Object.entries(bag).find(([key]) => key.toLowerCase() === alvo);
38
+ if (!encontrado)
39
+ return null;
40
+ const raw = Array.isArray(encontrado[1]) ? encontrado[1][0] : encontrado[1];
41
+ return raw ?? null;
42
+ }
43
+ function parseSignatureHeader(header) {
44
+ const out = {};
45
+ for (const part of header.split(",")) {
46
+ const separator = part.indexOf("=");
47
+ if (separator !== -1) {
48
+ const key = part.slice(0, separator).trim();
49
+ const value = part.slice(separator + 1).trim();
50
+ if (key === "t")
51
+ out.t = value;
52
+ else if (key === "v1")
53
+ out.v1 = value;
54
+ }
55
+ }
56
+ return out;
57
+ }
58
+ /**
59
+ * Confere a assinatura de uma entrega e diz POR QUE recusou.
60
+ *
61
+ * O veredito é motivado de propósito: um `false` nu manda o receptor devolver
62
+ * o mesmo erro para segredo trocado, relógio fora de hora e corpo adulterado —
63
+ * três incidentes com conduta diferente.
64
+ */
65
+ export function verifyWebhookSignature(rawBody, headers, secret, options = {}) {
66
+ if (!secret)
67
+ return { ok: false, reason: "MISSING_SECRET" };
68
+ if (!isRawBody(rawBody))
69
+ return { ok: false, reason: "RAW_BODY_REQUIRED" };
70
+ const signatureHeader = readHeader(headers, LEXNODE_WEBHOOK_HEADERS.signature);
71
+ if (!signatureHeader)
72
+ return { ok: false, reason: "MISSING_SIGNATURE" };
73
+ const { t, v1 } = parseSignatureHeader(signatureHeader);
74
+ if (!t || !v1 || !V1_DIGEST.test(v1)) {
75
+ return { ok: false, reason: "MALFORMED_SIGNATURE" };
76
+ }
77
+ const timestamp = Number(t);
78
+ if (!Number.isFinite(timestamp)) {
79
+ return { ok: false, reason: "MALFORMED_SIGNATURE" };
80
+ }
81
+ // O cabeçalho de timestamp é redundante com o `t` da assinatura — e é por
82
+ // isso que ele vale: divergir os dois é sinal de corpo remontado no caminho.
83
+ const timestampHeader = readHeader(headers, LEXNODE_WEBHOOK_HEADERS.timestamp);
84
+ if (timestampHeader !== null && timestampHeader.trim() !== t) {
85
+ return { ok: false, reason: "TIMESTAMP_MISMATCH" };
86
+ }
87
+ const tolerance = options.toleranceSeconds ?? DEFAULT_WEBHOOK_TOLERANCE_SECONDS;
88
+ const now = options.now ?? Math.floor(Date.now() / 1000);
89
+ if (Math.abs(now - timestamp) > tolerance) {
90
+ return { ok: false, reason: "TIMESTAMP_OUT_OF_TOLERANCE" };
91
+ }
92
+ // O prefixo e o corpo entram separados: o corpo em bytes é assinado como
93
+ // veio, sem passar por uma conversão de texto que possa alterá-lo.
94
+ const mac = createHmac("sha256", secret).update(`${t}.`, "utf8");
95
+ const expected = (typeof rawBody === "string"
96
+ ? mac.update(rawBody, "utf8")
97
+ : mac.update(rawBody)).digest();
98
+ const provided = Buffer.from(v1, "hex");
99
+ if (provided.length !== expected.length) {
100
+ return { ok: false, reason: "SIGNATURE_MISMATCH" };
101
+ }
102
+ if (!timingSafeEqual(provided, expected)) {
103
+ return { ok: false, reason: "SIGNATURE_MISMATCH" };
104
+ }
105
+ return { ok: true, timestamp };
106
+ }
107
+ /**
108
+ * A entrega de teste, que o endpoint recebe sem assinar.
109
+ *
110
+ * Ela é o PRIMEIRO corpo que um receptor novo vê — o botão de testar
111
+ * conectividade existe para ser apertado antes do primeiro evento real. Por
112
+ * isso está na união (quem faz `switch (event.type)` precisa enxergá-la) e
113
+ * fora do catálogo assinável (não se pede, não se recusa, chega a qualquer
114
+ * endpoint).
115
+ */
116
+ export const WEBHOOK_TEST_EVENT_TYPE = "WEBHOOK_TEST";
117
+ /** Os tipos que se pode ASSINAR ao cadastrar um endpoint. */
118
+ export const WEBHOOK_EVENT_TYPES = [
119
+ "PROCESS_MOVEMENT_CREATED",
120
+ "PUBLICATION_CREATED",
121
+ "MONITOR_SYNC_SUCCEEDED",
122
+ "MONITOR_SYNC_FAILED",
123
+ "SEARCH_JOB_COMPLETED",
124
+ "PUBLICATION_TEXT_ALERT_MATCHED",
125
+ "PUBLICATION_HARVEST_COMPLETED",
126
+ "QUOTA_LOW",
127
+ "PERIOD_ENDING",
128
+ "SUBSCRIPTION_PAST_DUE",
129
+ ];
130
+ const KNOWN_EVENT_TYPES = new Set([
131
+ ...WEBHOOK_EVENT_TYPES,
132
+ WEBHOOK_TEST_EVENT_TYPE,
133
+ ]);
134
+ /** Se o tipo está no catálogo que este pacote descreve. */
135
+ export function isKnownWebhookEventType(type) {
136
+ return KNOWN_EVENT_TYPES.has(type);
137
+ }
138
+ /** Se este evento é a entrega de teste, e não um fato do acervo. */
139
+ export function isWebhookTest(event) {
140
+ return event.type === WEBHOOK_TEST_EVENT_TYPE;
141
+ }
142
+ /**
143
+ * O status HTTP que cada recusa merece.
144
+ *
145
+ * A distinção que decide tudo é: o remetente deve voltar? Um corpo que não se
146
+ * autentica nunca passará a se autenticar, e insistir só queima a curva de
147
+ * reentrega — recusa definitiva. Já o segredo ausente é indisponibilidade
148
+ * NOSSA: responder 4xx ali faz o motor desistir de uma entrega que a nossa
149
+ * própria configuração derrubou, e o evento se perde para sempre.
150
+ *
151
+ * A regra é a mesma em todo receptor, então mora aqui em vez de ser reinventada
152
+ * — com esse erro — em cada integração.
153
+ */
154
+ const REJECTION_STATUS = {
155
+ MISSING_SECRET: 503,
156
+ RAW_BODY_REQUIRED: 500,
157
+ MISSING_SIGNATURE: 401,
158
+ MALFORMED_SIGNATURE: 401,
159
+ SIGNATURE_MISMATCH: 401,
160
+ TIMESTAMP_MISMATCH: 401,
161
+ TIMESTAMP_OUT_OF_TOLERANCE: 401,
162
+ INVALID_JSON: 400,
163
+ INVALID_ENVELOPE: 400,
164
+ };
165
+ export function webhookRejectionStatus(reason) {
166
+ return REJECTION_STATUS[reason];
167
+ }
168
+ /** Se a recusa é de responsabilidade do receptor, e o remetente deve voltar. */
169
+ export function isReceiverFault(reason) {
170
+ return REJECTION_STATUS[reason] >= 500;
171
+ }
172
+ /**
173
+ * Confere a assinatura e devolve o evento tipado.
174
+ *
175
+ * A assinatura prova a ORIGEM; a conferência de forma que vem depois prova que
176
+ * o corpo é um envelope de evento — sem ela, um corpo assinado mas malformado
177
+ * viraria um `as` silencioso dentro do consumidor.
178
+ */
179
+ export function parseWebhookEvent(rawBody, headers, secret, options = {}) {
180
+ const verification = verifyWebhookSignature(rawBody, headers, secret, options);
181
+ if (!verification.ok)
182
+ return verification;
183
+ let parsed;
184
+ try {
185
+ parsed = JSON.parse(typeof rawBody === "string"
186
+ ? rawBody
187
+ : Buffer.from(rawBody).toString("utf8"));
188
+ }
189
+ catch {
190
+ return { ok: false, reason: "INVALID_JSON" };
191
+ }
192
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
193
+ return { ok: false, reason: "INVALID_ENVELOPE" };
194
+ }
195
+ const body = parsed;
196
+ if (typeof body.type !== "string" || !body.type) {
197
+ return { ok: false, reason: "INVALID_ENVELOPE" };
198
+ }
199
+ if (!body.payload || typeof body.payload !== "object") {
200
+ return { ok: false, reason: "INVALID_ENVELOPE" };
201
+ }
202
+ // O id é a chave de idempotência de todo receptor: uma entrega repetida é
203
+ // normal, e é por ele que ela se reconhece. Aceitá-lo vazio faria a primeira
204
+ // entrega sem id ocupar a única vaga de deduplicação e todas as seguintes
205
+ // parecerem repetição dela — um evento por conta, e o resto em silêncio.
206
+ if (typeof body.id !== "string" || !body.id) {
207
+ return { ok: false, reason: "INVALID_ENVELOPE" };
208
+ }
209
+ if (typeof body.occurredAt !== "string" || !body.occurredAt) {
210
+ return { ok: false, reason: "INVALID_ENVELOPE" };
211
+ }
212
+ const event = {
213
+ id: body.id,
214
+ type: body.type,
215
+ occurredAt: body.occurredAt,
216
+ projectId: typeof body.projectId === "string" ? body.projectId : null,
217
+ monitorId: typeof body.monitorId === "string" ? body.monitorId : null,
218
+ payload: body.payload,
219
+ };
220
+ return isKnownWebhookEventType(event.type)
221
+ ? { ok: true, known: true, event: event }
222
+ : { ok: true, known: false, event };
223
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lexnode/legal-data-sdk",
3
- "version": "1.0.0",
4
- "description": "TypeScript types for the LexNode legal-data engine: a generic B2B API over Brazilian court process, movement and publication data.",
3
+ "version": "1.24.0",
4
+ "description": "Cliente TypeScript do LexNode, o motor de dados jur\u00eddicos do Brasil: transporte tipado, erros classificados, verifica\u00e7\u00e3o de webhook e os tipos gerados do contrato OpenAPI.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://www.lexnode.com.br/docs",
7
7
  "keywords": [
@@ -11,16 +11,40 @@
11
11
  "cnj",
12
12
  "djen",
13
13
  "openapi",
14
- "types",
14
+ "sdk",
15
+ "client",
16
+ "webhooks",
15
17
  "typescript"
16
18
  ],
17
19
  "private": false,
18
- "types": "schema.d.ts",
20
+ "type": "module",
21
+ "engines": {
22
+ "node": ">=20"
23
+ },
24
+ "main": "./dist/index.js",
25
+ "types": "./dist/index.d.ts",
26
+ "exports": {
27
+ ".": {
28
+ "types": "./dist/index.d.ts",
29
+ "import": "./dist/index.js",
30
+ "default": "./dist/index.js"
31
+ },
32
+ "./package.json": "./package.json"
33
+ },
19
34
  "files": [
35
+ "dist",
20
36
  "schema.d.ts",
21
37
  "README.md",
22
38
  "LICENSE"
23
39
  ],
40
+ "sideEffects": false,
41
+ "scripts": {
42
+ "build": "tsc -p tsconfig.json",
43
+ "prepack": "tsc -p tsconfig.json"
44
+ },
45
+ "devDependencies": {
46
+ "vitest": "^4.1.9"
47
+ },
24
48
  "publishConfig": {
25
49
  "access": "public"
26
50
  }