@open-apime/sdk 0.3.1 → 0.4.1
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 +2 -2
- package/dist/index.cjs +29 -0
- package/dist/index.d.cts +13 -1
- package/dist/index.d.ts +13 -1
- package/dist/index.js +27 -0
- package/docs/guia.md +211 -0
- package/docs/seguranca.md +13 -0
- package/package.json +2 -9
package/README.md
CHANGED
|
@@ -32,8 +32,8 @@ await conexao.messages.sendText(
|
|
|
32
32
|
|
|
33
33
|
| | |
|
|
34
34
|
|---|---|
|
|
35
|
-
| [Guia de uso](
|
|
36
|
-
| [Segurança](
|
|
35
|
+
| [Guia de uso](docs/guia.md) | clientes, envio, erros, retry, observabilidade e webhook |
|
|
36
|
+
| [Segurança](docs/seguranca.md) | o que o SDK protege, e como |
|
|
37
37
|
| [openapi.yaml](https://github.com/open-apime/apime/blob/main/openapi.yaml) | contrato da API |
|
|
38
38
|
| [webhook-payloads.md](https://github.com/open-apime/apime/blob/main/docs/webhook-payloads.md) | os 12 eventos |
|
|
39
39
|
|
package/dist/index.cjs
CHANGED
|
@@ -29,6 +29,7 @@ __export(index_exports, {
|
|
|
29
29
|
ConfigurationError: () => ConfigurationError,
|
|
30
30
|
ConflictError: () => ConflictError,
|
|
31
31
|
ConnectionError: () => ConnectionError,
|
|
32
|
+
DEFAULT_HEALTH_TIMEOUT_MS: () => DEFAULT_HEALTH_TIMEOUT_MS,
|
|
32
33
|
InvalidRequestError: () => InvalidRequestError,
|
|
33
34
|
InvalidSignatureError: () => InvalidSignatureError,
|
|
34
35
|
NotFoundError: () => NotFoundError,
|
|
@@ -37,6 +38,7 @@ __export(index_exports, {
|
|
|
37
38
|
SessionUnavailableError: () => SessionUnavailableError,
|
|
38
39
|
TimeoutError: () => TimeoutError,
|
|
39
40
|
UnprocessableError: () => UnprocessableError,
|
|
41
|
+
checkHealth: () => checkHealth,
|
|
40
42
|
constructEvent: () => constructEvent,
|
|
41
43
|
hasSentry: () => hasSentry,
|
|
42
44
|
verifyWebhookSignature: () => verifyWebhookSignature
|
|
@@ -785,6 +787,31 @@ var Apime = {
|
|
|
785
787
|
}
|
|
786
788
|
};
|
|
787
789
|
|
|
790
|
+
// src/health.ts
|
|
791
|
+
var DEFAULT_HEALTH_TIMEOUT_MS = 1e4;
|
|
792
|
+
async function checkHealth(baseUrl, options = {}) {
|
|
793
|
+
const url = buildUrl(assertUsableBaseUrl(baseUrl), "/healthz");
|
|
794
|
+
const impl = options.fetch ?? globalThis.fetch;
|
|
795
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_HEALTH_TIMEOUT_MS;
|
|
796
|
+
const controller = new AbortController();
|
|
797
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
798
|
+
try {
|
|
799
|
+
const response = await impl(url, { method: "GET", signal: controller.signal });
|
|
800
|
+
if (!response.ok) {
|
|
801
|
+
throw new ApimeError(`apime respondeu ${response.status} no healthz`, { status: response.status });
|
|
802
|
+
}
|
|
803
|
+
return await response.json();
|
|
804
|
+
} catch (cause) {
|
|
805
|
+
if (cause instanceof ApimeError) throw cause;
|
|
806
|
+
if (controller.signal.aborted) {
|
|
807
|
+
throw new TimeoutError(`o healthz passou de ${timeoutMs}ms`, { cause });
|
|
808
|
+
}
|
|
809
|
+
throw new ConnectionError("falha de rede ao falar com o apime", { cause });
|
|
810
|
+
} finally {
|
|
811
|
+
clearTimeout(timer);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
788
815
|
// src/webhook/index.ts
|
|
789
816
|
var import_node_crypto = require("crypto");
|
|
790
817
|
var InvalidSignatureError = class extends ApimeError {
|
|
@@ -822,6 +849,7 @@ function constructEvent(rawBody, signature, secret) {
|
|
|
822
849
|
ConfigurationError,
|
|
823
850
|
ConflictError,
|
|
824
851
|
ConnectionError,
|
|
852
|
+
DEFAULT_HEALTH_TIMEOUT_MS,
|
|
825
853
|
InvalidRequestError,
|
|
826
854
|
InvalidSignatureError,
|
|
827
855
|
NotFoundError,
|
|
@@ -830,6 +858,7 @@ function constructEvent(rawBody, signature, secret) {
|
|
|
830
858
|
SessionUnavailableError,
|
|
831
859
|
TimeoutError,
|
|
832
860
|
UnprocessableError,
|
|
861
|
+
checkHealth,
|
|
833
862
|
constructEvent,
|
|
834
863
|
hasSentry,
|
|
835
864
|
verifyWebhookSignature
|
package/dist/index.d.cts
CHANGED
|
@@ -523,4 +523,16 @@ declare const Apime: {
|
|
|
523
523
|
}, options: ClientOptions): ApimeInstanceClient;
|
|
524
524
|
};
|
|
525
525
|
|
|
526
|
-
|
|
526
|
+
interface HealthOptions {
|
|
527
|
+
/** Kept short on purpose: a health check that hangs is worse than one that fails. */
|
|
528
|
+
timeoutMs?: number;
|
|
529
|
+
fetch?: typeof globalThis.fetch;
|
|
530
|
+
}
|
|
531
|
+
interface HealthResult {
|
|
532
|
+
status?: string;
|
|
533
|
+
[key: string]: unknown;
|
|
534
|
+
}
|
|
535
|
+
declare const DEFAULT_HEALTH_TIMEOUT_MS = 10000;
|
|
536
|
+
declare function checkHealth(baseUrl: string, options?: HealthOptions): Promise<HealthResult>;
|
|
537
|
+
|
|
538
|
+
export { type ApiToken, type ApiTokenAuth, Apime, ApimeError, ApimeInstanceClient, ApimeUserClient, type Auth, type AuthKind, type CheckNumberResult, type ClientOptions, type ContactEntry, type CreateInstanceInput, DEFAULT_HEALTH_TIMEOUT_MS, EventLogEntry, type FailureInfo, type GroupInfo, type GroupParticipant, type HealthOptions, type HealthResult, Instance, InstanceInfo, type InstanceTokenAuth, type IpFamily, type JoinRequestAction, type MarkReadInput, type Observer, type ParticipantAction, type PresenceState, Profile, QrCode, type QuoteInput, type RequestInfo, type RequestOptions, type SendAudioInput, type SendContactInput, type SendDocumentInput, type SendLocationInput, type SendMediaInput, type SendTextInput, SentMessage, type SuccessInfo, type UpdateInstanceInput, type User, type UserAuth, type UserJwtAuth, checkHealth, hasSentry };
|
package/dist/index.d.ts
CHANGED
|
@@ -523,4 +523,16 @@ declare const Apime: {
|
|
|
523
523
|
}, options: ClientOptions): ApimeInstanceClient;
|
|
524
524
|
};
|
|
525
525
|
|
|
526
|
-
|
|
526
|
+
interface HealthOptions {
|
|
527
|
+
/** Kept short on purpose: a health check that hangs is worse than one that fails. */
|
|
528
|
+
timeoutMs?: number;
|
|
529
|
+
fetch?: typeof globalThis.fetch;
|
|
530
|
+
}
|
|
531
|
+
interface HealthResult {
|
|
532
|
+
status?: string;
|
|
533
|
+
[key: string]: unknown;
|
|
534
|
+
}
|
|
535
|
+
declare const DEFAULT_HEALTH_TIMEOUT_MS = 10000;
|
|
536
|
+
declare function checkHealth(baseUrl: string, options?: HealthOptions): Promise<HealthResult>;
|
|
537
|
+
|
|
538
|
+
export { type ApiToken, type ApiTokenAuth, Apime, ApimeError, ApimeInstanceClient, ApimeUserClient, type Auth, type AuthKind, type CheckNumberResult, type ClientOptions, type ContactEntry, type CreateInstanceInput, DEFAULT_HEALTH_TIMEOUT_MS, EventLogEntry, type FailureInfo, type GroupInfo, type GroupParticipant, type HealthOptions, type HealthResult, Instance, InstanceInfo, type InstanceTokenAuth, type IpFamily, type JoinRequestAction, type MarkReadInput, type Observer, type ParticipantAction, type PresenceState, Profile, QrCode, type QuoteInput, type RequestInfo, type RequestOptions, type SendAudioInput, type SendContactInput, type SendDocumentInput, type SendLocationInput, type SendMediaInput, type SendTextInput, SentMessage, type SuccessInfo, type UpdateInstanceInput, type User, type UserAuth, type UserJwtAuth, checkHealth, hasSentry };
|
package/dist/index.js
CHANGED
|
@@ -715,6 +715,31 @@ var Apime = {
|
|
|
715
715
|
);
|
|
716
716
|
}
|
|
717
717
|
};
|
|
718
|
+
|
|
719
|
+
// src/health.ts
|
|
720
|
+
var DEFAULT_HEALTH_TIMEOUT_MS = 1e4;
|
|
721
|
+
async function checkHealth(baseUrl, options = {}) {
|
|
722
|
+
const url = buildUrl(assertUsableBaseUrl(baseUrl), "/healthz");
|
|
723
|
+
const impl = options.fetch ?? globalThis.fetch;
|
|
724
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_HEALTH_TIMEOUT_MS;
|
|
725
|
+
const controller = new AbortController();
|
|
726
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
727
|
+
try {
|
|
728
|
+
const response = await impl(url, { method: "GET", signal: controller.signal });
|
|
729
|
+
if (!response.ok) {
|
|
730
|
+
throw new ApimeError(`apime respondeu ${response.status} no healthz`, { status: response.status });
|
|
731
|
+
}
|
|
732
|
+
return await response.json();
|
|
733
|
+
} catch (cause) {
|
|
734
|
+
if (cause instanceof ApimeError) throw cause;
|
|
735
|
+
if (controller.signal.aborted) {
|
|
736
|
+
throw new TimeoutError(`o healthz passou de ${timeoutMs}ms`, { cause });
|
|
737
|
+
}
|
|
738
|
+
throw new ConnectionError("falha de rede ao falar com o apime", { cause });
|
|
739
|
+
} finally {
|
|
740
|
+
clearTimeout(timer);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
718
743
|
export {
|
|
719
744
|
ApiError,
|
|
720
745
|
Apime,
|
|
@@ -725,6 +750,7 @@ export {
|
|
|
725
750
|
ConfigurationError,
|
|
726
751
|
ConflictError,
|
|
727
752
|
ConnectionError,
|
|
753
|
+
DEFAULT_HEALTH_TIMEOUT_MS,
|
|
728
754
|
InvalidRequestError,
|
|
729
755
|
InvalidSignatureError,
|
|
730
756
|
NotFoundError,
|
|
@@ -733,6 +759,7 @@ export {
|
|
|
733
759
|
SessionUnavailableError,
|
|
734
760
|
TimeoutError,
|
|
735
761
|
UnprocessableError,
|
|
762
|
+
checkHealth,
|
|
736
763
|
constructEvent,
|
|
737
764
|
hasSentry,
|
|
738
765
|
verifyWebhookSignature
|
package/docs/guia.md
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# Guia do `@open-apime/sdk`
|
|
2
|
+
|
|
3
|
+
Referência de uso. O [README](../README.md) tem o resumo e a instalação.
|
|
4
|
+
|
|
5
|
+
## Os três tokens decidem o que você pode chamar
|
|
6
|
+
|
|
7
|
+
A API do apime não é uniforme: 46 rotas só aceitam token de instância, 13 só aceitam token de
|
|
8
|
+
usuário. Por isso o SDK tem **dois clientes**, e o que você constrói define o que o compilador
|
|
9
|
+
deixa chamar. Sem isso, o erro só apareceria como `403` em produção.
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { Apime } from "@open-apime/sdk";
|
|
13
|
+
|
|
14
|
+
// token de usuário: administra instâncias
|
|
15
|
+
const admin = Apime.withApiToken(process.env.APIME_TOKEN!, {
|
|
16
|
+
baseUrl: "https://apime.example.com",
|
|
17
|
+
});
|
|
18
|
+
await admin.instances.list();
|
|
19
|
+
|
|
20
|
+
// token de instância: opera UMA instância, e o id vem da credencial
|
|
21
|
+
const conexao = Apime.withInstanceToken(
|
|
22
|
+
{ token: process.env.INSTANCE_TOKEN!, instanceId: "abc-123" },
|
|
23
|
+
{ baseUrl: "https://apime.example.com" },
|
|
24
|
+
);
|
|
25
|
+
await conexao.instance.info();
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`admin.instance` não existe, e `conexao.instances.create()` também não. É erro de compilação, não
|
|
29
|
+
de runtime.
|
|
30
|
+
|
|
31
|
+
### O que cada cliente tem
|
|
32
|
+
|
|
33
|
+
| Cliente | Recursos |
|
|
34
|
+
|---|---|
|
|
35
|
+
| `withApiToken` / `withUserJwt` | `instances`, `users` (exige admin), `tokens` |
|
|
36
|
+
| `withInstanceToken` | `instance`, `messages`, `whatsapp` (com `.groups` e `.newsletters`) |
|
|
37
|
+
|
|
38
|
+
## Enviando mensagem
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
await conexao.messages.sendText(
|
|
42
|
+
{ to: "5511999999999", text: "Olá" },
|
|
43
|
+
{ idempotencyKey: mensagem.id },
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
await conexao.messages.sendMedia(
|
|
47
|
+
{ to: "5511999999999", type: "image", file: blob, caption: "Segue" },
|
|
48
|
+
{ idempotencyKey: mensagem.id },
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
await conexao.whatsapp.groups.updateParticipants(grupoJid, {
|
|
52
|
+
action: "add",
|
|
53
|
+
participants: ["5511999999999@s.whatsapp.net"],
|
|
54
|
+
});
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Erros
|
|
58
|
+
|
|
59
|
+
Uma classe por condição, então dá para ramificar sem ler status code:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import { PermissionError, RateLimitError, SessionUnavailableError } from "@open-apime/sdk";
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
await conexao.instance.info();
|
|
66
|
+
} catch (erro) {
|
|
67
|
+
if (erro instanceof SessionUnavailableError) {/* sessão não pronta, pode repetir */}
|
|
68
|
+
if (erro instanceof RateLimitError) {/* erro.retryAfterMs */}
|
|
69
|
+
if (erro instanceof PermissionError) {/* token errado para esta rota */}
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Retry: por que o envio não retenta sozinho
|
|
74
|
+
|
|
75
|
+
Leitura retenta com backoff exponencial e jitter. **Escrita não**, a menos que você passe uma
|
|
76
|
+
`Idempotency-Key`, porque retentar um envio sem chave entrega a mesma mensagem duas vezes ao
|
|
77
|
+
cliente final.
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
await conexao.request({
|
|
81
|
+
method: "POST",
|
|
82
|
+
path: `/instances/${conexao.instanceId}/messages/text`,
|
|
83
|
+
body: { to: "5511999999999", text: "Olá" },
|
|
84
|
+
// a MESMA chave na retentativa. Um id estável da sua mensagem serve melhor que um uuid novo.
|
|
85
|
+
options: { idempotencyKey: mensagem.id },
|
|
86
|
+
});
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
A exceção é `503`: o apime devolve isso quando a sessão não está pronta, antes de tocar o WhatsApp,
|
|
90
|
+
então nada saiu e o SDK repete mesmo sem chave.
|
|
91
|
+
|
|
92
|
+
`4xx` (fora `408` e `429`) nunca retenta, porque tentar de novo não muda o resultado.
|
|
93
|
+
|
|
94
|
+
## Observabilidade: vigiando o próprio SDK
|
|
95
|
+
|
|
96
|
+
O SDK **não importa Sentry, nem OpenTelemetry, nem nada**, e mantém zero dependência de runtime.
|
|
97
|
+
|
|
98
|
+
**Rodando com Sentry já iniciado no projeto, a falha definitiva é reportada sozinha.** O SDK acha o
|
|
99
|
+
cliente global do `@sentry/node` sem importar o pacote, então não pina versão nem obriga ninguém.
|
|
100
|
+
**Sem Sentry, não faz nada e não quebra.** Para calar, `autoReport: false`.
|
|
101
|
+
|
|
102
|
+
Querendo controlar o que é reportado, o `observer` tem prioridade sobre o automático:
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
import * as Sentry from "@sentry/node";
|
|
106
|
+
|
|
107
|
+
const conexao = Apime.withInstanceToken(credenciais, {
|
|
108
|
+
baseUrl,
|
|
109
|
+
appName: "meu-app",
|
|
110
|
+
observer: {
|
|
111
|
+
// Dispara em toda tentativa falha, inclusive as que serão repetidas.
|
|
112
|
+
onError: ({ error, route, method, status, instanceId, attempt, willRetry, idempotencyKey }) => {
|
|
113
|
+
if (willRetry) return; // ainda vai tentar de novo, não é falha definitiva
|
|
114
|
+
Sentry.captureException(error, {
|
|
115
|
+
tags: { sdk: "open-apime", route, method, status: String(status) },
|
|
116
|
+
extra: { instanceId, attempt, idempotencyKey },
|
|
117
|
+
});
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
A `route` vem **normalizada** (`/instances/{id}/profile/{jid}`), e não com os ids reais. Sem isso o
|
|
124
|
+
error tracker criaria uma issue por instância e por contato, e o volume esconderia o problema.
|
|
125
|
+
|
|
126
|
+
Os quatro ganchos: `onRequest`, `onSuccess`, `onError` e `onRetry`. Um observer que lança exceção
|
|
127
|
+
**não derruba a requisição**: observabilidade nunca é motivo para perder um envio.
|
|
128
|
+
|
|
129
|
+
## Health
|
|
130
|
+
|
|
131
|
+
`/healthz` não pede credencial, então é função avulsa, e não método de cliente: exigir token para
|
|
132
|
+
perguntar se o servidor está de pé seria ao contrário.
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
import { checkHealth } from "@open-apime/sdk";
|
|
136
|
+
|
|
137
|
+
const health = await checkHealth("https://apime.example.com"); // { status: "ok", version, name }
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Passa pelo mesmo guard anti-SSRF do resto, e o timeout é curto (10s), porque health que pendura é
|
|
141
|
+
pior que health que falha.
|
|
142
|
+
|
|
143
|
+
## Rede: forçar IPv4
|
|
144
|
+
|
|
145
|
+
Host atrás de Cloudflare responde A e AAAA. Onde a rede não tem rota IPv6, o Node tenta os
|
|
146
|
+
endereços v6 primeiro e colhe `ENETUNREACH` em cada um antes de chegar ao v4. Nada falha, mas o
|
|
147
|
+
ruído acaba gravado no erro da mensagem que estava sendo enviada.
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
const conexao = Apime.withInstanceToken(credenciais, { baseUrl, ipFamily: 4 });
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
**Vale para o processo inteiro**, porque o Node não oferece esse ajuste por requisição: é o
|
|
154
|
+
`dns.setDefaultResultOrder`. Por isso o SDK aplica **uma vez só**, na primeira vez que alguém pede,
|
|
155
|
+
e ignora pedido conflitante depois. Sem `ipFamily`, o SDK não mexe na resolução do processo.
|
|
156
|
+
|
|
157
|
+
## Webhook
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
import { constructEvent } from "@open-apime/sdk/webhook";
|
|
161
|
+
|
|
162
|
+
app.post("/webhooks/apime", express.raw({ type: "application/json" }), (req, res) => {
|
|
163
|
+
let evento;
|
|
164
|
+
try {
|
|
165
|
+
// corpo CRU. JSON já parseado e reserializado quebra a assinatura.
|
|
166
|
+
evento = constructEvent(req.body, req.get("X-ApiMe-Signature"), process.env.WEBHOOK_SECRET!);
|
|
167
|
+
} catch {
|
|
168
|
+
return res.sendStatus(401);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
switch (evento.type) {
|
|
172
|
+
case "message":
|
|
173
|
+
console.log(evento.payload.text); // tipado, estreitado pelo `type`
|
|
174
|
+
break;
|
|
175
|
+
case "temporary_ban":
|
|
176
|
+
console.log(evento.payload.restrictedUntil);
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
res.sendStatus(200);
|
|
180
|
+
});
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
São **12 tipos de evento**. `reply`, `url`, `copy` e `call` **não** são eventos, são tipos de botão
|
|
184
|
+
dentro de `payload.buttons[].type`.
|
|
185
|
+
|
|
186
|
+
## Cobertura
|
|
187
|
+
|
|
188
|
+
Cobre **as 66 operações** do contrato do apime. Ficam de fora `/auth/login`, que é como se obtém a
|
|
189
|
+
credencial, e `/media/{instanceId}/{mediaId}`, que serve arquivo direto ao navegador.
|
|
190
|
+
|
|
191
|
+
Para qualquer rota que o SDK ainda não tenha, `client.request()` fala com a API mantendo auth,
|
|
192
|
+
retry e observabilidade.
|
|
193
|
+
|
|
194
|
+
A API completa está no [openapi.yaml](https://github.com/open-apime/apime/blob/main/openapi.yaml)
|
|
195
|
+
do apime, e os eventos em [webhook-payloads.md](https://github.com/open-apime/apime/blob/main/docs/webhook-payloads.md).
|
|
196
|
+
|
|
197
|
+
Enquanto está em `0.x`, minor pode trazer mudança incompatível.
|
|
198
|
+
|
|
199
|
+
## Segurança
|
|
200
|
+
|
|
201
|
+
- **Sem sourcemap no pacote.** O `.map` embute o código-fonte inteiro em `sourcesContent`, então
|
|
202
|
+
publicá-lo entregaria a fonte junto do build. O fonte vive no GitHub, que é onde se lê.
|
|
203
|
+
- **Segmento de path é escapado.** Id vindo do banco ou de entrada de usuário não escapa da rota:
|
|
204
|
+
sem isso, um id com `../` faria `/instances/{id}` cair em `/users/admin`.
|
|
205
|
+
- **Guard anti-SSRF na `baseUrl`.** IP privado, loopback, link-local e endpoint de metadados de
|
|
206
|
+
nuvem são recusados antes de qualquer requisição.
|
|
207
|
+
- **O token não aparece em erro.** Nem na mensagem, nem no corpo, nem no stack.
|
|
208
|
+
- **O relato ao Sentry não leva telefone.** Vai a rota normalizada (`/instances/{id}/profile/{jid}`),
|
|
209
|
+
nunca o path real com o jid do contato.
|
|
210
|
+
- **Verificação de webhook em tempo constante**, sobre o corpo cru.
|
|
211
|
+
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Segurança do `@open-apime/sdk`
|
|
2
|
+
|
|
3
|
+
- **Sem sourcemap no pacote.** O `.map` embute o código-fonte inteiro em `sourcesContent`, então
|
|
4
|
+
publicá-lo entregaria a fonte junto do build. O fonte vive no GitHub, que é onde se lê.
|
|
5
|
+
- **Segmento de path é escapado.** Id vindo do banco ou de entrada de usuário não escapa da rota:
|
|
6
|
+
sem isso, um id com `../` faria `/instances/{id}` cair em `/users/admin`.
|
|
7
|
+
- **Guard anti-SSRF na `baseUrl`.** IP privado, loopback, link-local e endpoint de metadados de
|
|
8
|
+
nuvem são recusados antes de qualquer requisição.
|
|
9
|
+
- **O token não aparece em erro.** Nem na mensagem, nem no corpo, nem no stack.
|
|
10
|
+
- **O relato ao Sentry não leva telefone.** Vai a rota normalizada (`/instances/{id}/profile/{jid}`),
|
|
11
|
+
nunca o path real com o jid do contato.
|
|
12
|
+
- **Verificação de webhook em tempo constante**, sobre o corpo cru.
|
|
13
|
+
|
package/package.json
CHANGED
|
@@ -1,16 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-apime/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Cliente TypeScript da API do apime: WhatsApp, instâncias e webhooks",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"repository": {
|
|
7
|
-
"type": "git",
|
|
8
|
-
"url": "git+https://github.com/open-apime/sdk.git"
|
|
9
|
-
},
|
|
10
|
-
"homepage": "https://github.com/open-apime/sdk#readme",
|
|
11
|
-
"bugs": {
|
|
12
|
-
"url": "https://github.com/open-apime/sdk/issues"
|
|
13
|
-
},
|
|
14
6
|
"keywords": [
|
|
15
7
|
"apime",
|
|
16
8
|
"whatsapp",
|
|
@@ -31,6 +23,7 @@
|
|
|
31
23
|
"dist/**/*.cjs",
|
|
32
24
|
"dist/**/*.d.ts",
|
|
33
25
|
"dist/**/*.d.cts",
|
|
26
|
+
"docs/*.md",
|
|
34
27
|
"README.md",
|
|
35
28
|
"LICENSE"
|
|
36
29
|
],
|