npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@be-enlighten/enspace-sdk-core

v0.11.2

Published

Core client for the Enspace API — HTTP, auth strategies, typed errors, resources.

Readme

@be-enlighten/enspace-sdk-core

Cliente HTTP da API do Enspace, agnóstico de framework. Funciona em Node, browser e edge runtimes.

Entrega autenticação (API key, bearer, Keycloak), erros tipados, resources de domínio com tipos derivados da API, streaming SSE, realtime opcional e utilitários para camadas de cache (enspaceKeys) e formulários (toFieldErrors).

Instalação

pnpm add @be-enlighten/enspace-sdk-core

Realtime é opt-in e depende de um peer opcional:

pnpm add pusher-js

Quick start

import { createEnspace } from '@be-enlighten/enspace-sdk-core'

const enspace = createEnspace({
  baseUrl: process.env.ENSPACE_BASE_URL!,
  auth: { type: 'api-key', key: process.env.ENSPACE_API_KEY! },
})

const me = await enspace.account.getProfile()
const workspaces = await enspace.workspaces.list()

// Escopo de workspace por chamada, sem mutar o client:
const members = await enspace.workspaces
  .workspace(workspaces[0]!.reference)
  .members.list()

Configuração

createEnspace(config: EnspaceConfig, strategy?: AuthStrategy): EnspaceClient

| Campo | Tipo | Descrição | |---|---|---| | baseUrl | string | Raiz da API. Obrigatório. | | auth | AuthConfig | União discriminada. Obrigatório. Ver Autenticação. | | workspace | string? | Workspace ativo. Quando setado, todo request envia en-workspace. | | language | string? | Locale enviado como Accept-Language (ex.: pt-BR). Omitido quando ausente. | | retry | RetryConfig? | Política de retry. Ver Retry. | | timeoutMs | number? | Timeout por request. Default 30000. | | hooks | HttpHooks? | Callbacks de observabilidade. Ver Observabilidade. | | realtime | RealtimeConfig? | Habilita enspace.realtime. Ver Realtime. |

baseUrl e auth ausentes lançam Error na criação.

Bodies de respostas 2xx têm strings ISO 8601 revividas como Date, casando com os tipos dos schemas: o tipo diz Date e o runtime entrega Date.

EnspaceClient

| Resource | O que é | |---|---| | account | Perfil do usuário autenticado. | | apiKeys | API Keys do usuário (user-level). | | workspaces | Workspace atual + atalho workspace(id). | | members | Membros do workspace ativo. | | memberGroups | Grupos de usuários. | | invites | Lifecycle de convites (autenticado + público). | | roles | Roles + permissões por role. | | dictionaries | Lexicons de tradução. | | modelViews | Views (kanban/table). | | uploads | Upload de arquivos (File/Blob → multipart). | | types | Content types + type(slug).{items,fields}. | | workflows | Workflows + .executions/.versions/.nodes/.logs. | | ai | .chats/.inference/.agents/.models/.documents/.review, com streaming SSE. | | tasks | Tarefas do workspace + listFormatted(). | | financial | .user/.admin/.workspace(id) — carteiras e pedidos de crédito. | | plans | .admin/.workspace(id) — catálogo de planos, assinaturas e quotas. | | communications | .user/.admin/.workspace(id) — notificações, comments e threads. |

items e fields não são propriedades do client: a rota exige o slug do type. Use enspace.types.type(slug).items e enspace.types.type(slug).fields.

| Método | O que faz | |---|---| | setWorkspace(id) | Define o workspace ativo (header en-workspace). | | getWorkspace() | Retorna o id ativo ou undefined. | | login(credentials) | Login interativo (apenas Keycloak). | | logout() | Limpa tokens e derruba a conexão de realtime. | | request<T>(method, path, options?) | Request crua pelo mesmo pipeline (auth, retry, erros). | | stream(method, path, options?) | Request SSE. Retorna ReadableStream<Uint8Array>. | | getTokenStore() | Acesso read-only ao TokenStore. | | realtime | EnspaceRealtimeClient (lazy). Ver Realtime. | | realtimeEnabled | boolean — se config.realtime foi fornecido. |

