Vai al contenuto

Widget — Integração técnica

Guia para programadores: script, payload de init(), associação e criação de empresa, ciclo de vida, restauro autenticado (restore_id), CSP e verificação.

Páginas de percurso: Visão geral · B2C · B2B.

Edição de referência: 26 de outubro de 2026. O restauro entre dispositivos acrescenta-se ao payload de empresa. Antes de o ativar, confirma que provisionRestoreId está disponível na tua instalação.

A interface corre num iframe. O site anfitrião não precisa de Angular, de um cliente Supabase nem de acesso à base da Quoting24.


Escolher o tipo de integração

Caso Implementação Continuidade
Site público, visitante desconhecido Só script. Formulário de contacto Sessão de convidado no mesmo browser
Site com dados do cliente Script e init() com perfil e empresa Sessão de convidado pré-preenchida e associação de empresa
Portal com histórico pessoal entre dispositivos Também um backend para external_user_id e restore_id Histórico pessoal no canal widget autorizado

Associar uma empresa e restaurar o histórico são operações distintas. external_id, company_name, o nome, o e-mail e o telefone não autorizam as conversas de outra pessoa. Um utilizador autenticado no site continua a ser um convidado do widget até forneceres uma credencial de restauro válida.


1. Configuração a partir da Quoting24

Em Definições → Canais, abre o canal widget e recupera:

  • URL completo do script e chave pública
  • domínios autorizados, incluindo testes e desenvolvimento local
  • logótipo, cor, posição, saudação, boas-vindas e horários

Usa o URL e a chave do mesmo ambiente. Nos exemplos substitui HOST_WIDGET e CHAVE_PUBLICA_WIDGET. A chave no URL é pública e seleciona o canal: não é uma credencial pessoal nem um segredo de integração de backend.

Os domínios são nomes de host (shop.example.com, localhost), sem https://, porta, caminho ou prefixo *.. example.com autoriza também os subdomínios. Uma lista vazia não aplica restrições. Para o teste local serve a página por HTTP, não como ficheiro.

Valor Onde o conservar Finalidade
Chave pública URL do script Seleciona o canal widget
Segredo privado da integração Só segredos do backend Autoriza criar e reutilizar restore_id
restore_id Arquivo privado por utilizador; na página só com sessão autenticada Indica que histórico restaurar
Credencial de sessão de convidado localStorage do site, por chave de widget, gerida pelo loader Continuidade de convidado no mesmo browser

O restauro exige integração de backend, endpoint privado e segredo, separados do código público. Vê a secção 6.


2. Instalação

Botão flutuante

Uma só vez, antes de </body>:

<script
  src="https://HOST_WIDGET/assets/widget/script.js?key=CHAVE_PUBLICA_WIDGET"
  async
></script>

O botão aparece depois de carregar as definições gráficas. Um visitante novo abre o formulário com telefone e nome facultativo. O loader guarda a credencial de convidado no localStorage, separada por chave. Se o armazenamento estiver bloqueado, a continuidade pode limitar-se à página atual.

O loader expõe um único window.Quoting24Widget. Um segundo script não cria um segundo widget.

Chat incorporado

Contentor antes do script, altura explícita:

<section aria-labelledby="titulo-assistencia">
  <h2 id="titulo-assistencia">Assistência</h2>
  <div id="chat-assistencia" style="width: 100%; height: 600px;"></div>
</section>

<script
  src="https://HOST_WIDGET/assets/widget/script.js?key=CHAVE_PUBLICA_WIDGET&amp;mode=embedded&amp;target=chat-assistencia"
  async
></script>

target é o ID, sem #. O iframe preenche o contentor. Não há botão flutuante. open(), close() e toggle() não têm efeito.

Esperar pelo loader

Para dados pré-preenchidos usa este módulo em vez da etiqueta estática, importado uma só vez no layout principal. Os exemplos são módulos JavaScript: bundler, ou <script type="module" src="/assets/assistencia.js"></script>.

// /assets/quoting24-loader.js
const widgetScriptUrl =
  "https://HOST_WIDGET/assets/widget/script.js?key=CHAVE_PUBLICA_WIDGET";

let widgetPromise;

