@notifica/node 0.1.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/LICENSE +21 -0
- package/README.md +444 -0
- package/dist/index.cjs +1751 -0
- package/dist/index.d.cts +1912 -0
- package/dist/index.d.ts +1912 -0
- package/dist/index.js +1705 -0
- package/package.json +60 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Notifica
|
|
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,444 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<h1 align="center">@notifica/node</h1>
|
|
3
|
+
<p align="center">SDK oficial do <a href="https://usenotifica.com.br">Notifica</a> para Node.js</p>
|
|
4
|
+
<p align="center">Infraestrutura de notificações para o Brasil 🇧🇷</p>
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="https://github.com/notifica-tech/notifica-node/actions"><img src="https://github.com/notifica-tech/notifica-node/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
|
9
|
+
<a href="https://www.npmjs.com/package/@notifica/node"><img src="https://img.shields.io/npm/v/@notifica/node.svg" alt="npm"></a>
|
|
10
|
+
<a href="https://github.com/notifica-tech/notifica-node/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/@notifica/node.svg" alt="license"></a>
|
|
11
|
+
<a href="https://www.npmjs.com/package/@notifica/node"><img src="https://img.shields.io/npm/dm/@notifica/node.svg" alt="downloads"></a>
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
WhatsApp, Email, SMS, Push e In-App em uma API unificada. Zero dependências. TypeScript nativo.
|
|
17
|
+
|
|
18
|
+
## Instalação
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install @notifica/node
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
> Requer **Node.js 18+** (usa `fetch` nativo).
|
|
25
|
+
|
|
26
|
+
## Início Rápido
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
import { Notifica } from '@notifica/node';
|
|
30
|
+
|
|
31
|
+
const notifica = new Notifica('nk_live_...');
|
|
32
|
+
|
|
33
|
+
// Enviar uma notificação via WhatsApp
|
|
34
|
+
const notification = await notifica.notifications.send({
|
|
35
|
+
channel: 'whatsapp',
|
|
36
|
+
to: '+5511999999999',
|
|
37
|
+
template: 'welcome',
|
|
38
|
+
data: { name: 'João' },
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
console.log(notification.id); // 'notif-uuid'
|
|
42
|
+
console.log(notification.status); // 'pending'
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Configuração
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
// Simples — apenas API key
|
|
49
|
+
const notifica = new Notifica('nk_live_...');
|
|
50
|
+
|
|
51
|
+
// Configuração completa
|
|
52
|
+
const notifica = new Notifica({
|
|
53
|
+
apiKey: 'nk_live_...',
|
|
54
|
+
baseUrl: 'https://app.usenotifica.com.br/v1', // padrão
|
|
55
|
+
timeout: 15000, // 15s (padrão: 30s)
|
|
56
|
+
maxRetries: 5, // padrão: 3
|
|
57
|
+
autoIdempotency: true, // padrão: true
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
| Opção | Tipo | Padrão | Descrição |
|
|
62
|
+
|-------|------|--------|-----------|
|
|
63
|
+
| `apiKey` | `string` | — | **Obrigatório.** Sua API key (`nk_live_...` ou `nk_test_...`) |
|
|
64
|
+
| `baseUrl` | `string` | `https://app.usenotifica.com.br/v1` | URL base da API |
|
|
65
|
+
| `timeout` | `number` | `30000` | Timeout em ms |
|
|
66
|
+
| `maxRetries` | `number` | `3` | Retentativas automáticas em 429/5xx |
|
|
67
|
+
| `autoIdempotency` | `boolean` | `true` | Gerar chave de idempotência automática para POSTs |
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Recursos
|
|
72
|
+
|
|
73
|
+
### Notifications — Envio e consulta
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
// Enviar notificação
|
|
77
|
+
const notification = await notifica.notifications.send({
|
|
78
|
+
channel: 'whatsapp', // 'email' | 'whatsapp' | 'sms' | 'in_app' | 'push'
|
|
79
|
+
to: '+5511999999999',
|
|
80
|
+
template: 'welcome',
|
|
81
|
+
data: { name: 'João' },
|
|
82
|
+
metadata: { source: 'signup-flow' },
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Listar (com paginação manual)
|
|
86
|
+
const { data, meta } = await notifica.notifications.list({
|
|
87
|
+
channel: 'email',
|
|
88
|
+
status: 'delivered',
|
|
89
|
+
limit: 50,
|
|
90
|
+
});
|
|
91
|
+
const page2 = await notifica.notifications.list({ cursor: meta.cursor });
|
|
92
|
+
|
|
93
|
+
// Auto-paginação com async iterator
|
|
94
|
+
for await (const n of notifica.notifications.listAll({ channel: 'email' })) {
|
|
95
|
+
console.log(n.id, n.status);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Detalhes + tentativas de entrega
|
|
99
|
+
const detail = await notifica.notifications.get('notif-uuid');
|
|
100
|
+
const attempts = await notifica.notifications.listAttempts('notif-uuid');
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Templates — Gerenciamento
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
// Criar
|
|
107
|
+
const template = await notifica.templates.create({
|
|
108
|
+
channel: 'email',
|
|
109
|
+
slug: 'welcome-email',
|
|
110
|
+
name: 'Email de Boas-Vindas',
|
|
111
|
+
content: 'Olá {{name}}, bem-vindo ao {{company}}!',
|
|
112
|
+
variants: {
|
|
113
|
+
subject: 'Bem-vindo, {{name}}!',
|
|
114
|
+
html_body: '<h1>Olá {{name}}</h1><p>Bem-vindo ao {{company}}!</p>',
|
|
115
|
+
},
|
|
116
|
+
status: 'active',
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Listar / obter / atualizar / deletar
|
|
120
|
+
const { data } = await notifica.templates.list({ channel: 'email' });
|
|
121
|
+
await notifica.templates.update('tpl-uuid', { status: 'active' });
|
|
122
|
+
await notifica.templates.delete('tpl-uuid');
|
|
123
|
+
|
|
124
|
+
// Preview com variáveis
|
|
125
|
+
const preview = await notifica.templates.preview('tpl-uuid', {
|
|
126
|
+
variables: { name: 'João', company: 'Empresa Ltda' },
|
|
127
|
+
});
|
|
128
|
+
console.log(preview.rendered.subject); // "Bem-vindo, João!"
|
|
129
|
+
|
|
130
|
+
// Preview de conteúdo arbitrário (para editor em tempo real)
|
|
131
|
+
const livePreview = await notifica.templates.previewContent({
|
|
132
|
+
content: 'Oi {{name}}!',
|
|
133
|
+
channel: 'email',
|
|
134
|
+
variables: { name: 'Maria' },
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Validar template
|
|
138
|
+
const validation = await notifica.templates.validate('tpl-uuid');
|
|
139
|
+
console.log(validation.valid, validation.warnings);
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Workflows — Orquestração
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
// Criar workflow
|
|
146
|
+
const workflow = await notifica.workflows.create({
|
|
147
|
+
slug: 'welcome-flow',
|
|
148
|
+
name: 'Fluxo de Boas-Vindas',
|
|
149
|
+
steps: [
|
|
150
|
+
{ type: 'send', channel: 'email', template: 'welcome-email' },
|
|
151
|
+
{ type: 'delay', duration: '1h' },
|
|
152
|
+
{ type: 'send', channel: 'whatsapp', template: 'welcome-whatsapp' },
|
|
153
|
+
],
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// Disparar workflow
|
|
157
|
+
const run = await notifica.workflows.trigger('welcome-flow', {
|
|
158
|
+
recipient: '+5511999999999',
|
|
159
|
+
data: { name: 'João', plan: 'pro' },
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// Gerenciar execuções
|
|
163
|
+
const { data: runs } = await notifica.workflows.listRuns({ status: 'running' });
|
|
164
|
+
const runDetail = await notifica.workflows.getRun('run-uuid');
|
|
165
|
+
await notifica.workflows.cancelRun('run-uuid');
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
**Tipos de step:**
|
|
169
|
+
|
|
170
|
+
| Tipo | Campos | Descrição |
|
|
171
|
+
|------|--------|-----------|
|
|
172
|
+
| `send` | `channel`, `template` | Envia notificação pelo canal |
|
|
173
|
+
| `delay` | `duration` (`5m`, `1h`, `1d`) | Pausa a execução |
|
|
174
|
+
| `fallback` | `channels[]`, `template` | Tenta canais em ordem até sucesso |
|
|
175
|
+
|
|
176
|
+
### Subscribers — Gerenciamento de destinatários
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
// Criar/atualizar (upsert por external_id)
|
|
180
|
+
const subscriber = await notifica.subscribers.create({
|
|
181
|
+
external_id: 'user-123',
|
|
182
|
+
email: 'joao@empresa.com.br',
|
|
183
|
+
phone: '+5511999998888',
|
|
184
|
+
name: 'João Silva',
|
|
185
|
+
locale: 'pt_BR',
|
|
186
|
+
timezone: 'America/Sao_Paulo',
|
|
187
|
+
custom_properties: { plan: 'pro' },
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// Listar (com busca)
|
|
191
|
+
const { data } = await notifica.subscribers.list({ search: 'joao' });
|
|
192
|
+
|
|
193
|
+
// Atualizar / deletar (LGPD — nullifica PII, irreversível!)
|
|
194
|
+
await notifica.subscribers.update('sub-uuid', { name: 'João S.' });
|
|
195
|
+
await notifica.subscribers.delete('sub-uuid');
|
|
196
|
+
|
|
197
|
+
// Preferências de notificação
|
|
198
|
+
const prefs = await notifica.subscribers.getPreferences('sub-uuid');
|
|
199
|
+
await notifica.subscribers.updatePreferences('sub-uuid', {
|
|
200
|
+
preferences: [
|
|
201
|
+
{ category: 'marketing', channel: 'email', enabled: false },
|
|
202
|
+
{ category: 'transactional', channel: 'whatsapp', enabled: true },
|
|
203
|
+
],
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Import em lote (transacional — tudo ou nada)
|
|
207
|
+
const result = await notifica.subscribers.bulkImport({
|
|
208
|
+
subscribers: [
|
|
209
|
+
{ external_id: 'user-1', email: 'a@empresa.com.br', name: 'Ana' },
|
|
210
|
+
{ external_id: 'user-2', email: 'b@empresa.com.br', name: 'Bruno' },
|
|
211
|
+
],
|
|
212
|
+
});
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
**Notificações In-App:**
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
const { data } = await notifica.subscribers.listNotifications('sub-uuid', { unread_only: true });
|
|
219
|
+
await notifica.subscribers.markRead('sub-uuid', 'notif-uuid');
|
|
220
|
+
await notifica.subscribers.markAllRead('sub-uuid');
|
|
221
|
+
const count = await notifica.subscribers.getUnreadCount('sub-uuid');
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Channels — Configuração de canais
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
const channel = await notifica.channels.create({
|
|
228
|
+
channel: 'email',
|
|
229
|
+
provider: 'aws_ses',
|
|
230
|
+
credentials: { access_key_id: 'AKIA...', secret_access_key: '...', region: 'us-east-1' },
|
|
231
|
+
settings: { from_address: 'noreply@empresa.com.br', from_name: 'Empresa' },
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const channels = await notifica.channels.list();
|
|
235
|
+
const test = await notifica.channels.test('email');
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Domains — Domínios de envio
|
|
239
|
+
|
|
240
|
+
```typescript
|
|
241
|
+
// Registrar domínio e configurar DNS
|
|
242
|
+
const domain = await notifica.domains.create({ domain: 'suaempresa.com.br' });
|
|
243
|
+
// → Configure os registros em domain.dns_records no seu provedor DNS
|
|
244
|
+
|
|
245
|
+
const verified = await notifica.domains.verify(domain.id);
|
|
246
|
+
const health = await notifica.domains.getHealth(domain.id);
|
|
247
|
+
const alerts = await notifica.domains.listAlerts();
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Webhooks — Eventos outbound
|
|
251
|
+
|
|
252
|
+
```typescript
|
|
253
|
+
const webhook = await notifica.webhooks.create({
|
|
254
|
+
url: 'https://meuapp.com.br/webhooks/notifica',
|
|
255
|
+
events: ['notification.delivered', 'notification.failed'],
|
|
256
|
+
});
|
|
257
|
+
// ⚠️ Salve webhook.signing_secret — mostrado apenas na criação!
|
|
258
|
+
|
|
259
|
+
await notifica.webhooks.test(webhook.id);
|
|
260
|
+
const deliveries = await notifica.webhooks.listDeliveries(webhook.id);
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
**Verificação de assinatura:**
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
266
|
+
app.post('/webhooks/notifica', async (req, res) => {
|
|
267
|
+
const payload = req.body; // raw body string
|
|
268
|
+
const signature = req.headers['x-notifica-signature'];
|
|
269
|
+
const secret = process.env.WEBHOOK_SECRET!;
|
|
270
|
+
|
|
271
|
+
const valid = await notifica.webhooks.verify(payload, signature, secret);
|
|
272
|
+
if (!valid) return res.status(401).send('Assinatura inválida');
|
|
273
|
+
|
|
274
|
+
// Processar evento...
|
|
275
|
+
res.status(200).send('OK');
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// Ou a versão que lança erro automaticamente:
|
|
279
|
+
await notifica.webhooks.verifyOrThrow(payload, signature, secret);
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### API Keys — Gerenciamento de chaves
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
const key = await notifica.apiKeys.create({
|
|
286
|
+
key_type: 'secret',
|
|
287
|
+
label: 'Backend Production',
|
|
288
|
+
environment: 'production',
|
|
289
|
+
});
|
|
290
|
+
// ⚠️ Salve key.raw_key — mostrado apenas na criação!
|
|
291
|
+
|
|
292
|
+
const keys = await notifica.apiKeys.list();
|
|
293
|
+
await notifica.apiKeys.revoke('key-uuid');
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### Analytics — Métricas
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
const overview = await notifica.analytics.overview({ period: '7d' });
|
|
300
|
+
const channels = await notifica.analytics.byChannel({ period: '24h' });
|
|
301
|
+
const timeseries = await notifica.analytics.timeseries({ period: '7d', granularity: 'day' });
|
|
302
|
+
const top = await notifica.analytics.topTemplates({ period: '30d', limit: 5 });
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
## Tratamento de Erros
|
|
308
|
+
|
|
309
|
+
O SDK lança erros tipados para cada cenário:
|
|
310
|
+
|
|
311
|
+
```typescript
|
|
312
|
+
import {
|
|
313
|
+
NotificaError, // Erro base
|
|
314
|
+
ApiError, // Qualquer erro da API (4xx, 5xx)
|
|
315
|
+
ValidationError, // 422 — dados inválidos
|
|
316
|
+
RateLimitError, // 429 — rate limit excedido
|
|
317
|
+
TimeoutError, // Timeout de conexão
|
|
318
|
+
} from '@notifica/node';
|
|
319
|
+
|
|
320
|
+
try {
|
|
321
|
+
await notifica.notifications.send({ channel: 'email', to: 'x' });
|
|
322
|
+
} catch (error) {
|
|
323
|
+
if (error instanceof ValidationError) {
|
|
324
|
+
console.log(error.status); // 422
|
|
325
|
+
console.log(error.message); // "Email inválido"
|
|
326
|
+
console.log(error.details); // { email: ["is invalid"] }
|
|
327
|
+
console.log(error.requestId); // "req-abc-123" (para suporte)
|
|
328
|
+
}
|
|
329
|
+
if (error instanceof RateLimitError) {
|
|
330
|
+
console.log(error.retryAfter); // 30 (segundos)
|
|
331
|
+
}
|
|
332
|
+
if (error instanceof ApiError) {
|
|
333
|
+
console.log(error.status, error.code);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
## Idempotência
|
|
339
|
+
|
|
340
|
+
O SDK gera automaticamente chaves de idempotência para todas as requests POST, prevenindo operações duplicadas em caso de retry de rede.
|
|
341
|
+
|
|
342
|
+
```typescript
|
|
343
|
+
// Automático (padrão)
|
|
344
|
+
await notifica.notifications.send({ channel: 'email', to: 'a@b.com' });
|
|
345
|
+
|
|
346
|
+
// Chave customizada (útil para deduplicação por lógica de negócio)
|
|
347
|
+
await notifica.notifications.send(
|
|
348
|
+
{ channel: 'email', to: 'a@b.com' },
|
|
349
|
+
{ idempotencyKey: 'signup-user-123' },
|
|
350
|
+
);
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
## Retries Automáticos
|
|
354
|
+
|
|
355
|
+
Retenta automaticamente em falhas transientes:
|
|
356
|
+
|
|
357
|
+
- **429** Too Many Requests — respeita header `Retry-After`
|
|
358
|
+
- **5xx** Server errors — backoff exponencial com jitter
|
|
359
|
+
|
|
360
|
+
```typescript
|
|
361
|
+
const notifica = new Notifica({
|
|
362
|
+
apiKey: 'nk_live_...',
|
|
363
|
+
maxRetries: 5, // padrão: 3
|
|
364
|
+
});
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
## Paginação
|
|
368
|
+
|
|
369
|
+
```typescript
|
|
370
|
+
// 1. Manual (cursor-based)
|
|
371
|
+
let cursor: string | undefined;
|
|
372
|
+
do {
|
|
373
|
+
const page = await notifica.notifications.list({ cursor, limit: 100 });
|
|
374
|
+
for (const n of page.data) { console.log(n.id); }
|
|
375
|
+
cursor = page.meta.has_more ? page.meta.cursor ?? undefined : undefined;
|
|
376
|
+
} while (cursor);
|
|
377
|
+
|
|
378
|
+
// 2. Auto-paginação (async iterator)
|
|
379
|
+
for await (const n of notifica.notifications.listAll()) {
|
|
380
|
+
console.log(n.id);
|
|
381
|
+
}
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
## Tipos TypeScript
|
|
385
|
+
|
|
386
|
+
Todos os tipos são exportados:
|
|
387
|
+
|
|
388
|
+
```typescript
|
|
389
|
+
import type {
|
|
390
|
+
Notification,
|
|
391
|
+
SendNotificationParams,
|
|
392
|
+
Template,
|
|
393
|
+
Workflow,
|
|
394
|
+
WorkflowStep,
|
|
395
|
+
Subscriber,
|
|
396
|
+
Channel,
|
|
397
|
+
NotificationStatus,
|
|
398
|
+
PaginatedResponse,
|
|
399
|
+
} from '@notifica/node';
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
---
|
|
403
|
+
|
|
404
|
+
## Desenvolvimento
|
|
405
|
+
|
|
406
|
+
```bash
|
|
407
|
+
# Instalar dependências
|
|
408
|
+
npm install
|
|
409
|
+
|
|
410
|
+
# Type check
|
|
411
|
+
npm run typecheck
|
|
412
|
+
|
|
413
|
+
# Rodar testes (requer Node 22+)
|
|
414
|
+
npm test
|
|
415
|
+
|
|
416
|
+
# Build
|
|
417
|
+
npm run build
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
## Contribuindo
|
|
421
|
+
|
|
422
|
+
Contribuições são bem-vindas! Por favor:
|
|
423
|
+
|
|
424
|
+
1. Faça fork do repositório
|
|
425
|
+
2. Crie sua branch (`git checkout -b feature/minha-feature`)
|
|
426
|
+
3. Commit suas mudanças (`git commit -m 'feat: minha feature'`)
|
|
427
|
+
4. Push para a branch (`git push origin feature/minha-feature`)
|
|
428
|
+
5. Abra um Pull Request
|
|
429
|
+
|
|
430
|
+
## Requisitos
|
|
431
|
+
|
|
432
|
+
- **Node.js 18+** (usa `fetch` nativo e `crypto.subtle`)
|
|
433
|
+
- **Zero dependências** externas
|
|
434
|
+
|
|
435
|
+
## Links
|
|
436
|
+
|
|
437
|
+
- [Documentação](https://docs.usenotifica.com.br)
|
|
438
|
+
- [API Reference](https://docs.usenotifica.com.br/api-reference)
|
|
439
|
+
- [Dashboard](https://app.usenotifica.com.br)
|
|
440
|
+
- [GitHub](https://github.com/notifica-tech/notifica-node)
|
|
441
|
+
|
|
442
|
+
## Licença
|
|
443
|
+
|
|
444
|
+
[MIT](./LICENSE) © [Notifica](https://usenotifica.com.br)
|