@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.
- package/README.md +385 -11
- package/dist/client.d.ts +100 -0
- package/dist/client.js +476 -0
- package/dist/errors.d.ts +94 -0
- package/dist/errors.js +217 -0
- package/dist/freshness.d.ts +48 -0
- package/dist/freshness.js +105 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +17 -0
- package/dist/paginate.d.ts +21 -0
- package/dist/paginate.js +106 -0
- package/dist/search.d.ts +64 -0
- package/dist/search.js +75 -0
- package/dist/types.d.ts +182 -0
- package/dist/types.js +1 -0
- package/dist/watch.d.ts +93 -0
- package/dist/watch.js +95 -0
- package/dist/webhooks.d.ts +330 -0
- package/dist/webhooks.js +223 -0
- package/package.json +28 -4
- package/schema.d.ts +972 -95
|
@@ -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 {};
|
package/dist/webhooks.js
ADDED
|
@@ -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.
|
|
4
|
-
"description": "TypeScript
|
|
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
|
-
"
|
|
14
|
+
"sdk",
|
|
15
|
+
"client",
|
|
16
|
+
"webhooks",
|
|
15
17
|
"typescript"
|
|
16
18
|
],
|
|
17
19
|
"private": false,
|
|
18
|
-
"
|
|
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
|
}
|