@tmf/platform-core (6.12.0-rc.2)
Installation
@tmf:registry=npm install @tmf/platform-core@6.12.0-rc.2"@tmf/platform-core": "6.12.0-rc.2"About this package
@tmf/platform-core
Base transversal do Modelo A da plataforma TMF: o pacote agnóstico de produto que todo módulo @tmf/* adota para falar com o host e com o Hub. Reúne o HttpClient (fetch nativo com Bearer + X-Client-Code), o cliente do contrato do Hub (createHubContractClient / useAppContract, com suporte a ETag), o ModuleProvider que recebe os ports injetados pelo host (i18n, toast, theme, http) e expõe contract, tenant, user, permissions e featureFlags por contexto, além das permissões (can / usePermission / Gate) e dos adapters opcionais (sonner, react-i18next, design-tokens). Não contém nenhuma lógica de domínio — é só o contrato de integração.
Versão atual:
6.12.0-rc.2— linha gerada depackage.jsonporscripts/gen-packages-table.py; não editar à mão.
Nota:
@tmf/module-kité o nome histórico deste pacote. Hoje todo import usa@tmf/platform-core; o nome antigo só sobrevive como palavra-chave empackage.jsone no checklist legado doUSAGE.md.
Instalação
O pacote é privado e servido pelo registry npm do Forgejo (organização TMF):
npm config set @tmf:registry https://git.trademarketingforce.com/api/packages/TMF/npm/
npm i @tmf/platform-core
react e react-dom (>=18) são peerDependencies obrigatórias. sonner, react-i18next e @tmf/design-tokens são peerDeps opcionais — só necessárias se você importar os adapters correspondentes.
Uso básico
1. HttpClient — createHttpClient
Cliente HTTP sobre fetch nativo (sem axios). Injeta Authorization: Bearer via getToken e X-Client-Code quando clientCode é definido (multi-tenant). Em resposta não-ok lança HttpError (com status, body, url, method); há retry automático para 429/5xx — por padrão 2 tentativas extras, com backoff exponencial, só em GET/HEAD (configurável por retry; maxRetries: 0 desliga):
import { createHttpClient } from '@tmf/platform-core';
const http = createHttpClient({
baseUrl: 'https://hub.trademarketingforce.com',
getToken: () => getMemoryToken() ?? '',
clientCode: 'ACME', // opcional — vira X-Client-Code
});
const tickets = await http.request<Ticket[]>('/api/tickets');
Opções para hosts reais (todas aditivas — request continua devolvendo só o data):
| Opção | Onde | Efeito |
|---|---|---|
credentials |
cliente e request | Repassado ao fetch ('include' p/ cookie). A request sobrescreve o cliente. |
body cru |
request | FormData, Blob, ArrayBuffer/views e URLSearchParams vão sem JSON.stringify e sem Content-Type forçado (o fetch define o boundary). Demais valores seguem como JSON. |
onUploadProgress |
request | O fetch não tem progresso de upload. Quando informado (e XMLHttpRequest existe — só navegador), a request é feita via XHR, com os mesmos headers/credentials/signal/timeout; sem ele, usa fetch. Recebe { loaded, total, progress? }. |
responseType |
request | 'json' (padrão) | 'text' | 'blob' | 'arraybuffer'. |
params |
request | Objeto serializado na query: arrays repetem a chave, undefined/null são omitidos, mescla com ?x= já presente no path. |
timeout (ms) |
cliente e request | Aborta via AbortController, combinado com o signal do chamador. Estouro lança HttpTimeoutError (code: 'ETIMEDOUT'); abort do chamador segue sendo AbortError. |
requestFull |
cliente | Como request, mas devolve { data, status, headers } (headers com nomes em minúsculas). |
retry |
cliente e request | Retry de 429/5xx (padrão: GET/HEAD, 2 tentativas adicionais, backoff 200ms). false desliga (1 chamada); true = padrão; no cliente aceita também { maxRetries, baseDelayMs, methods }; na request, { attempts }. A request sobrescreve o cliente. |
onUnauthorized |
cliente | () => Promise<void | boolean>. Em 401, renova a credencial (getToken passa a devolver o token novo) uma vez para N requests simultâneas; cada uma se repete uma vez. false/rejeição = falha: rejeita com o HttpError 401 original. A repetição com 401 não dispara novo refresh. |
onAuthFailure |
cliente | (error: HttpError) => void, chamado quando o refresh falha — ponto para deslogar. |
HttpError expõe error.response = { status, data, headers } (compatível com quem lê error.response do axios), além de status, body e headers.
const { data, headers } = await http.requestFull<Row[]>('/api/rows', { params: { ids: [1, 2], q: 'x' } });
const pdf = await http.request<Blob>('/api/report', { responseType: 'blob', timeout: 30_000 });
await http.request('/api/upload', { method: 'POST', body: formData, onUploadProgress: (e) => setPct(e.progress ?? 0) });
2. Contrato do Hub — createHubContractClient / useAppContract
Busca o AppContract em GET /api/platform/app-contract, normalizando o wire snake_case do Hub para o AppContract canônico (camelCase). Suporta cache condicional via ETag (If-None-Match → 304):
import { useAppContract } from '@tmf/platform-core';
const { contract, loading, error, refresh } = useAppContract({
baseUrl: 'https://hub.trademarketingforce.com',
getToken: () => getMemoryToken() ?? '',
});
O hook faz refresh automático por contract.ttlSeconds (ou por refreshIntervalMs, se informado; fallback de 60s). Para uso imperativo, use createHubContractClient({ baseUrl, getToken }).fetch(etag).
error não é o sinal de tela de erro — contract === null é. Um refresh em background que falha (aba inativa, token expirado, rede) mantém o contrato anterior e preenche error. O contrato continua válido. Só a 1ª carga sem sucesso deixa contract nulo, e nesse caso o hook já retenta com backoff (5s → 15s → 60s):
const { contract, loading, error } = useAppContract({ baseUrl, getToken });
if (loading) return <Spinner />;
if (!contract) return <Indisponivel />; // ✅ fail-closed só sem contrato
if (error) console.warn('[contract] refresh falhou, usando cache:', error.message);
return <ModuleProvider contract={contract}>…</ModuleProvider>;
// ❌ if (error || !contract) — derruba o módulo quando um refresh falha (TH-64619)
3. Provider — ModuleProvider
Monta o root do módulo com o contrato e os ports injetados pelo host. Aplica theme.cssVars no :root, carrega locale.messages no i18n e cria o http automaticamente se não for passado:
import { ModuleProvider } from '@tmf/platform-core';
import { toast } from 'sonner';
import { createSonnerToastPort } from '@tmf/platform-core/adapters/sonner';
<ModuleProvider
contract={contract}
moduleKey="meu-modulo" // seleciona services[key='meu-modulo'] para o http default
toast={createSonnerToastPort(toast)} // instância do sonner injetada pelo host
getToken={() => getMemoryToken() ?? ''}
>
<MeuModulo />
</ModuleProvider>
moduleKey resolve a baseUrl do http pelo service do próprio módulo em
contract.services[] (key, ou alias de wire module) — sem entrada, cai em ''
(same-origin) com aviso, nunca no service de outro módulo. Omitido, um contrato com um
único service usa esse service; com vários, services[0] (legado). [TH-63406]
Dentro da árvore, consuma via hooks: useModule(), useTenant(), useCurrentUser(), useModuleHttp(), useFeatureFlag(key), useServiceEndpoint(key). Guarde permissões com usePermission('perm') ou <Gate require="perm">…</Gate>.
4. Escopo (ScopeContext v2), seções, boundary e widgets
usePrincipal()devolve{ ref, can, permissions }euseScope()devolve{ tenantId, clientId, principal }— o mesmo vocabulário doScopeContextv2 do back (CONTRATO-MODULO-MONTAVEL §2).cané match exato, sem wildcard nem hierarquia;useClientId()expõe a ref opaca do cliente (vem do gateway do host, nunca do request).permissions.granteddeve ser sempre emitido pelo host comostring[]; ausente/não-array virafalseem toda verificação (fail-closed) e, em desenvolvimento, umconsole.error(hostGrantStateclassifica o estado).ModuleSections/sectionAllowed/allowedSections: autorização de navegação por seção, fail-closed (CONTRATO §2.2).ModuleBoundary(error boundary por módulo, dever D7 do host),ModuleHeader,ModuleSubTabs(submenu segmentado),ModuleVersionFooter+useModuleMeta(versão npm + PyPI) ePACKAGE_VERSION.defineWidget/WidgetShell(também em@tmf/platform-core/widgets): contrato de widgets das libs (ADR-011); widget não recebe prop de acesso.@tmf/platform-core/testingexportamakeMockContracte os ports no-op para testes.
Detalhes e exemplos: INTEGRATION.md §4.
Posição no Modelo A
@tmf/platform-core é a base de todos os módulos da plataforma. No Modelo A, a configuração (tenant, usuário, permissões, tema, locale, feature flags, endpoints de serviço) é servida pelo Hub via app-contract e entregue ao módulo por contexto — o módulo nunca lê localStorage direto nem importa sonner/react-i18next/design-tokens diretamente, apenas via ports/adapters do core.
Isso mantém o pacote agnóstico de produto: ele define o contrato de integração (ports, contrato Hub, permissões, provider) e nenhum comportamento de negócio. Os fronts @tmf/*-front do monorepo consomem o core embutido no bundle (devDependency file:../../platform-core + noExternal no tsup) — ver INTEGRATION.md §3. A conformidade estrutural do próprio pacote está em CONFORMANCE.md.
Dependencies
Development dependencies
| ID | Version |
|---|---|
| @dnd-kit/core | ^6.3.0 |
| @types/react | ^18.2.47 |
| @types/react-dom | ^18.2.18 |
| jsdom | ^25.0.1 |
| sonner | ^1.7.4 |
| typescript | ^5.3.3 |
Peer dependencies
| ID | Version |
|---|---|
| @dnd-kit/core | >=6.0.0 |
| @tmf/design-tokens | >=0.6.0 |
| react | >=18.0.0 |
| react-dom | >=18.0.0 |