export function loadWidget() {
  if (window.Quoting24Widget) {
    return Promise.resolve(window.Quoting24Widget);
  }
  if (widgetPromise) return widgetPromise;

  widgetPromise = new Promise((resolve, reject) => {
    const script = document.createElement("script");
    script.id = "quoting24-widget-script";
    script.src = widgetScriptUrl;
    script.async = true;
    script.onload = () => {
      if (window.Quoting24Widget) resolve(window.Quoting24Widget);
      else {
        script.remove();
        reject(new Error("A API do widget não está disponível"));
      }
    };
    script.onerror = () => {
      script.remove();
      reject(new Error("Não foi possível carregar o widget"));
    };
    document.body.appendChild(script);
  }).catch((error) => {
    widgetPromise = undefined;
    throw error;
  });

  return widgetPromise;
}
import { loadWidget } from "/assets/quoting24-loader.js";

const widget = await loadWidget();
widget.init({
  external_id: "52",
  company_name: "Ricambi Stella S.r.l.",
  name: "mario.rossi",
  phone: "+393331234567",
  email: "amministrazione@ricambi-stella.example",
});

Para o modo incorporado acrescenta &mode=embedded&target=chat-assistencia ao URL e monta primeiro o contentor. Não dupliques este carregamento com uma etiqueta script estática.

loadWidget() indica que a API está disponível. init() devolve void: põe o perfil em fila se o iframe não estiver pronto e não espera pela sessão. Para o resultado usa isReady() e hasSessionError().


3. Campos de init()

Todos os valores são cadeias. Omite os desconhecidos. Não envies null, IDs numéricos nem um e-mail vazio. Se um campo ultrapassar os limites ou o e-mail estiver mal formado, o pedido é rejeitado.

Campo Limite e comportamento
external_id Máximo de 100 caracteres. Referência de empresa comparada exatamente com web_code, na organização do widget. Os espaços não são retirados
company_name Máximo de 250 caracteres depois de remover os espaços iniciais e finais. Cria uma empresa em falta só juntamente com external_id. Uma cadeia vazia ou só de espaços não ativa a criação
name Máximo de 250 caracteres. Nome do contacto; pode atualizá-lo na sessão atual
email Endereço válido. Na inicialização pública só é guardado numa empresa nova. Não é escrito no contacto nem numa empresa já existente
phone Máximo de 80 caracteres. Número do contacto; prefere o formato internacional, por exemplo +393331234567. Um conflito com um número existente não junta contactos nem chats
external_user_id Facultativo no caso base. ID estável da pessoa, não vazio, máximo de 100 caracteres após o trim. Por si só não restaura o histórico
restore_id Credencial privada, máximo de 200 caracteres, obtida do backend. Exige o external_user_id correspondente. Usa o valor devolvido sem o modificar

Exemplo quando o site conhece a conta. Os nomes de campo do ERP podem ser diferentes: mapeia a referência da empresa para external_id, a denominação social para company_name, a pessoa para name e o e-mail da conta para email.

widget.init({
  external_id: "52",
  company_name: "Ricambi Stella S.r.l.",
  name: "mario.rossi",
  phone: "+393331234567",
  email: "amministrazione@ricambi-stella.example",
});

As etiquetas Nome e Telefone do formulário enviam-se como name e phone. O e-mail é email. Não envies Nome, Telefone nem Email.

widget.init({
  name: "Mario Rossi",
  phone: "+393331234567",
});
widget.init({
  email: "mario.rossi@ricambi-stella.example",
});

A primeira chamada abre o chat com nome e telefone e não procura uma empresa. A segunda chega com um e-mail válido. No perfil completo acima, phone é o número do contacto e email fica na empresa só se esta chamada a criar.

Mantém name e company_name separados. Não concatenes a denominação social ao nome de utilizador para obter a associação. Não envies um campo web_code: o nome público é external_id. Não uses o UUID interno da Quoting24 nem companies.code no lugar do valor em web_code.

init(profile) substitui o perfil guardado pelo loader. Não faz merge: passa os campos que queres manter.

Um convidado novo pode começar com pelo menos um nome, um telefone, um e-mail válido ou um external_user_id, ou com uma empresa encontrada ou criada através de external_id. init({}) e company_name sozinho não iniciam uma sessão nova. Se enviares apenas um external_id desconhecido, sem company_name, o widget pede os dados através do formulário.

init({ restoreId: "..." }) não transmite a credencial. O campo é restore_id.


4. Associar ou criar a empresa