Autenticação

api-key

auth: { type: 'api-key', key: 'ens_…' }

Header x-api-key em toda request. Sem refresh.

bearer

auth: { type: 'bearer', token: 'eyJ…' }

Header Authorization: Bearer <token>. Sem refresh.

keycloak

auth: {
  type: 'keycloak',
  url: process.env.KEYCLOAK_URL!,
  realm: 'enspace',
  clientId: 'enspace-app',
  credentials: { username, password },  // opcional — login lazy
}

Resource Owner Password (Direct Grant), com refresh proativo (buffer de 30s), dedup de refresh concorrente, retry automático após refresh e re-login como fallback.

external

Para plugar uma strategy própria, como a KeycloakBrowserStrategy do adapter Vue (SSO por redirect). O segundo argumento de createEnspace passa a ser obrigatório:

import { createEnspace } from '@be-enlighten/enspace-sdk-core'
import { KeycloakBrowserStrategy } from '@be-enlighten/enspace-sdk-vue'

const enspace = createEnspace(
  { baseUrl, auth: { type: 'external' } },
  new KeycloakBrowserStrategy(keycloakInstance),
)

Workspace scoping

Duas formas equivalentes:

// 1) Set once — todas as requests carregam o header
enspace.setWorkspace('ws_abc')
await enspace.members.list()

// 2) Per-call — sem mutar o client (útil para multi-tenant)
await enspace.workspaces.workspace('ws_abc').members.list()

Domain resources

account

await enspace.account.getProfile()
await enspace.account.updateProfile({ fullname: 'Ada Lovelace' })
await enspace.account.updatePassword({ password: 'new-secret-123' })

Métodos: getProfile(), updateProfile(body), updatePassword(body), prepareForCompatibility(version, workspace).

apiKeys

User-level: independente de workspace.

const { key } = await enspace.apiKeys.create({ description: 'CI' })
// `key` é exibida apenas uma vez — persista imediatamente.
const keys = await enspace.apiKeys.list()
await enspace.apiKeys.delete(keys[0]!.id)

Métodos: list(), count(), findOne(id), create(body), update(id, body), delete(id).

workspaces

const list = await enspace.workspaces.list()
const ctx = await enspace.workspaces.context()
const stats = await enspace.workspaces.stats()

enspace.workspaces.setActive('ws_abc')
await enspace.workspaces.workspace('ws_abc').context()

Métodos: list(), context(), stats(), create(body), updateSettings(body), delete(reference), leave(), checkReference(reference), setActive(id), workspace(id).

members

await enspace.members.list({ status: 'active' })
await enspace.members.create({ email: '[email protected]', type: 'standard' })

Métodos: list(), count(), findOne(id), create(body), update(id, body), delete(id), cleanDuplicates(), findDuplicates(), resendInvite(body), resendInvites(), sync(), syncPreview().

memberGroups

const group = await enspace.memberGroups.create({ name: 'Engineering' })
await enspace.memberGroups.addUsers(group.id, { userIds: [1, 2, 3] })
await enspace.memberGroups.removeUsers(group.id, { userIds: [2] })

Métodos: list(), count(), findOne(id), create(body), update(id, body), delete(id), addUsers(id, body), removeUsers(id, body).

invites

const invites = await enspace.invites.list()
const info = await enspace.invites.getInfo('inv_tok_abc')   // rota pública
await enspace.invites.acceptPublic('inv_tok_abc')           // rota pública
await enspace.invites.accept('ws_abc')                      // rota autenticada

Métodos: list(), requestResend() (reenvia os convites pendentes do workspace, sem body), accept(workspace), cancel(workspace), getInfo(token), acceptPublic(token).

roles

const role = await enspace.roles.create({ name: 'Editor' })
await enspace.roles.addPermission(role.id, { action: 'read', subject: 'items' })
const perms = await enspace.roles.listPermissions(role.id)
await enspace.roles.removePermission(role.id, perms[0]!.id)

