@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 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
+ });
@@ -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 };
@@ -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
+ }