O fluxo dispara-se quando a Quoting24 processa a inicialização, mesmo sem uma mensagem:

  1. Se recebe um external_id não vazio, procura uma empresa da mesma organização cujo web_code seja igual a esse valor
  2. Se a encontrar, liga o contacto. O nome e o e-mail da empresa existente não mudam
  3. Se não a encontrar e também receber um company_name não vazio, cria a empresa com name = company_name, web_code = external_id e o e-mail se existir, e depois liga o contacto
  4. Se não puder encontrar nem criar a empresa por falta de dados, continua com o perfil disponível. Se não chegar para um convidado novo, pede o formulário. Erros de base de dados ou o limite de pedidos fazem falhar a inicialização
Dados enviados Situação na Quoting24 Resultado
external_id: "52", company_name: "Ricambi Stella S.r.l." Existe web_code = "52" Usa a empresa existente; não muda o nome
Os mesmos dados, mais um e-mail válido Não existe web_code = "52" Cria a empresa com código web 52, denominação social e e-mail, e associa o contacto
external_id: "52", name: "mario.rossi" Nenhuma correspondência Inicia o convidado sem uma associação de empresa nova
external_id: "52", sem outros dados Existe uma correspondência Inicia o convidado associado, com o nome predefinido Visitatore
external_id: "52", sem outros dados Nenhuma correspondência Pede os dados do visitante
company_name sem external_id, mais name Qualquer Inicia o visitante. Não procura nem cria uma empresa

A pesquisa não usa denominação social, e-mail, telefone, NIF nem companies.code. "052" e "52" são valores diferentes. Se importares primeiro o ficheiro, preenche web_code com o mesmo valor que o site vai enviar em external_id.

A associação diz respeito ao contacto. Para um contacto já ligado, um código válido novo pode mudar a empresa. Omitir o código, ou enviar um que não se resolve, não apaga a associação anterior. Na mudança de pessoa ou de conta executa reset(): não te limites a mudar o perfil.

Limites

web_code não tem uma restrição de unicidade. Com várias correspondências seleciona-se a primeira, por data de criação crescente. Evita os duplicados: pedidos simultâneos que ainda não encontram a empresa podem criar mais do que uma. O fluxo não garante uma criação atómica e não substitui uma importação ou uma sincronização a partir do ERP.

A criação implícita está limitada a 20 tentativas por janela de 60 segundos e endereço IP, no processo de backend que trata o pedido. Ao ultrapassar, a inicialização falha com TOO_MANY_REQUESTS. Tenta de novo com espera. Evita ciclos contínuos de init().

Os dados chegam do browser e o visitante pode modificá-los. A associação não prova pertença nem identidade e não concede o histórico da empresa. Envia company_name se quiseres permitir esta criação pública. Para atualizar empresas existentes usa a Quoting24 ou uma integração de ERP autorizada.


5. Login, logout e mudança de conta

Mantém uma única instância na estrutura principal de uma SPA. Uma mudança de página não exige um script novo nem um reset.

Na mudança de pessoa ou de conta, e no logout, chama reset() antes do perfil novo. Num recarregamento com o mesmo login ainda ativo, usa init() sem reset, como no arranque: assim conservas a sessão de convidado guardada. O site tem de distinguir o recarregamento da mudança efetiva.

Chamada nas mudanças de autenticação. profile é o perfil do utilizador novo, ou null no logout.

import { loadWidget } from "/assets/quoting24-loader.js";

let identityRevision = 0;

export async function applyWidgetIdentity(profile) {
  const revision = ++identityRevision;
  const snapshot = profile ? { ...profile } : null;

  window.Quoting24Widget?.reset();
  if (!snapshot) return;

  const widget = await loadWidget();
  if (revision !== identityRevision) return;
  widget.reset();
  widget.init(snapshot);
}

Trata o erro de carregamento na interface do site, com uma nova tentativa. Se recuperas o perfil do backend antes desta função, invalida também esses pedidos na mudança de utilizador. Durante a espera, apaga de imediato o perfil anterior com applyWidgetIdentity(null).

O reset esquece a sessão de convidado desta instância, a credencial guardada, o perfil e o contador de não lidos. Não é uma revogação no servidor e não termina as sessões noutros dispositivos. A mudança de autenticação também tem de ser aplicada nos outros separadores abertos.

Com restauro do histórico