Métodos: list(), count(), findOne(id), create(body), update(id, body), delete(id), listPermissions(roleId), addPermission(roleId, body), updatePermission(roleId, permissionId, body), removePermission(roleId, permissionId), findDuplicatePermissions(roleId).

dictionaries

const ptBR = await enspace.dictionaries.findByLocale('pt-BR')
await enspace.dictionaries.create({ locale: 'pt-BR', words: { hello: 'olá' } })

Métodos: list(), count(), keys() (retorna DictionaryKeysTree), findByLocale(locale), findOne(id), create(body), update(id, body), delete(id).

modelViews

const views = await enspace.modelViews.list()
await enspace.modelViews.create({ model: 'contratos', model_type: 'kanban' })

Métodos: list(), count(), findOne(id), create(body), update(id, body), delete(id).

uploads

upload() monta o FormData internamente a partir de File/Blob (um arquivo ou um array) e retorna UploadFile[], um por arquivo enviado. Esse é o mesmo formato (UploadFileValue) do valor de campos uploadFile/uploadImage/EnPDF em itens.

const uploaded = await enspace.uploads.upload(file, { path: 'documents/contracts' })
await enspace.uploads.getSignedUrl(uploaded[0]!.id)

// Múltiplos arquivos + vínculo com um registro de entidade
await enspace.uploads.upload([fileA, blobB], {
  fileName: 'anexo.pdf',
  related: { ref: 'c-document-templates', refId: '42', field: 'file' },
})

Opções viram campos multipart: path (sub-path lógico), fieldRef/type/data/userInfo (fluxo EnPDF), related (vínculo com entidade) e fileName (fallback para Blob sem nome).

Filtros de list(): path exato, path_prefix (navegação hierárquica), name_contains, mime_contains.

Métodos: list(), count(), findOne(id), upload(files, options?), delete(id), getSignedUrl(id).

types, items e fields

const ct = await enspace.types.create({ name: 'Contratos', slug: 'contratos' })
const scope = enspace.types.type('contratos')
// scope.items e scope.fields têm o slug pré-bound

Métodos de types: list(), count(), findOne(slug), create(body), update(slug, body), delete(slug), type(slug).

const items = await enspace.types.type('contratos').items.list({ _limit: 20 })
await enspace.types.type('contratos').items.create({ data: { title: 'X' } })
await enspace.types.type('contratos').items.versions('ref_123')

const fields = await enspace.types.type('contratos').fields.list()
await enspace.types.type('contratos').fields.create({
  refId: 'priority',
  type: 'EnlDropdown',
  label: 'Prioridade',
})

Métodos de items: list(), count(), findOne({ reference }), create(body), update({ reference, data }), delete({ reference }), restore({ reference }), duplicate({ reference }), versions(reference), scoped(options), form(identification).

Métodos de fields: list(), findOne(id), create(body), update(id, body), delete(id). refId e type são imutáveis.

workflows

const wf = await enspace.workflows.create({ name: 'Approval', version: '1.0.0', nodes: [], edges: [] })
await enspace.workflows.start(wf.id)
await enspace.workflows.executions.list({ _limit: 10 })
await enspace.workflows.nodes.retry(failedNodeId)
await enspace.workflows.webhookTrigger('wh_ref', 'wh.sk.secret', { payload: 1 })

Métodos: list(), count(), findOne(id), create(body), update(id, body), delete(id), restore(id), start(id, body?), metrics(id, params?), workspaceMetrics(params?), resyncCron(id), getWebhookToken(id), webhookTrigger(reference, token, body?).

Sub-resources: .executions, .versions, .nodes, .logs.

ai

