@bravophone/webphone 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,479 @@
1
+ # @bravophone/webphone
2
+
3
+ Webphone BRAVOPHONE embutível em qualquer página web. Uma linha de `<script>` e o
4
+ softphone aparece como uma janela flutuante arrastável, com as mesmas
5
+ funcionalidades da extensão de navegador.
6
+
7
+ ```html
8
+ <script src="https://cdn.jsdelivr.net/npm/@bravophone/webphone"></script>
9
+ <script>
10
+ Bravophone.init({ token: 'TOKEN_DO_USUARIO' })
11
+ </script>
12
+ ```
13
+
14
+ ```js
15
+ // ou via npm
16
+ import Bravophone from '@bravophone/webphone'
17
+
18
+ Bravophone.init({ token, position: 'bottom-right' })
19
+ Bravophone.on('call:incoming', ({ number }) => console.log('ligação de', number))
20
+ await Bravophone.call('11987654321')
21
+ ```
22
+
23
+ ---
24
+
25
+ ## A decisão de arquitetura
26
+
27
+ O ponto de partida é uma restrição concreta: **`popup.js` tem 932 KB de build Vue
28
+ minificado e o código-fonte não está disponível.** Recompilar não é uma opção, então
29
+ o projeto foi desenhado para **reaproveitar o bundle exatamente como está**.
30
+
31
+ O levantamento do uso de `chrome.*` no bundle mostrou que isso é viável — a
32
+ superfície é pequena e concentrada:
33
+
34
+ | API | Usos | Tratamento |
35
+ |---|---|---|
36
+ | `chrome.storage.sync` | 58 | shim → `localStorage` |
37
+ | `chrome.storage.local` | 10 | shim → `localStorage` |
38
+ | `chrome.storage.onChanged` | 4 | shim → emissor próprio |
39
+ | `chrome.runtime.onMessage` | 3 | shim → barramento local |
40
+ | `chrome.tabs.create` | 3 | shim → `window.open` |
41
+ | `chrome.windows.*` | 6 | shim → delega ao widget via bridge |
42
+
43
+ São **três superfícies reais**, todas sem estado remoto. Um shim de ~230 linhas cobre
44
+ todas — é o [`host/shim/chrome-shim.js`](host/shim/chrome-shim.js).
45
+
46
+ ### Dois artefatos, não um
47
+
48
+ ```
49
+ ┌─ SITE DO CLIENTE (qualquer origem) ────────────────────────┐
50
+ │ │
51
+ │ <script src="cdn.../@bravophone/webphone"> │
52
+ │ │ │
53
+ │ ▼ │
54
+ │ ┌─ SDK (11 KB) ──────────────────────┐ │
55
+ │ │ Shadow DOM · janela arrastável │ │
56
+ │ │ API pública · ponte postMessage │ │
57
+ │ │ │ │
58
+ │ │ ┌─ <iframe> ──────────────────┐ │ │
59
+ │ │ │ origem FIXA: │ │ │
60
+ │ │ │ webphone.bravophone.com │ │ │
61
+ │ │ │ │ │ │
62
+ │ │ │ chrome-shim.js │ │ │
63
+ │ │ │ libwebphone.js (604 KB) │ │ │
64
+ │ │ │ popup.js (932 KB) │ │ ← bundle intacto │
65
+ │ │ │ guest-bridge.js │ │ │
66
+ │ │ └─────────────────────────────┘ │ │
67
+ │ └────────────────────────────────────┘ │
68
+ └─────────────────────────────────────────────────────────────┘
69
+ ```
70
+
71
+ **O SDK no npm/CDN é leve (11 KB / 4,6 KB gzip).** Todo o peso do webphone fica no
72
+ host e carrega sob demanda, quando o usuário abre a janela.
73
+
74
+ ### Por que iframe, e não montar o Vue direto na página
75
+
76
+ Testei mentalmente as duas rotas; o iframe ganha em quatro frentes de uma vez:
77
+
78
+ 1. **CSS.** O bundle traz Tailwind + `dark-theme.css` globais. Injetado na página do
79
+ cliente, ele quebraria o site do cliente — e o CSS do cliente quebraria o webphone.
80
+ 2. **CORS, e este é o argumento decisivo.** Dentro do iframe, todo request para
81
+ `api.bravophone.com`, `reports.teambravotech.com` e `devices.wavoip.com` sai com
82
+ `Origin: https://webphone.bravophone.com` — **uma origem só, fixa**. Sem iframe,
83
+ cada cliente novo exigiria liberar mais uma origem no CORS de três backends. Com
84
+ iframe, a lista de origens do backend nunca cresce.
85
+ 3. **Microfone.** `allow="microphone"` no iframe é um contrato explícito e auditável.
86
+ 4. **Atualização.** Corrigiu algo no webphone? Republique o host. Todos os clientes
87
+ recebem sem trocar a versão do pacote npm.
88
+
89
+ ---
90
+
91
+ ## O que muda em relação à extensão
92
+
93
+ ### Login: o ponto que exige decisão de produto
94
+
95
+ Este é o único item que **não** tem solução puramente técnica, e vale ler antes de
96
+ começar.
97
+
98
+ Desde o Chrome 115, o *storage partitioning* é padrão: o `localStorage` de um iframe
99
+ cross-origin é **particionado pelo site que o contém**. Na prática, um usuário logado
100
+ no webphone em `clienteA.com` **não** estará logado em `clienteB.com` — mesmo sendo o
101
+ mesmo iframe, o mesmo usuário e a mesma origem. A extensão nunca teve esse problema
102
+ porque tinha um storage único.
103
+
104
+ Três caminhos, em ordem de recomendação:
105
+
106
+ 1. **Token do integrador (recomendado).** O backend do cliente emite um token de sessão
107
+ e passa em `Bravophone.init({ token })`. O SDK entrega ao iframe pela ponte e o
108
+ `guest-bridge` grava onde o bundle já procura (`vxToken`). Sem tela de login, sem
109
+ depender de cookie de terceiros, e é o modelo que Intercom/Twilio usam. Combina bem
110
+ com o fato de que [o `vxToken` é eterno](#) — só logout explícito o encerra.
111
+ 2. **Login em popup window.** `window.open` para a origem do webphone (contexto
112
+ *first-party*, sem partição), token volta por `postMessage`. Bom se não houver
113
+ backend do lado do cliente.
114
+ 3. **Storage Access API.** Exige gesto do usuário e o suporte varia entre navegadores.
115
+ Serve como *fallback*, não como plano principal.
116
+
117
+ O SDK já implementa o caminho 1 de ponta a ponta.
118
+
119
+ ### Funcionalidades que não portam
120
+
121
+ Os ~25 `content-script-*.js` (Pipedrive, HubSpot, Kommo, Salesforce…) injetam
122
+ click-to-call em CRMs de terceiros. **Isso é território exclusivo de extensão** — uma
123
+ biblioteca só roda onde foi incluída.
124
+
125
+ A substituição é a inversão do controle: em vez de o Bravophone entrar no CRM, o CRM
126
+ chama o Bravophone.
127
+
128
+ ```js
129
+ document.querySelectorAll('[data-phone]').forEach((el) => {
130
+ el.onclick = () => Bravophone.call(el.dataset.phone, { source: 'crm', id: el.dataset.id })
131
+ })
132
+ ```
133
+
134
+ Também ficam de fora `contextMenus` (menu de contexto do navegador), `devtools.js` e a
135
+ leitura de clipboard sem gesto do usuário.
136
+
137
+ ### O que se mantém idêntico
138
+
139
+ Registro SIP, áudio WebRTC, supressão de ruído, seleção de rota, histórico, contatos,
140
+ transferência, DTMF, e a **normalização de número** — inclusive a regra de
141
+ [nunca inserir o 9º dígito](#): `dialpad.call()` continua sendo o funil único de
142
+ ligações, então toda essa lógica é exatamente a mesma da extensão.
143
+
144
+ ---
145
+
146
+ ## Estrutura
147
+
148
+ ```
149
+ Bravophone-SDK/
150
+ ├── src/ ← vira o pacote npm (11 KB)
151
+ │ ├── index.js API pública + registro de eventos
152
+ │ ├── widget.js Shadow DOM, iframe, launcher
153
+ │ ├── draggable.js arraste/resize com Pointer Events + persistência
154
+ │ ├── bridge.js RPC postMessage (lado host)
155
+ │ └── styles.js CSS isolado do widget
156
+
157
+ ├── host/ ← vira webphone.bravophone.com (RAIZ do domínio)
158
+ │ ├── index.html gerado pelo sync (ordem de scripts importa)
159
+ │ ├── shim/chrome-shim.js emula chrome.* para o bundle
160
+ │ ├── shim/guest-bridge.js RPC (lado iframe) + eventos + arraste interno
161
+ │ ├── allowed-origins.json origens autorizadas a embutir
162
+ │ ├── popup.js js/ css/ ┐ copiados da extensão pelo sync,
163
+ │ ├── fonts/ images/ ┘ na RAIZ — não versionados (ver abaixo)
164
+ │ ├── mock.html ┐ só desenvolvimento:
165
+ │ └── mock-webphone.js ┘ webphone falso, sem SIP nem backend
166
+
167
+ ├── scripts/
168
+ │ ├── sync-from-extension.mjs copia os assets da extensão
169
+ │ ├── dev-server.mjs duas origens locais (5173 / 5174)
170
+ │ └── smoke-shim.mjs testes do chrome-shim
171
+ ├── types/index.d.ts
172
+ └── examples/
173
+ ├── test.html painel de teste completo (usa o build UMD)
174
+ └── basic.html exemplo mínimo de integração
175
+ ```
176
+
177
+ **A extensão é a fonte da verdade.** Nada copiado é editado à mão. Quando a extensão
178
+ for atualizada:
179
+
180
+ ```bash
181
+ npm run sync # copia popup.js, libwebphone.js, css, fonts… (ignora .bak-*)
182
+ ```
183
+
184
+ O script recusa rodar se um asset obrigatório sumir, em vez de gerar um host quebrado
185
+ silenciosamente.
186
+
187
+ ### Por que os assets ficam na raiz de `host/`, e não num `vendor/`
188
+
189
+ O bundle foi buildado com `__webpack_public_path__ = "/"`. Duas fontes são resolvidas
190
+ por esse caminho absoluto:
191
+
192
+ ```js
193
+ n.p + "fonts/Audiowide-Regular.ttf" // Audiowide — a fonte da marca
194
+ n.p + "fonts/Seguiemj.ttf" // SegoeUIEmoji — os emojis
195
+ ```
196
+
197
+ Sob um subdiretório, esses dois pedidos dão 404 e o navegador cai no fallback
198
+ silenciosamente — sem erro visível, só a tipografia errada, e justamente **depois do
199
+ login**, que é onde a Audiowide aparece. Replicar o layout de URL da extensão faz tudo
200
+ resolver sem tocar no bundle: `npm run sync` termina verificando que as duas fontes
201
+ aterrissaram em `/fonts/`, e falha alto se não.
202
+
203
+ **Consequência de deploy:** o host precisa ficar na **raiz** de um domínio ou
204
+ subdomínio. Para servir sob um subpath, use
205
+ `npm run sync -- --public-path=/embed/` — troca só essa constante no bundle, de forma
206
+ determinística e refeita a cada sync (e aborta se não encontrar exatamente uma
207
+ ocorrência, em vez de adivinhar).
208
+
209
+ ---
210
+
211
+ ## Desenvolvimento
212
+
213
+ ### Pré-requisito: a extensão ao lado
214
+
215
+ Este repositório **não versiona o webphone** — só o SDK e a camada que faz o bundle da
216
+ extensão rodar fora dela. O `popup.js` (932 KB), o `libwebphone.js`, as fontes e os
217
+ `_locales` são copiados da extensão pelo `npm run sync` e ficam fora do git.
218
+
219
+ Clone os dois como irmãos:
220
+
221
+ ```
222
+ algum-diretorio/
223
+ ├── Bravophone/ ← a extensão (fonte da verdade do webphone)
224
+ └── bravophone-sdk/ ← este repositório
225
+ ```
226
+
227
+ ```bash
228
+ git clone https://github.com/teambravotech/bravophone-sdk.git
229
+ cd bravophone-sdk
230
+ npm install
231
+ npm run sync # copia os assets da extensão irmã
232
+ npm run build # gera dist/ (ESM + UMD + sourcemaps)
233
+ npm start # sobe as duas origens de teste
234
+ ```
235
+
236
+ Se a extensão estiver em outro lugar, passe o caminho:
237
+ `npm run sync -- /caminho/para/Bravophone`.
238
+
239
+ Sem o `sync`, o host não tem o que servir — `npm start` sobe, mas o webphone real não
240
+ carrega (o mock em `?host=mock` continua funcionando).
241
+
242
+ ### Scripts
243
+
244
+ | Script | O que faz |
245
+ |---|---|
246
+ | `npm run sync` | Copia os assets da extensão, gera `host/index.html` e `shim/messages.js`, e roda a auditoria de tema |
247
+ | `npm run build` | Gera `dist/` — o que vai para o npm |
248
+ | `npm start` | Sobe as duas origens locais (5173 site, 5174 host) |
249
+ | `npm test` | 100 asserções: shim, geometria da janela e aba de abertura |
250
+ | `npm run audit:theme` | Procura texto invisível no tema escuro |
251
+
252
+ Abra **http://localhost:5173/**.
253
+
254
+ O `npm start` sobe duas portas de propósito — origens diferentes fazem o teste
255
+ exercitar o postMessage cross-origin de verdade, incluindo a validação de origem:
256
+
257
+ | Porta | Papel | Serve |
258
+ |---|---|---|
259
+ | 5173 | site do cliente | `examples/test.html`, carrega `dist/bravophone.umd.cjs` por `<script>`, como no CDN |
260
+ | 5174 | host do webphone | `host/mock.html`, com os headers `frame-ancestors` e `Permissions-Policy` de produção |
261
+
262
+ ### Testar sem SIP nem backend
263
+
264
+ O `host/mock.html` carrega **o `chrome-shim.js` e o `guest-bridge.js` reais** e troca
265
+ só o bundle por [`host/mock-webphone.js`](host/mock-webphone.js), que expõe os mesmos
266
+ dois handles que o guest-bridge procura (`window.dialpad` e `window.libwebphone`).
267
+ Ou seja: o caminho testado é o de produção, sem depender de registro SIP.
268
+
269
+ Dá para verificar ponta a ponta o arraste e o resize, a persistência da posição, os
270
+ comandos (`call`/`hangup`/`mute`/`transfer`…), os eventos de volta, uma chamada
271
+ entrante abrindo a janela sozinha, e o `init({ token })` chegando ao storage via shim —
272
+ o painel do mock mostra o `vxToken` gravado.
273
+
274
+ Para testar contra o **webphone real**, rode `npm run sync` e aponte o `hostUrl` do
275
+ `examples/test.html` para `http://localhost:5174/index.html` em vez de `mock.html`.
276
+
277
+ ```bash
278
+ npm test # 18 asserções sobre o chrome-shim, sem browser
279
+ ```
280
+
281
+ ---
282
+
283
+ ## Deploy
284
+
285
+ ### 1. Host — `webphone.bravophone.com` (raiz)
286
+
287
+ Estático (S3+CloudFront, Vercel, nginx). Três headers importam:
288
+
289
+ ```
290
+ Content-Security-Policy: frame-ancestors 'self' https://clienteA.com https://clienteB.com;
291
+ Permissions-Policy: microphone=(self)
292
+ Cross-Origin-Opener-Policy: same-origin-allow-popups
293
+ ```
294
+
295
+ `frame-ancestors` é o que impede qualquer site de embutir o webphone — deve ser gerado
296
+ a partir de `allowed-origins.json`. É a mesma disciplina de
297
+ [autorização por origem](#) que a extensão já adota; não troque por `*`.
298
+
299
+ Cache: `popup.js`, `js/*`, `fonts/*` com `max-age=31536000` (invalide o CDN a cada
300
+ sync, ou versione por query string); `index.html` sempre com `no-cache`.
301
+
302
+ ### 2. Backends — CORS
303
+
304
+ Liberar **uma única origem** em `api.bravophone.com`, `reports.teambravotech.com` e
305
+ `devices.wavoip.com`:
306
+
307
+ ```
308
+ Access-Control-Allow-Origin: https://webphone.bravophone.com
309
+ Access-Control-Allow-Credentials: true
310
+ ```
311
+
312
+ Como o iframe tem origem fixa, essa lista não cresce com o número de clientes.
313
+
314
+ ### 3. npm
315
+
316
+ ```bash
317
+ npm publish --access public
318
+ ```
319
+
320
+ Disponível em `cdn.jsdelivr.net/npm/@bravophone/webphone` e `unpkg.com` logo após.
321
+ Recomende aos integradores a versão travada — `@bravophone/webphone@0.1` — para que
322
+ um major não quebre a página deles.
323
+
324
+ ---
325
+
326
+ ## API
327
+
328
+ ### `Bravophone.init(options)`
329
+
330
+ | Opção | Tipo | Padrão | Descrição |
331
+ |---|---|---|---|
332
+ | `token` | `string` | — | Token de sessão emitido pelo seu backend |
333
+ | `hostUrl` | `string` | `https://webphone.bravophone.com/embed/` | Origem do webphone |
334
+ | `position` | `string` | `'bottom-right'` | Canto inicial |
335
+ | `open` | `boolean` | `false` | Abrir já visível |
336
+ | `launcher` | `boolean` | `true` | Exibir a aba lateral de abertura |
337
+ | `launcherSide` | `'right' \| 'left'` | `'right'` | Lado em que a aba fica colada |
338
+ | `frame` | `'none' \| 'bar'` | `'none'` | Moldura da janela — ver abaixo |
339
+ | `dockTop` | `'max' \| 'top-half'` | `'max'` | O que arrastar até a borda superior faz |
340
+ | `title` | `string` | `'BRAVOPHONE'` | Texto da barra (só com `frame: 'bar'`) |
341
+
342
+ ### Moldura: preservando 100% da UI
343
+
344
+ Por padrão (`frame: 'none'`) **não há barra de título** — a UI do `popup.js` ocupa a
345
+ janela inteira, exatamente como na extensão. Nenhum pixel é tomado.
346
+
347
+ O arraste continua funcionando porque a detecção do gesto acontece **dentro** do
348
+ iframe, no `guest-bridge.js`: o host é cross-origin e não pode tocar naquele DOM, então
349
+ o gesto viaja como delta pela ponte. Qualquer área que não seja botão, campo ou link
350
+ arrasta a janela; o resto continua clicável, e uma seleção de texto em andamento nunca
351
+ é sequestrada. As coordenadas usam `screenX/screenY` — absolutas na tela, imunes ao
352
+ fato de o próprio iframe estar se movendo durante o arraste.
353
+
354
+ Um botão de fechar aparece sobreposto no canto ao passar o mouse, sem empurrar o
355
+ conteúdo. Recolher para o launcher faz o papel de minimizar.
356
+
357
+ Use `frame: 'bar'` se preferir a barra com título, indicador de estado e controles.
358
+
359
+ ### Métodos
360
+
361
+ **Janela** — `show()` · `hide()` · `toggle()` · `minimize(force?)` · `move(x, y)` ·
362
+ `resize(w, h)` · `dock(zone)` · `destroy()` · `isOpen` · `geometry`
363
+
364
+ ### A aba de abertura
365
+
366
+ Com a janela fechada, o webphone fica acessível por uma **aba colada na lateral** da
367
+ viewport — não um botão circular solto no canto. Ela é arrastável na vertical e
368
+ guarda a posição entre sessões.
369
+
370
+ No repouso mostra só o ícone. No **hover** (ou com foco de teclado) ela expande e
371
+ revela a alça de pontinhos, sinalizando que dá para arrastar.
372
+
373
+ Um detalhe que decide se o componente é agradável ou irritante: **arrastar não abre o
374
+ webphone**. O gesto vira arraste depois de 4px percorridos; abaixo disso continua sendo
375
+ clique. Sem esse limiar, uma tremida de mouse no clique abriria a janela sem querer —
376
+ ou pior, todo arraste terminaria abrindo.
377
+
378
+ A aba também responde a teclado (`Enter` / `Espaço`) e, numa chamada entrante, pulsa em
379
+ vermelho com o contador — visível mesmo com a janela fechada.
380
+
381
+ ```js
382
+ Bravophone.init({ launcherSide: 'left' }) // cola do outro lado
383
+ Bravophone.setLauncherSide('right') // troca em runtime
384
+ Bravophone.init({ launcher: false }) // sem aba: você controla com show()
385
+ ```
386
+
387
+ ### Redimensionar e encaixar
388
+
389
+ A janela redimensiona por **qualquer borda ou canto** — as alças laterais são o que
390
+ permite alargar a janela para o histórico de chamadas respirar. O teto é a viewport,
391
+ não um valor fixo.
392
+
393
+ Dois comportamentos de encaixe, ambos com o mesmo vocabulário do Canva:
394
+
395
+ **Arrastando**, encostar numa região da viewport mostra uma prévia azul do encaixe
396
+ antes de soltar:
397
+
398
+ | Onde o cursor chega | Encaixe |
399
+ |---|---|
400
+ | borda esquerda / direita | altura cheia, largura mantida |
401
+ | **borda inferior** | **metade inferior, largura cheia** |
402
+ | borda superior | maximizado (configurável) |
403
+ | os quatro cantos | meia tela esquerda/direita |
404
+ | qualquer outro lugar | segue flutuando |
405
+
406
+ ```
407
+ left-half │ max │ right-half
408
+ ──────────┼────────────┼──────────
409
+ left │ (flutua) │ right
410
+ ──────────┼────────────┼──────────
411
+ left-half │bottom-half │ right-half
412
+ ```
413
+
414
+ A borda superior é a única disputada: `max` é o gesto universal (Windows, macOS), mas
415
+ quem trabalha com metades verticais costuma preferir a metade de cima ali. Daí a opção
416
+ `dockTop`:
417
+
418
+ ```js
419
+ Bravophone.init({ dockTop: 'top-half' }) // topo encaixa na metade superior
420
+ ```
421
+
422
+ Com ela, o máximo continua acessível por `dock('max')`.
423
+
424
+ Arrastar uma janela encaixada de volta para o meio a solta e devolve o tamanho que ela
425
+ tinha antes — e a janela nasce **sob o cursor**, proporcional a onde você a pegou, em
426
+ vez de saltar.
427
+
428
+ **Redimensionando**, chegar a ~32px de uma borda da viewport completa até ela
429
+ sozinha — o "completamento sugestivo".
430
+
431
+ Programaticamente:
432
+
433
+ ```js
434
+ Bravophone.dock('right') // altura cheia à direita, largura mantida
435
+ Bravophone.dock('right-half') // metade direita (W/2 × altura cheia)
436
+ Bravophone.dock('bottom-half') // metade inferior (largura cheia × H/2)
437
+ Bravophone.dock('top-half') // metade superior
438
+ Bravophone.dock('bottom') // metade inferior, mantendo a largura atual
439
+ Bravophone.dock('max') // maximiza
440
+ Bravophone.dock('float') // solta e restaura o tamanho anterior
441
+
442
+ Bravophone.on('resize', ({ width, height, dock }) => { /* … */ })
443
+ ```
444
+
445
+ O encaixe persiste entre sessões e é **recalculado para a viewport atual** ao recarregar
446
+ — uma janela docada ontem numa tela larga não volta com a geometria de ontem.
447
+
448
+ **Telefonia** — todos retornam `Promise`:
449
+ `call(number, meta?)` · `hangup()` · `answer()` · `mute(on?)` · `hold(on?)` ·
450
+ `sendDTMF(tone)` · `transfer(to)` · `getStatus()` · `setAuth(token)` · `logout()`
451
+
452
+ ### Eventos
453
+
454
+ ```js
455
+ const off = Bravophone.on('call:incoming', (call) => { /* … */ })
456
+ off() // remove o listener
457
+ ```
458
+
459
+ `ready` · `state` · `call:incoming` · `call:answered` · `call:ended` · `call:failed` ·
460
+ `open` · `close` · `error`. Use `'*'` para receber todos como `{ event, payload }`.
461
+
462
+ ---
463
+
464
+ ## Requisitos
465
+
466
+ - **HTTPS obrigatório** no site do cliente — `getUserMedia` só existe em *secure
467
+ context*. `localhost` funciona no desenvolvimento; o SDK avisa no console se detectar
468
+ contexto inseguro.
469
+ - Navegadores com WebRTC e Shadow DOM: Chrome/Edge 88+, Firefox 90+, Safari 14+.
470
+ - O site do cliente **não** pode ter um CSP `frame-src` que bloqueie
471
+ `webphone.bravophone.com` — vale documentar isso no onboarding.
472
+
473
+ ---
474
+
475
+ ## Licença
476
+
477
+ Software proprietário da **BravoTech**. Todos os direitos reservados.
478
+ O pacote npm é publicado para consumo pelos integradores; o código deste repositório
479
+ não é open source.