Quando o perfil inclui restore_id, descarrega-o do endpoint autenticado do site depois do reset(), e descarta a resposta se o utilizador mudou entretanto. O recarregamento com o mesmo utilizador reutiliza a credencial já guardada e não deve chamar reset().

import { loadWidget } from "/assets/quoting24-loader.js";

let authRevision = 0;
let pendingRecovery;

// Só no login, no logout e na mudança de utilizador.
// Não a chamar num recarregamento com o mesmo utilizador: reset() descartaria a sessão já em curso.
export async function applyWidgetAuth(user) {
  const revision = ++authRevision;
  const expectedUserId = user ? String(user.id) : null;
  pendingRecovery?.abort();
  const controller = new AbortController();
  pendingRecovery = controller;

  window.Quoting24Widget?.reset();

  try {
    const widget = await loadWidget();
    if (revision !== authRevision) return;
    widget.reset();
    if (expectedUserId === null) return;

    const response = await fetch("/api/widget/session", {
      method: "POST",
      credentials: "same-origin",
      cache: "no-store",
      signal: controller.signal,
      headers: { "Content-Type": "application/json" },
      body: "{}",
    });
    if (!response.ok) throw new Error("O restauro do widget não está disponível");
    const profile = await response.json();
    if (revision !== authRevision) return;
    if (profile.external_user_id !== expectedUserId || !profile.restore_id) {
      throw new Error("Resposta de restauro inesperada");
    }

    const payload = {
      external_user_id: profile.external_user_id,
      restore_id: profile.restore_id,
    };
    for (const key of ["external_id", "company_name", "name", "email", "phone"]) {
      if (profile[key]) payload[key] = profile[key];
    }
    widget.init(payload);
  } catch (error) {
    if (controller.signal.aborted || revision !== authRevision) return;
    console.warn("Chat de assistência: o restauro falhou.", error);
  }
}

O ciclo copia só os campos presentes e não vazios. Um e-mail "" ou um ID numérico não devem ser enviados.

API JavaScript

Método Uso
init(profile) Fornece ou substitui o perfil atual. O loader não funde os campos com um perfil anterior
open() / close() / toggle() Abre, fecha ou alterna o widget flutuante. Fechar não é um logout. No modo incorporado não têm efeito
isReady() A inicialização da sessão de visitante teve sucesso. Continua falso durante o formulário de contacto
hasSessionError() Erro de sessão assinalado pelo iframe
reset() Apaga perfil, sessão local, credencial de convidado guardada e contador de não lidos
dispose() Desmonta a interface e os listeners. Não apaga a credencial de convidado guardada nem o objeto API global

Podes ligar um botão do site a window.Quoting24Widget?.open() quando o botão flutuante tiver aparecido. Não esperes por isReady() para um convidado novo abrir o formulário. Não existe um evento público de disponibilidade. Estes indicadores não descrevem todo o estado da rede.

loadWidget() resolvido não significa que o branding e a sessão estejam prontos.

Desmontagem completa

Se tiveres de retirar o widget, executa dispose(). Se a desmontagem coincidir com o logout, executa primeiro reset(). Antes de voltar a carregar o script, elimina o global e repõe a Promise do loader.

export function removeWidget() {
  window.Quoting24Widget?.reset();
  window.Quoting24Widget?.dispose();
  delete window.Quoting24Widget;
  document.getElementById("quoting24-widget-script")?.remove();
  widgetPromise = undefined;
}

widgetPromise é a definida no módulo do loader. Executa a desmontagem depois de terminar o carregamento. Não a faças ao mesmo tempo que uma nova loadWidget(). Para as mudanças de autenticação usa applyWidgetIdentity() ou applyWidgetAuth(), que esperam pelo loader.

O reset local não revoga restore_id no servidor.


6. Restauro autenticado

A sessão de convidado lê a sua conversa mais recente mesmo depois de a fechar. As conversas visíveis da mesma pessoa noutro browser exigem juntos external_user_id e restore_id.

widget.init({
  external_id: "52",
  company_name: "Ricambi Stella S.r.l.",
  name: "Mario Rossi",
  phone: "+393331234567",
  email: "mario.rossi@ricambi-stella.example",
  external_user_id: "1042",
  restore_id: session.restore_id,
});

O restauro não junta sessões de convidado antigas e não partilha o histórico entre colegas da mesma empresa.

Endpoint no backend do site