const chat = await enspace.ai.chats.create({ input: 'Olá' })
const { text } = await enspace.ai.inference.generateText({ prompt: 'Resuma' })
const { object } = await enspace.ai.inference.generateObject({
  prompt: 'Extraia dados',
  fields: [{ refId: 'name', type: 'text', label: 'Nome' }],
})
const { inference, agent } = await enspace.ai.models.list()
const doc = await enspace.ai.documents.download('doc_abc')
const stats = await enspace.ai.review.stats.get()
  • .chats: list, findOne, create, update, delete, messages, deleteMessage, clearMessages, respond(reference, body, signal?) → streaming.
  • .inference: generateText, generateObject, editorStream(body, signal?) → streaming de texto.
  • .agents: list, findOne, create, update, delete, restore, tools, testAgent(body, signal?) → streaming.
  • .models: list(){ inference, agent } (modelos permitidos por contexto).
  • .documents: download(reference) → signed URL + metadata.
  • .review: .playbooks (list, findOne, create, update, delete), .sessions (list, findOne, delete), .stats (get()).

Streaming SSE

respond, testAgent e editorStream retornam AsyncIterable<T>, não Promise<T>. Passe um AbortSignal para cancelar o stream em andamento:

const ctrl = new AbortController()
setTimeout(() => ctrl.abort(), 5000)

for await (const chunk of enspace.ai.chats.respond('chat_ref', body, ctrl.signal)) {
  console.log(chunk)
}

tasks

const task = await enspace.tasks.create({ name: 'Revisar', priority: 'high' })
await enspace.tasks.update(task.id, { status: 'completed' })
const formatted = await enspace.tasks.listFormatted({ status: 'pending', _limit: 20 })

Métodos: list(), listFormatted(body?), count(), findOne(id | reference), create(body), update(id, body), delete(id), restore(id).

financial

Três escopos, expostos como namespaces:

// User — operações do próprio usuário, sem header de workspace
await enspace.financial.user.listWallets()
await enspace.financial.user.createCreditRequest({ amount: 100, reason: 'Onboarding' })
await enspace.financial.user.cancelCreditRequest(42)

// Admin — plataforma toda, requer permissão admin
await enspace.financial.admin.listCreditRequests({ status: 'pending' })
await enspace.financial.admin.approveCreditRequest(7, { reviewer_note: 'OK' })
await enspace.financial.admin.createWalletTransaction(walletId, {
  type: 'credit', amount: 50, description: 'Bonus',
})
await enspace.financial.admin.listInvoices({ origin: 'renewal' })
await enspace.financial.admin.voidInvoice(9)

// Workspace — vinculado a um workspace
await enspace.financial.workspace('ws_abc').getWallet()
await enspace.workspaces.workspace('ws_abc').financial.listCreditRequests()
await enspace.workspaces.workspace('ws_abc').financial.listInvoices()
await enspace.workspaces.workspace('ws_abc').financial.findInvoice('inv_ref') // com itens

Credit requests: listCreditRequests, countCreditRequests, findCreditRequest, createCreditRequest, cancelCreditRequest (user/workspace) ou approveCreditRequest/rejectCreditRequest (admin). Wallets: listWallets, countWallets, findWallet, createWallet (user) e getWallet (workspace). Transações: listWalletTransactions, countWalletTransactions, createWalletTransaction (admin). Invoices (faturas de cobrança do plano): listInvoices, findInvoice (workspace, por reference, com itens) e listInvoices, countInvoices, findInvoice (por id, com itens), voidInvoice (admin).

plans

Dois escopos: planos pertencem ao workspace, não ao usuário.

// Admin — catálogo da plataforma
const plan = await enspace.plans.admin.createPlan({
  name: 'Basic', code: 'basic', interval: 'monthly', price_credits: 12300,
})
await enspace.plans.admin.createProduct({ code: 'ai_credits', name: 'Créditos de IA', aggregation: 'sum' })
await enspace.plans.admin.addPlanProduct(plan.id, { product: productId, included_units: 1000, allow_overage: true })
await enspace.plans.admin.createSubscription({ workspaceId: 180, planCode: 'basic' })

// Workspace — catálogo público, assinatura e quotas
const plans = await enspace.plans.workspace('ws_abc').listPlans()
const sub = await enspace.plans.workspace('ws_abc').getCurrentSubscription()
const quotas = await enspace.plans.workspace('ws_abc').listQuotas()
await enspace.plans.workspace('ws_abc').upgrade({ plan_code: 'standard' })

