@authyon/auth 0.2.0-beta.21 → 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 +371 -362
- package/dist/{ability-BTcayvkM.d.cts → ability-DjEMCVjx.d.cts} +96 -8
- package/dist/{ability-BTcayvkM.d.ts → ability-DjEMCVjx.d.ts} +96 -8
- package/dist/{chunk-3XJY6BCI.js → chunk-IX6U6A72.js} +33 -6
- package/dist/index.cjs +117 -11
- package/dist/index.d.cts +53 -3
- package/dist/index.d.ts +53 -3
- package/dist/index.js +83 -6
- package/dist/react/index.cjs +11 -1
- package/dist/react/index.d.cts +2 -2
- package/dist/react/index.d.ts +2 -2
- package/dist/react/index.js +1 -1
- package/package.json +71 -71
package/README.md
CHANGED
|
@@ -1,362 +1,371 @@
|
|
|
1
|
-
# @authyon/auth
|
|
2
|
-
|
|
3
|
-
SDK JS/TS para o [Authyon](https://authyon.com) — autenticação, sessões, multi-tenant e 2FA, com armazenamento e refresh de tokens transparentes.
|
|
4
|
-
|
|
5
|
-
Cobre os endpoints públicos do Authyon usando a **publishable key** (`pk_...`) — segura para identificar o ambiente no navegador, sem conceder privilégios administrativos.
|
|
6
|
-
|
|
7
|
-
Para gestão de organização/membros (secret key) e verificação de token no backend, use `@authyon/server`.
|
|
8
|
-
|
|
9
|
-
## Instalação
|
|
10
|
-
|
|
11
|
-
```bash
|
|
12
|
-
npm install --save-exact @authyon/auth@0.2.0-beta.4
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
Durante a beta, `npm install @authyon/auth@beta` acompanha o prerelease mais recente. Fixar a versão exata é recomendado para builds reproduzíveis.
|
|
16
|
-
|
|
17
|
-
## Uso rápido
|
|
18
|
-
|
|
19
|
-
```ts
|
|
20
|
-
import { createClient } from "@authyon/auth";
|
|
21
|
-
|
|
22
|
-
const authyon = createClient({ envKey: "pk_live_..." }); // tokens somente em memória por padrão
|
|
23
|
-
|
|
24
|
-
// Login (com suporte a 2FA)
|
|
25
|
-
const result = await authyon.login({
|
|
26
|
-
email: "alice@acme.com",
|
|
27
|
-
password: "...",
|
|
28
|
-
organizationSlug: "acme", // opcional
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
if (result.twoFactorRequired) {
|
|
32
|
-
const code = prompt(`Código 2FA (${result.methods.join(", ")})`);
|
|
33
|
-
await authyon.verifyTwoFactor({
|
|
34
|
-
challengeToken: result.challengeToken,
|
|
35
|
-
method: "authenticator",
|
|
36
|
-
code: code!,
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Usuário atual — o access token é renovado automaticamente quando necessário
|
|
41
|
-
const user = await authyon.user.me();
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
## Next.js e React
|
|
45
|
-
|
|
46
|
-
Use o entrypoint dedicado `@authyon/auth/react`:
|
|
47
|
-
|
|
48
|
-
```tsx
|
|
49
|
-
<AuthyonProvider client={authyon}>
|
|
50
|
-
<SessionGuard loadingFallback={<Loading />}>
|
|
51
|
-
<PermissionGuard action="read" subject="reports">
|
|
52
|
-
<Reports />
|
|
53
|
-
</PermissionGuard>
|
|
54
|
-
</SessionGuard>
|
|
55
|
-
</AuthyonProvider>
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
O provider faz refresh automático, valida a sessão por `GET /auth/me`, atualiza usuário e permissions, revalida quando a aba volta ao foco e não libera guards enquanto a validação inicial estiver pendente. Use `useAuthyon()`, `useAuthyonAbility()` e `useCan()` para fluxos programáticos.
|
|
59
|
-
|
|
60
|
-
Use `transformUser` para expor campos derivados sem alterar a sessão mantida pela lib:
|
|
61
|
-
|
|
62
|
-
```tsx
|
|
63
|
-
type ApplicationUser = User & { fullName: string };
|
|
64
|
-
|
|
65
|
-
<AuthyonProvider<ApplicationUser>
|
|
66
|
-
client={authyon}
|
|
67
|
-
transformUser={(user) => ({
|
|
68
|
-
...user,
|
|
69
|
-
fullName: [user.firstName, user.lastName].filter(Boolean).join(" "),
|
|
70
|
-
})}
|
|
71
|
-
>
|
|
72
|
-
<App />
|
|
73
|
-
</AuthyonProvider>;
|
|
74
|
-
|
|
75
|
-
const { user, session } = useAuthyon<ApplicationUser>();
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
O usuário transformado é retornado em `user` e em `session.user` após validação, refresh, login e troca de organização. O transformador deve ser puro e só derivar dados do perfil do Authyon; dados externos pertencem ao contexto da aplicação.
|
|
79
|
-
|
|
80
|
-
Para configuração progressiva, use o builder:
|
|
81
|
-
|
|
82
|
-
```ts
|
|
83
|
-
const authyon = new AuthyonClientBuilder("pk_live_...")
|
|
84
|
-
.withStorage(createMemoryStorage())
|
|
85
|
-
.withTimeout(10_000)
|
|
86
|
-
.build();
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
## Sessão e tokens
|
|
90
|
-
|
|
91
|
-
- Tokens ficam somente em memória por padrão, reduzindo o impacto de XSS e sendo removidos ao recarregar a página.
|
|
92
|
-
- Persistência é opt-in com `storage: createLocalStorage()`. Isso melhora conveniência, mas qualquer XSS na aplicação poderá ler os tokens; criptografar o valor não elimina esse risco porque o JavaScript também precisa acessar a chave.
|
|
93
|
-
- `getAccessToken()` renova o token automaticamente antes de expirar (refresh token é single-use e rotacionado, com single-flight para evitar corridas).
|
|
94
|
-
- Chamadas autenticadas que retornam 401 fazem um refresh e uma retentativa automática.
|
|
95
|
-
|
|
96
|
-
```ts
|
|
97
|
-
import { createClient, createLocalStorage } from "@authyon/auth";
|
|
98
|
-
|
|
99
|
-
// Use somente quando a persistência for um requisito aceito conscientemente.
|
|
100
|
-
const persistentClient = createClient({
|
|
101
|
-
envKey: "pk_live_...",
|
|
102
|
-
storage: createLocalStorage(),
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
const token = await authyon.getAccessToken(); // sempre válido, ou null se deslogado
|
|
106
|
-
|
|
107
|
-
const unsubscribe = authyon.onAuthStateChange((event) => {
|
|
108
|
-
// "signed_in" | "refreshed" | "signed_out"
|
|
109
|
-
console.log(event.type);
|
|
110
|
-
});
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
Para aplicações de maior risco, prefira um BFF que mantenha o refresh token em cookie `HttpOnly`, `Secure` e `SameSite`, com proteção CSRF. O SDK também exige HTTPS para APIs remotas e aplica timeout de 15 segundos por padrão (`timeoutMs: 0` desabilita).
|
|
114
|
-
|
|
115
|
-
## HTTP Adapter
|
|
116
|
-
|
|
117
|
-
Use `httpAdapter` para integrar mocks, tracing ou outra biblioteca HTTP. O mesmo contrato está disponível em `@authyon/auth` e `@authyon/server`:
|
|
118
|
-
|
|
119
|
-
```ts
|
|
120
|
-
import { createClient, type HttpAdapter } from "@authyon/auth";
|
|
121
|
-
|
|
122
|
-
const httpAdapter: HttpAdapter = {
|
|
123
|
-
async request(request) {
|
|
124
|
-
// tracing, métricas ou adaptação para sua stack HTTP
|
|
125
|
-
return fetch(request.url, {
|
|
126
|
-
method: request.method,
|
|
127
|
-
headers: request.headers,
|
|
128
|
-
body: request.body,
|
|
129
|
-
signal: request.signal,
|
|
130
|
-
});
|
|
131
|
-
},
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
const authyon = createClient({ envKey: "pk_live_...", httpAdapter });
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
`FetchHttpAdapter` é a implementação padrão e também pode encapsular um `fetch` customizado. Não configure `httpAdapter` e `fetch` ao mesmo tempo.
|
|
138
|
-
|
|
139
|
-
O logger HTTP é desabilitado por padrão. Passe uma configuração mutável para observar e manipular eventos de request, response e erro; altere `enabled` a qualquer momento sem recriar o client:
|
|
140
|
-
|
|
141
|
-
```ts
|
|
142
|
-
import { createClient, type HttpLoggerOptions } from "@authyon/auth";
|
|
143
|
-
|
|
144
|
-
const httpLogger: HttpLoggerOptions = {
|
|
145
|
-
enabled: true,
|
|
146
|
-
logger(event) {
|
|
147
|
-
observability.track(event.type, event);
|
|
148
|
-
},
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
const authyon = createClient({ envKey: "pk_live_...", httpLogger });
|
|
152
|
-
|
|
153
|
-
httpLogger.enabled = false; // desabilita
|
|
154
|
-
httpLogger.enabled = true; // habilita novamente
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
Sem um `logger` customizado, os eventos usam `console.debug`. Headers, bodies e valores de query string são removidos dos eventos para evitar vazamento de tokens, senhas e dados pessoais. Falhas do logger são isoladas e não interrompem a autenticação. Para decorar um adapter manualmente, use `LoggingHttpAdapter`.
|
|
158
|
-
|
|
159
|
-
## API
|
|
160
|
-
|
|
161
|
-
## Autorização baseada em permissões
|
|
162
|
-
|
|
163
|
-
Crie uma ability diretamente do usuário da sessão. Permissões Authyon seguem `subject:action`, por exemplo `tickets:read` e `documents:update`:
|
|
164
|
-
|
|
165
|
-
```ts
|
|
166
|
-
import { createAuthyonAbility } from "@authyon/auth";
|
|
167
|
-
|
|
168
|
-
const ability = createAuthyonAbility(session.user, {
|
|
169
|
-
rules: [
|
|
170
|
-
{
|
|
171
|
-
action: "update",
|
|
172
|
-
subject: "documents",
|
|
173
|
-
inverted: true,
|
|
174
|
-
conditions: { locked: true },
|
|
175
|
-
reason: "Documento bloqueado",
|
|
176
|
-
},
|
|
177
|
-
],
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
ability.can("read", "tickets");
|
|
181
|
-
ability.can("update", { __type: "documents", locked: false });
|
|
182
|
-
ability.cannot("update", { __type: "documents", locked: true });
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
Também estão disponíveis `AuthyonAbilityBuilder`, regras por role, campos, condições com operadores (`$eq`, `$ne`, `$in`, `$nin`, `$gt`, `$gte`, `$lt`, `$lte`, `$exists`, `$and`, `$or`), `rulesFor()`, `update()` e o evento `updated`. A autorização é local e nega por padrão; o backend continua responsável por validar toda ação sensível.
|
|
186
|
-
|
|
187
|
-
### Next.js Client Side
|
|
188
|
-
|
|
189
|
-
`@authyon/auth` e o motor de abilities são isomórficos e podem ser usados em Client Components:
|
|
190
|
-
|
|
191
|
-
```tsx
|
|
192
|
-
"use client";
|
|
193
|
-
|
|
194
|
-
import { createAuthyonAbility } from "@authyon/auth";
|
|
195
|
-
|
|
196
|
-
export function EditButton({ user, document }) {
|
|
197
|
-
const ability = createAuthyonAbility(user);
|
|
198
|
-
if (ability.cannot("update", { __type: "documents", ...document })) return null;
|
|
199
|
-
return <button>Editar</button>;
|
|
200
|
-
}
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
Essa verificação controla a interface, não a segurança do backend. Nunca importe `@authyon/server` em Client Components nem envie `clientSecret` ao navegador.
|
|
204
|
-
|
|
205
|
-
Métodos de sessão/auth ficam soltos no client; os que giram em torno de um recurso específico ficam agrupados em namespaces (`user`, `organization`, `twoFactor`, `webauthn`, `sso`).
|
|
206
|
-
|
|
207
|
-
| Método | Endpoint |
|
|
208
|
-
| ------------------------------------------------------------------------ | --------------------------------- |
|
|
209
|
-
| `register({ email, username, password })` | `POST /auth/register` |
|
|
210
|
-
| `login({ email \| username, password, organizationSlug? })` | `POST /auth/login` |
|
|
211
|
-
| `verifyTwoFactor({ challengeToken, method, code?, webAuthnAssertion? })` | `POST /auth/2fa/verify` |
|
|
212
|
-
| `refresh()` | `POST /auth/refresh` |
|
|
213
|
-
| `logout({ everywhere? })` | `POST /auth/logout` |
|
|
214
|
-
| `introspect(token?)` | Deprecated: use `@authyon/server` |
|
|
215
|
-
| `validate(token?)` | Deprecated: use `@authyon/server` |
|
|
216
|
-
|
|
217
|
-
### `authyon.user`
|
|
218
|
-
|
|
219
|
-
| Método | Endpoint |
|
|
220
|
-
| ----------------------------------------------- | ------------------------------------ |
|
|
221
|
-
| `user.me()` | `GET /auth/me` |
|
|
222
|
-
| `user.sessions()` | `GET /auth/sessions` |
|
|
223
|
-
| `user.revokeSession(sessionId)` | `DELETE /auth/sessions/{id}` |
|
|
224
|
-
| `user.activities(params?)` | `GET /auth/me/activities` |
|
|
225
|
-
| `user.requestPasswordReset(email, options?)` | `POST /auth/password-reset/request` |
|
|
226
|
-
| `user.validatePasswordReset(token)` | `POST /auth/password-reset/validate` |
|
|
227
|
-
| `user.confirmPasswordReset(token, newPassword)` | `POST /auth/password-reset/confirm` |
|
|
228
|
-
|
|
229
|
-
### `authyon.organization`
|
|
230
|
-
|
|
231
|
-
| Método | Endpoint |
|
|
232
|
-
| ---------------------------------------------------------------- | --------------------------------------------------- |
|
|
233
|
-
| `organization.list({ search?, skip?, take? })` | `GET /auth/tenants` |
|
|
234
|
-
| `organization.create(params?)` | `POST /auth/tenants` |
|
|
235
|
-
| `organization.get(organizationId)` | `GET /auth/tenants/{id}` |
|
|
236
|
-
| `organization.rename(organizationId, name)` | `PATCH /auth/tenants/{id}` |
|
|
237
|
-
| `organization.switch(slug)` | `POST /auth/switch-tenant` |
|
|
238
|
-
| `organization.current()` | — (lê `activeOrganization` da sessão em cache) |
|
|
239
|
-
| `organization.members.list(organizationId, params?)` | `GET /auth/tenants/{id}/members` |
|
|
240
|
-
| `organization.members.invite(organizationId, params)` | `POST /auth/tenants/{id}/members` |
|
|
241
|
-
| `organization.members.remove(organizationId, userId)` | `DELETE /auth/tenants/{id}/members/{userId}` |
|
|
242
|
-
| `organization.roles.list(organizationId)` | `GET /auth/tenants/{id}/roles` |
|
|
243
|
-
| `organization.invites.list(organizationId)` | `GET /auth/tenants/{id}/invites` |
|
|
244
|
-
| `organization.invites.create(organizationId, params)` | `POST /auth/tenants/{id}/invites` |
|
|
245
|
-
| `organization.invites.resend(organizationId, inviteId, params?)` | `POST /auth/tenants/{id}/invites/{inviteId}/resend` |
|
|
246
|
-
| `organization.invites.revoke(organizationId, inviteId)` | `DELETE /auth/tenants/{id}/invites/{inviteId}` |
|
|
247
|
-
| `organization.invites.preview(token)` | `POST /auth/tenant-invites/preview` (público) |
|
|
248
|
-
| `organization.invites.accept(params)` | `POST /auth/tenant-invites/accept` (público) |
|
|
249
|
-
|
|
250
|
-
`organization.members.invite` adiciona um usuário que já existe no ambiente. Para convidar
|
|
251
|
-
um e-mail que talvez ainda não tenha conta, use `organization.invites.create`: a resposta
|
|
252
|
-
sempre traz o `acceptUrl` completo e `sendEmail: false` só gera o link, sem enviar e-mail.
|
|
253
|
-
O link vale 7 dias, uma única vez, e não pode ser lido de novo depois. Na página de aceite,
|
|
254
|
-
leia o `token` da URL, chame `preview(token)` e, se `accountExists` for falso, peça senha
|
|
255
|
-
(e usuário quando `usernameRequired`) antes de `accept`. Aceitar não faz login.
|
|
256
|
-
Passe `redirectUri` no `create` para indicar para onde mandar a pessoa depois do aceite: ele
|
|
257
|
-
precisa estar cadastrado (match exato) nos redirect URIs do ambiente e volta em `preview` e
|
|
258
|
-
`accept` — a página hospedada da Authyon redireciona sozinha; numa página própria, é com você.
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
|
272
|
-
|
|
|
273
|
-
| `twoFactor.
|
|
274
|
-
| `twoFactor.
|
|
275
|
-
| `twoFactor.
|
|
276
|
-
| `twoFactor.
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
|
281
|
-
|
|
|
282
|
-
| `webauthn.
|
|
283
|
-
| `webauthn.
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
|
290
|
-
|
|
|
291
|
-
| `
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
`
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
```
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
1
|
+
# @authyon/auth
|
|
2
|
+
|
|
3
|
+
SDK JS/TS para o [Authyon](https://authyon.com) — autenticação, sessões, multi-tenant e 2FA, com armazenamento e refresh de tokens transparentes.
|
|
4
|
+
|
|
5
|
+
Cobre os endpoints públicos do Authyon usando a **publishable key** (`pk_...`) — segura para identificar o ambiente no navegador, sem conceder privilégios administrativos.
|
|
6
|
+
|
|
7
|
+
Para gestão de organização/membros (secret key) e verificação de token no backend, use `@authyon/server`.
|
|
8
|
+
|
|
9
|
+
## Instalação
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install --save-exact @authyon/auth@0.2.0-beta.4
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Durante a beta, `npm install @authyon/auth@beta` acompanha o prerelease mais recente. Fixar a versão exata é recomendado para builds reproduzíveis.
|
|
16
|
+
|
|
17
|
+
## Uso rápido
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { createClient } from "@authyon/auth";
|
|
21
|
+
|
|
22
|
+
const authyon = createClient({ envKey: "pk_live_..." }); // tokens somente em memória por padrão
|
|
23
|
+
|
|
24
|
+
// Login (com suporte a 2FA)
|
|
25
|
+
const result = await authyon.login({
|
|
26
|
+
email: "alice@acme.com",
|
|
27
|
+
password: "...",
|
|
28
|
+
organizationSlug: "acme", // opcional
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (result.twoFactorRequired) {
|
|
32
|
+
const code = prompt(`Código 2FA (${result.methods.join(", ")})`);
|
|
33
|
+
await authyon.verifyTwoFactor({
|
|
34
|
+
challengeToken: result.challengeToken,
|
|
35
|
+
method: "authenticator",
|
|
36
|
+
code: code!,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Usuário atual — o access token é renovado automaticamente quando necessário
|
|
41
|
+
const user = await authyon.user.me();
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Next.js e React
|
|
45
|
+
|
|
46
|
+
Use o entrypoint dedicado `@authyon/auth/react`:
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
<AuthyonProvider client={authyon}>
|
|
50
|
+
<SessionGuard loadingFallback={<Loading />}>
|
|
51
|
+
<PermissionGuard action="read" subject="reports">
|
|
52
|
+
<Reports />
|
|
53
|
+
</PermissionGuard>
|
|
54
|
+
</SessionGuard>
|
|
55
|
+
</AuthyonProvider>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
O provider faz refresh automático, valida a sessão por `GET /auth/me`, atualiza usuário e permissions, revalida quando a aba volta ao foco e não libera guards enquanto a validação inicial estiver pendente. Use `useAuthyon()`, `useAuthyonAbility()` e `useCan()` para fluxos programáticos.
|
|
59
|
+
|
|
60
|
+
Use `transformUser` para expor campos derivados sem alterar a sessão mantida pela lib:
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
type ApplicationUser = User & { fullName: string };
|
|
64
|
+
|
|
65
|
+
<AuthyonProvider<ApplicationUser>
|
|
66
|
+
client={authyon}
|
|
67
|
+
transformUser={(user) => ({
|
|
68
|
+
...user,
|
|
69
|
+
fullName: [user.firstName, user.lastName].filter(Boolean).join(" "),
|
|
70
|
+
})}
|
|
71
|
+
>
|
|
72
|
+
<App />
|
|
73
|
+
</AuthyonProvider>;
|
|
74
|
+
|
|
75
|
+
const { user, session } = useAuthyon<ApplicationUser>();
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
O usuário transformado é retornado em `user` e em `session.user` após validação, refresh, login e troca de organização. O transformador deve ser puro e só derivar dados do perfil do Authyon; dados externos pertencem ao contexto da aplicação.
|
|
79
|
+
|
|
80
|
+
Para configuração progressiva, use o builder:
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
const authyon = new AuthyonClientBuilder("pk_live_...")
|
|
84
|
+
.withStorage(createMemoryStorage())
|
|
85
|
+
.withTimeout(10_000)
|
|
86
|
+
.build();
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Sessão e tokens
|
|
90
|
+
|
|
91
|
+
- Tokens ficam somente em memória por padrão, reduzindo o impacto de XSS e sendo removidos ao recarregar a página.
|
|
92
|
+
- Persistência é opt-in com `storage: createLocalStorage()`. Isso melhora conveniência, mas qualquer XSS na aplicação poderá ler os tokens; criptografar o valor não elimina esse risco porque o JavaScript também precisa acessar a chave.
|
|
93
|
+
- `getAccessToken()` renova o token automaticamente antes de expirar (refresh token é single-use e rotacionado, com single-flight para evitar corridas).
|
|
94
|
+
- Chamadas autenticadas que retornam 401 fazem um refresh e uma retentativa automática.
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
import { createClient, createLocalStorage } from "@authyon/auth";
|
|
98
|
+
|
|
99
|
+
// Use somente quando a persistência for um requisito aceito conscientemente.
|
|
100
|
+
const persistentClient = createClient({
|
|
101
|
+
envKey: "pk_live_...",
|
|
102
|
+
storage: createLocalStorage(),
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const token = await authyon.getAccessToken(); // sempre válido, ou null se deslogado
|
|
106
|
+
|
|
107
|
+
const unsubscribe = authyon.onAuthStateChange((event) => {
|
|
108
|
+
// "signed_in" | "refreshed" | "signed_out"
|
|
109
|
+
console.log(event.type);
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Para aplicações de maior risco, prefira um BFF que mantenha o refresh token em cookie `HttpOnly`, `Secure` e `SameSite`, com proteção CSRF. O SDK também exige HTTPS para APIs remotas e aplica timeout de 15 segundos por padrão (`timeoutMs: 0` desabilita).
|
|
114
|
+
|
|
115
|
+
## HTTP Adapter
|
|
116
|
+
|
|
117
|
+
Use `httpAdapter` para integrar mocks, tracing ou outra biblioteca HTTP. O mesmo contrato está disponível em `@authyon/auth` e `@authyon/server`:
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
import { createClient, type HttpAdapter } from "@authyon/auth";
|
|
121
|
+
|
|
122
|
+
const httpAdapter: HttpAdapter = {
|
|
123
|
+
async request(request) {
|
|
124
|
+
// tracing, métricas ou adaptação para sua stack HTTP
|
|
125
|
+
return fetch(request.url, {
|
|
126
|
+
method: request.method,
|
|
127
|
+
headers: request.headers,
|
|
128
|
+
body: request.body,
|
|
129
|
+
signal: request.signal,
|
|
130
|
+
});
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const authyon = createClient({ envKey: "pk_live_...", httpAdapter });
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
`FetchHttpAdapter` é a implementação padrão e também pode encapsular um `fetch` customizado. Não configure `httpAdapter` e `fetch` ao mesmo tempo.
|
|
138
|
+
|
|
139
|
+
O logger HTTP é desabilitado por padrão. Passe uma configuração mutável para observar e manipular eventos de request, response e erro; altere `enabled` a qualquer momento sem recriar o client:
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
import { createClient, type HttpLoggerOptions } from "@authyon/auth";
|
|
143
|
+
|
|
144
|
+
const httpLogger: HttpLoggerOptions = {
|
|
145
|
+
enabled: true,
|
|
146
|
+
logger(event) {
|
|
147
|
+
observability.track(event.type, event);
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const authyon = createClient({ envKey: "pk_live_...", httpLogger });
|
|
152
|
+
|
|
153
|
+
httpLogger.enabled = false; // desabilita
|
|
154
|
+
httpLogger.enabled = true; // habilita novamente
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Sem um `logger` customizado, os eventos usam `console.debug`. Headers, bodies e valores de query string são removidos dos eventos para evitar vazamento de tokens, senhas e dados pessoais. Falhas do logger são isoladas e não interrompem a autenticação. Para decorar um adapter manualmente, use `LoggingHttpAdapter`.
|
|
158
|
+
|
|
159
|
+
## API
|
|
160
|
+
|
|
161
|
+
## Autorização baseada em permissões
|
|
162
|
+
|
|
163
|
+
Crie uma ability diretamente do usuário da sessão. Permissões Authyon seguem `subject:action`, por exemplo `tickets:read` e `documents:update`:
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
import { createAuthyonAbility } from "@authyon/auth";
|
|
167
|
+
|
|
168
|
+
const ability = createAuthyonAbility(session.user, {
|
|
169
|
+
rules: [
|
|
170
|
+
{
|
|
171
|
+
action: "update",
|
|
172
|
+
subject: "documents",
|
|
173
|
+
inverted: true,
|
|
174
|
+
conditions: { locked: true },
|
|
175
|
+
reason: "Documento bloqueado",
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
ability.can("read", "tickets");
|
|
181
|
+
ability.can("update", { __type: "documents", locked: false });
|
|
182
|
+
ability.cannot("update", { __type: "documents", locked: true });
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Também estão disponíveis `AuthyonAbilityBuilder`, regras por role, campos, condições com operadores (`$eq`, `$ne`, `$in`, `$nin`, `$gt`, `$gte`, `$lt`, `$lte`, `$exists`, `$and`, `$or`), `rulesFor()`, `update()` e o evento `updated`. A autorização é local e nega por padrão; o backend continua responsável por validar toda ação sensível.
|
|
186
|
+
|
|
187
|
+
### Next.js Client Side
|
|
188
|
+
|
|
189
|
+
`@authyon/auth` e o motor de abilities são isomórficos e podem ser usados em Client Components:
|
|
190
|
+
|
|
191
|
+
```tsx
|
|
192
|
+
"use client";
|
|
193
|
+
|
|
194
|
+
import { createAuthyonAbility } from "@authyon/auth";
|
|
195
|
+
|
|
196
|
+
export function EditButton({ user, document }) {
|
|
197
|
+
const ability = createAuthyonAbility(user);
|
|
198
|
+
if (ability.cannot("update", { __type: "documents", ...document })) return null;
|
|
199
|
+
return <button>Editar</button>;
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Essa verificação controla a interface, não a segurança do backend. Nunca importe `@authyon/server` em Client Components nem envie `clientSecret` ao navegador.
|
|
204
|
+
|
|
205
|
+
Métodos de sessão/auth ficam soltos no client; os que giram em torno de um recurso específico ficam agrupados em namespaces (`user`, `organization`, `twoFactor`, `webauthn`, `sso`).
|
|
206
|
+
|
|
207
|
+
| Método | Endpoint |
|
|
208
|
+
| ------------------------------------------------------------------------ | --------------------------------- |
|
|
209
|
+
| `register({ email, username, password })` | `POST /auth/register` |
|
|
210
|
+
| `login({ email \| username, password, organizationSlug? })` | `POST /auth/login` |
|
|
211
|
+
| `verifyTwoFactor({ challengeToken, method, code?, webAuthnAssertion? })` | `POST /auth/2fa/verify` |
|
|
212
|
+
| `refresh()` | `POST /auth/refresh` |
|
|
213
|
+
| `logout({ everywhere? })` | `POST /auth/logout` |
|
|
214
|
+
| `introspect(token?)` | Deprecated: use `@authyon/server` |
|
|
215
|
+
| `validate(token?)` | Deprecated: use `@authyon/server` |
|
|
216
|
+
|
|
217
|
+
### `authyon.user`
|
|
218
|
+
|
|
219
|
+
| Método | Endpoint |
|
|
220
|
+
| ----------------------------------------------- | ------------------------------------ |
|
|
221
|
+
| `user.me()` | `GET /auth/me` |
|
|
222
|
+
| `user.sessions()` | `GET /auth/sessions` |
|
|
223
|
+
| `user.revokeSession(sessionId)` | `DELETE /auth/sessions/{id}` |
|
|
224
|
+
| `user.activities(params?)` | `GET /auth/me/activities` |
|
|
225
|
+
| `user.requestPasswordReset(email, options?)` | `POST /auth/password-reset/request` |
|
|
226
|
+
| `user.validatePasswordReset(token)` | `POST /auth/password-reset/validate` |
|
|
227
|
+
| `user.confirmPasswordReset(token, newPassword)` | `POST /auth/password-reset/confirm` |
|
|
228
|
+
|
|
229
|
+
### `authyon.organization`
|
|
230
|
+
|
|
231
|
+
| Método | Endpoint |
|
|
232
|
+
| ---------------------------------------------------------------- | --------------------------------------------------- |
|
|
233
|
+
| `organization.list({ search?, skip?, take? })` | `GET /auth/tenants` |
|
|
234
|
+
| `organization.create(params?)` | `POST /auth/tenants` |
|
|
235
|
+
| `organization.get(organizationId)` | `GET /auth/tenants/{id}` |
|
|
236
|
+
| `organization.rename(organizationId, name)` | `PATCH /auth/tenants/{id}` |
|
|
237
|
+
| `organization.switch(slug)` | `POST /auth/switch-tenant` |
|
|
238
|
+
| `organization.current()` | — (lê `activeOrganization` da sessão em cache) |
|
|
239
|
+
| `organization.members.list(organizationId, params?)` | `GET /auth/tenants/{id}/members` |
|
|
240
|
+
| `organization.members.invite(organizationId, params)` | `POST /auth/tenants/{id}/members` |
|
|
241
|
+
| `organization.members.remove(organizationId, userId)` | `DELETE /auth/tenants/{id}/members/{userId}` |
|
|
242
|
+
| `organization.roles.list(organizationId)` | `GET /auth/tenants/{id}/roles` |
|
|
243
|
+
| `organization.invites.list(organizationId)` | `GET /auth/tenants/{id}/invites` |
|
|
244
|
+
| `organization.invites.create(organizationId, params)` | `POST /auth/tenants/{id}/invites` |
|
|
245
|
+
| `organization.invites.resend(organizationId, inviteId, params?)` | `POST /auth/tenants/{id}/invites/{inviteId}/resend` |
|
|
246
|
+
| `organization.invites.revoke(organizationId, inviteId)` | `DELETE /auth/tenants/{id}/invites/{inviteId}` |
|
|
247
|
+
| `organization.invites.preview(token)` | `POST /auth/tenant-invites/preview` (público) |
|
|
248
|
+
| `organization.invites.accept(params)` | `POST /auth/tenant-invites/accept` (público) |
|
|
249
|
+
|
|
250
|
+
`organization.members.invite` adiciona um usuário que já existe no ambiente. Para convidar
|
|
251
|
+
um e-mail que talvez ainda não tenha conta, use `organization.invites.create`: a resposta
|
|
252
|
+
sempre traz o `acceptUrl` completo e `sendEmail: false` só gera o link, sem enviar e-mail.
|
|
253
|
+
O link vale 7 dias, uma única vez, e não pode ser lido de novo depois. Na página de aceite,
|
|
254
|
+
leia o `token` da URL, chame `preview(token)` e, se `accountExists` for falso, peça senha
|
|
255
|
+
(e usuário quando `usernameRequired`) antes de `accept`. Aceitar não faz login.
|
|
256
|
+
Passe `redirectUri` no `create` para indicar para onde mandar a pessoa depois do aceite: ele
|
|
257
|
+
precisa estar cadastrado (match exato) nos redirect URIs do ambiente e volta em `preview` e
|
|
258
|
+
`accept` — a página hospedada da Authyon redireciona sozinha; numa página própria, é com você.
|
|
259
|
+
|
|
260
|
+
Um tenant pode ficar embaixo de outro. `organization.list()` traz, além dos tenants do
|
|
261
|
+
usuário, os que ele pode tornar ativos sem ser membro — marcados `inherited: true`, e só os
|
|
262
|
+
que de fato permitem a troca, então o switcher nunca oferece um tenant que
|
|
263
|
+
`organization.switch()` depois recusaria. `depth` indenta a lista na árvore que ela
|
|
264
|
+
descreve, e `organization.create({ parentTenantId })` cria embaixo de um tenant ao qual o
|
|
265
|
+
usuário já pertence (precisa de `subtenants:tree:create` lá). `organization.rename()` exige
|
|
266
|
+
`tenants:settings:manage` — o valor antes escrito `tenants:manage`, que a API não
|
|
267
|
+
armazenava. Ver o [guia de hierarquia](../../docs/tenantHierarchy.md).
|
|
268
|
+
|
|
269
|
+
### `authyon.twoFactor`
|
|
270
|
+
|
|
271
|
+
| Método | Endpoint |
|
|
272
|
+
| ---------------------------------------------------- | -------------------------------------------- |
|
|
273
|
+
| `twoFactor.status()` | `GET /auth/2fa/status` |
|
|
274
|
+
| `twoFactor.resendEmail(challengeToken)` | `POST /auth/2fa/resend-email` |
|
|
275
|
+
| `twoFactor.setupAuthenticator()` | `POST /auth/2fa/authenticator/setup` |
|
|
276
|
+
| `twoFactor.confirmAuthenticator(code)` | `POST /auth/2fa/authenticator/confirm` |
|
|
277
|
+
| `twoFactor.enableEmail(code?)` | `POST /auth/2fa/email/enable` |
|
|
278
|
+
| `twoFactor.disable(method, currentPassword)` | `POST /auth/2fa/disable` |
|
|
279
|
+
| `twoFactor.regenerateRecoveryCodes(currentPassword)` | `POST /auth/2fa/recovery-codes/regenerate` |
|
|
280
|
+
| `twoFactor.webauthn.registerStart()` | `POST /auth/2fa/webauthn/register/start` |
|
|
281
|
+
| `twoFactor.webauthn.registerFinish(...)` | `POST /auth/2fa/webauthn/register/finish` |
|
|
282
|
+
| `twoFactor.webauthn.credentials()` | `GET /auth/2fa/webauthn/credentials` |
|
|
283
|
+
| `twoFactor.webauthn.renameCredential(id, nickname)` | `PATCH /auth/2fa/webauthn/credentials/{id}` |
|
|
284
|
+
| `twoFactor.webauthn.removeCredential(id, pwd)` | `DELETE /auth/2fa/webauthn/credentials/{id}` |
|
|
285
|
+
| `twoFactor.webauthn.assertionStart(challengeToken)` | `POST /auth/2fa/webauthn/assertion/start` |
|
|
286
|
+
|
|
287
|
+
### `authyon.webauthn` (login sem senha)
|
|
288
|
+
|
|
289
|
+
| Método | Endpoint |
|
|
290
|
+
| --------------------------------- | ---------------------------------- |
|
|
291
|
+
| `webauthn.loginStart(email?)` | `POST /auth/webauthn/login/start` |
|
|
292
|
+
| `webauthn.loginFinish(assertion)` | `POST /auth/webauthn/login/finish` |
|
|
293
|
+
|
|
294
|
+
### `authyon.sso` (login social)
|
|
295
|
+
|
|
296
|
+
| Método | Endpoint |
|
|
297
|
+
| -------------------------------- | ------------------------------------------------------------------------------------- |
|
|
298
|
+
| `sso.providers()` | `GET /auth/sso/providers` |
|
|
299
|
+
| `sso.startUrl(provider, params)` | monta a URL de `GET /auth/sso/{provider}/start` (não faz a chamada — navegue até ela) |
|
|
300
|
+
| `sso.exchange(code)` | `POST /auth/sso/exchange` |
|
|
301
|
+
|
|
302
|
+
### `authyon.security` (aprovação com OTP/passkey)
|
|
303
|
+
|
|
304
|
+
`security.approvals`: o cliente aprova o que o seu backend pediu
|
|
305
|
+
(`@authyon/server` → `security.approvals.create`, com um `payload` livre) usando o código
|
|
306
|
+
do app autenticador ou a passkey. Para só validar um código, use `security.otp.check` no
|
|
307
|
+
`@authyon/server`. Guia completo: [`docs/security-otp-approvals.md`](../../docs/security-otp-approvals.md).
|
|
308
|
+
|
|
309
|
+
```ts
|
|
310
|
+
const pending = await authyon.security.approvals.get(approvalId);
|
|
311
|
+
// mostre pending.payload ao cliente
|
|
312
|
+
await authyon.security.approvals.confirm(approvalId, {
|
|
313
|
+
method: "authenticator",
|
|
314
|
+
code: "123456",
|
|
315
|
+
});
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
| Método | Endpoint |
|
|
319
|
+
| ---------------------------------------- | ------------------------------------------------- |
|
|
320
|
+
| `security.methods()` | `GET /auth/me` (campo `security`) |
|
|
321
|
+
| `security.approvals.get(id)` | `GET /auth/authorizations/{id}` |
|
|
322
|
+
| `security.approvals.webauthnOptions(id)` | `POST /auth/authorizations/{id}/webauthn/options` |
|
|
323
|
+
| `security.approvals.confirm(id, input?)` | `POST /auth/authorizations/{id}/confirm` |
|
|
324
|
+
| `security.approvals.reject(id)` | `POST /auth/authorizations/{id}/reject` |
|
|
325
|
+
|
|
326
|
+
## Invalidação de token
|
|
327
|
+
|
|
328
|
+
- **Sessão atual**: `logout()` revoga o refresh token atual; `logout({ everywhere: true })` revoga todos os refresh tokens do usuário.
|
|
329
|
+
- **Uma sessão específica**: `user.revokeSession(sessionId)`, usando o `id` retornado por `user.sessions()` — derruba um dispositivo sem afetar a sessão atual.
|
|
330
|
+
- **Access token**: por ser um JWT stateless, o access token continua "válido" até expirar (`expiresIn`, tipicamente 30 min) mesmo após revogar o refresh token. Para checar revogação em tempo real no seu backend, use `validate()` (cross-checa o estado no banco) em vez de `introspect()`.
|
|
331
|
+
|
|
332
|
+
## Erros
|
|
333
|
+
|
|
334
|
+
Toda resposta não-2xx vira um `AuthyonError` (problem+json). Compare pelo `code` legível por máquina, nunca pelo `title`:
|
|
335
|
+
|
|
336
|
+
Falhas de transporte usam `request.network_error` ou `request.timeout`. Quando presentes, `requestId` e `retryAfter` ajudam suporte e tratamento de rate limit sem expor o corpo da requisição.
|
|
337
|
+
|
|
338
|
+
`interpret()` converte códigos e status em uma decisão estável para a interface:
|
|
339
|
+
|
|
340
|
+
```ts
|
|
341
|
+
try {
|
|
342
|
+
await authyon.login({ email, password });
|
|
343
|
+
} catch (cause) {
|
|
344
|
+
if (!(cause instanceof AuthyonError)) throw cause;
|
|
345
|
+
const { category, action, retryable, retryAfter } = cause.interpret();
|
|
346
|
+
// category: authentication | validation | rate_limit | network | ...
|
|
347
|
+
// action: reauthenticate | fix_input | retry | ...
|
|
348
|
+
}
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
Veja o catálogo completo em `ERRORS.md` no repositório.
|
|
352
|
+
|
|
353
|
+
```ts
|
|
354
|
+
import { AuthyonError, ErrorCodes } from "@authyon/auth";
|
|
355
|
+
|
|
356
|
+
try {
|
|
357
|
+
await authyon.register({ email, password });
|
|
358
|
+
} catch (err) {
|
|
359
|
+
if (err instanceof AuthyonError && err.is(ErrorCodes.EmailTaken)) {
|
|
360
|
+
// e-mail já cadastrado
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
## Build
|
|
366
|
+
|
|
367
|
+
```bash
|
|
368
|
+
npm install
|
|
369
|
+
npm run build # dist/ (ESM + CJS + .d.ts via tsup)
|
|
370
|
+
npm run typecheck
|
|
371
|
+
```
|