Exemplo a implementar no portal, não um URL da Quoting24.

POST /api/widget/session
Content-Type: application/json

{}
{
  "external_user_id": "1042",
  "external_id": "52",
  "company_name": "Ricambi Stella S.r.l.",
  "name": "Mario Rossi",
  "phone": "+393331234567",
  "email": "mario.rossi@ricambi-stella.example",
  "restore_id": "<credencial-devolvida-pela-quoting24>"
}

Define Cache-Control: no-store. Identidade e empresa só a partir da sessão do servidor. Aplica a auth e o CSRF da aplicação. Não registes corpos que contenham a credencial.

Se o portal já expõe um endpoint autenticado que devolve o restore_id do utilizador atual para este widget, usa essa resposta e combina-a com o ID da pessoa e a referência da empresa.

Provisioning de servidor para servidor

Caminho típico: /fastify/trpc/widget.provisionRestoreId
(ou /trpc/widget.provisionRestoreId se o backend estiver exposto diretamente).

POST /fastify/trpc/widget.provisionRestoreId
Authorization: Bearer <SEGREDO_INTEGRACAO>
Content-Type: application/json

{
  "widget_api_key": "<uuid-publico-do-widget>",
  "external_user_id": "1042",
  "external_id": "52",
  "name": "Mario Rossi",
  "phone": "+393331234567",
  "email": "mario.rossi@ricambi-stella.example",
  "attempt_id": "1042"
}
{
  "result": {
    "data": {
      "restore_id": "<credencial-opaca>"
    }
  }
}

Os valores gerados começam por q24-restore-v1:. Não os inventes nem os derives de IDs públicos.

Entrada Requisito
widget_api_key UUID obrigatório, autorizado para o segredo
external_user_id ID de pessoa estável, igual ao de init()
attempt_id Inteiro positivo estritamente crescente, como cadeia decimal, máximo 9223372036854775807
saved_restore_id Credencial já guardada. Omite-a no primeiro provisioning
external_id, name, email, phone Facultativos, com os mesmos limites que init()

Padrão ensure, recupera ou cria:

  1. Autentica o utilizador e adquire um bloqueio sobre a linha de restauro, numa transação
  2. Atribui um attempt_id novo a partir de uma sequência depois do bloqueio (CACHE 1, não cíclica; a sequência sobrevive ao rollback)
  3. Chama a Quoting24 com attempt_id e, se existir, saved_restore_id
  4. Guarda result.data.restore_id e faz commit
  5. Só depois do commit devolve a credencial ao browser

Um saved_restore_id válido tem de ser reutilizado. Omiti-lo quando já existe uma chave válida substitui a chave e invalida as sessões restauradas. Não geres uma chave nova em cada visualização de página.

Se o provisioning remoto tiver sucesso e a gravação local falhar, devolve um erro. Não devolvas a chave não guardada. Tempos limite reduzidos.

company_name envia-se em init(). O corpo de provisioning acima não o inclui: a associação e a criação da empresa, descritas na secção 4, ocorrem quando o browser inicializa a sessão. O e-mail da inicialização pública só é escrito numa empresa nova.

Resultado no widget

A Quoting24 verifica a credencial e carrega o histórico do widget visível desse utilizador. Os chats já fechados podem aparecer como conversa anterior: consultá-los não reabre a conversa antiga; o envio fica na conversa atual.

Uma credencial inválida, de outro utilizador ou revogada é rejeitada, sem conversão automática em sessão de convidado. O botão «Riprova» do widget não pede por si só um restore_id novo. A interface atual está em italiano.


7. Página, rede e aspeto

  • Uma só instância por página
  • CSP. Permite a origem do widget em script-src e frame-src, e a origem de API indicada para a instalação em connect-src (carregamento das definições gráficas). Autoriza as imagens configuradas em img-src. O loader insere estilos em linha e não expõe uma opção pública de nonce: verifica a CSP do site. O CSS da página não modifica o interior do iframe de outra origem
  • Ligações do iframe. Tem de alcançar a API, Realtime/WebSocket e o armazenamento da própria instalação. Para filtros de rede usa os hosts efetivos do ambiente
  • Aspeto e idioma. Logótipo, cor, posição e mensagens configuram-se na Quoting24. company_name é a denominação social do cliente. Não existe um parâmetro público language ou locale; a interface atual está em italiano
  • Telemóvel. O chat flutuante pode ocupar o viewport. No modo incorporado o site controla altura e largura
  • Anexos e horários. PDF, DOC/DOCX, JPG/JPEG e PNG. Até 10 MiB por ficheiro e 100 MiB de anexos referenciados por conversa. Fora de horário o widget mostra o banner e deixa disponível a composição