.admin: planos (listPlans, countPlans, findPlan, createPlan, updatePlan, deletePlan — deprecia, nunca hard-delete), produtos (listProducts, countProducts, findProduct, createProduct, updateProduct, deleteProduct), plan-products (listPlanProducts, addPlanProduct — body com product, o plan vem da URL, updatePlanProduct, deletePlanProduct), plan-features (listPlanFeatures, addPlanFeature, updatePlanFeature, deletePlanFeature), subscriptions (listSubscriptions, countSubscriptions, findSubscription, createSubscription, updateSubscription — despache upgrade/downgrade/cancel_pending via action, cancelSubscription), inspeção (getWorkspaceQuotas(workspaceId), getFeaturesRegistry()) e operações em lote (runRenewal(), runReconciliation(), requeueBlockedEmails(body?) — reenvio em massa de e-mails bloqueados por quota).

.workspace(id): listPlans() (catálogo ativo em projeção pública), getFeatures() (feature flags efetivas do plano ativo), listQuotas()/getQuota(product) (used/remaining/limit por produto), getCurrentSubscription(), previewPlanChange(planCode) (classificação upgrade/downgrade + proration + data efetiva), upgrade(body) (imediato, com cobrança prorrateada), downgrade(body) (agendado para o fim do período, seta pending_plan) e cancelPendingChange().

communications

Três escopos, no mesmo padrão de financial:

// User — notificações e preferências do usuário autenticado
await enspace.communications.user.listNotifications({ read: 'false' })
await enspace.communications.user.countUnreadNotifications()
await enspace.communications.user.markNotificationsRead({ references: ['n_1'] })
await enspace.communications.user.markAllNotificationsRead()
await enspace.communications.user.dismissNotification('n_1')
await enspace.communications.user.updateNotificationPreference('comment.mention', { enabled: false })

// Admin — tipos de notificação da plataforma
await enspace.communications.admin.listNotificationTypes({ active: 'true' })
await enspace.communications.admin.createNotificationType({ key: 'comment.mention', name: 'Menção' })

// Workspace — comments e threads
await enspace.communications.workspace('ws_abc')
  .listComments({ entity: 'c-items', entity_reference: '42' })  // entity/entity_reference obrigatórios
await enspace.communications.workspace('ws_abc')
  .createThread({ title: 'Revisão', type: 'discussion' })
await enspace.communications.workspace('ws_abc')
  .addThreadMessage('t_1', { content: 'Primeira mensagem' })

User: listNotifications, listNotificationsExpanded (com type e actor populados), countNotifications, countUnreadNotifications, findNotification, findNotificationExpanded, markNotificationsRead, markAllNotificationsRead, dismissNotification, listNotificationPreferences, updateNotificationPreference.

Admin: listNotificationTypes, countNotificationTypes, findNotificationType, createNotificationType, updateNotificationType, deleteNotificationType.

Workspace: comments (listComments, countComments, findComment, createComment, updateComment, deleteComment), threads (listThreads, countThreads, findThreadsByEntity, findThread, createThread, updateThread, deleteThread) e messages (listThreadMessages, countThreadMessages, addThreadMessage, updateThreadMessage, deleteThreadMessage).

Realtime

Eventos ao vivo sobre Pusher/Soketi. Opt-in: sem config.realtime, enspace.realtimeEnabled é false e o getter enspace.realtime lança EnspaceError com code: 'realtime.not_configured'.

Requer pusher-js@^8 instalado (peer opcional, carregado por import dinâmico no primeiro connect()).

const enspace = createEnspace({
  baseUrl: process.env.ENSPACE_BASE_URL!,
  auth: { type: 'bearer', token },
  realtime: {
    appKey: process.env.ENSPACE_REALTIME_APP_KEY!,
    host: process.env.ENSPACE_REALTIME_HOST,
    port: 6001,
  },
})

