@authyon/server 0.2.0-beta.20 → 0.2.0-beta.22

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 CHANGED
@@ -1,473 +1,541 @@
1
- # @authyon/server
2
-
3
- SDK server-side para o [Authyon](https://authyon.com) — administração de ambiente/tenants (OAuth client-credentials) e verificação de access token (publishable key). Nunca importe este pacote em código de browser.
4
-
5
- ## Instalação da beta
6
-
7
- ```bash
8
- npm install --save-exact @authyon/server@0.2.0-beta.4
9
- ```
10
-
11
- Durante a beta, `npm install @authyon/server@beta` acompanha o prerelease mais recente. Fixar a versão exata é recomendado para builds reproduzíveis.
12
-
13
- O pacote possui uma condição de exportação `browser` que falha imediatamente se um bundler tentar incluí-lo no frontend. URLs remotas exigem HTTPS e cada request tem timeout padrão de 15 segundos, configurável por `timeoutMs`.
14
-
15
- O client aceita o mesmo contrato `HttpAdapter` de `@authyon/auth`, útil para tracing, mocks e stacks HTTP próprias:
16
-
17
- ```ts
18
- import { createClient, FetchHttpAdapter } from "@authyon/server";
19
-
20
- const authyon = createClient({
21
- envKey: process.env.AUTHYON_ENV_KEY,
22
- httpAdapter: new FetchHttpAdapter(globalThis.fetch),
23
- });
24
- ```
25
-
26
- Quando houver credenciais, adapter e observabilidade, o builder mantém cada etapa explícita:
27
-
28
- ```ts
29
- const authyon = new AuthyonServerClientBuilder()
30
- .withEnvironmentKey(process.env.AUTHYON_ENV_KEY)
31
- .withEnvironmentCredentials(process.env.AUTHYON_CLIENT_ID, process.env.AUTHYON_CLIENT_SECRET)
32
- .withHttpLogger(httpLogger)
33
- .build();
34
- ```
35
-
36
- O client também aceita `httpLogger: { enabled, logger? }`. A configuração é mutável, portanto `enabled` pode ser alterado em runtime. Os eventos não incluem headers, bodies nem valores de query string, e erros do logger nunca interrompem requests.
37
-
38
- `AuthyonError`, `ErrorCodes` e `error.interpret()` também são exportados por este pacote. Use `category`, `action`, `retryable`, `retryAfter` e `requestId` para implementar middleware e observabilidade sem comparar mensagens humanas.
39
-
40
- ## Sessão web no BFF (código ainda não publicado)
41
-
42
- O novo entrypoint `@authyon/server/bff` oferece login, 2FA, sessão HttpOnly,
43
- renovação sob demanda, logout e troca de organização. Os tokens ficam no servidor,
44
- com adaptador Redis cifrado e compare-and-swap para concorrência entre instâncias.
45
- Reutiliza os endpoints existentes e o cliente de validação server-side.
46
-
47
- Veja [contrato e integração](../../docs/bff.md) e [análise/plano](../../docs/bff-session-plan.md).
48
- O entrypoint padrão e `@authyon/server/next` mantêm seu comportamento anterior;
49
- a adoção do transporte BFF pelo consumidor é explícita.
50
-
51
- ## Validação local com JWKS
52
-
53
- Para APIs de alto volume, crie uma vez um verificador com discovery automático:
54
-
55
- ```ts
56
- const verifier = await authyon.createJwksTokenVerifier({
57
- audience: process.env.AUTHYON_AUDIENCE,
58
- algorithms: ["RS256"],
59
- });
60
-
61
- const { claims } = await verifier.verifyAccessToken(token);
62
- ```
63
-
64
- Ele valida assinatura, algoritmo, issuer, audience, tipo e claims temporais, com cache e rotação de chaves. Use `verification: "jwks"` em `authorizeRequest()` ou `createExpressAuthorizationMiddleware()`. Para ações sensíveis que precisam detectar revogação imediatamente, mantenha o padrão `validate`.
65
-
66
- ## Instalação
67
-
68
- ```bash
69
- npm install @authyon/server
70
- ```
71
-
72
- ## Chaves e credenciais
73
-
74
- | Credencial | Uso |
75
- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
76
- | `envKey` (`pk_...`) | Selecionado em **toda** chamada via `X-Authyon-Environment` (Test/Live). Não é sensível — é a mesma chave do frontend, mas sozinha não autoriza nada. |
77
- | `clientId` / `clientSecret` (ambiente) | Credenciais OAuth client-credentials de nível ambiente, mintadas no console. O SDK troca por um access token (`POST /env/oauth/token`) e o renova sozinho, usado em todo `environment.*` / `permissions.*`. **Nunca** exponha ao navegador. |
78
- | `clientId` / `clientSecret` (tenant) | Credenciais por-tenant, usadas via `authyon.tenant({ clientId, clientSecret })` para as chamadas `TenantManagement` (`/tenant/...`). |
79
-
80
- ```ts
81
- import { createClient } from "@authyon/server";
82
-
83
- const authyon = createClient({
84
- envKey: process.env.AUTHYON_ENV_KEY, // pk_live_...
85
- clientId: process.env.AUTHYON_CLIENT_ID, // ec_live_...
86
- clientSecret: process.env.AUTHYON_CLIENT_SECRET,
87
- });
88
- ```
89
-
90
- > `@authyon/auth` também exporta um `createClient`. Se algum dia precisar dos dois no mesmo arquivo, use um alias no import: `import { createClient as createServerClient } from "@authyon/server"`.
91
-
92
- ## Verificação de token
93
-
94
- ```ts
95
- const { active, sub } = await authyon.introspect(token); // leve
96
- const { user, organization } = await authyon.validate(token); // recomendado — cross-checa revogação
97
- ```
98
-
99
- Veja [`examples/tokenVerification.ts`](./examples/tokenVerification.ts) para um middleware completo.
100
-
101
- ## Administração de ambiente (`environment.*`)
102
-
103
- Usa as credenciais de ambiente (`clientId`/`clientSecret`) automaticamente — o SDK minta e renova o token sozinho.
104
-
105
- ```ts
106
- const org = await authyon.environment.tenants.create({ name: "Acme", slug: "acme" });
107
- const tenants = await authyon.environment.tenants.list({
108
- search: "acme",
109
- skip: 0,
110
- take: 20,
111
- });
112
- await authyon.environment.tenants.members.add(org.id, userId, ["owner"]);
113
- await authyon.environment.tenants.members.assignRole(org.id, userId, "billing-admin");
114
- await authyon.environment.tenants.members.remove(org.id, userId);
115
-
116
- await authyon.environment.users.list({ search: "acme.com" });
117
- await authyon.environment.roles.create({ name: "support", permissions: ["tickets:read"] });
118
- await authyon.environment.audit.list({ take: 50 });
119
- ```
120
-
121
- Veja [`examples/organizationMembership.ts`](./examples/organizationMembership.ts) para o fluxo completo, incluindo as rotas de backend que o `@authyon/auth` chamaria.
122
-
123
- Namespaces disponíveis: `environment.users`, `environment.tenants` (com `.members` e `.roles` aninhados), `environment.roles`, `environment.permissions`, `environment.audit`. OTP e aprovações ficam em `security` (veja abaixo).
124
-
125
- ## Autorização local
126
-
127
- `createAuthyonAbility()` transforma o resultado de `validate()`, `introspect()` ou qualquer objeto com `permissions`, `roles` e `scope` em verificações locais no estilo CASL:
128
-
129
- ```ts
130
- import { createAuthyonAbility } from "@authyon/server";
131
-
132
- const identity = await authyon.validate(token);
133
- const ability = createAuthyonAbility(identity);
134
-
135
- if (!ability.can("read", "reports")) {
136
- throw new Error("Forbidden");
137
- }
138
- ```
139
-
140
- Regras condicionais e de domínio podem ser acrescentadas no segundo argumento. O mecanismo é deny-by-default e não substitui a validação do token nem a autorização aplicada pela API.
141
-
142
- ### Next.js Server Side
143
-
144
- Em Route Handlers, Server Actions ou SSR, mantenha `@authyon/server` em um módulo marcado com `server-only`:
145
-
146
- ```ts
147
- import "server-only";
148
- import { createAuthorizationErrorResponse, authorizeRequest, createClient } from "@authyon/server";
149
-
150
- const authyon = createClient({
151
- envKey: process.env.AUTHYON_ENV_KEY,
152
- clientId: process.env.AUTHYON_CLIENT_ID,
153
- clientSecret: process.env.AUTHYON_CLIENT_SECRET,
154
- });
155
-
156
- export async function GET(request: Request) {
157
- try {
158
- const auth = await authorizeRequest(authyon, request, {
159
- requirement: { action: "read", subject: "reports" },
160
- });
161
- return Response.json({ userId: auth.userId });
162
- } catch (error) {
163
- return createAuthorizationErrorResponse(error);
164
- }
165
- }
166
- ```
167
-
168
- ### Express e frameworks Connect
169
-
170
- O middleware não adiciona Express como dependência e disponibiliza `request.authyon`:
171
-
172
- ```ts
173
- import { createExpressAuthorizationMiddleware } from "@authyon/server";
174
-
175
- app.get(
176
- "/reports",
177
- createExpressAuthorizationMiddleware(authyon, {
178
- requirement: { action: "read", subject: "reports" },
179
- }),
180
- (request, response) => response.json({ userId: request.authyon.userId }),
181
- );
182
- ```
183
-
184
- Use `authorizeToken()` em NestJS, Fastify, Hono, tRPC ou integrações próprias. `passErrorsToNext: true` encaminha erros ao middleware de erros do Express.
185
-
186
- ### IP real do cliente
187
-
188
- Chamadas server-side propagam `clientIp` em `X-Forwarded-For`. O middleware Express usa o IP do socket por padrão; atrás de um proxy confiável, configure o próprio Express e use `trustProxy: true`. Para Next.js, passe `resolveClientIp` ao `authorizeRequest()` lendo somente headers que sua infraestrutura sobrescreve. Não confie cegamente em `X-Forwarded-For` recebido da internet.
189
-
190
- ```ts
191
- await authyon.validate(token, { clientIp: "203.0.113.10" });
192
- ```
193
-
194
- ## Segurança (`security.*`)
195
-
196
- OTP e aprovações ficam agrupados em `authyon.security`. Exige uma credencial de ambiente
197
- com o escopo `authyon:otp:verify` (para `security.otp`) e/ou `authyon:financial:authorize`
198
- (para `security.approvals`). Guia completo:
199
- [`docs/security-otp-approvals.md`](../../docs/security-otp-approvals.md).
200
-
201
- ```ts
202
- // security.otp — só validar o código do app autenticador
203
- const result = await authyon.security.otp.check(customerId, code);
204
- if (!result.valid) console.log(result.attemptsRemaining, result.sessionsRevoked);
205
-
206
- // security.approvals — aprovação presa a um payload livre e de uso único
207
- const { id } = await authyon.security.approvals.create({
208
- subjectId: customerId,
209
- payload: { type: "pix", amount: 150.5, to: "maria@example.com" },
210
- });
211
- // ...cliente confirma com @authyon/auth → security.approvals.confirm
212
- const { payload } = await authyon.security.approvals.consume(id);
213
- ```
214
-
215
- | Método | Endpoint |
216
- | ------------------------------------ | --------------------------------------- |
217
- | `security.otp.check(userId, code)` | `POST /env/users/{userId}/otp/verify` |
218
- | `security.approvals.create(input)` | `POST /env/authorizations` |
219
- | `security.approvals.get(id)` | `GET /env/authorizations/{id}` |
220
- | `security.approvals.consume(id, p?)` | `POST /env/authorizations/{id}/consume` |
221
-
222
- Cada código vale uma vez; 3 erros seguidos deslogam o usuário de todos os dispositivos e
223
- 5 bloqueiam a verificação por 15 minutos. Se o seu servidor guarda o token do cliente
224
- (BFF), o lado do cliente está em `authyon.user(accessToken).security.approvals`.
225
-
226
- ## Administração por tenant (`tenant()`)
227
-
228
- As credenciais de um tenant são próprias dele — use `authyon.tenant(credentials)` para obter um cliente escopado, que minta e cacheia seu próprio token via `POST /tenant/oauth/token`:
229
-
230
- ```ts
231
- const acme = authyon.tenant({ clientId: acmeClientId, clientSecret: acmeClientSecret });
232
-
233
- const identity = await acme.validate(); // POST /tenant/auth/validate
234
- await acme.members.list();
235
- await acme.members.add(userId, ["member"]);
236
- await acme.roles.create({ name: "viewer", permissions: ["reports:read"] });
237
- ```
238
-
239
- `validate()` confirma o bearer tenant-client atual e devolve `tenantId`,
240
- `credentialId`, `clientId` e as permissões vigentes da credencial. A resposta
241
- vem do backend depois da checagem de revogação, rotação de secret e tenant
242
- desabilitado; não confie só na claim `scope` quando precisar do estado atual.
243
-
244
- Em middleware, quando você já recebeu o bearer tenant-client na requisição, não
245
- use `clientId`/`clientSecret`; valide o token diretamente:
246
-
247
- ```ts
248
- const validation = await authyon.tenantAuth.validate(tenantBearerToken, {
249
- clientIp: "203.0.113.10",
250
- });
251
-
252
- if (!validation.valid) {
253
- // responda 401
254
- }
255
- ```
256
-
257
- ## Descoberta
258
-
259
- ```ts
260
- await authyon.discovery.jwks(); // GET /.well-known/jwks.json
261
- await authyon.discovery.openidConfiguration(); // GET /.well-known/openid-configuration
262
- ```
263
-
264
- ## Build
265
-
266
- ```bash
267
- npm install
268
- npm run build # dist/ (ESM + CJS + .d.ts via tsup)
269
- npm run typecheck
270
- ```
271
-
272
- ### Emitir uma credencial de integração do tenant
273
-
274
- Requer o endpoint `POST /env/tenants/{tenantId}/credentials` no backend Authyon
275
- e uma credencial de ambiente com `authyon:credentials:manage`.
276
-
277
- ```ts
278
- const credential = await authyon.environment.tenants.credentials.create(tenantId, {
279
- description: "ERP financeiro",
280
- permissions: ["monkeypay:transactions:read"],
281
- });
282
- // Entregue clientId e clientSecret ao operador: o segredo só é retornado uma vez.
283
- ```
284
-
285
- As permissões são explícitas e pertencem à integração; não há concessão automática de acesso total.
286
-
287
- ## Credenciais: paginação, busca e detalhe
288
-
289
- ```ts
290
- const page = await authyon.environment.tenants.credentials.listPage(tenantId, {
291
- search: "transactions",
292
- skip: 0,
293
- take: 25,
294
- });
295
- const detail = await authyon.environment.tenants.credentials.get(tenantId, credentialId);
296
- ```
297
-
298
- `page` contém `data`, `page` (a página atual, começando em 1), `perPage` (o
299
- limite por página), `total`, `pages`, `hasNext` e `hasPrev`. Até a API de
300
- 2026-10-21 os dois campos se chamavam `perPage` (página) e `pageSize`
301
- (limite), nessa ordem trocada. `take` aceita 1–100 (padrão da API: 25),
302
- `skip` é um deslocamento a partir de zero e `search` aceita até 200 caracteres.
303
- A API pesquisa clientId, descrição e permissões antes de paginar.
304
-
305
- O método existente `credentials.list(tenantId)` continua retornando um array:
306
- aceita a resposta antiga e percorre todas as páginas da API atual. Prefira
307
- `listPage` para tabelas ou grandes volumes. A leitura completa detecta páginas
308
- sobrepostas/sem progresso e tem limite de 1.000 páginas; alterações concorrentes
309
- podem exigir reiniciar a leitura, pois a API usa paginação por deslocamento.
310
-
311
- O detalhe contém `permissions`, `createdBy` (`id`, `type`, `displayName`),
312
- `createdAt`, `updatedAt`, `lastUsedAt`, `secretRotatedAt`, `revokedAt`, `isActive`,
313
- `expiresAt`, `ageSeconds`, `lifetimeSeconds` e `accessTokenLifetimeSeconds`.
314
- Listagem e detalhe projetam somente campos permitidos, sem segredo ou hash.
315
- Registros antigos podem ter autor e atualização nulos. A credencial atual não
316
- expira automaticamente: `expiresAt` é nulo; `lifetimeSeconds` é nulo enquanto
317
- ativa e representa sua duração até a revogação quando revogada. A validade do
318
- access token é separada (atualmente 1.800 segundos).
319
-
320
- ## Scopes e ciclo de vida das credenciais
321
-
322
- `scopes` é um alias de `permissions` na criação/atualização. Use somente um dos
323
- campos, com uma lista explícita no formato `namespace:resource:action`. O SDK
324
- normaliza espaços, caixa e duplicatas; não concede permissões automaticamente.
325
- A API continua responsável pela autorização. O contrato antigo
326
- `CreateTenantCredentialInput` e chamadas usando `permissions` são preservados.
327
-
328
- ```ts
329
- const issued = await authyon.environment.tenants.credentials.create(tenantId, {
330
- description: "ERP financeiro",
331
- scopes: ["monkeypay:transactions:read"],
332
- });
333
- // Armazene issued.clientSecret no gerenciador de segredos. Não registre a resposta.
334
-
335
- await authyon.environment.tenants.credentials.listPage(tenantId, {
336
- search: "ERP",
337
- skip: 0,
338
- take: 25,
339
- });
340
- await authyon.environment.tenants.credentials.get(tenantId, credentialId);
341
- await authyon.environment.tenants.credentials.create(tenantId, {
342
- description: "Leitura",
343
- permissions: ["monkeypay:transactions:read"],
344
- });
345
- await authyon.environment.tenants.credentials.updateScopes(tenantId, credentialId, [
346
- "monkeypay:transactions:read",
347
- ]);
348
- await authyon.environment.tenants.credentials.updatePermissions(tenantId, credentialId, {
349
- permissions: ["monkeypay:transactions:read"],
350
- });
351
- const rotated = await authyon.environment.tenants.credentials.rotate(tenantId, credentialId);
352
- await authyon.environment.tenants.credentials.revoke(tenantId, credentialId);
353
- ```
354
-
355
- Criação e rotação retornam o segredo uma única vez. Alterar permissões substitui a lista
356
- completa. A concessão OAuth continua recebendo apenas `clientId`/`clientSecret`;
357
- `scopes` não é um parâmetro de redução de acesso na troca de token.
358
-
359
- Todas as operações usam os endpoints externos `/env` com a credencial de
360
- ambiente configurada no cliente. O SDK não repete mutações automaticamente.
361
-
362
- ## Membros do tenant
363
-
364
- ```ts
365
- const user = authyon.user(() => getCurrentUserAccessToken());
366
- await user.tenants.members.invite(tenantId, {
367
- email: "pessoa@example.com",
368
- roles: ["reader"],
369
- });
370
- await user.tenants.members.list(tenantId, { skip: 0, take: 25 });
371
- await user.tenants.members.remove(tenantId, userId);
372
-
373
- // Pelo plano administrativo, a listagem vem no envelope paginado de sempre
374
- // (`data`, `total`, `hasNext`), com `take` 50 por padrão e teto 100.
375
- const { data: membros, total } = await authyon.environment.tenants.members.list(tenantId);
376
-
377
- // Administração por máquina: adicione uma conta existente por ID.
378
- await authyon.environment.tenants.members.add(tenantId, userId, ["reader"]);
379
- await authyon.environment.tenants.members.remove(tenantId, userId);
380
- // Os métodos tenant(credentials).members.add/remove existentes continuam disponíveis.
381
- ```
382
-
383
- Configure `envKey` para `user()`. `members.invite` associa um usuário já
384
- cadastrado no ambiente pelo e-mail; não cria conta e não envia link por e-mail.
385
- Para isso, use os convites abaixo.
386
- A API exige associação/permissões do usuário chamador. No browser, os métodos
387
- existentes `@authyon/auth` → `organization.members.invite/remove` continuam
388
- atendendo esse fluxo. Nunca importe `@authyon/server` no browser.
389
-
390
- ## Convites de tenant (e-mail ou link)
391
-
392
- Convide um e-mail que talvez ainda não tenha conta — aceitar cria a conta, já com o
393
- e-mail confirmado. A resposta sempre traz o `acceptUrl` completo; `sendEmail: false`
394
- só gera o link para você enviar pelo seu canal.
395
-
396
- ```ts
397
- // Pelo ambiente (escopo authyon:users:write; listar exige authyon:tenants:read).
398
- const invite = await authyon.environment.tenants.invites.create(tenantId, {
399
- email: "pessoa@example.com",
400
- roles: ["reader"],
401
- sendEmail: false, // padrão: true
402
- // Opcional: para onde mandar a pessoa depois de aceitar. Precisa estar
403
- // cadastrado (match exato) nos redirect URIs do ambiente.
404
- redirectUri: "https://app.example.com/bem-vindo",
405
- });
406
- invite.acceptUrl; // só aparece aqui — a API guarda apenas o hash do token
407
-
408
- await authyon.environment.tenants.invites.list(tenantId);
409
- await authyon.environment.tenants.invites.resend(tenantId, invite.id, { sendEmail: true });
410
- await authyon.environment.tenants.invites.revoke(tenantId, invite.id);
411
-
412
- // Pela credencial do tenant, ou agindo como o usuário (permissão tenants:members:invite).
413
- await authyon.tenant(credentials).invites.create({ email: "pessoa@example.com" });
414
- await authyon.user(token).tenants.invites.create(tenantId, { email: "pessoa@example.com" });
415
-
416
- // Página de aceite no seu backend: públicas, usam só o envKey.
417
- const preview = await authyon.environment.tenants.invites.preview(token);
418
- const accepted = await authyon.environment.tenants.invites.accept({
419
- token,
420
- password: preview.accountExists ? undefined : password,
421
- // Opcional. Com metadata, a chamada vai para /env/tenant-invites/accept usando
422
- // clientId/clientSecret (escopo authyon:users:write) — só o backend pode definir.
423
- publicMetadata: { plano: "pro" },
424
- privateMetadata: { crmId: "42" },
425
- // customFields: { ... } — obsoleto, prefira os metadata acima.
426
- });
427
- if (accepted.redirectUri) redirect(accepted.redirectUri); // o redirect fica por sua conta
428
- ```
429
-
430
- O link vale 7 dias e uma única vez. `resend` gera um link novo e invalida o anterior.
431
- Por padrão o link aponta para a página do Authyon; configure a _URL do convite para
432
- tenant_ nas configurações do ambiente para usar a sua (ela recebe `token` e `env`).
433
- Aceitar não faz login.
434
-
435
- ## Redefinição de senha pelo seu próprio e-mail
436
-
437
- Para a sua aplicação disparar o e-mail de redefinição em vez do Authyon, emita o token no
438
- backend (escopo `authyon:users:password`) e entregue você mesmo:
439
-
440
- ```ts
441
- const reset = await authyon.environment.users.issuePasswordReset({
442
- email: "pessoa@example.com",
443
- // Opcional: para onde mandar a pessoa depois de trocar a senha. Precisa estar
444
- // cadastrado (match exato) nos redirect URIs do ambiente.
445
- redirectUri: "https://app.example.com/login",
446
- // sendEmail: true faria o Authyon enviar também; o padrão é false.
447
- });
448
- reset.token; // só aparece aqui — a API guarda apenas o hash
449
- reset.resetUrl; // o link que o e-mail do Authyon levaria
450
- reset.expiresAt; // 30 minutos
451
-
452
- // Página de redefinição no seu backend: públicas, usam só o envKey.
453
- const preview = await authyon.environment.users.validatePasswordReset(token); // não gasta o token
454
- const done = await authyon.environment.users.confirmPasswordReset(token, newPassword);
455
- if (done.redirectUri) redirect(done.redirectUri); // o redirect fica por sua conta
456
- ```
457
-
458
- Quem tem o token consegue definir a senha: envie somente para o endereço da própria conta e
459
- não registre em log. Emitir um token novo invalida o anterior da mesma conta. Um endereço
460
- sem conta responde `404 user.not_found`; token desconhecido, usado ou expirado responde
461
- `401 user.password_reset.invalid`.
462
-
463
- ## Compatibilidade e segurança do transporte
464
-
465
- As chamadas exigem os endpoints externos atuais da API e a migração
466
- `ClientCredentialMetadata` para metadados
467
- persistidos. O SDK não altera o banco nem publica uma versão automaticamente.
468
-
469
- O adaptador Fetch agora rejeita redirects para evitar reenviar tokens/segredos;
470
- configure a URL final HTTPS da API. Adaptadores HTTP personalizados devem manter
471
- essa política. Os logs internos omitem corpos/headers e ocultam tokens de
472
- convite presentes no caminho. A auditoria de operações permanece na API; o SDK
473
- não gera eventos duplicados ao consultar listagens de auditoria.
1
+ # @authyon/server
2
+
3
+ SDK server-side para o [Authyon](https://authyon.com) — administração de ambiente/tenants (OAuth client-credentials) e verificação de access token (publishable key). Nunca importe este pacote em código de browser.
4
+
5
+ ## Instalação da beta
6
+
7
+ ```bash
8
+ npm install --save-exact @authyon/server@0.2.0-beta.4
9
+ ```
10
+
11
+ Durante a beta, `npm install @authyon/server@beta` acompanha o prerelease mais recente. Fixar a versão exata é recomendado para builds reproduzíveis.
12
+
13
+ O pacote possui uma condição de exportação `browser` que falha imediatamente se um bundler tentar incluí-lo no frontend. URLs remotas exigem HTTPS e cada request tem timeout padrão de 15 segundos, configurável por `timeoutMs`.
14
+
15
+ O client aceita o mesmo contrato `HttpAdapter` de `@authyon/auth`, útil para tracing, mocks e stacks HTTP próprias:
16
+
17
+ ```ts
18
+ import { createClient, FetchHttpAdapter } from "@authyon/server";
19
+
20
+ const authyon = createClient({
21
+ envKey: process.env.AUTHYON_ENV_KEY,
22
+ httpAdapter: new FetchHttpAdapter(globalThis.fetch),
23
+ });
24
+ ```
25
+
26
+ Quando houver credenciais, adapter e observabilidade, o builder mantém cada etapa explícita:
27
+
28
+ ```ts
29
+ const authyon = new AuthyonServerClientBuilder()
30
+ .withEnvironmentKey(process.env.AUTHYON_ENV_KEY)
31
+ .withEnvironmentCredentials(process.env.AUTHYON_CLIENT_ID, process.env.AUTHYON_CLIENT_SECRET)
32
+ .withHttpLogger(httpLogger)
33
+ .build();
34
+ ```
35
+
36
+ O client também aceita `httpLogger: { enabled, logger? }`. A configuração é mutável, portanto `enabled` pode ser alterado em runtime. Os eventos não incluem headers, bodies nem valores de query string, e erros do logger nunca interrompem requests.
37
+
38
+ `AuthyonError`, `ErrorCodes` e `error.interpret()` também são exportados por este pacote. Use `category`, `action`, `retryable`, `retryAfter` e `requestId` para implementar middleware e observabilidade sem comparar mensagens humanas.
39
+
40
+ ## Sessão web no BFF (código ainda não publicado)
41
+
42
+ O novo entrypoint `@authyon/server/bff` oferece login, 2FA, sessão HttpOnly,
43
+ renovação sob demanda, logout e troca de organização. Os tokens ficam no servidor,
44
+ com adaptador Redis cifrado e compare-and-swap para concorrência entre instâncias.
45
+ Reutiliza os endpoints existentes e o cliente de validação server-side.
46
+
47
+ Veja [contrato e integração](../../docs/bff.md) e [análise/plano](../../docs/bff-session-plan.md).
48
+ O entrypoint padrão e `@authyon/server/next` mantêm seu comportamento anterior;
49
+ a adoção do transporte BFF pelo consumidor é explícita.
50
+
51
+ ## Validação local com JWKS
52
+
53
+ Para APIs de alto volume, crie uma vez um verificador com discovery automático:
54
+
55
+ ```ts
56
+ const verifier = await authyon.createJwksTokenVerifier({
57
+ audience: process.env.AUTHYON_AUDIENCE,
58
+ algorithms: ["RS256"],
59
+ });
60
+
61
+ const { claims } = await verifier.verifyAccessToken(token);
62
+ ```
63
+
64
+ Ele valida assinatura, algoritmo, issuer, audience, tipo e claims temporais, com cache e rotação de chaves. Use `verification: "jwks"` em `authorizeRequest()` ou `createExpressAuthorizationMiddleware()`. Para ações sensíveis que precisam detectar revogação imediatamente, mantenha o padrão `validate`.
65
+
66
+ ## Instalação
67
+
68
+ ```bash
69
+ npm install @authyon/server
70
+ ```
71
+
72
+ ## Chaves e credenciais
73
+
74
+ | Credencial | Uso |
75
+ | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
76
+ | `envKey` (`pk_...`) | Selecionado em **toda** chamada via `X-Authyon-Environment` (Test/Live). Não é sensível — é a mesma chave do frontend, mas sozinha não autoriza nada. |
77
+ | `clientId` / `clientSecret` (ambiente) | Credenciais OAuth client-credentials de nível ambiente, mintadas no console. O SDK troca por um access token (`POST /env/oauth/token`) e o renova sozinho, usado em todo `environment.*` / `permissions.*`. **Nunca** exponha ao navegador. |
78
+ | `clientId` / `clientSecret` (tenant) | Credenciais por-tenant, usadas via `authyon.tenant({ clientId, clientSecret })` para as chamadas `TenantManagement` (`/tenant/...`). |
79
+
80
+ ```ts
81
+ import { createClient } from "@authyon/server";
82
+
83
+ const authyon = createClient({
84
+ envKey: process.env.AUTHYON_ENV_KEY, // pk_live_...
85
+ clientId: process.env.AUTHYON_CLIENT_ID, // ec_live_...
86
+ clientSecret: process.env.AUTHYON_CLIENT_SECRET,
87
+ });
88
+ ```
89
+
90
+ > `@authyon/auth` também exporta um `createClient`. Se algum dia precisar dos dois no mesmo arquivo, use um alias no import: `import { createClient as createServerClient } from "@authyon/server"`.
91
+
92
+ ## Verificação de token
93
+
94
+ ```ts
95
+ const { active, sub } = await authyon.introspect(token); // leve
96
+ const { user, organization } = await authyon.validate(token); // recomendado — cross-checa revogação
97
+ ```
98
+
99
+ Veja [`examples/tokenVerification.ts`](./examples/tokenVerification.ts) para um middleware completo.
100
+
101
+ ## Administração de ambiente (`environment.*`)
102
+
103
+ Usa as credenciais de ambiente (`clientId`/`clientSecret`) automaticamente — o SDK minta e renova o token sozinho.
104
+
105
+ ```ts
106
+ const org = await authyon.environment.tenants.create({ name: "Acme", slug: "acme" });
107
+ const tenants = await authyon.environment.tenants.list({
108
+ search: "acme",
109
+ skip: 0,
110
+ take: 20,
111
+ });
112
+ await authyon.environment.tenants.members.add(org.id, userId, ["owner"]);
113
+ await authyon.environment.tenants.members.assignRole(org.id, userId, "billing-admin");
114
+ await authyon.environment.tenants.members.remove(org.id, userId);
115
+
116
+ await authyon.environment.users.list({ search: "acme.com" });
117
+ await authyon.environment.roles.create({ name: "support", permissions: ["tickets:read"] });
118
+ await authyon.environment.audit.list({ take: 50 });
119
+ ```
120
+
121
+ Veja [`examples/organizationMembership.ts`](./examples/organizationMembership.ts) para o fluxo completo, incluindo as rotas de backend que o `@authyon/auth` chamaria.
122
+
123
+ Namespaces disponíveis: `environment.users`, `environment.tenants` (com `.members` e `.roles` aninhados), `environment.roles`, `environment.permissions`, `environment.audit`. OTP e aprovações ficam em `security` (veja abaixo).
124
+
125
+ ## Autorização local
126
+
127
+ `createAuthyonAbility()` transforma o resultado de `validate()`, `introspect()` ou qualquer objeto com `permissions`, `roles` e `scope` em verificações locais no estilo CASL:
128
+
129
+ ```ts
130
+ import { createAuthyonAbility } from "@authyon/server";
131
+
132
+ const identity = await authyon.validate(token);
133
+ const ability = createAuthyonAbility(identity);
134
+
135
+ if (!ability.can("read", "reports")) {
136
+ throw new Error("Forbidden");
137
+ }
138
+ ```
139
+
140
+ Regras condicionais e de domínio podem ser acrescentadas no segundo argumento. O mecanismo é deny-by-default e não substitui a validação do token nem a autorização aplicada pela API.
141
+
142
+ ### Next.js Server Side
143
+
144
+ Em Route Handlers, Server Actions ou SSR, mantenha `@authyon/server` em um módulo marcado com `server-only`:
145
+
146
+ ```ts
147
+ import "server-only";
148
+ import { createAuthorizationErrorResponse, authorizeRequest, createClient } from "@authyon/server";
149
+
150
+ const authyon = createClient({
151
+ envKey: process.env.AUTHYON_ENV_KEY,
152
+ clientId: process.env.AUTHYON_CLIENT_ID,
153
+ clientSecret: process.env.AUTHYON_CLIENT_SECRET,
154
+ });
155
+
156
+ export async function GET(request: Request) {
157
+ try {
158
+ const auth = await authorizeRequest(authyon, request, {
159
+ requirement: { action: "read", subject: "reports" },
160
+ });
161
+ return Response.json({ userId: auth.userId });
162
+ } catch (error) {
163
+ return createAuthorizationErrorResponse(error);
164
+ }
165
+ }
166
+ ```
167
+
168
+ ### Express e frameworks Connect
169
+
170
+ O middleware não adiciona Express como dependência e disponibiliza `request.authyon`:
171
+
172
+ ```ts
173
+ import { createExpressAuthorizationMiddleware } from "@authyon/server";
174
+
175
+ app.get(
176
+ "/reports",
177
+ createExpressAuthorizationMiddleware(authyon, {
178
+ requirement: { action: "read", subject: "reports" },
179
+ }),
180
+ (request, response) => response.json({ userId: request.authyon.userId }),
181
+ );
182
+ ```
183
+
184
+ Use `authorizeToken()` em NestJS, Fastify, Hono, tRPC ou integrações próprias. `passErrorsToNext: true` encaminha erros ao middleware de erros do Express.
185
+
186
+ ### IP real do cliente
187
+
188
+ Chamadas server-side propagam `clientIp` em `X-Authyon-Client-Ip` (e em `X-Forwarded-For`, para o que estiver na frente do seu backend). A API resolve o endereço do chamador pelo primeiro header e acredita nele quando a chamada está autenticada por credencial de máquina — uma chamada servidor-a-servidor não tem navegador dentro dela, e sem isso toda linha de auditoria da sua integração nomeia o gateway do contêiner, que não identifica ninguém. Muda a trilha, nunca um portão: todo controle com chave de IP roda antes da autenticação e segue decidindo pelo peer real.
189
+
190
+ O middleware Express usa o IP do socket por padrão; atrás de um proxy confiável, configure o próprio Express e use `trustProxy: true`. Para Next.js, passe `resolveClientIp` ao `authorizeRequest()` lendo somente headers que sua infraestrutura sobrescreve. Não confie cegamente em `X-Forwarded-For` recebido da internet.
191
+
192
+ ```ts
193
+ await authyon.validate(token, { clientIp: "203.0.113.10" });
194
+ ```
195
+
196
+ ## Segurança (`security.*`)
197
+
198
+ OTP e aprovações ficam agrupados em `authyon.security`. Exige uma credencial de ambiente
199
+ com o escopo `authyon:otp:verify` (para `security.otp`) e/ou `authyon:financial:authorize`
200
+ (para `security.approvals`). Guia completo:
201
+ [`docs/security-otp-approvals.md`](../../docs/security-otp-approvals.md).
202
+
203
+ ```ts
204
+ // security.otp — só validar o código do app autenticador
205
+ const result = await authyon.security.otp.check(customerId, code);
206
+ if (!result.valid) console.log(result.attemptsRemaining, result.sessionsRevoked);
207
+
208
+ // security.approvals — aprovação presa a um payload livre e de uso único
209
+ const { id } = await authyon.security.approvals.create({
210
+ subjectId: customerId,
211
+ payload: { type: "pix", amount: 150.5, to: "maria@example.com" },
212
+ });
213
+ // ...cliente confirma com @authyon/auth → security.approvals.confirm
214
+ const { payload } = await authyon.security.approvals.consume(id);
215
+ ```
216
+
217
+ | Método | Endpoint |
218
+ | ------------------------------------ | --------------------------------------- |
219
+ | `security.otp.check(userId, code)` | `POST /env/users/{userId}/otp/verify` |
220
+ | `security.approvals.create(input)` | `POST /env/authorizations` |
221
+ | `security.approvals.get(id)` | `GET /env/authorizations/{id}` |
222
+ | `security.approvals.consume(id, p?)` | `POST /env/authorizations/{id}/consume` |
223
+
224
+ Cada código vale uma vez; 3 erros seguidos deslogam o usuário de todos os dispositivos e
225
+ 5 bloqueiam a verificação por 15 minutos. Se o seu servidor guarda o token do cliente
226
+ (BFF), o lado do cliente está em `authyon.user(accessToken).security.approvals`.
227
+
228
+ ## Administração por tenant (`tenant()`)
229
+
230
+ As credenciais de um tenant são próprias dele — use `authyon.tenant(credentials)` para obter um cliente escopado, que minta e cacheia seu próprio token via `POST /tenant/oauth/token`:
231
+
232
+ ```ts
233
+ const acme = authyon.tenant({ clientId: acmeClientId, clientSecret: acmeClientSecret });
234
+
235
+ const identity = await acme.validate(); // POST /tenant/auth/validate
236
+ await acme.members.list();
237
+ await acme.members.add(userId, ["member"]);
238
+ await acme.roles.create({ name: "viewer", permissions: ["reports:read"] });
239
+ ```
240
+
241
+ `validate()` confirma o bearer tenant-client atual e devolve `tenantId`,
242
+ `credentialId`, `clientId` e as permissões vigentes da credencial. A resposta
243
+ vem do backend depois da checagem de revogação, rotação de secret e tenant
244
+ desabilitado; não confie só na claim `scope` quando precisar do estado atual.
245
+
246
+ Em middleware, quando você já recebeu o bearer tenant-client na requisição, não
247
+ use `clientId`/`clientSecret`; valide o token diretamente:
248
+
249
+ ```ts
250
+ const validation = await authyon.tenantAuth.validate(tenantBearerToken, {
251
+ clientIp: "203.0.113.10",
252
+ });
253
+
254
+ if (!validation.valid) {
255
+ // responda 401
256
+ }
257
+ ```
258
+
259
+ ## Descoberta
260
+
261
+ ```ts
262
+ await authyon.discovery.jwks(); // GET /.well-known/jwks.json
263
+ await authyon.discovery.openidConfiguration(); // GET /.well-known/openid-configuration
264
+ ```
265
+
266
+ ## Build
267
+
268
+ ```bash
269
+ npm install
270
+ npm run build # dist/ (ESM + CJS + .d.ts via tsup)
271
+ npm run typecheck
272
+ ```
273
+
274
+ ### Emitir uma credencial de integração do tenant
275
+
276
+ Requer o endpoint `POST /env/tenants/{tenantId}/credentials` no backend Authyon
277
+ e uma credencial de ambiente com `authyon:credentials:manage`.
278
+
279
+ ```ts
280
+ const credential = await authyon.environment.tenants.credentials.create(tenantId, {
281
+ description: "ERP financeiro",
282
+ permissions: ["monkeypay:transactions:read"],
283
+ });
284
+ // Entregue clientId e clientSecret ao operador: o segredo só é retornado uma vez.
285
+ ```
286
+
287
+ As permissões são explícitas e pertencem à integração; não há concessão automática de acesso total.
288
+
289
+ ## Credenciais: paginação, busca e detalhe
290
+
291
+ ```ts
292
+ const page = await authyon.environment.tenants.credentials.listPage(tenantId, {
293
+ search: "transactions",
294
+ skip: 0,
295
+ take: 25,
296
+ });
297
+ const detail = await authyon.environment.tenants.credentials.get(tenantId, credentialId);
298
+ ```
299
+
300
+ `page` contém `data`, `page` (a página atual, começando em 1), `perPage` (o
301
+ limite por página), `total`, `pages`, `hasNext` e `hasPrev`. Até a API de
302
+ 2026-10-21 os dois campos se chamavam `perPage` (página) e `pageSize`
303
+ (limite), nessa ordem trocada. `take` aceita 1–100 (padrão da API: 25),
304
+ `skip` é um deslocamento a partir de zero e `search` aceita até 200 caracteres.
305
+ A API pesquisa clientId, descrição e permissões antes de paginar.
306
+
307
+ O método existente `credentials.list(tenantId)` continua retornando um array:
308
+ aceita a resposta antiga e percorre todas as páginas da API atual. Prefira
309
+ `listPage` para tabelas ou grandes volumes. A leitura completa detecta páginas
310
+ sobrepostas/sem progresso e tem limite de 1.000 páginas; alterações concorrentes
311
+ podem exigir reiniciar a leitura, pois a API usa paginação por deslocamento.
312
+
313
+ O detalhe contém `permissions`, `createdBy` (`id`, `type`, `displayName`),
314
+ `createdAt`, `updatedAt`, `lastUsedAt`, `secretRotatedAt`, `revokedAt`, `isActive`,
315
+ `expiresAt`, `ageSeconds`, `lifetimeSeconds` e `accessTokenLifetimeSeconds`.
316
+ Listagem e detalhe projetam somente campos permitidos, sem segredo ou hash.
317
+ Registros antigos podem ter autor e atualização nulos. A credencial atual não
318
+ expira automaticamente: `expiresAt` é nulo; `lifetimeSeconds` é nulo enquanto
319
+ ativa e representa sua duração até a revogação quando revogada. A validade do
320
+ access token é separada (atualmente 1.800 segundos).
321
+
322
+ ## Scopes e ciclo de vida das credenciais
323
+
324
+ `scopes` é um alias de `permissions` na criação/atualização. Use somente um dos
325
+ campos, com uma lista explícita no formato `namespace:resource:action`. O SDK
326
+ normaliza espaços, caixa e duplicatas; não concede permissões automaticamente.
327
+ A API continua responsável pela autorização. O contrato antigo
328
+ `CreateTenantCredentialInput` e chamadas usando `permissions` são preservados.
329
+
330
+ ```ts
331
+ const issued = await authyon.environment.tenants.credentials.create(tenantId, {
332
+ description: "ERP financeiro",
333
+ scopes: ["monkeypay:transactions:read"],
334
+ });
335
+ // Armazene issued.clientSecret no gerenciador de segredos. Não registre a resposta.
336
+
337
+ await authyon.environment.tenants.credentials.listPage(tenantId, {
338
+ search: "ERP",
339
+ skip: 0,
340
+ take: 25,
341
+ });
342
+ await authyon.environment.tenants.credentials.get(tenantId, credentialId);
343
+ await authyon.environment.tenants.credentials.create(tenantId, {
344
+ description: "Leitura",
345
+ permissions: ["monkeypay:transactions:read"],
346
+ });
347
+ await authyon.environment.tenants.credentials.updateScopes(tenantId, credentialId, [
348
+ "monkeypay:transactions:read",
349
+ ]);
350
+ await authyon.environment.tenants.credentials.updatePermissions(tenantId, credentialId, {
351
+ permissions: ["monkeypay:transactions:read"],
352
+ });
353
+ const rotated = await authyon.environment.tenants.credentials.rotate(tenantId, credentialId);
354
+ await authyon.environment.tenants.credentials.revoke(tenantId, credentialId);
355
+ ```
356
+
357
+ Criação e rotação retornam o segredo uma única vez. Alterar permissões substitui a lista
358
+ completa. A concessão OAuth continua recebendo apenas `clientId`/`clientSecret`;
359
+ `scopes` não é um parâmetro de redução de acesso na troca de token.
360
+
361
+ Todas as operações usam os endpoints externos `/env` com a credencial de
362
+ ambiente configurada no cliente. O SDK não repete mutações automaticamente.
363
+
364
+ ## Hierarquia de tenants
365
+
366
+ Um tenant pode ficar embaixo de outro. A listagem pagina um nível por vez, que é o
367
+ que permite desenhar o primeiro nível sem ler a subárvore inteira:
368
+
369
+ ```ts
370
+ const { data: raizes } = await authyon.environment.tenants.roots({ take: 50 });
371
+
372
+ // childCount já vem na página: expanda só onde há o que expandir.
373
+ for (const t of raizes.filter((t) => (t.childCount ?? 0) > 0)) {
374
+ const { data: filhos } = await authyon.environment.tenants.children(t.id);
375
+ }
376
+
377
+ // Criar embaixo de outro.
378
+ await authyon.environment.tenants.create({
379
+ name: "Filial Sul",
380
+ slug: "filial-sul",
381
+ parentTenantId: holdingId,
382
+ });
383
+ ```
384
+
385
+ `list()` continua devolvendo todos os tenants em qualquer profundidade — é o que um
386
+ seletor plano quer. `get()` traz `ancestors`, a cadeia acima do tenant com a raiz
387
+ primeiro, então um breadcrumb custa uma chamada e não uma por nível.
388
+
389
+ O ambiente nasce com `maxTenantDepth: 1` (só raízes), então criar um sub-tenant
390
+ responde `tenant.depth.not_allowed` até um operador aumentar o teto no console.
391
+ Alcançar para baixo é opt-in: as permissões `subtenants:tree:read|create|manage|switch`
392
+ são concedidas no nó a que o chamador pertence, e ter `tenants:settings:manage` no pai
393
+ não concede nada no filho. `disable()` e `delete()` desativam a subárvore junto.
394
+
395
+ Detalhes e erros no [guia de hierarquia](../../docs/tenantHierarchy.md).
396
+
397
+ ## Filtros do tenant
398
+
399
+ `search` é um substring só, ORado entre nome, slug, descrição e os dois metadata.
400
+ Quando mais de uma condição precisa valer — `kind IN (...) AND cnpj IN (...)` — use
401
+ `filter`, que roda dentro da consulta e por isso continua paginando:
402
+
403
+ ```ts
404
+ import { tenantFilter } from "@authyon/server";
405
+
406
+ const page = await authyon.environment.tenants.list({
407
+ filter: [
408
+ tenantFilter.in("public.kind", ["reseller", "partner"]),
409
+ tenantFilter.in("custom.cnpj", ["00000000000100", "11111111000199"]),
410
+ ],
411
+ take: 100,
412
+ });
413
+
414
+ page.total; // quantos casam com o filtro, não quantos existem no ambiente
415
+ ```
416
+
417
+ Cada entrada é uma condição; várias são ANDadas entre si e com o `search`, e a
418
+ vírgula dentro de um `in` é o OR. Os escopos são `public` (publicMetadata),
419
+ `private` (privateMetadata) e `custom` (customFields — este o `search` nunca
420
+ cobriu). Operadores: `eq`, `in`, `like` (ILIKE, com os metacaracteres escapados)
421
+ e `exists`. Caminho aninhado vale até três níveis: `private.billing.plan`.
422
+
423
+ O construtor valida antes de sair: escopo desconhecido, chave com espaço, mais de
424
+ 25 valores num `in` ou vírgula dentro de um valor levantam `RangeError` em vez de
425
+ virarem `400 tenant.filter.invalid`. No máximo 10 filtros por requisição.
426
+
427
+ Precisa da API Authyon de 2026-10-22 ou mais nova; contra uma anterior o `filter`
428
+ é ignorado, o que aparece como página larga demais e não como erro.
429
+
430
+ ## Membros do tenant
431
+
432
+ ```ts
433
+ const user = authyon.user(() => getCurrentUserAccessToken());
434
+ await user.tenants.members.invite(tenantId, {
435
+ email: "pessoa@example.com",
436
+ roles: ["reader"],
437
+ });
438
+ await user.tenants.members.list(tenantId, { skip: 0, take: 25 });
439
+ await user.tenants.members.remove(tenantId, userId);
440
+
441
+ // Pelo plano administrativo, a listagem vem no envelope paginado de sempre
442
+ // (`data`, `total`, `hasNext`), com `take` 50 por padrão e teto 100.
443
+ const { data: membros, total } = await authyon.environment.tenants.members.list(tenantId);
444
+
445
+ // Administração por máquina: adicione uma conta existente por ID.
446
+ await authyon.environment.tenants.members.add(tenantId, userId, ["reader"]);
447
+ await authyon.environment.tenants.members.remove(tenantId, userId);
448
+ // Os métodos tenant(credentials).members.add/remove existentes continuam disponíveis.
449
+ ```
450
+
451
+ Configure `envKey` para `user()`. `members.invite` associa um usuário já
452
+ cadastrado no ambiente pelo e-mail; não cria conta e não envia link por e-mail.
453
+ Para isso, use os convites abaixo.
454
+ A API exige associação/permissões do usuário chamador. No browser, os métodos
455
+ existentes `@authyon/auth` → `organization.members.invite/remove` continuam
456
+ atendendo esse fluxo. Nunca importe `@authyon/server` no browser.
457
+
458
+ ## Convites de tenant (e-mail ou link)
459
+
460
+ Convide um e-mail que talvez ainda não tenha conta — aceitar cria a conta, já com o
461
+ e-mail confirmado. A resposta sempre traz o `acceptUrl` completo; `sendEmail: false`
462
+ só gera o link para você enviar pelo seu canal.
463
+
464
+ ```ts
465
+ // Pelo ambiente (escopo authyon:users:write; listar exige authyon:tenants:read).
466
+ const invite = await authyon.environment.tenants.invites.create(tenantId, {
467
+ email: "pessoa@example.com",
468
+ roles: ["reader"],
469
+ sendEmail: false, // padrão: true
470
+ // Opcional: para onde mandar a pessoa depois de aceitar. Precisa estar
471
+ // cadastrado (match exato) nos redirect URIs do ambiente.
472
+ redirectUri: "https://app.example.com/bem-vindo",
473
+ });
474
+ invite.acceptUrl; // só aparece aqui — a API guarda apenas o hash do token
475
+
476
+ await authyon.environment.tenants.invites.list(tenantId);
477
+ await authyon.environment.tenants.invites.resend(tenantId, invite.id, { sendEmail: true });
478
+ await authyon.environment.tenants.invites.revoke(tenantId, invite.id);
479
+
480
+ // Pela credencial do tenant, ou agindo como o usuário (permissão tenants:members:invite).
481
+ await authyon.tenant(credentials).invites.create({ email: "pessoa@example.com" });
482
+ await authyon.user(token).tenants.invites.create(tenantId, { email: "pessoa@example.com" });
483
+
484
+ // Página de aceite no seu backend: públicas, usam só o envKey.
485
+ const preview = await authyon.environment.tenants.invites.preview(token);
486
+ const accepted = await authyon.environment.tenants.invites.accept({
487
+ token,
488
+ password: preview.accountExists ? undefined : password,
489
+ // Opcional. Com metadata, a chamada vai para /env/tenant-invites/accept usando
490
+ // clientId/clientSecret (escopo authyon:users:write) — só o backend pode definir.
491
+ publicMetadata: { plano: "pro" },
492
+ privateMetadata: { crmId: "42" },
493
+ // customFields: { ... } — obsoleto, prefira os metadata acima.
494
+ });
495
+ if (accepted.redirectUri) redirect(accepted.redirectUri); // o redirect fica por sua conta
496
+ ```
497
+
498
+ O link vale 7 dias e uma única vez. `resend` gera um link novo e invalida o anterior.
499
+ Por padrão o link aponta para a página do Authyon; configure a _URL do convite para
500
+ tenant_ nas configurações do ambiente para usar a sua (ela recebe `token` e `env`).
501
+ Aceitar não faz login.
502
+
503
+ ## Redefinição de senha pelo seu próprio e-mail
504
+
505
+ Para a sua aplicação disparar o e-mail de redefinição em vez do Authyon, emita o token no
506
+ backend (escopo `authyon:users:password`) e entregue você mesmo:
507
+
508
+ ```ts
509
+ const reset = await authyon.environment.users.issuePasswordReset({
510
+ email: "pessoa@example.com",
511
+ // Opcional: para onde mandar a pessoa depois de trocar a senha. Precisa estar
512
+ // cadastrado (match exato) nos redirect URIs do ambiente.
513
+ redirectUri: "https://app.example.com/login",
514
+ // sendEmail: true faria o Authyon enviar também; o padrão é false.
515
+ });
516
+ reset.token; // só aparece aqui — a API guarda apenas o hash
517
+ reset.resetUrl; // o link que o e-mail do Authyon levaria
518
+ reset.expiresAt; // 30 minutos
519
+
520
+ // Página de redefinição no seu backend: públicas, usam só o envKey.
521
+ const preview = await authyon.environment.users.validatePasswordReset(token); // não gasta o token
522
+ const done = await authyon.environment.users.confirmPasswordReset(token, newPassword);
523
+ if (done.redirectUri) redirect(done.redirectUri); // o redirect fica por sua conta
524
+ ```
525
+
526
+ Quem tem o token consegue definir a senha: envie somente para o endereço da própria conta e
527
+ não registre em log. Emitir um token novo invalida o anterior da mesma conta. Um endereço
528
+ sem conta responde `404 user.not_found`; token desconhecido, usado ou expirado responde
529
+ `401 user.password_reset.invalid`.
530
+
531
+ ## Compatibilidade e segurança do transporte
532
+
533
+ As chamadas exigem os endpoints externos atuais da API e a migração
534
+ `ClientCredentialMetadata` para metadados
535
+ persistidos. O SDK não altera o banco nem publica uma versão automaticamente.
536
+
537
+ O adaptador Fetch agora rejeita redirects para evitar reenviar tokens/segredos;
538
+ configure a URL final HTTPS da API. Adaptadores HTTP personalizados devem manter
539
+ essa política. Os logs internos omitem corpos/headers e ocultam tokens de
540
+ convite presentes no caminho. A auditoria de operações permanece na API; o SDK
541
+ não gera eventos duplicados ao consultar listagens de auditoria.