8. Verificar a integração

Usa uma conta e um canal de teste. Confirma o ficheiro na Quoting24, além do chat.

Prova Resultado esperado
Browser novo, só script Formulário de contacto, depois conversa
Payload completo com external_id já presente como web_code Contacto associado à empresa existente; nome e e-mail da empresa inalterados
Payload completo com código novo Empresa criada com código web, denominação social e e-mail; contacto associado
Código novo e nome, sem company_name Chat iniciado, nenhuma empresa criada
Só código novo Formulário de contacto
Só denominação social e nome Nenhuma empresa procurada ou criada por nome
E-mail vazio, ID numérico ou denominação social demasiado longa Payload rejeitado
Segundo init() na mesma sessão Continua a sessão de convidado. O e-mail e o telefone não recuperam o chat de outra pessoa
Recarregamento, navegação SPA, telemóvel Uma só instância; continuidade de convidado se houver armazenamento; layout utilizável
Logout ou mudança de conta durante o carregamento Sessão anterior apagada; os resultados tardios não restauram o perfil antigo
O mesmo utilizador autenticado, outro browser, restore_id válido Histórico dessa pessoa
Dois utilizadores da mesma empresa Credenciais e históricos distintos
Convidado e depois restauro reset() pelo meio; as sessões não se juntam
Credencial inválida ou revogada Erro, sem recurso automático a convidado
Conversas anteriores Só consulta; o envio fica na atual
Domínio não autorizado Acesso rejeitado
Rede interrompida Reconexão; rascunho conservado se o envio falhar

Resolução de problemas

Sintoma Verificação
O botão não aparece URL, chave, canal ativo, domínios, CSP, pedido widget.getBranding. O só global JavaScript não prova que o botão exista. open() não tem efeito até o branding carregar, e não tem efeito no modo incorporado
Quoting24Widget não está definido Espera pelo carregamento. Verifica o parâmetro key
O chat incorporado é invisível Contentor antes do loader, ID correto, altura maior do que zero
Domain not allowed Nome de host do site, sem esquema, porta nem caminho. Verifica também os testes. A página tem de ser servida por HTTP
Formulário de telefone apesar do perfil Ordem assíncrona, snake_case, valores vazios, reset() anterior, perfil insuficiente para um convidado novo
A empresa não é encontrada Compara external_id com web_code na mesma organização, espaços e zeros iniciais incluídos
A empresa em falta não é criada São precisos external_id e company_name ambos não vazios. Verifica a validação e o limite de criação
O nome ou o e-mail da empresa não mudam Previsto: init() cria as empresas em falta e não atualiza as existentes
O e-mail não aparece no contacto Previsto na inicialização pública: o e-mail só se usa ao criar a empresa
TOO_MANY_REQUESTS Espera e tenta de novo. Evita chamadas repetidas ou importações em massa através do widget
Falta o histórico noutro dispositivo O payload de empresa não o restaura. São precisos external_user_id e restore_id
Sessões restauradas invalidadas As chamadas ensure têm de reenviar saved_restore_id. Não emitas uma chave nova em cada visualização de página
«Riprova» não chega Um restore_id novo a partir do teu endpoint, depois reset() e init()
Depois de uma mudança de conta fica o chat antigo reset() de imediato na mudança de identidade. Descarta as respostas assíncronas anteriores
Depois de dispose() o script não recria o widget Elimina o global e o script, repõe a Promise do loader e volta a carregar
A interface continua em italiano Nenhuma opção pública language ou locale no loader

Referências de implementação

  • Loader e API JavaScript: @frontend/src/public/widget/script.ts
  • Inicialização, associação de empresa e criação implícita: @backend/fastify/routes/widget/initialize.ts
  • Testes de inicialização e empresas: @backend/fastify/__tests__/widget/widgetInitialize.test.ts
  • Domínio e canal: @backend/fastify/routes/widget/procedures/publicWidgetProcedure.ts
  • Provisioning de restore_id: @backend/fastify/routes/widget/provisionRestoreId.ts