@creativeproject/fiscal-gateway 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/README.md +166 -0
- package/dist/index.cjs +315 -0
- package/dist/index.d.cts +301 -0
- package/dist/index.d.ts +301 -0
- package/dist/index.js +284 -0
- package/package.json +40 -0
package/README.md
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# @creativeproject/fiscal-gateway
|
|
2
|
+
|
|
3
|
+
SDK Node.js/TypeScript do **Fiscal Gateway** da Creative Project — emissão de NFS-e Padrão Nacional com uma chamada de função.
|
|
4
|
+
|
|
5
|
+
- Zero dependências (usa `fetch` nativo — Node 18+)
|
|
6
|
+
- ESM + CJS + tipos TypeScript completos
|
|
7
|
+
- Retry automático com backoff para 429/5xx/falhas de rede (seguro: a emissão é idempotente)
|
|
8
|
+
- Verificação de assinatura HMAC de webhooks embutida (com replay protection)
|
|
9
|
+
|
|
10
|
+
## Instalação
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install @creativeproject/fiscal-gateway
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Uso
|
|
17
|
+
|
|
18
|
+
### Emitir uma NFS-e
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
import { FiscalGateway } from '@creativeproject/fiscal-gateway';
|
|
22
|
+
|
|
23
|
+
const fiscal = new FiscalGateway({
|
|
24
|
+
apiKey: process.env.FISCAL_API_KEY!,
|
|
25
|
+
baseUrl: 'https://api.fiscal.creativeproject.com.br',
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const nota = await fiscal.nfse.emit({
|
|
29
|
+
organizationId: 'org_...',
|
|
30
|
+
externalId: 'charge_987', // seu ID — reenviar nunca duplica a nota
|
|
31
|
+
customer: { name: 'João da Silva', cpfCnpj: '39053344705' },
|
|
32
|
+
service: { description: 'Taxa de administração 06/2026', amount: 200.0 },
|
|
33
|
+
metadata: { source: 'mylapro', contractId: 'contract_123' },
|
|
34
|
+
});
|
|
35
|
+
// nota.status === 'pending' — o resultado chega via webhook
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Receber o resultado via webhook (recomendado)
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
import express from 'express';
|
|
42
|
+
import { createWebhookHandler } from '@creativeproject/fiscal-gateway';
|
|
43
|
+
|
|
44
|
+
const app = express();
|
|
45
|
+
|
|
46
|
+
// IMPORTANTE: express.raw — o HMAC é calculado sobre o corpo cru
|
|
47
|
+
app.post(
|
|
48
|
+
'/hooks/fiscal',
|
|
49
|
+
express.raw({ type: 'application/json' }),
|
|
50
|
+
createWebhookHandler({
|
|
51
|
+
secret: process.env.FISCAL_WEBHOOK_SECRET!, // retornado ao criar o endpoint
|
|
52
|
+
onAuthorized: async (nota) => {
|
|
53
|
+
await marcarCobrancaComNota(nota.externalId, nota.accessKey!);
|
|
54
|
+
},
|
|
55
|
+
onRejected: async (nota) => {
|
|
56
|
+
await alertarOperacao(nota.externalId, nota.rejectionReason!);
|
|
57
|
+
},
|
|
58
|
+
onCancelled: async (nota) => { /* ... */ },
|
|
59
|
+
}),
|
|
60
|
+
);
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Ou verifique manualmente (qualquer framework):
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
import { verifyWebhook } from '@creativeproject/fiscal-gateway';
|
|
67
|
+
|
|
68
|
+
const event = verifyWebhook(rawBody, req.headers['x-fiscal-signature'], secret);
|
|
69
|
+
// lança WebhookVerificationError se a assinatura for inválida/expirada
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Outras operações
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
// Consulta e polling (scripts/testes — em produção prefira webhooks)
|
|
76
|
+
const doc = await fiscal.nfse.get(id);
|
|
77
|
+
const final = await fiscal.nfse.waitForResult(id, { timeoutMs: 30_000 });
|
|
78
|
+
|
|
79
|
+
// Listagem, XML, DANFSe, cancelamento
|
|
80
|
+
const lista = await fiscal.nfse.list({ status: 'authorized', limit: 20 });
|
|
81
|
+
const xml = await fiscal.nfse.xml(id);
|
|
82
|
+
const { url } = await fiscal.nfse.pdf(id); // URL pré-assinada (15 min)
|
|
83
|
+
await fiscal.nfse.cancel(id, 'Valor incorreto');
|
|
84
|
+
|
|
85
|
+
// Onboarding de organizações
|
|
86
|
+
const org = await fiscal.organizations.create({
|
|
87
|
+
cnpj: '12345678000190',
|
|
88
|
+
legalName: 'Imobiliária Alfa Ltda',
|
|
89
|
+
cityCode: '3550308',
|
|
90
|
+
state: 'SP',
|
|
91
|
+
taxRegime: 'simples_nacional',
|
|
92
|
+
fiscalSettings: {
|
|
93
|
+
provider: 'nacional',
|
|
94
|
+
serviceCode: '070101',
|
|
95
|
+
serviceDescription: 'Administração imobiliária',
|
|
96
|
+
issRate: 2.0,
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
await fiscal.organizations.addCredential(org.id, {
|
|
100
|
+
type: 'certificate_a1',
|
|
101
|
+
payload: { pfxBase64: '...', password: '...' }, // validado no upload
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Webhook endpoints
|
|
105
|
+
const endpoint = await fiscal.webhookEndpoints.create('https://meuapp.com/hooks/fiscal');
|
|
106
|
+
console.log(endpoint.secret); // exibido SÓ aqui — guarde no seu secret manager
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Tratamento de erros
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
import { FiscalGatewayError } from '@creativeproject/fiscal-gateway';
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
await fiscal.nfse.emit(params);
|
|
116
|
+
} catch (err) {
|
|
117
|
+
if (err instanceof FiscalGatewayError) {
|
|
118
|
+
err.status; // HTTP status
|
|
119
|
+
err.body; // corpo da resposta do gateway
|
|
120
|
+
err.isClientError; // 4xx — corrija o payload, não retente
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Erros 429/5xx/rede são retentados automaticamente (3x, backoff exponencial). Configure com `maxRetries` e `timeoutMs` no construtor.
|
|
126
|
+
|
|
127
|
+
## Desenvolvimento
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
npm install
|
|
131
|
+
npm test # vitest — 14 testes (HMAC, retry, contrato HTTP)
|
|
132
|
+
npm run build # tsup — dist/ com ESM + CJS + .d.ts
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Publicação no npm
|
|
136
|
+
|
|
137
|
+
O `prepublishOnly` roda testes + build automaticamente.
|
|
138
|
+
|
|
139
|
+
### Opção A — npm público (npmjs.com)
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
# 1ª vez: criar a organização "creativeproject" em npmjs.com e fazer login
|
|
143
|
+
npm login
|
|
144
|
+
|
|
145
|
+
cd packages/sdk
|
|
146
|
+
npm version patch # ou minor/major — atualiza package.json e cria tag
|
|
147
|
+
npm publish # access public já configurado no package.json
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Opção B — GitHub Packages (privado, dentro da org)
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
# .npmrc do projeto consumidor:
|
|
154
|
+
# @creativeproject:registry=https://npm.pkg.github.com
|
|
155
|
+
|
|
156
|
+
cd packages/sdk
|
|
157
|
+
npm publish --registry=https://npm.pkg.github.com
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Opção C — automática por tag (workflow já configurado)
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
git tag sdk-v0.1.0 && git push origin sdk-v0.1.0
|
|
164
|
+
# o workflow .github/workflows/publish-sdk.yml testa, builda e publica
|
|
165
|
+
# requer o secret NPM_TOKEN configurado no repositório (npmjs → Access Tokens → Automation)
|
|
166
|
+
```
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
FiscalGateway: () => FiscalGateway,
|
|
24
|
+
FiscalGatewayError: () => FiscalGatewayError,
|
|
25
|
+
WebhookVerificationError: () => WebhookVerificationError,
|
|
26
|
+
createWebhookHandler: () => createWebhookHandler,
|
|
27
|
+
verifyWebhook: () => verifyWebhook
|
|
28
|
+
});
|
|
29
|
+
module.exports = __toCommonJS(index_exports);
|
|
30
|
+
|
|
31
|
+
// src/errors.ts
|
|
32
|
+
var FiscalGatewayError = class extends Error {
|
|
33
|
+
status;
|
|
34
|
+
body;
|
|
35
|
+
constructor(message, status, body) {
|
|
36
|
+
super(message);
|
|
37
|
+
this.name = "FiscalGatewayError";
|
|
38
|
+
this.status = status;
|
|
39
|
+
this.body = body;
|
|
40
|
+
}
|
|
41
|
+
/** Erro de validação ou regra de negócio — corrigir o payload, não retentar */
|
|
42
|
+
get isClientError() {
|
|
43
|
+
return this.status >= 400 && this.status < 500;
|
|
44
|
+
}
|
|
45
|
+
/** Rate limit — aguarde e retente (o SDK já retenta automaticamente) */
|
|
46
|
+
get isRateLimited() {
|
|
47
|
+
return this.status === 429;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
var WebhookVerificationError = class extends Error {
|
|
51
|
+
constructor(message) {
|
|
52
|
+
super(message);
|
|
53
|
+
this.name = "WebhookVerificationError";
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// src/http.ts
|
|
58
|
+
var RETRYABLE_STATUS = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
|
|
59
|
+
var HttpClient = class {
|
|
60
|
+
constructor(options) {
|
|
61
|
+
this.options = options;
|
|
62
|
+
this.fetchFn = options.fetch ?? globalThis.fetch;
|
|
63
|
+
if (!this.fetchFn) {
|
|
64
|
+
throw new Error("fetch global n\xE3o dispon\xEDvel \u2014 Node 18+ \xE9 obrigat\xF3rio");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
options;
|
|
68
|
+
fetchFn;
|
|
69
|
+
async request(method, path, body, query) {
|
|
70
|
+
const url = new URL(path, this.options.baseUrl);
|
|
71
|
+
for (const [key, value] of Object.entries(query ?? {})) {
|
|
72
|
+
if (value !== void 0) url.searchParams.set(key, String(value));
|
|
73
|
+
}
|
|
74
|
+
const maxRetries = this.options.maxRetries ?? 3;
|
|
75
|
+
let lastError;
|
|
76
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
77
|
+
if (attempt > 0) {
|
|
78
|
+
const backoff = Math.min(1e3 * 2 ** (attempt - 1), 8e3);
|
|
79
|
+
await sleep(backoff + Math.random() * 250);
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
const response = await this.fetchFn(url, {
|
|
83
|
+
method,
|
|
84
|
+
headers: {
|
|
85
|
+
"x-api-key": this.options.apiKey,
|
|
86
|
+
...body !== void 0 && { "content-type": "application/json" }
|
|
87
|
+
},
|
|
88
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0,
|
|
89
|
+
signal: AbortSignal.timeout(this.options.timeoutMs ?? 3e4)
|
|
90
|
+
});
|
|
91
|
+
const text = await response.text();
|
|
92
|
+
const parsed = text ? safeJsonParse(text) : null;
|
|
93
|
+
if (response.ok) return parsed;
|
|
94
|
+
if (RETRYABLE_STATUS.has(response.status) && attempt < maxRetries) {
|
|
95
|
+
lastError = new FiscalGatewayError(
|
|
96
|
+
`HTTP ${response.status} em ${method} ${path}`,
|
|
97
|
+
response.status,
|
|
98
|
+
parsed
|
|
99
|
+
);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const message = parsed?.message ?? `HTTP ${response.status}`;
|
|
103
|
+
throw new FiscalGatewayError(
|
|
104
|
+
Array.isArray(message) ? message.join("; ") : message,
|
|
105
|
+
response.status,
|
|
106
|
+
parsed
|
|
107
|
+
);
|
|
108
|
+
} catch (err) {
|
|
109
|
+
if (err instanceof FiscalGatewayError) throw err;
|
|
110
|
+
lastError = err;
|
|
111
|
+
if (attempt === maxRetries) break;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
throw lastError;
|
|
115
|
+
}
|
|
116
|
+
get(path, query) {
|
|
117
|
+
return this.request("GET", path, void 0, query);
|
|
118
|
+
}
|
|
119
|
+
post(path, body) {
|
|
120
|
+
return this.request("POST", path, body);
|
|
121
|
+
}
|
|
122
|
+
patch(path, body) {
|
|
123
|
+
return this.request("PATCH", path, body);
|
|
124
|
+
}
|
|
125
|
+
delete(path) {
|
|
126
|
+
return this.request("DELETE", path);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
function sleep(ms) {
|
|
130
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
131
|
+
}
|
|
132
|
+
function safeJsonParse(text) {
|
|
133
|
+
try {
|
|
134
|
+
return JSON.parse(text);
|
|
135
|
+
} catch {
|
|
136
|
+
return { raw: text };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// src/resources.ts
|
|
141
|
+
var NfseResource = class {
|
|
142
|
+
constructor(http) {
|
|
143
|
+
this.http = http;
|
|
144
|
+
}
|
|
145
|
+
http;
|
|
146
|
+
/**
|
|
147
|
+
* Solicita a emissão de uma NFS-e. Assíncrono: retorna `pending` e o
|
|
148
|
+
* resultado final chega via webhook (ou polling com get()).
|
|
149
|
+
* Idempotente por (organizationId, externalId).
|
|
150
|
+
*/
|
|
151
|
+
emit(params) {
|
|
152
|
+
return this.http.post("/v1/nfse", params);
|
|
153
|
+
}
|
|
154
|
+
get(id) {
|
|
155
|
+
return this.http.get(`/v1/nfse/${id}`);
|
|
156
|
+
}
|
|
157
|
+
list(params = {}) {
|
|
158
|
+
return this.http.get("/v1/nfse", params);
|
|
159
|
+
}
|
|
160
|
+
events(id) {
|
|
161
|
+
return this.http.get(`/v1/nfse/${id}/events`);
|
|
162
|
+
}
|
|
163
|
+
xml(id) {
|
|
164
|
+
return this.http.get(`/v1/nfse/${id}/xml`);
|
|
165
|
+
}
|
|
166
|
+
/** DANFSe (PDF) — retorna URL pré-assinada com expiração */
|
|
167
|
+
pdf(id) {
|
|
168
|
+
return this.http.get(`/v1/nfse/${id}/pdf`);
|
|
169
|
+
}
|
|
170
|
+
cancel(id, reason) {
|
|
171
|
+
return this.http.post(`/v1/nfse/${id}/cancel`, { reason });
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Aguarda o documento sair de pending/processing (polling).
|
|
175
|
+
* Use webhooks em produção; isto é conveniência para scripts e testes.
|
|
176
|
+
*/
|
|
177
|
+
async waitForResult(id, opts = {}) {
|
|
178
|
+
const deadline = Date.now() + (opts.timeoutMs ?? 6e4);
|
|
179
|
+
for (; ; ) {
|
|
180
|
+
const doc = await this.get(id);
|
|
181
|
+
if (doc.status !== "pending" && doc.status !== "processing") return doc;
|
|
182
|
+
if (Date.now() > deadline) {
|
|
183
|
+
throw new Error(`Timeout aguardando resultado da NFS-e ${id} (status: ${doc.status})`);
|
|
184
|
+
}
|
|
185
|
+
await new Promise((resolve) => setTimeout(resolve, opts.intervalMs ?? 1500));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
var OrganizationsResource = class {
|
|
190
|
+
constructor(http) {
|
|
191
|
+
this.http = http;
|
|
192
|
+
}
|
|
193
|
+
http;
|
|
194
|
+
create(params) {
|
|
195
|
+
return this.http.post("/v1/organizations", params);
|
|
196
|
+
}
|
|
197
|
+
list() {
|
|
198
|
+
return this.http.get("/v1/organizations");
|
|
199
|
+
}
|
|
200
|
+
get(id) {
|
|
201
|
+
return this.http.get(`/v1/organizations/${id}`);
|
|
202
|
+
}
|
|
203
|
+
update(id, params) {
|
|
204
|
+
return this.http.patch(`/v1/organizations/${id}`, params);
|
|
205
|
+
}
|
|
206
|
+
/** Cadastra credencial fiscal (certificado A1 é validado no upload) */
|
|
207
|
+
addCredential(organizationId, params) {
|
|
208
|
+
return this.http.post(`/v1/organizations/${organizationId}/credentials`, params);
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
var WebhookEndpointsResource = class {
|
|
212
|
+
constructor(http) {
|
|
213
|
+
this.http = http;
|
|
214
|
+
}
|
|
215
|
+
http;
|
|
216
|
+
/** O `secret` retornado é exibido apenas aqui — guarde-o para verificar assinaturas */
|
|
217
|
+
create(url) {
|
|
218
|
+
return this.http.post("/v1/webhook-endpoints", { url });
|
|
219
|
+
}
|
|
220
|
+
list() {
|
|
221
|
+
return this.http.get("/v1/webhook-endpoints");
|
|
222
|
+
}
|
|
223
|
+
deactivate(id) {
|
|
224
|
+
return this.http.delete(`/v1/webhook-endpoints/${id}`);
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
// src/webhooks.ts
|
|
229
|
+
var import_node_crypto = require("crypto");
|
|
230
|
+
var DEFAULT_TOLERANCE_SECONDS = 300;
|
|
231
|
+
function verifyWebhook(rawBody, signatureHeader, secret, options = {}) {
|
|
232
|
+
if (!signatureHeader) {
|
|
233
|
+
throw new WebhookVerificationError("Header x-fiscal-signature ausente");
|
|
234
|
+
}
|
|
235
|
+
const parts = Object.fromEntries(
|
|
236
|
+
signatureHeader.split(",").map((part) => part.split("=", 2))
|
|
237
|
+
);
|
|
238
|
+
const timestamp = Number(parts.t);
|
|
239
|
+
const signature = parts.v1;
|
|
240
|
+
if (!timestamp || !signature) {
|
|
241
|
+
throw new WebhookVerificationError("Formato de assinatura inv\xE1lido (esperado t=...,v1=...)");
|
|
242
|
+
}
|
|
243
|
+
const tolerance = options.toleranceSeconds ?? DEFAULT_TOLERANCE_SECONDS;
|
|
244
|
+
const ageSeconds = Math.abs(Date.now() / 1e3 - timestamp);
|
|
245
|
+
if (ageSeconds > tolerance) {
|
|
246
|
+
throw new WebhookVerificationError(`Timestamp fora da toler\xE2ncia (${Math.round(ageSeconds)}s)`);
|
|
247
|
+
}
|
|
248
|
+
const body = typeof rawBody === "string" ? rawBody : rawBody.toString("utf8");
|
|
249
|
+
const expected = (0, import_node_crypto.createHmac)("sha256", secret).update(`${timestamp}.${body}`).digest("hex");
|
|
250
|
+
const a = Buffer.from(signature, "hex");
|
|
251
|
+
const b = Buffer.from(expected, "hex");
|
|
252
|
+
if (a.length !== b.length || !(0, import_node_crypto.timingSafeEqual)(a, b)) {
|
|
253
|
+
throw new WebhookVerificationError("Assinatura HMAC inv\xE1lida");
|
|
254
|
+
}
|
|
255
|
+
return JSON.parse(body);
|
|
256
|
+
}
|
|
257
|
+
function createWebhookHandler(options) {
|
|
258
|
+
return async (req, res) => {
|
|
259
|
+
const header = req.headers["x-fiscal-signature"];
|
|
260
|
+
const rawBody = Buffer.isBuffer(req.body) || typeof req.body === "string" ? req.body : JSON.stringify(req.body);
|
|
261
|
+
let event;
|
|
262
|
+
try {
|
|
263
|
+
event = verifyWebhook(rawBody, Array.isArray(header) ? header[0] : header, options.secret, options);
|
|
264
|
+
} catch (err) {
|
|
265
|
+
res.status(400).send({ error: err.message });
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
try {
|
|
269
|
+
switch (event.type) {
|
|
270
|
+
case "nfse.authorized":
|
|
271
|
+
await options.onAuthorized?.(event.data, event);
|
|
272
|
+
break;
|
|
273
|
+
case "nfse.rejected":
|
|
274
|
+
await options.onRejected?.(event.data, event);
|
|
275
|
+
break;
|
|
276
|
+
case "nfse.cancelled":
|
|
277
|
+
await options.onCancelled?.(event.data, event);
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
await options.onEvent?.(event);
|
|
281
|
+
res.status(200).send({ received: true });
|
|
282
|
+
} catch (err) {
|
|
283
|
+
options.onError?.(err);
|
|
284
|
+
res.status(500).send({ error: "handler failed" });
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// src/index.ts
|
|
290
|
+
var FiscalGateway = class {
|
|
291
|
+
nfse;
|
|
292
|
+
organizations;
|
|
293
|
+
webhookEndpoints;
|
|
294
|
+
constructor(options) {
|
|
295
|
+
if (!options.apiKey) throw new Error("apiKey \xE9 obrigat\xF3ria");
|
|
296
|
+
const http = new HttpClient({
|
|
297
|
+
baseUrl: options.baseUrl ?? "http://localhost:3333",
|
|
298
|
+
apiKey: options.apiKey,
|
|
299
|
+
timeoutMs: options.timeoutMs,
|
|
300
|
+
maxRetries: options.maxRetries,
|
|
301
|
+
fetch: options.fetch
|
|
302
|
+
});
|
|
303
|
+
this.nfse = new NfseResource(http);
|
|
304
|
+
this.organizations = new OrganizationsResource(http);
|
|
305
|
+
this.webhookEndpoints = new WebhookEndpointsResource(http);
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
309
|
+
0 && (module.exports = {
|
|
310
|
+
FiscalGateway,
|
|
311
|
+
FiscalGatewayError,
|
|
312
|
+
WebhookVerificationError,
|
|
313
|
+
createWebhookHandler,
|
|
314
|
+
verifyWebhook
|
|
315
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
interface HttpClientOptions {
|
|
2
|
+
baseUrl: string;
|
|
3
|
+
apiKey: string;
|
|
4
|
+
/** Timeout por requisição em ms (padrão: 30000) */
|
|
5
|
+
timeoutMs?: number;
|
|
6
|
+
/** Máximo de tentativas para 429/5xx/erros de rede (padrão: 3) */
|
|
7
|
+
maxRetries?: number;
|
|
8
|
+
fetch?: typeof fetch;
|
|
9
|
+
}
|
|
10
|
+
declare class HttpClient {
|
|
11
|
+
private readonly options;
|
|
12
|
+
private readonly fetchFn;
|
|
13
|
+
constructor(options: HttpClientOptions);
|
|
14
|
+
request<T>(method: string, path: string, body?: unknown, query?: Record<string, string | number | undefined>): Promise<T>;
|
|
15
|
+
get<T>(path: string, query?: Record<string, string | number | undefined>): Promise<T>;
|
|
16
|
+
post<T>(path: string, body?: unknown): Promise<T>;
|
|
17
|
+
patch<T>(path: string, body?: unknown): Promise<T>;
|
|
18
|
+
delete<T>(path: string): Promise<T>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type NfseStatus = 'draft' | 'pending' | 'processing' | 'authorized' | 'rejected' | 'cancelled' | 'error';
|
|
22
|
+
type TaxRegime = 'simples_nacional' | 'lucro_presumido' | 'lucro_real' | 'mei';
|
|
23
|
+
type FiscalEnvironment = 'production' | 'restricted_production';
|
|
24
|
+
type FiscalProvider = 'mock' | 'nacional';
|
|
25
|
+
interface Customer {
|
|
26
|
+
name: string;
|
|
27
|
+
/** 11 dígitos (CPF) ou 14 (CNPJ), somente números */
|
|
28
|
+
cpfCnpj?: string;
|
|
29
|
+
email?: string;
|
|
30
|
+
}
|
|
31
|
+
interface ServiceInfo {
|
|
32
|
+
description: string;
|
|
33
|
+
amount: number;
|
|
34
|
+
/** Sobrescreve o código de tributação padrão da organização */
|
|
35
|
+
serviceCode?: string;
|
|
36
|
+
/** Competência da prestação (ISO date) */
|
|
37
|
+
competence?: string;
|
|
38
|
+
}
|
|
39
|
+
interface EmitNfseParams {
|
|
40
|
+
organizationId: string;
|
|
41
|
+
/** ID do seu sistema — chave de idempotência: reenviar nunca duplica a nota */
|
|
42
|
+
externalId: string;
|
|
43
|
+
customer: Customer;
|
|
44
|
+
service: ServiceInfo;
|
|
45
|
+
metadata?: Record<string, unknown>;
|
|
46
|
+
}
|
|
47
|
+
interface NfseDocument {
|
|
48
|
+
id: string;
|
|
49
|
+
organizationId: string;
|
|
50
|
+
externalId: string;
|
|
51
|
+
status: NfseStatus;
|
|
52
|
+
amount: string;
|
|
53
|
+
nfseNumber: string | null;
|
|
54
|
+
accessKey: string | null;
|
|
55
|
+
verificationCode: string | null;
|
|
56
|
+
rejectionReason: string | null;
|
|
57
|
+
createdAt: string;
|
|
58
|
+
/** true quando o POST devolveu um documento já existente (replay idempotente) */
|
|
59
|
+
idempotentReplay?: boolean;
|
|
60
|
+
}
|
|
61
|
+
interface NfseListResult {
|
|
62
|
+
total: number;
|
|
63
|
+
limit: number;
|
|
64
|
+
offset: number;
|
|
65
|
+
items: NfseDocument[];
|
|
66
|
+
}
|
|
67
|
+
interface ListNfseParams {
|
|
68
|
+
organizationId?: string;
|
|
69
|
+
status?: NfseStatus;
|
|
70
|
+
externalId?: string;
|
|
71
|
+
limit?: number;
|
|
72
|
+
offset?: number;
|
|
73
|
+
}
|
|
74
|
+
interface FiscalEvent {
|
|
75
|
+
id: string;
|
|
76
|
+
documentId: string;
|
|
77
|
+
type: string;
|
|
78
|
+
status: string | null;
|
|
79
|
+
payload: Record<string, unknown> | null;
|
|
80
|
+
createdAt: string;
|
|
81
|
+
}
|
|
82
|
+
interface XmlArtifact {
|
|
83
|
+
type: string;
|
|
84
|
+
content: string | null;
|
|
85
|
+
url: string | null;
|
|
86
|
+
checksum: string;
|
|
87
|
+
createdAt: string;
|
|
88
|
+
}
|
|
89
|
+
interface DanfseResult {
|
|
90
|
+
documentId: string;
|
|
91
|
+
type: 'danfse_pdf';
|
|
92
|
+
/** URL pré-assinada (expira) */
|
|
93
|
+
url: string;
|
|
94
|
+
expiresInSeconds: number;
|
|
95
|
+
}
|
|
96
|
+
interface FiscalSettingsInput {
|
|
97
|
+
provider?: FiscalProvider;
|
|
98
|
+
environment?: FiscalEnvironment;
|
|
99
|
+
serviceCode: string;
|
|
100
|
+
serviceDescription: string;
|
|
101
|
+
cnae?: string;
|
|
102
|
+
issRate: number;
|
|
103
|
+
issWithheld?: boolean;
|
|
104
|
+
}
|
|
105
|
+
interface CreateOrganizationParams {
|
|
106
|
+
cnpj: string;
|
|
107
|
+
legalName: string;
|
|
108
|
+
tradeName?: string;
|
|
109
|
+
/** Código IBGE de 7 dígitos */
|
|
110
|
+
cityCode: string;
|
|
111
|
+
state: string;
|
|
112
|
+
municipalRegistration?: string;
|
|
113
|
+
taxRegime: TaxRegime;
|
|
114
|
+
fiscalSettings?: FiscalSettingsInput;
|
|
115
|
+
}
|
|
116
|
+
interface Organization {
|
|
117
|
+
id: string;
|
|
118
|
+
accountId: string;
|
|
119
|
+
cnpj: string;
|
|
120
|
+
legalName: string;
|
|
121
|
+
tradeName: string | null;
|
|
122
|
+
cityCode: string;
|
|
123
|
+
state: string;
|
|
124
|
+
municipalRegistration: string | null;
|
|
125
|
+
taxRegime: TaxRegime;
|
|
126
|
+
status: 'active' | 'inactive' | 'pending_setup';
|
|
127
|
+
fiscalSettings: (FiscalSettingsInput & {
|
|
128
|
+
id: string;
|
|
129
|
+
}) | null;
|
|
130
|
+
createdAt: string;
|
|
131
|
+
updatedAt: string;
|
|
132
|
+
}
|
|
133
|
+
interface CreateCredentialParams {
|
|
134
|
+
type: 'certificate_a1' | 'token' | 'credential';
|
|
135
|
+
/** Para certificate_a1: { pfxBase64, password } — validado no upload */
|
|
136
|
+
payload: Record<string, unknown>;
|
|
137
|
+
expiresAt?: string;
|
|
138
|
+
}
|
|
139
|
+
interface Credential {
|
|
140
|
+
id: string;
|
|
141
|
+
type: string;
|
|
142
|
+
status: string;
|
|
143
|
+
expiresAt: string | null;
|
|
144
|
+
certificate?: {
|
|
145
|
+
subject: string;
|
|
146
|
+
notAfter: string;
|
|
147
|
+
};
|
|
148
|
+
createdAt: string;
|
|
149
|
+
}
|
|
150
|
+
interface WebhookEndpoint {
|
|
151
|
+
id: string;
|
|
152
|
+
url: string;
|
|
153
|
+
/** Exibido apenas na criação — guarde para verificar assinaturas */
|
|
154
|
+
secret?: string;
|
|
155
|
+
active: boolean;
|
|
156
|
+
}
|
|
157
|
+
type WebhookEventType = 'nfse.authorized' | 'nfse.rejected' | 'nfse.cancelled' | (string & {});
|
|
158
|
+
interface WebhookEvent {
|
|
159
|
+
id: string;
|
|
160
|
+
type: WebhookEventType;
|
|
161
|
+
createdAt: string;
|
|
162
|
+
data: {
|
|
163
|
+
documentId: string;
|
|
164
|
+
organizationId: string;
|
|
165
|
+
externalId: string;
|
|
166
|
+
status: NfseStatus;
|
|
167
|
+
nfseNumber: string | null;
|
|
168
|
+
accessKey: string | null;
|
|
169
|
+
rejectionReason: string | null;
|
|
170
|
+
payload: Record<string, unknown> | null;
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
declare class NfseResource {
|
|
175
|
+
private readonly http;
|
|
176
|
+
constructor(http: HttpClient);
|
|
177
|
+
/**
|
|
178
|
+
* Solicita a emissão de uma NFS-e. Assíncrono: retorna `pending` e o
|
|
179
|
+
* resultado final chega via webhook (ou polling com get()).
|
|
180
|
+
* Idempotente por (organizationId, externalId).
|
|
181
|
+
*/
|
|
182
|
+
emit(params: EmitNfseParams): Promise<NfseDocument>;
|
|
183
|
+
get(id: string): Promise<NfseDocument>;
|
|
184
|
+
list(params?: ListNfseParams): Promise<NfseListResult>;
|
|
185
|
+
events(id: string): Promise<FiscalEvent[]>;
|
|
186
|
+
xml(id: string): Promise<XmlArtifact>;
|
|
187
|
+
/** DANFSe (PDF) — retorna URL pré-assinada com expiração */
|
|
188
|
+
pdf(id: string): Promise<DanfseResult>;
|
|
189
|
+
cancel(id: string, reason: string): Promise<{
|
|
190
|
+
id: string;
|
|
191
|
+
status: string;
|
|
192
|
+
}>;
|
|
193
|
+
/**
|
|
194
|
+
* Aguarda o documento sair de pending/processing (polling).
|
|
195
|
+
* Use webhooks em produção; isto é conveniência para scripts e testes.
|
|
196
|
+
*/
|
|
197
|
+
waitForResult(id: string, opts?: {
|
|
198
|
+
intervalMs?: number;
|
|
199
|
+
timeoutMs?: number;
|
|
200
|
+
}): Promise<NfseDocument>;
|
|
201
|
+
}
|
|
202
|
+
declare class OrganizationsResource {
|
|
203
|
+
private readonly http;
|
|
204
|
+
constructor(http: HttpClient);
|
|
205
|
+
create(params: CreateOrganizationParams): Promise<Organization>;
|
|
206
|
+
list(): Promise<Organization[]>;
|
|
207
|
+
get(id: string): Promise<Organization>;
|
|
208
|
+
update(id: string, params: Partial<CreateOrganizationParams> & {
|
|
209
|
+
status?: string;
|
|
210
|
+
}): Promise<Organization>;
|
|
211
|
+
/** Cadastra credencial fiscal (certificado A1 é validado no upload) */
|
|
212
|
+
addCredential(organizationId: string, params: CreateCredentialParams): Promise<Credential>;
|
|
213
|
+
}
|
|
214
|
+
declare class WebhookEndpointsResource {
|
|
215
|
+
private readonly http;
|
|
216
|
+
constructor(http: HttpClient);
|
|
217
|
+
/** O `secret` retornado é exibido apenas aqui — guarde-o para verificar assinaturas */
|
|
218
|
+
create(url: string): Promise<WebhookEndpoint>;
|
|
219
|
+
list(): Promise<WebhookEndpoint[]>;
|
|
220
|
+
deactivate(id: string): Promise<{
|
|
221
|
+
id: string;
|
|
222
|
+
active: boolean;
|
|
223
|
+
}>;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
interface VerifyOptions {
|
|
227
|
+
/** Tolerância do timestamp em segundos (padrão: 300) */
|
|
228
|
+
toleranceSeconds?: number;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Verifica a assinatura `x-fiscal-signature: t=<unix>,v1=<hmac>` e retorna o
|
|
232
|
+
* evento parseado. Lança WebhookVerificationError se inválida ou expirada.
|
|
233
|
+
*
|
|
234
|
+
* IMPORTANTE: `rawBody` deve ser o corpo EXATO recebido (Buffer ou string),
|
|
235
|
+
* antes de qualquer JSON.parse — re-serializar quebra o HMAC.
|
|
236
|
+
*/
|
|
237
|
+
declare function verifyWebhook(rawBody: string | Buffer, signatureHeader: string | undefined, secret: string, options?: VerifyOptions): WebhookEvent;
|
|
238
|
+
interface WebhookHandlerOptions extends VerifyOptions {
|
|
239
|
+
secret: string;
|
|
240
|
+
onAuthorized?: (data: WebhookEvent['data'], event: WebhookEvent) => void | Promise<void>;
|
|
241
|
+
onRejected?: (data: WebhookEvent['data'], event: WebhookEvent) => void | Promise<void>;
|
|
242
|
+
onCancelled?: (data: WebhookEvent['data'], event: WebhookEvent) => void | Promise<void>;
|
|
243
|
+
/** Chamado para qualquer evento (além dos handlers específicos) */
|
|
244
|
+
onEvent?: (event: WebhookEvent) => void | Promise<void>;
|
|
245
|
+
/** Erros dos seus handlers (padrão: responde 500 para o gateway retentar) */
|
|
246
|
+
onError?: (error: unknown) => void;
|
|
247
|
+
}
|
|
248
|
+
interface MinimalRequest {
|
|
249
|
+
headers: Record<string, string | string[] | undefined>;
|
|
250
|
+
body?: unknown;
|
|
251
|
+
}
|
|
252
|
+
interface MinimalResponse {
|
|
253
|
+
status(code: number): {
|
|
254
|
+
send(body?: unknown): unknown;
|
|
255
|
+
json(body?: unknown): unknown;
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Handler pronto para Express/Fastify/Nest. A rota PRECISA receber o corpo
|
|
260
|
+
* cru — em Express use:
|
|
261
|
+
*
|
|
262
|
+
* app.post('/hooks/fiscal',
|
|
263
|
+
* express.raw({ type: 'application/json' }),
|
|
264
|
+
* createWebhookHandler({ secret, onAuthorized: ... }),
|
|
265
|
+
* );
|
|
266
|
+
*/
|
|
267
|
+
declare function createWebhookHandler(options: WebhookHandlerOptions): (req: MinimalRequest, res: MinimalResponse) => Promise<void>;
|
|
268
|
+
|
|
269
|
+
declare class FiscalGatewayError extends Error {
|
|
270
|
+
readonly status: number;
|
|
271
|
+
readonly body: unknown;
|
|
272
|
+
constructor(message: string, status: number, body: unknown);
|
|
273
|
+
/** Erro de validação ou regra de negócio — corrigir o payload, não retentar */
|
|
274
|
+
get isClientError(): boolean;
|
|
275
|
+
/** Rate limit — aguarde e retente (o SDK já retenta automaticamente) */
|
|
276
|
+
get isRateLimited(): boolean;
|
|
277
|
+
}
|
|
278
|
+
declare class WebhookVerificationError extends Error {
|
|
279
|
+
constructor(message: string);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
interface FiscalGatewayOptions {
|
|
283
|
+
/** API key da sua account (header x-api-key) */
|
|
284
|
+
apiKey: string;
|
|
285
|
+
/** URL base do gateway (padrão: http://localhost:3333) */
|
|
286
|
+
baseUrl?: string;
|
|
287
|
+
/** Timeout por requisição em ms (padrão: 30000) */
|
|
288
|
+
timeoutMs?: number;
|
|
289
|
+
/** Retentativas automáticas para 429/5xx/rede (padrão: 3) */
|
|
290
|
+
maxRetries?: number;
|
|
291
|
+
/** Implementação customizada de fetch (testes) */
|
|
292
|
+
fetch?: typeof fetch;
|
|
293
|
+
}
|
|
294
|
+
declare class FiscalGateway {
|
|
295
|
+
readonly nfse: NfseResource;
|
|
296
|
+
readonly organizations: OrganizationsResource;
|
|
297
|
+
readonly webhookEndpoints: WebhookEndpointsResource;
|
|
298
|
+
constructor(options: FiscalGatewayOptions);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export { type CreateCredentialParams, type CreateOrganizationParams, type Credential, type Customer, type DanfseResult, type EmitNfseParams, type FiscalEnvironment, type FiscalEvent, FiscalGateway, FiscalGatewayError, type FiscalGatewayOptions, type FiscalProvider, type FiscalSettingsInput, type ListNfseParams, type NfseDocument, type NfseListResult, type NfseStatus, type Organization, type ServiceInfo, type TaxRegime, type VerifyOptions, type WebhookEndpoint, type WebhookEvent, type WebhookEventType, type WebhookHandlerOptions, WebhookVerificationError, type XmlArtifact, createWebhookHandler, verifyWebhook };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
interface HttpClientOptions {
|
|
2
|
+
baseUrl: string;
|
|
3
|
+
apiKey: string;
|
|
4
|
+
/** Timeout por requisição em ms (padrão: 30000) */
|
|
5
|
+
timeoutMs?: number;
|
|
6
|
+
/** Máximo de tentativas para 429/5xx/erros de rede (padrão: 3) */
|
|
7
|
+
maxRetries?: number;
|
|
8
|
+
fetch?: typeof fetch;
|
|
9
|
+
}
|
|
10
|
+
declare class HttpClient {
|
|
11
|
+
private readonly options;
|
|
12
|
+
private readonly fetchFn;
|
|
13
|
+
constructor(options: HttpClientOptions);
|
|
14
|
+
request<T>(method: string, path: string, body?: unknown, query?: Record<string, string | number | undefined>): Promise<T>;
|
|
15
|
+
get<T>(path: string, query?: Record<string, string | number | undefined>): Promise<T>;
|
|
16
|
+
post<T>(path: string, body?: unknown): Promise<T>;
|
|
17
|
+
patch<T>(path: string, body?: unknown): Promise<T>;
|
|
18
|
+
delete<T>(path: string): Promise<T>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type NfseStatus = 'draft' | 'pending' | 'processing' | 'authorized' | 'rejected' | 'cancelled' | 'error';
|
|
22
|
+
type TaxRegime = 'simples_nacional' | 'lucro_presumido' | 'lucro_real' | 'mei';
|
|
23
|
+
type FiscalEnvironment = 'production' | 'restricted_production';
|
|
24
|
+
type FiscalProvider = 'mock' | 'nacional';
|
|
25
|
+
interface Customer {
|
|
26
|
+
name: string;
|
|
27
|
+
/** 11 dígitos (CPF) ou 14 (CNPJ), somente números */
|
|
28
|
+
cpfCnpj?: string;
|
|
29
|
+
email?: string;
|
|
30
|
+
}
|
|
31
|
+
interface ServiceInfo {
|
|
32
|
+
description: string;
|
|
33
|
+
amount: number;
|
|
34
|
+
/** Sobrescreve o código de tributação padrão da organização */
|
|
35
|
+
serviceCode?: string;
|
|
36
|
+
/** Competência da prestação (ISO date) */
|
|
37
|
+
competence?: string;
|
|
38
|
+
}
|
|
39
|
+
interface EmitNfseParams {
|
|
40
|
+
organizationId: string;
|
|
41
|
+
/** ID do seu sistema — chave de idempotência: reenviar nunca duplica a nota */
|
|
42
|
+
externalId: string;
|
|
43
|
+
customer: Customer;
|
|
44
|
+
service: ServiceInfo;
|
|
45
|
+
metadata?: Record<string, unknown>;
|
|
46
|
+
}
|
|
47
|
+
interface NfseDocument {
|
|
48
|
+
id: string;
|
|
49
|
+
organizationId: string;
|
|
50
|
+
externalId: string;
|
|
51
|
+
status: NfseStatus;
|
|
52
|
+
amount: string;
|
|
53
|
+
nfseNumber: string | null;
|
|
54
|
+
accessKey: string | null;
|
|
55
|
+
verificationCode: string | null;
|
|
56
|
+
rejectionReason: string | null;
|
|
57
|
+
createdAt: string;
|
|
58
|
+
/** true quando o POST devolveu um documento já existente (replay idempotente) */
|
|
59
|
+
idempotentReplay?: boolean;
|
|
60
|
+
}
|
|
61
|
+
interface NfseListResult {
|
|
62
|
+
total: number;
|
|
63
|
+
limit: number;
|
|
64
|
+
offset: number;
|
|
65
|
+
items: NfseDocument[];
|
|
66
|
+
}
|
|
67
|
+
interface ListNfseParams {
|
|
68
|
+
organizationId?: string;
|
|
69
|
+
status?: NfseStatus;
|
|
70
|
+
externalId?: string;
|
|
71
|
+
limit?: number;
|
|
72
|
+
offset?: number;
|
|
73
|
+
}
|
|
74
|
+
interface FiscalEvent {
|
|
75
|
+
id: string;
|
|
76
|
+
documentId: string;
|
|
77
|
+
type: string;
|
|
78
|
+
status: string | null;
|
|
79
|
+
payload: Record<string, unknown> | null;
|
|
80
|
+
createdAt: string;
|
|
81
|
+
}
|
|
82
|
+
interface XmlArtifact {
|
|
83
|
+
type: string;
|
|
84
|
+
content: string | null;
|
|
85
|
+
url: string | null;
|
|
86
|
+
checksum: string;
|
|
87
|
+
createdAt: string;
|
|
88
|
+
}
|
|
89
|
+
interface DanfseResult {
|
|
90
|
+
documentId: string;
|
|
91
|
+
type: 'danfse_pdf';
|
|
92
|
+
/** URL pré-assinada (expira) */
|
|
93
|
+
url: string;
|
|
94
|
+
expiresInSeconds: number;
|
|
95
|
+
}
|
|
96
|
+
interface FiscalSettingsInput {
|
|
97
|
+
provider?: FiscalProvider;
|
|
98
|
+
environment?: FiscalEnvironment;
|
|
99
|
+
serviceCode: string;
|
|
100
|
+
serviceDescription: string;
|
|
101
|
+
cnae?: string;
|
|
102
|
+
issRate: number;
|
|
103
|
+
issWithheld?: boolean;
|
|
104
|
+
}
|
|
105
|
+
interface CreateOrganizationParams {
|
|
106
|
+
cnpj: string;
|
|
107
|
+
legalName: string;
|
|
108
|
+
tradeName?: string;
|
|
109
|
+
/** Código IBGE de 7 dígitos */
|
|
110
|
+
cityCode: string;
|
|
111
|
+
state: string;
|
|
112
|
+
municipalRegistration?: string;
|
|
113
|
+
taxRegime: TaxRegime;
|
|
114
|
+
fiscalSettings?: FiscalSettingsInput;
|
|
115
|
+
}
|
|
116
|
+
interface Organization {
|
|
117
|
+
id: string;
|
|
118
|
+
accountId: string;
|
|
119
|
+
cnpj: string;
|
|
120
|
+
legalName: string;
|
|
121
|
+
tradeName: string | null;
|
|
122
|
+
cityCode: string;
|
|
123
|
+
state: string;
|
|
124
|
+
municipalRegistration: string | null;
|
|
125
|
+
taxRegime: TaxRegime;
|
|
126
|
+
status: 'active' | 'inactive' | 'pending_setup';
|
|
127
|
+
fiscalSettings: (FiscalSettingsInput & {
|
|
128
|
+
id: string;
|
|
129
|
+
}) | null;
|
|
130
|
+
createdAt: string;
|
|
131
|
+
updatedAt: string;
|
|
132
|
+
}
|
|
133
|
+
interface CreateCredentialParams {
|
|
134
|
+
type: 'certificate_a1' | 'token' | 'credential';
|
|
135
|
+
/** Para certificate_a1: { pfxBase64, password } — validado no upload */
|
|
136
|
+
payload: Record<string, unknown>;
|
|
137
|
+
expiresAt?: string;
|
|
138
|
+
}
|
|
139
|
+
interface Credential {
|
|
140
|
+
id: string;
|
|
141
|
+
type: string;
|
|
142
|
+
status: string;
|
|
143
|
+
expiresAt: string | null;
|
|
144
|
+
certificate?: {
|
|
145
|
+
subject: string;
|
|
146
|
+
notAfter: string;
|
|
147
|
+
};
|
|
148
|
+
createdAt: string;
|
|
149
|
+
}
|
|
150
|
+
interface WebhookEndpoint {
|
|
151
|
+
id: string;
|
|
152
|
+
url: string;
|
|
153
|
+
/** Exibido apenas na criação — guarde para verificar assinaturas */
|
|
154
|
+
secret?: string;
|
|
155
|
+
active: boolean;
|
|
156
|
+
}
|
|
157
|
+
type WebhookEventType = 'nfse.authorized' | 'nfse.rejected' | 'nfse.cancelled' | (string & {});
|
|
158
|
+
interface WebhookEvent {
|
|
159
|
+
id: string;
|
|
160
|
+
type: WebhookEventType;
|
|
161
|
+
createdAt: string;
|
|
162
|
+
data: {
|
|
163
|
+
documentId: string;
|
|
164
|
+
organizationId: string;
|
|
165
|
+
externalId: string;
|
|
166
|
+
status: NfseStatus;
|
|
167
|
+
nfseNumber: string | null;
|
|
168
|
+
accessKey: string | null;
|
|
169
|
+
rejectionReason: string | null;
|
|
170
|
+
payload: Record<string, unknown> | null;
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
declare class NfseResource {
|
|
175
|
+
private readonly http;
|
|
176
|
+
constructor(http: HttpClient);
|
|
177
|
+
/**
|
|
178
|
+
* Solicita a emissão de uma NFS-e. Assíncrono: retorna `pending` e o
|
|
179
|
+
* resultado final chega via webhook (ou polling com get()).
|
|
180
|
+
* Idempotente por (organizationId, externalId).
|
|
181
|
+
*/
|
|
182
|
+
emit(params: EmitNfseParams): Promise<NfseDocument>;
|
|
183
|
+
get(id: string): Promise<NfseDocument>;
|
|
184
|
+
list(params?: ListNfseParams): Promise<NfseListResult>;
|
|
185
|
+
events(id: string): Promise<FiscalEvent[]>;
|
|
186
|
+
xml(id: string): Promise<XmlArtifact>;
|
|
187
|
+
/** DANFSe (PDF) — retorna URL pré-assinada com expiração */
|
|
188
|
+
pdf(id: string): Promise<DanfseResult>;
|
|
189
|
+
cancel(id: string, reason: string): Promise<{
|
|
190
|
+
id: string;
|
|
191
|
+
status: string;
|
|
192
|
+
}>;
|
|
193
|
+
/**
|
|
194
|
+
* Aguarda o documento sair de pending/processing (polling).
|
|
195
|
+
* Use webhooks em produção; isto é conveniência para scripts e testes.
|
|
196
|
+
*/
|
|
197
|
+
waitForResult(id: string, opts?: {
|
|
198
|
+
intervalMs?: number;
|
|
199
|
+
timeoutMs?: number;
|
|
200
|
+
}): Promise<NfseDocument>;
|
|
201
|
+
}
|
|
202
|
+
declare class OrganizationsResource {
|
|
203
|
+
private readonly http;
|
|
204
|
+
constructor(http: HttpClient);
|
|
205
|
+
create(params: CreateOrganizationParams): Promise<Organization>;
|
|
206
|
+
list(): Promise<Organization[]>;
|
|
207
|
+
get(id: string): Promise<Organization>;
|
|
208
|
+
update(id: string, params: Partial<CreateOrganizationParams> & {
|
|
209
|
+
status?: string;
|
|
210
|
+
}): Promise<Organization>;
|
|
211
|
+
/** Cadastra credencial fiscal (certificado A1 é validado no upload) */
|
|
212
|
+
addCredential(organizationId: string, params: CreateCredentialParams): Promise<Credential>;
|
|
213
|
+
}
|
|
214
|
+
declare class WebhookEndpointsResource {
|
|
215
|
+
private readonly http;
|
|
216
|
+
constructor(http: HttpClient);
|
|
217
|
+
/** O `secret` retornado é exibido apenas aqui — guarde-o para verificar assinaturas */
|
|
218
|
+
create(url: string): Promise<WebhookEndpoint>;
|
|
219
|
+
list(): Promise<WebhookEndpoint[]>;
|
|
220
|
+
deactivate(id: string): Promise<{
|
|
221
|
+
id: string;
|
|
222
|
+
active: boolean;
|
|
223
|
+
}>;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
interface VerifyOptions {
|
|
227
|
+
/** Tolerância do timestamp em segundos (padrão: 300) */
|
|
228
|
+
toleranceSeconds?: number;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Verifica a assinatura `x-fiscal-signature: t=<unix>,v1=<hmac>` e retorna o
|
|
232
|
+
* evento parseado. Lança WebhookVerificationError se inválida ou expirada.
|
|
233
|
+
*
|
|
234
|
+
* IMPORTANTE: `rawBody` deve ser o corpo EXATO recebido (Buffer ou string),
|
|
235
|
+
* antes de qualquer JSON.parse — re-serializar quebra o HMAC.
|
|
236
|
+
*/
|
|
237
|
+
declare function verifyWebhook(rawBody: string | Buffer, signatureHeader: string | undefined, secret: string, options?: VerifyOptions): WebhookEvent;
|
|
238
|
+
interface WebhookHandlerOptions extends VerifyOptions {
|
|
239
|
+
secret: string;
|
|
240
|
+
onAuthorized?: (data: WebhookEvent['data'], event: WebhookEvent) => void | Promise<void>;
|
|
241
|
+
onRejected?: (data: WebhookEvent['data'], event: WebhookEvent) => void | Promise<void>;
|
|
242
|
+
onCancelled?: (data: WebhookEvent['data'], event: WebhookEvent) => void | Promise<void>;
|
|
243
|
+
/** Chamado para qualquer evento (além dos handlers específicos) */
|
|
244
|
+
onEvent?: (event: WebhookEvent) => void | Promise<void>;
|
|
245
|
+
/** Erros dos seus handlers (padrão: responde 500 para o gateway retentar) */
|
|
246
|
+
onError?: (error: unknown) => void;
|
|
247
|
+
}
|
|
248
|
+
interface MinimalRequest {
|
|
249
|
+
headers: Record<string, string | string[] | undefined>;
|
|
250
|
+
body?: unknown;
|
|
251
|
+
}
|
|
252
|
+
interface MinimalResponse {
|
|
253
|
+
status(code: number): {
|
|
254
|
+
send(body?: unknown): unknown;
|
|
255
|
+
json(body?: unknown): unknown;
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Handler pronto para Express/Fastify/Nest. A rota PRECISA receber o corpo
|
|
260
|
+
* cru — em Express use:
|
|
261
|
+
*
|
|
262
|
+
* app.post('/hooks/fiscal',
|
|
263
|
+
* express.raw({ type: 'application/json' }),
|
|
264
|
+
* createWebhookHandler({ secret, onAuthorized: ... }),
|
|
265
|
+
* );
|
|
266
|
+
*/
|
|
267
|
+
declare function createWebhookHandler(options: WebhookHandlerOptions): (req: MinimalRequest, res: MinimalResponse) => Promise<void>;
|
|
268
|
+
|
|
269
|
+
declare class FiscalGatewayError extends Error {
|
|
270
|
+
readonly status: number;
|
|
271
|
+
readonly body: unknown;
|
|
272
|
+
constructor(message: string, status: number, body: unknown);
|
|
273
|
+
/** Erro de validação ou regra de negócio — corrigir o payload, não retentar */
|
|
274
|
+
get isClientError(): boolean;
|
|
275
|
+
/** Rate limit — aguarde e retente (o SDK já retenta automaticamente) */
|
|
276
|
+
get isRateLimited(): boolean;
|
|
277
|
+
}
|
|
278
|
+
declare class WebhookVerificationError extends Error {
|
|
279
|
+
constructor(message: string);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
interface FiscalGatewayOptions {
|
|
283
|
+
/** API key da sua account (header x-api-key) */
|
|
284
|
+
apiKey: string;
|
|
285
|
+
/** URL base do gateway (padrão: http://localhost:3333) */
|
|
286
|
+
baseUrl?: string;
|
|
287
|
+
/** Timeout por requisição em ms (padrão: 30000) */
|
|
288
|
+
timeoutMs?: number;
|
|
289
|
+
/** Retentativas automáticas para 429/5xx/rede (padrão: 3) */
|
|
290
|
+
maxRetries?: number;
|
|
291
|
+
/** Implementação customizada de fetch (testes) */
|
|
292
|
+
fetch?: typeof fetch;
|
|
293
|
+
}
|
|
294
|
+
declare class FiscalGateway {
|
|
295
|
+
readonly nfse: NfseResource;
|
|
296
|
+
readonly organizations: OrganizationsResource;
|
|
297
|
+
readonly webhookEndpoints: WebhookEndpointsResource;
|
|
298
|
+
constructor(options: FiscalGatewayOptions);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export { type CreateCredentialParams, type CreateOrganizationParams, type Credential, type Customer, type DanfseResult, type EmitNfseParams, type FiscalEnvironment, type FiscalEvent, FiscalGateway, FiscalGatewayError, type FiscalGatewayOptions, type FiscalProvider, type FiscalSettingsInput, type ListNfseParams, type NfseDocument, type NfseListResult, type NfseStatus, type Organization, type ServiceInfo, type TaxRegime, type VerifyOptions, type WebhookEndpoint, type WebhookEvent, type WebhookEventType, type WebhookHandlerOptions, WebhookVerificationError, type XmlArtifact, createWebhookHandler, verifyWebhook };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var FiscalGatewayError = class extends Error {
|
|
3
|
+
status;
|
|
4
|
+
body;
|
|
5
|
+
constructor(message, status, body) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "FiscalGatewayError";
|
|
8
|
+
this.status = status;
|
|
9
|
+
this.body = body;
|
|
10
|
+
}
|
|
11
|
+
/** Erro de validação ou regra de negócio — corrigir o payload, não retentar */
|
|
12
|
+
get isClientError() {
|
|
13
|
+
return this.status >= 400 && this.status < 500;
|
|
14
|
+
}
|
|
15
|
+
/** Rate limit — aguarde e retente (o SDK já retenta automaticamente) */
|
|
16
|
+
get isRateLimited() {
|
|
17
|
+
return this.status === 429;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
var WebhookVerificationError = class extends Error {
|
|
21
|
+
constructor(message) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.name = "WebhookVerificationError";
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// src/http.ts
|
|
28
|
+
var RETRYABLE_STATUS = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
|
|
29
|
+
var HttpClient = class {
|
|
30
|
+
constructor(options) {
|
|
31
|
+
this.options = options;
|
|
32
|
+
this.fetchFn = options.fetch ?? globalThis.fetch;
|
|
33
|
+
if (!this.fetchFn) {
|
|
34
|
+
throw new Error("fetch global n\xE3o dispon\xEDvel \u2014 Node 18+ \xE9 obrigat\xF3rio");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
options;
|
|
38
|
+
fetchFn;
|
|
39
|
+
async request(method, path, body, query) {
|
|
40
|
+
const url = new URL(path, this.options.baseUrl);
|
|
41
|
+
for (const [key, value] of Object.entries(query ?? {})) {
|
|
42
|
+
if (value !== void 0) url.searchParams.set(key, String(value));
|
|
43
|
+
}
|
|
44
|
+
const maxRetries = this.options.maxRetries ?? 3;
|
|
45
|
+
let lastError;
|
|
46
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
47
|
+
if (attempt > 0) {
|
|
48
|
+
const backoff = Math.min(1e3 * 2 ** (attempt - 1), 8e3);
|
|
49
|
+
await sleep(backoff + Math.random() * 250);
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
const response = await this.fetchFn(url, {
|
|
53
|
+
method,
|
|
54
|
+
headers: {
|
|
55
|
+
"x-api-key": this.options.apiKey,
|
|
56
|
+
...body !== void 0 && { "content-type": "application/json" }
|
|
57
|
+
},
|
|
58
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0,
|
|
59
|
+
signal: AbortSignal.timeout(this.options.timeoutMs ?? 3e4)
|
|
60
|
+
});
|
|
61
|
+
const text = await response.text();
|
|
62
|
+
const parsed = text ? safeJsonParse(text) : null;
|
|
63
|
+
if (response.ok) return parsed;
|
|
64
|
+
if (RETRYABLE_STATUS.has(response.status) && attempt < maxRetries) {
|
|
65
|
+
lastError = new FiscalGatewayError(
|
|
66
|
+
`HTTP ${response.status} em ${method} ${path}`,
|
|
67
|
+
response.status,
|
|
68
|
+
parsed
|
|
69
|
+
);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const message = parsed?.message ?? `HTTP ${response.status}`;
|
|
73
|
+
throw new FiscalGatewayError(
|
|
74
|
+
Array.isArray(message) ? message.join("; ") : message,
|
|
75
|
+
response.status,
|
|
76
|
+
parsed
|
|
77
|
+
);
|
|
78
|
+
} catch (err) {
|
|
79
|
+
if (err instanceof FiscalGatewayError) throw err;
|
|
80
|
+
lastError = err;
|
|
81
|
+
if (attempt === maxRetries) break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
throw lastError;
|
|
85
|
+
}
|
|
86
|
+
get(path, query) {
|
|
87
|
+
return this.request("GET", path, void 0, query);
|
|
88
|
+
}
|
|
89
|
+
post(path, body) {
|
|
90
|
+
return this.request("POST", path, body);
|
|
91
|
+
}
|
|
92
|
+
patch(path, body) {
|
|
93
|
+
return this.request("PATCH", path, body);
|
|
94
|
+
}
|
|
95
|
+
delete(path) {
|
|
96
|
+
return this.request("DELETE", path);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
function sleep(ms) {
|
|
100
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
101
|
+
}
|
|
102
|
+
function safeJsonParse(text) {
|
|
103
|
+
try {
|
|
104
|
+
return JSON.parse(text);
|
|
105
|
+
} catch {
|
|
106
|
+
return { raw: text };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// src/resources.ts
|
|
111
|
+
var NfseResource = class {
|
|
112
|
+
constructor(http) {
|
|
113
|
+
this.http = http;
|
|
114
|
+
}
|
|
115
|
+
http;
|
|
116
|
+
/**
|
|
117
|
+
* Solicita a emissão de uma NFS-e. Assíncrono: retorna `pending` e o
|
|
118
|
+
* resultado final chega via webhook (ou polling com get()).
|
|
119
|
+
* Idempotente por (organizationId, externalId).
|
|
120
|
+
*/
|
|
121
|
+
emit(params) {
|
|
122
|
+
return this.http.post("/v1/nfse", params);
|
|
123
|
+
}
|
|
124
|
+
get(id) {
|
|
125
|
+
return this.http.get(`/v1/nfse/${id}`);
|
|
126
|
+
}
|
|
127
|
+
list(params = {}) {
|
|
128
|
+
return this.http.get("/v1/nfse", params);
|
|
129
|
+
}
|
|
130
|
+
events(id) {
|
|
131
|
+
return this.http.get(`/v1/nfse/${id}/events`);
|
|
132
|
+
}
|
|
133
|
+
xml(id) {
|
|
134
|
+
return this.http.get(`/v1/nfse/${id}/xml`);
|
|
135
|
+
}
|
|
136
|
+
/** DANFSe (PDF) — retorna URL pré-assinada com expiração */
|
|
137
|
+
pdf(id) {
|
|
138
|
+
return this.http.get(`/v1/nfse/${id}/pdf`);
|
|
139
|
+
}
|
|
140
|
+
cancel(id, reason) {
|
|
141
|
+
return this.http.post(`/v1/nfse/${id}/cancel`, { reason });
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Aguarda o documento sair de pending/processing (polling).
|
|
145
|
+
* Use webhooks em produção; isto é conveniência para scripts e testes.
|
|
146
|
+
*/
|
|
147
|
+
async waitForResult(id, opts = {}) {
|
|
148
|
+
const deadline = Date.now() + (opts.timeoutMs ?? 6e4);
|
|
149
|
+
for (; ; ) {
|
|
150
|
+
const doc = await this.get(id);
|
|
151
|
+
if (doc.status !== "pending" && doc.status !== "processing") return doc;
|
|
152
|
+
if (Date.now() > deadline) {
|
|
153
|
+
throw new Error(`Timeout aguardando resultado da NFS-e ${id} (status: ${doc.status})`);
|
|
154
|
+
}
|
|
155
|
+
await new Promise((resolve) => setTimeout(resolve, opts.intervalMs ?? 1500));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
var OrganizationsResource = class {
|
|
160
|
+
constructor(http) {
|
|
161
|
+
this.http = http;
|
|
162
|
+
}
|
|
163
|
+
http;
|
|
164
|
+
create(params) {
|
|
165
|
+
return this.http.post("/v1/organizations", params);
|
|
166
|
+
}
|
|
167
|
+
list() {
|
|
168
|
+
return this.http.get("/v1/organizations");
|
|
169
|
+
}
|
|
170
|
+
get(id) {
|
|
171
|
+
return this.http.get(`/v1/organizations/${id}`);
|
|
172
|
+
}
|
|
173
|
+
update(id, params) {
|
|
174
|
+
return this.http.patch(`/v1/organizations/${id}`, params);
|
|
175
|
+
}
|
|
176
|
+
/** Cadastra credencial fiscal (certificado A1 é validado no upload) */
|
|
177
|
+
addCredential(organizationId, params) {
|
|
178
|
+
return this.http.post(`/v1/organizations/${organizationId}/credentials`, params);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
var WebhookEndpointsResource = class {
|
|
182
|
+
constructor(http) {
|
|
183
|
+
this.http = http;
|
|
184
|
+
}
|
|
185
|
+
http;
|
|
186
|
+
/** O `secret` retornado é exibido apenas aqui — guarde-o para verificar assinaturas */
|
|
187
|
+
create(url) {
|
|
188
|
+
return this.http.post("/v1/webhook-endpoints", { url });
|
|
189
|
+
}
|
|
190
|
+
list() {
|
|
191
|
+
return this.http.get("/v1/webhook-endpoints");
|
|
192
|
+
}
|
|
193
|
+
deactivate(id) {
|
|
194
|
+
return this.http.delete(`/v1/webhook-endpoints/${id}`);
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// src/webhooks.ts
|
|
199
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
200
|
+
var DEFAULT_TOLERANCE_SECONDS = 300;
|
|
201
|
+
function verifyWebhook(rawBody, signatureHeader, secret, options = {}) {
|
|
202
|
+
if (!signatureHeader) {
|
|
203
|
+
throw new WebhookVerificationError("Header x-fiscal-signature ausente");
|
|
204
|
+
}
|
|
205
|
+
const parts = Object.fromEntries(
|
|
206
|
+
signatureHeader.split(",").map((part) => part.split("=", 2))
|
|
207
|
+
);
|
|
208
|
+
const timestamp = Number(parts.t);
|
|
209
|
+
const signature = parts.v1;
|
|
210
|
+
if (!timestamp || !signature) {
|
|
211
|
+
throw new WebhookVerificationError("Formato de assinatura inv\xE1lido (esperado t=...,v1=...)");
|
|
212
|
+
}
|
|
213
|
+
const tolerance = options.toleranceSeconds ?? DEFAULT_TOLERANCE_SECONDS;
|
|
214
|
+
const ageSeconds = Math.abs(Date.now() / 1e3 - timestamp);
|
|
215
|
+
if (ageSeconds > tolerance) {
|
|
216
|
+
throw new WebhookVerificationError(`Timestamp fora da toler\xE2ncia (${Math.round(ageSeconds)}s)`);
|
|
217
|
+
}
|
|
218
|
+
const body = typeof rawBody === "string" ? rawBody : rawBody.toString("utf8");
|
|
219
|
+
const expected = createHmac("sha256", secret).update(`${timestamp}.${body}`).digest("hex");
|
|
220
|
+
const a = Buffer.from(signature, "hex");
|
|
221
|
+
const b = Buffer.from(expected, "hex");
|
|
222
|
+
if (a.length !== b.length || !timingSafeEqual(a, b)) {
|
|
223
|
+
throw new WebhookVerificationError("Assinatura HMAC inv\xE1lida");
|
|
224
|
+
}
|
|
225
|
+
return JSON.parse(body);
|
|
226
|
+
}
|
|
227
|
+
function createWebhookHandler(options) {
|
|
228
|
+
return async (req, res) => {
|
|
229
|
+
const header = req.headers["x-fiscal-signature"];
|
|
230
|
+
const rawBody = Buffer.isBuffer(req.body) || typeof req.body === "string" ? req.body : JSON.stringify(req.body);
|
|
231
|
+
let event;
|
|
232
|
+
try {
|
|
233
|
+
event = verifyWebhook(rawBody, Array.isArray(header) ? header[0] : header, options.secret, options);
|
|
234
|
+
} catch (err) {
|
|
235
|
+
res.status(400).send({ error: err.message });
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
try {
|
|
239
|
+
switch (event.type) {
|
|
240
|
+
case "nfse.authorized":
|
|
241
|
+
await options.onAuthorized?.(event.data, event);
|
|
242
|
+
break;
|
|
243
|
+
case "nfse.rejected":
|
|
244
|
+
await options.onRejected?.(event.data, event);
|
|
245
|
+
break;
|
|
246
|
+
case "nfse.cancelled":
|
|
247
|
+
await options.onCancelled?.(event.data, event);
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
await options.onEvent?.(event);
|
|
251
|
+
res.status(200).send({ received: true });
|
|
252
|
+
} catch (err) {
|
|
253
|
+
options.onError?.(err);
|
|
254
|
+
res.status(500).send({ error: "handler failed" });
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// src/index.ts
|
|
260
|
+
var FiscalGateway = class {
|
|
261
|
+
nfse;
|
|
262
|
+
organizations;
|
|
263
|
+
webhookEndpoints;
|
|
264
|
+
constructor(options) {
|
|
265
|
+
if (!options.apiKey) throw new Error("apiKey \xE9 obrigat\xF3ria");
|
|
266
|
+
const http = new HttpClient({
|
|
267
|
+
baseUrl: options.baseUrl ?? "http://localhost:3333",
|
|
268
|
+
apiKey: options.apiKey,
|
|
269
|
+
timeoutMs: options.timeoutMs,
|
|
270
|
+
maxRetries: options.maxRetries,
|
|
271
|
+
fetch: options.fetch
|
|
272
|
+
});
|
|
273
|
+
this.nfse = new NfseResource(http);
|
|
274
|
+
this.organizations = new OrganizationsResource(http);
|
|
275
|
+
this.webhookEndpoints = new WebhookEndpointsResource(http);
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
export {
|
|
279
|
+
FiscalGateway,
|
|
280
|
+
FiscalGatewayError,
|
|
281
|
+
WebhookVerificationError,
|
|
282
|
+
createWebhookHandler,
|
|
283
|
+
verifyWebhook
|
|
284
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@creativeproject/fiscal-gateway",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "SDK Node.js/TypeScript do Fiscal Gateway — emissão de NFS-e Padrão Nacional",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/CreativeProjectBR/cp-fiscal-gateway.git",
|
|
9
|
+
"directory": "packages/sdk"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"main": "./dist/index.cjs",
|
|
13
|
+
"module": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"import": "./dist/index.js",
|
|
19
|
+
"require": "./dist/index.cjs"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"files": ["dist", "README.md"],
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=18"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsup src/index.ts --format esm,cjs --dts --clean",
|
|
28
|
+
"test": "vitest run",
|
|
29
|
+
"prepublishOnly": "npm run test && npm run build"
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"keywords": ["nfse", "nota-fiscal", "fiscal", "nfse-nacional", "sdk"],
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"tsup": "^8.0.0",
|
|
37
|
+
"typescript": "^5.6.0",
|
|
38
|
+
"vitest": "^3.0.0"
|
|
39
|
+
}
|
|
40
|
+
}
|