| Campo | Default | Descrição | |---|---|---| | appKey | — | Obrigatório. Chave pública da app de websocket. | | host / port | — | Host e porta do servidor de websocket. | | cluster | 'mt1' | Cluster Pusher. | | useTLS | false | Força wss. | | authEndpoint | ${baseUrl}/websocket/auth | Autorização de canais privados. | | userAuthEndpoint | ${baseUrl}/websocket/auth/user | Autenticação do canal de usuário. | | autoSignin | true | Habilita eventos de usuário. | | logToConsole | false | Logs do cliente Pusher. |

A autorização passa pelo mesmo pipeline HTTP do SDK: headers da strategy ativa, refresh automático de token e erros tipados.

import { enspaceChannels, realtimeEvents } from '@be-enlighten/enspace-sdk-core'
import type { EntityEventPayload } from '@be-enlighten/enspace-sdk-core'

const rt = enspace.realtime
await rt.connect()

// Canal privado de entidade
const off = await rt.bindChannel(
  enspaceChannels.tasksList('acme-corp'),
  'task.created',
  (payload: EntityEventPayload) => console.log(payload.entity),
)

// Canal do usuário autenticado
const offUser = await rt.onUserEvent(
  realtimeEvents.user.notificationCreated,
  notification => console.log(notification),
)

off()
offUser()
rt.disconnect()

Métodos: connect(), whenConnected(), disconnect(), subscribe(channel), unsubscribe(channel), bindChannel(channel, event, handler), onUserEvent(event, handler), on(event, handler), getState(), onStateChange(handler). Os binders retornam a função de cleanup.

enspaceChannels monta os nomes de canal — nunca concatene a string na mão:

| Builder | Argumentos | |---|---| | itemsList | (workspaceRef, typeSlug) | | item | (workspaceRef, typeSlug, itemRef) | | tasksList | (workspaceRef) | | task | (workspaceRef, taskRef) | | flowItemTasksList | (workspaceRef) | | flowItemTask | (workspaceRef, taskId) | | thread | (workspaceRef, threadRef) | | comments | (workspaceRef, entityModel, entityRef) | | workspaceLegacy | (workspaceRef) |

realtimeEvents é o catálogo de eventos: items, tasks, flowItemTasks, members, comments, threads (arrays de nomes) e user (objeto com notificationCreated, notificationRead, notificationDismissed, workspaceJoined, workspaceMemberUpdated, workspaceRemoved).

Códigos de erro: realtime.not_configured, realtime.missing_app_key, realtime.pusher_js_missing, realtime.connection_failed, realtime.user_channel_unavailable.

Em Vue e Nuxt, use os composables do subpath /realtime do @be-enlighten/enspace-sdk-vue em vez de manipular o client direto.

Erros

Tudo deriva de EnspaceError, sempre com code, status e details?.

| Classe | Status | Quando | |---|---|---| | NetworkError | 0 | DNS, conexão recusada, timeout. | | AuthError | 401/403 | Token inválido ou expirado. | | ValidationError | 400/422 | Body inválido. | | NotFoundError | 404 | Recurso não existe. | | EnspaceError | outros | 5xx e fallback. |

import { AuthError, isRetryable, NotFoundError, ValidationError } from '@be-enlighten/enspace-sdk-core'

try {
  await enspace.members.create({ email: 'invalid' })
}
catch (e) {
  if (e instanceof ValidationError) console.error(e.code, e.details)
  else if (e instanceof NotFoundError) console.error(e.message)
  else if (e instanceof AuthError) console.warn('Reautentique')
  else if (e instanceof Error && isRetryable(e)) console.warn('Transiente — retry')
}

Retry

  • 5xx, 429 e NetworkError são retentados até retry.maxAttempts (default 3).
  • 401/403 disparam uma retentativa após refresh do token (Keycloak).
  • O header Retry-After (429/503) é honrado como delay mínimo e exposto em EnspaceError.retryAfterMs.
createEnspace({
  baseUrl,
  auth,
  retry: {
    maxAttempts: 4,
    backoff: 'exponential',   // ou 'linear'
    baseDelayMs: 500,
    retryMethods: 'idempotent', // default 'all'
  },
})

retryMethods: 'idempotent' retenta apenas GET/HEAD/OPTIONS/PUT/DELETE. Use quando um 5xx em POST/PATCH puder ter persistido o write no servidor.

Erros de formulário

toFieldErrors normaliza ValidationError (400/422) e ZodError para Record<campo, mensagens[]>:

import { toFieldErrors, ValidationError } from '@be-enlighten/enspace-sdk-core'

try {
  await enspace.tasks.create({ name: '' })
}
catch (err) {
  if (err instanceof ValidationError) {
    toFieldErrors(err) // { name: ['String must contain at least 1 character'] }
  }
}

Cobre o formato de erro da API, variantes com Zod issues cruas e o ZodError de um schema.safeParse client-side. Retorna {} para erros que não são de validação, que devem ser tratados como erro global.

Query params

Todo list() e count() aceita filtros por campo, operadores (_lt, _lte, _gt, _gte, _ne, _in, _nin, _contains, _ncontains, _containss, _ncontainss, _null), paginação (_limit, _start), sort (_sort), busca (_q) e a cláusula composta _where com grupos _or/_and/_nor aninháveis.

await enspace.members.list({
  status: 'active',
  _limit: 20,
  _start: 0,
  _sort: 'created_at:desc',
  _q: 'ana',
  _where: {
    _or: [
      { role_in: [1, 2] },
      { created_at_gte: '2026-01-01T00:00:00.000Z' },
    ],
  },
})

Os params são tipados por modelo via FindQuery<T>/CountQuery<T>, aliases sobre EnlightenQueryParams<T> do @be-enlighten/enspace-sdk-schemas. Valores aceitam T[P] | string, porque a query string é serializada: envie datas em ISO 8601 ('2026-07-21T00:00:00.000Z'), booleans como 'true' | 'false' e números em decimal.

Cache keys

enspaceKeys é uma factory hierárquica de keys para camadas de cache (Pinia Colada, TanStack Query, SWR). TypeScript puro, zero dependência de runtime. Invalidar um prefixo invalida tudo abaixo dele.

import { enspaceKeys } from '@be-enlighten/enspace-sdk-core'

enspaceKeys.tasks.root                          // ['enspace', 'tasks'] — tudo de tasks
enspaceKeys.tasks.lists()                       // só listagens
enspaceKeys.tasks.list({ _limit: 50 })          // listagem com params
enspaceKeys.tasks.count({ status: 'pending' })  // contagens
enspaceKeys.tasks.byId('t_1')                   // registro único

// Sub-resources aninham sob o mesmo prefixo
enspaceKeys.types.items('contratos').byId('i_9')
enspaceKeys.workflows.versions('wf_1').lists()

// Workspace-scoped: `workspace(id).root` é exatamente `workspaces.byId(id)`,
// então invalidar o workspace cascateia para todo o cache escopado nele.
enspaceKeys.workspace('ws_1').tasks.list()

Keys são normalizadas contra fragmentação: list()list({})list({ campo: undefined }). Todos os resources são cobertos, incluindo especializações como roles.permissions(id), dictionaries.byLocale(locale), invites.byToken(token), types.itemVersions(ref), ai.review.*, financial.* e communications.*.

Observabilidade

config.hooks recebe callbacks read-only disparados por tentativa de request (retries disparam novamente). Use para logging, tracing e métricas: mutar options não altera o comportamento do request.

createEnspace({
  baseUrl,
  auth,
  hooks: {
    onRequest: ctx => console.debug(ctx.options.method, ctx.request),
    onResponseError: ctx => reportError(ctx.response?.status, ctx.request),
  },
})

Erros lançados dentro de um hook são engolidos e nunca quebram o request. Hooks são funções, portanto não serializáveis: no adapter Nuxt, que lê a config de runtimeConfig, use setupEnspace do adapter Vue ou construa o client com createEnspace.

Licença

MIT.