@adonis-agora/authkit-server 0.48.0 → 0.50.0
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/build/host/views/login.edge +18 -0
- package/build/index.d.ts +18 -2
- package/build/index.js +16 -1
- package/build/src/accounts/account_store.d.ts +59 -1
- package/build/src/accounts/account_store.js +5 -0
- package/build/src/accounts/lucid_store/core.d.ts +2 -2
- package/build/src/accounts/lucid_store/core.js +105 -4
- package/build/src/audit/audit_sink.d.ts +1 -1
- package/build/src/define_config.d.ts +22 -0
- package/build/src/define_config.js +5 -0
- package/build/src/host/account_screen_props.d.ts +163 -0
- package/build/src/host/account_screen_props.js +23 -0
- package/build/src/host/controllers/account_confirm_controller.js +3 -2
- package/build/src/host/controllers/account_mfa_controller.js +9 -6
- package/build/src/host/controllers/account_security_controller.js +3 -2
- package/build/src/host/controllers/account_session_controller.js +8 -3
- package/build/src/host/controllers/interaction_controller.d.ts +11 -0
- package/build/src/host/controllers/interaction_controller.js +136 -6
- package/build/src/host/default_mailer.d.ts +1 -0
- package/build/src/host/default_mailer.js +3 -0
- package/build/src/host/email_templates.d.ts +8 -0
- package/build/src/host/email_templates.js +12 -1
- package/build/src/host/i18n.d.ts +14 -0
- package/build/src/host/i18n.js +16 -0
- package/build/src/host/otp_login.d.ts +155 -0
- package/build/src/host/otp_login.js +206 -0
- package/build/src/host/rate_limit.d.ts +6 -0
- package/build/src/host/rate_limit.js +3 -0
- package/build/src/host/register_auth_host.js +8 -0
- package/build/src/host/renderers/inertia_renderer.d.ts +23 -66
- package/build/src/host/renderers/inertia_renderer.js +23 -66
- package/package.json +2 -2
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Login por OTP (código digitável) — helpers puros + máquina de estados da
|
|
3
|
+
* verificação.
|
|
4
|
+
*
|
|
5
|
+
* ── Por que este módulo existe (e o porquê da decisão de armazenamento) ───────
|
|
6
|
+
* O host passwordless já tem magic link (token de 256 bits, IMPOSSÍVEL de
|
|
7
|
+
* adivinhar). O código de 6 dígitos é ADIVINHÁVEL: exige lockout dedicado +
|
|
8
|
+
* throttle — segurança que não se reimplementa por host. O mesmo e-mail passa a
|
|
9
|
+
* carregar LINK e CÓDIGO; os dois completam a MESMA interaction OIDC.
|
|
10
|
+
*
|
|
11
|
+
* ── Decisão de armazenamento (investigação registrada no código) ─────────────
|
|
12
|
+
* O SPEC ranqueia três opções e manda a investigação decidir. Resultado:
|
|
13
|
+
*
|
|
14
|
+
* 1. (preferida no spec) Guardar `otpHash`/`otpExpiresAt`/`otpAttempts` no
|
|
15
|
+
* REGISTRO DA INTERACTION do oidc-provider — **INVIÁVEL**. O modelo
|
|
16
|
+
* `Interaction` do oidc-provider só persiste os campos listados em
|
|
17
|
+
* `IN_PAYLOAD` (`base_model.js` filtra o payload por
|
|
18
|
+
* `IN_PAYLOAD.includes(key)` no construtor; `save()` chama
|
|
19
|
+
* `getValueAndPayload`). Campos custom de topo são DESCARTADOS ao persistir.
|
|
20
|
+
* O único slot livre persistido é `lastSubmission`, dono do mecanismo
|
|
21
|
+
* `mergeWithLastSubmission` — sequestrá-lo é frágil. Ver
|
|
22
|
+
* `node_modules/oidc-provider/lib/models/interaction.js:57` e
|
|
23
|
+
* `.../base_model.js:34`.
|
|
24
|
+
*
|
|
25
|
+
* 2. (ESCOLHIDA) Formato composto no slot já existente do token de magic link
|
|
26
|
+
* (`passwordResetToken`, hoje `ml:<token>`). Passa a `ml2:<...>` quando o
|
|
27
|
+
* OTP está ligado. Esta opção resolve os TRÊS requisitos duros de uma vez:
|
|
28
|
+
* • **Single-use conjunto** — código e link vivem no MESMO slot da MESMA
|
|
29
|
+
* linha: consumir qualquer um limpa o slot → o outro morre junto, sem
|
|
30
|
+
* coordenação entre stores.
|
|
31
|
+
* • **Contador de tentativas persistido SEM limiter** — o contador vive
|
|
32
|
+
* DENTRO do slot. O lockout é imposto pelo próprio contador persistido
|
|
33
|
+
* (fail-CLOSED: não depende do `@adonisjs/limiter`), ao contrário do
|
|
34
|
+
* `otp_lockout.ts`, que vira no-op sem limiter — perigoso para um código
|
|
35
|
+
* curto. O throttle de rota (`authkit_otp_login`) é camada EXTRA por IP.
|
|
36
|
+
* • **TTL herdado** — a coluna `passwordResetExpiresAt` já dá validade ao
|
|
37
|
+
* link; o código carrega o próprio `codeExpMs` embutido (mais curto).
|
|
38
|
+
*
|
|
39
|
+
* 3. Coluna nova via ensure-schema — desnecessária (a opção 2 não exige
|
|
40
|
+
* migração), então descartada.
|
|
41
|
+
*
|
|
42
|
+
* ── Formato do slot (`ml2:`) ─────────────────────────────────────────────────
|
|
43
|
+
* Armazenado: `ml2:<linkToken>:<codeHash>:<codeExpMs>:<attempts>`
|
|
44
|
+
* Na URL: `ml2:<linkToken>` (SÓ o token do link — o código, o hash e o
|
|
45
|
+
* contador NUNCA saem no e-mail/URL, então o atacante não tem como
|
|
46
|
+
* zerar o contador manipulando o que ele recebe).
|
|
47
|
+
*
|
|
48
|
+
* • `linkToken` — 32 bytes hex; é o token do magic link (mesma força de antes).
|
|
49
|
+
* • `codeHash` — `sha256(<uid>:<code>)` em hex, ou VAZIO quando o código foi
|
|
50
|
+
* invalidado por lockout (o link continua válido e localizável).
|
|
51
|
+
* Atrelar ao `uid` da interaction honra o escopo "por
|
|
52
|
+
* interaction" do spec: um código emitido numa interaction não
|
|
53
|
+
* verifica em outra, mesmo para o mesmo e-mail.
|
|
54
|
+
* • `codeExpMs` — epoch ms de expiração DO CÓDIGO (TTL curto, default 10 min).
|
|
55
|
+
* • `attempts` — contador server-side de tentativas erradas (começa em 0).
|
|
56
|
+
*
|
|
57
|
+
* Segurança do contador: como o link e o código compartilham o slot mas o
|
|
58
|
+
* LOCKOUT do código NÃO pode matar o link (spec), a invalidação por lockout zera
|
|
59
|
+
* o `codeHash` (mantendo `linkToken`) em vez de limpar o slot inteiro.
|
|
60
|
+
*/
|
|
61
|
+
import { createHash, randomInt, timingSafeEqual } from 'node:crypto';
|
|
62
|
+
export const OTP_LOGIN_DEFAULTS = {
|
|
63
|
+
enabled: false,
|
|
64
|
+
digits: 6,
|
|
65
|
+
ttlMinutes: 10,
|
|
66
|
+
maxAttempts: 5,
|
|
67
|
+
};
|
|
68
|
+
/** Resolve/normaliza a config `login.otp` com os defaults e limites de sanidade. */
|
|
69
|
+
export function resolveOtpLoginConfig(input) {
|
|
70
|
+
const digitsRaw = input?.digits;
|
|
71
|
+
const digits = typeof digitsRaw === 'number' && digitsRaw >= 4 && digitsRaw <= 10
|
|
72
|
+
? Math.floor(digitsRaw)
|
|
73
|
+
: OTP_LOGIN_DEFAULTS.digits;
|
|
74
|
+
const ttlRaw = input?.ttlMinutes;
|
|
75
|
+
const ttlMinutes = typeof ttlRaw === 'number' && ttlRaw >= 1 ? Math.floor(ttlRaw) : OTP_LOGIN_DEFAULTS.ttlMinutes;
|
|
76
|
+
const maxRaw = input?.maxAttempts;
|
|
77
|
+
const maxAttempts = typeof maxRaw === 'number' && maxRaw >= 1 ? Math.floor(maxRaw) : OTP_LOGIN_DEFAULTS.maxAttempts;
|
|
78
|
+
return {
|
|
79
|
+
enabled: input?.enabled ?? OTP_LOGIN_DEFAULTS.enabled,
|
|
80
|
+
digits,
|
|
81
|
+
ttlMinutes,
|
|
82
|
+
maxAttempts,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
// Geração e hashing do código
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
/**
|
|
89
|
+
* Gera um código numérico de `digits` dígitos, zero-padded, SEM viés de módulo.
|
|
90
|
+
*
|
|
91
|
+
* Usa `crypto.randomInt(0, 10 ** digits)` — o `randomInt` do Node faz rejection
|
|
92
|
+
* sampling internamente, então a distribuição é uniforme (nada de `% 10`, que
|
|
93
|
+
* enviesaria os dígitos baixos). Para `digits=6` o teto é 1_000_000, bem abaixo
|
|
94
|
+
* do limite de `randomInt` (2**48).
|
|
95
|
+
*/
|
|
96
|
+
export function generateOtpCode(digits) {
|
|
97
|
+
const max = 10 ** digits;
|
|
98
|
+
const n = randomInt(0, max);
|
|
99
|
+
return String(n).padStart(digits, '0');
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Hash do código atrelado ao `uid` da interaction: `sha256(<uid>:<code>)` em hex.
|
|
103
|
+
* Atrelar ao uid escopa o código à interaction que o emitiu.
|
|
104
|
+
*/
|
|
105
|
+
export function hashLoginOtp(uid, code) {
|
|
106
|
+
return createHash('sha256').update(`${uid}:${code}`).digest('hex');
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Comparação constant-time de dois digests hex de MESMO tamanho.
|
|
110
|
+
*
|
|
111
|
+
* `timingSafeEqual` exige buffers de tamanho igual — comprimentos diferentes
|
|
112
|
+
* lançam. Por isso a guarda de tamanho vem antes (retorno `false` sem vazar
|
|
113
|
+
* timing útil: o atacante não controla o tamanho do digest server-side, que é
|
|
114
|
+
* sempre 64 hex de um sha256).
|
|
115
|
+
*/
|
|
116
|
+
export function safeEqualHex(a, b) {
|
|
117
|
+
if (a.length !== b.length || a.length === 0)
|
|
118
|
+
return false;
|
|
119
|
+
const bufA = Buffer.from(a, 'hex');
|
|
120
|
+
const bufB = Buffer.from(b, 'hex');
|
|
121
|
+
if (bufA.length !== bufB.length)
|
|
122
|
+
return false;
|
|
123
|
+
return timingSafeEqual(bufA, bufB);
|
|
124
|
+
}
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
// Codec do slot composto `ml2:`
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
/** Prefixo do slot `passwordResetToken` quando o login por OTP está ativo. */
|
|
129
|
+
export const OTP_LOGIN_PREFIX = 'ml2:';
|
|
130
|
+
/** Só hex minúsculo (64 chars = sha256). Guard contra metacaracteres de LIKE. */
|
|
131
|
+
const HEX_64 = /^[0-9a-f]{64}$/;
|
|
132
|
+
/** Serializa o estado do OTP no formato de slot `ml2:...`. */
|
|
133
|
+
export function encodeOtpToken(state) {
|
|
134
|
+
return `${OTP_LOGIN_PREFIX}${state.linkToken}:${state.codeHash}:${state.codeExpMs}:${state.attempts}`;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Decodifica o valor ARMAZENADO no slot (`ml2:<linkToken>:<codeHash>:<exp>:<att>`).
|
|
138
|
+
* Retorna `null` se não for um slot `ml2:` bem-formado.
|
|
139
|
+
*/
|
|
140
|
+
export function decodeOtpToken(value) {
|
|
141
|
+
if (!value || !value.startsWith(OTP_LOGIN_PREFIX))
|
|
142
|
+
return null;
|
|
143
|
+
const rest = value.slice(OTP_LOGIN_PREFIX.length);
|
|
144
|
+
const parts = rest.split(':');
|
|
145
|
+
if (parts.length !== 4)
|
|
146
|
+
return null;
|
|
147
|
+
const [linkToken, codeHash, expStr, attStr] = parts;
|
|
148
|
+
if (!HEX_64.test(linkToken))
|
|
149
|
+
return null;
|
|
150
|
+
if (codeHash !== '' && !HEX_64.test(codeHash))
|
|
151
|
+
return null;
|
|
152
|
+
const codeExpMs = Number(expStr);
|
|
153
|
+
const attempts = Number(attStr);
|
|
154
|
+
if (!Number.isFinite(codeExpMs) || !Number.isInteger(attempts) || attempts < 0)
|
|
155
|
+
return null;
|
|
156
|
+
return { linkToken, codeHash, codeExpMs, attempts };
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Extrai o `linkToken` de uma URL de magic link `ml2:<linkToken>` (a forma que
|
|
160
|
+
* vai no e-mail, SEM o estado do código). Retorna `null` se não casar o formato
|
|
161
|
+
* ou se o token não for hex de 64 (guarda contra LIKE injection na busca).
|
|
162
|
+
*/
|
|
163
|
+
export function linkTokenFromOtpUrl(urlToken) {
|
|
164
|
+
if (!urlToken.startsWith(OTP_LOGIN_PREFIX))
|
|
165
|
+
return null;
|
|
166
|
+
const linkToken = urlToken.slice(OTP_LOGIN_PREFIX.length);
|
|
167
|
+
return HEX_64.test(linkToken) ? linkToken : null;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Avalia UMA tentativa de código, na ORDEM travada pelo spec:
|
|
171
|
+
* lockout (contador/estado do código) → TTL do código → comparação constant-time.
|
|
172
|
+
*
|
|
173
|
+
* O throttle de rota e a validade da interaction são resolvidos ANTES, no
|
|
174
|
+
* controller. Aqui mora só a lógica que precisa do estado persistido do código.
|
|
175
|
+
*
|
|
176
|
+
* IMPORTANTE (prova de mutação): a checagem de LOCKOUT é a primeira guarda. Se
|
|
177
|
+
* removida, um atacante que já esgotou as tentativas volta a poder chutar — o
|
|
178
|
+
* teste `remove-lockout` cobre exatamente isso.
|
|
179
|
+
*/
|
|
180
|
+
export function evaluateLoginOtp(input) {
|
|
181
|
+
const { parsed, uid, code, nowMs, maxAttempts } = input;
|
|
182
|
+
// Sem código pendente (slot vazio, `ml:` legado ou token de reset).
|
|
183
|
+
if (!parsed)
|
|
184
|
+
return { result: 'no_code' };
|
|
185
|
+
// LOCKOUT: código já invalidado (hash vazio) OU tentativas esgotadas.
|
|
186
|
+
// Fail-CLOSED — imposto pelo contador PERSISTIDO, sem depender de limiter.
|
|
187
|
+
if (parsed.codeHash === '' || parsed.attempts >= maxAttempts) {
|
|
188
|
+
return { result: 'locked' };
|
|
189
|
+
}
|
|
190
|
+
// TTL do código (mais curto que o do link).
|
|
191
|
+
if (parsed.codeExpMs < nowMs)
|
|
192
|
+
return { result: 'expired' };
|
|
193
|
+
// Comparação constant-time do hash atrelado ao uid.
|
|
194
|
+
const candidate = hashLoginOtp(uid, code);
|
|
195
|
+
if (safeEqualHex(candidate, parsed.codeHash)) {
|
|
196
|
+
// Sucesso: limpa o slot → mata o magic link junto (single-use conjunto).
|
|
197
|
+
return { result: 'ok', nextToken: null };
|
|
198
|
+
}
|
|
199
|
+
// Falha: incrementa o contador.
|
|
200
|
+
const attempts = parsed.attempts + 1;
|
|
201
|
+
if (attempts >= maxAttempts) {
|
|
202
|
+
// Última tentativa: INVALIDA o código (zera o hash) mas PRESERVA o link.
|
|
203
|
+
return { result: 'locked', nextToken: encodeOtpToken({ ...parsed, codeHash: '', attempts }) };
|
|
204
|
+
}
|
|
205
|
+
return { result: 'invalid', nextToken: encodeOtpToken({ ...parsed, attempts }) };
|
|
206
|
+
}
|
|
@@ -30,6 +30,12 @@ export interface AuthThrottles {
|
|
|
30
30
|
* orçamentos não poderem se consumir.
|
|
31
31
|
*/
|
|
32
32
|
sudo: ThrottleMiddleware;
|
|
33
|
+
/**
|
|
34
|
+
* Throttle da verificação de código OTP de login, keyed por IP em bucket
|
|
35
|
+
* PRÓPRIO (`authkit_otp_login`) e mais apertado que o `login`. Primeira barreira
|
|
36
|
+
* anti-brute-force do código adivinhável, ANTES do lockout por interaction.
|
|
37
|
+
*/
|
|
38
|
+
otpLogin: ThrottleMiddleware;
|
|
33
39
|
}
|
|
34
40
|
/**
|
|
35
41
|
* Service do `@adonisjs/limiter` resolvido de forma preguiçosa. Tipado como `any`
|
|
@@ -96,5 +96,8 @@ export function createAuthThrottles(config) {
|
|
|
96
96
|
// mesmo vindo do mesmo IP. Sem `usingKey` próprio de propósito: inventar uma
|
|
97
97
|
// key aqui seria mudar o EIXO da contagem, e o eixo certo continua sendo o IP.
|
|
98
98
|
sudo: buildThrottle('authkit_sudo', config.sudo, config.store),
|
|
99
|
+
// Verificação de código OTP: keyed por IP (default), bucket próprio e mais
|
|
100
|
+
// apertado que o login. O namespace do nome mantém a contagem separada.
|
|
101
|
+
otpLogin: buildThrottle('authkit_otp_login', config.otpLogin, config.store),
|
|
99
102
|
};
|
|
100
103
|
}
|
|
@@ -256,6 +256,11 @@ export function registerAuthHost(router, opts = {}) {
|
|
|
256
256
|
if (throttles)
|
|
257
257
|
route.use([throttles.sudo]);
|
|
258
258
|
};
|
|
259
|
+
// Bucket PRÓPRIO da verificação de código OTP: mais apertado que o login, por IP.
|
|
260
|
+
const withOtpLogin = (route) => {
|
|
261
|
+
if (throttles)
|
|
262
|
+
route.use([throttles.otpLogin]);
|
|
263
|
+
};
|
|
259
264
|
// ─── Assets estáticos do host-kit (públicos, sem autenticação) ─────────────
|
|
260
265
|
// Bundle do @simplewebauthn/browser servido pelo próprio app, no lugar do
|
|
261
266
|
// import de CDN público que as views de login/MFA/confirm faziam.
|
|
@@ -286,6 +291,9 @@ export function registerAuthHost(router, opts = {}) {
|
|
|
286
291
|
// Magic link (passwordless): POST emite (throttled), GET consome o token do link.
|
|
287
292
|
withLogin(router.post('/auth/interaction/:uid/magic', [C.interaction, 'magicLinkRequest']));
|
|
288
293
|
router.get('/auth/interaction/:uid/magic', [C.interaction, 'magicLinkConsume']);
|
|
294
|
+
// Login por OTP (código digitável): verifica o código no bucket dedicado
|
|
295
|
+
// `authkit_otp_login` (por IP, mais apertado que o login).
|
|
296
|
+
withOtpLogin(router.post('/auth/interaction/:uid/otp-verify', [C.interaction, 'otpVerify']));
|
|
289
297
|
router.post('/auth/interaction/:uid/consent', [C.interaction, 'consent']);
|
|
290
298
|
router.get('/auth/interaction/:uid/switch', [C.interaction, 'switchIdentifier']);
|
|
291
299
|
// OTP unlock: link enviado por e-mail quando o fator TOTP/recovery é travado.
|
|
@@ -58,76 +58,33 @@ export type AuthkitScreen = 'login' | 'signup' | 'consent' | 'forgot' | 'reset'
|
|
|
58
58
|
*
|
|
59
59
|
* ---
|
|
60
60
|
*
|
|
61
|
-
* ### Props
|
|
61
|
+
* ### Props das telas de conta — tipos exportados (fonte única)
|
|
62
62
|
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
63
|
+
* Um host que escreve as telas do console em React tipa cada página com os tipos
|
|
64
|
+
* de props exportados do pacote, em vez de copiar o shape à mão deste docblock —
|
|
65
|
+
* que já ficou desatualizado no passado. Cada tipo é a FONTE ÚNICA da verdade: os
|
|
66
|
+
* controllers satisfazem (`satisfies Omit<…, 'messages'>`) exatamente o mesmo
|
|
67
|
+
* tipo ao chamar `render()`, então divergência quebra o `tsc` do pacote. Ver
|
|
68
|
+
* `src/host/account_screen_props.ts`.
|
|
69
69
|
*
|
|
70
|
-
*
|
|
70
|
+
* | Tela | Tipo exportado |
|
|
71
|
+
* |---------------------------|----------------------------|
|
|
72
|
+
* | `account/login` | {@link AccountLoginProps} |
|
|
73
|
+
* | `account/security` | {@link AccountSecurityProps} |
|
|
74
|
+
* | `account/mfa` | {@link AccountMfaProps} |
|
|
75
|
+
* | `account/confirm` | {@link AccountConfirmProps} |
|
|
76
|
+
* | `account/email-confirmed` | {@link AccountEmailConfirmedProps} |
|
|
71
77
|
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
* | `supported` | `boolean` | `false` quando o store não suporta o self-service de segurança (troca de senha/e-mail) — a tela deve degradar. |
|
|
76
|
-
* | `profileSupported` | `boolean` | `true` quando o store suporta editar nome/avatar (`updateProfile`). |
|
|
77
|
-
* | `avatarUploadSupported` | `boolean` | `true` quando algum backend (drive OU media) pode armazenar o upload de avatar. |
|
|
78
|
-
* | `email` | `string` | E-mail atual da conta (`''` se ausente). |
|
|
79
|
-
* | `name` | `string` | Nome atual da conta (`''` se ausente). |
|
|
80
|
-
* | `avatarUrl` | `string` | URL do avatar atual (`''` se ausente). |
|
|
81
|
-
* | `passwordChanged` | `string \| null` | Flash de sucesso da troca de senha (mensagem localizada) ou `null`. |
|
|
82
|
-
* | `emailChangeRequested` | `string \| null` | Flash: link de confirmação de troca de e-mail enviado (ou cancelamento) ou `null`. |
|
|
83
|
-
* | `emailChanged` | `string \| null` | Flash: troca de e-mail concluída ou `null`. |
|
|
84
|
-
* | `profileUpdated` | `string \| null` | Flash: perfil atualizado ou `null`. |
|
|
85
|
-
* | `error` | `string \| null` | Flash de erro de segurança (senha inválida, e-mail em uso, política violada) ou `null`. |
|
|
86
|
-
* | `trustedDevicesEnabled` | `boolean` | `true` quando o recurso de dispositivos confiáveis está ligado. |
|
|
87
|
-
* | `trustedDevicesRevoked` | `string \| null` | Flash: confiança deste navegador revogada ou `null`. |
|
|
88
|
-
* | `sessionsSupported` | `boolean` | `true` quando o adapter OIDC enumera as sessões ativas da conta. |
|
|
89
|
-
* | `sessions` | `Array<{ loginTs: string; browser: string; os: string; ip: string; location: string }>` | Sessões ativas da própria conta (vazio quando não suportado). `loginTs` é ISO ou `''`. |
|
|
90
|
-
* | `exportSupported` | `boolean` | Sempre `true` — export de dados (portabilidade/LGPD) disponível para a conta logada. |
|
|
91
|
-
* | `deletionSupported` | `boolean` | `true` quando o store suporta hard delete (danger zone). |
|
|
92
|
-
* | `deleteError` | `string \| null` | Flash de erro da confirmação de deleção ou `null`. |
|
|
93
|
-
* | `messages` | `AuthMessages` | Catálogo de mensagens i18n. |
|
|
78
|
+
* A prop `messages` (catálogo i18n {@link AuthMessages}) é injetada por este
|
|
79
|
+
* renderer como shared prop e faz parte de todos esses tipos — os controllers,
|
|
80
|
+
* que não a passam, satisfazem `Omit<…, 'messages'>`.
|
|
94
81
|
*
|
|
95
|
-
*
|
|
82
|
+
* ```tsx
|
|
83
|
+
* import type { AccountSecurityProps } from '@adonis-agora/authkit-server'
|
|
96
84
|
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
* | Prop | Tipo | Descrição |
|
|
102
|
-
* |---------------------|---------------------|-----------|
|
|
103
|
-
* | `csrfToken` | `string` | Token CSRF para os formulários de enroll/confirm/disable e passkeys. |
|
|
104
|
-
* | `enabled` | `boolean` | `true` quando o TOTP já está confirmado (habilitado) para a conta. |
|
|
105
|
-
* | `recoveryCodes` | `string[] \| null` | Códigos de recuperação recém-gerados (exibidos UMA vez após enroll/confirm) ou `null`. |
|
|
106
|
-
* | `passkeysSupported` | `boolean` | `true` quando o store persiste credenciais WebAuthn (passkeys). |
|
|
107
|
-
* | `passkeys` | `Array<{ id: string; label?: string; createdAt: string }>` | Passkeys cadastradas (vazio quando não suportado). `id` é base64url; `createdAt` é ISO. Presente em `index`. |
|
|
108
|
-
* | `enrolling` | `boolean \| undefined` | `true` no passo de enrollment (após `POST /mfa/enroll` e na reexibição do `confirm` com código inválido) — a tela mostra o QR/segredo e o campo de código. Ausente em `index`. |
|
|
109
|
-
* | `secret` | `string \| null \| undefined` | Segredo TOTP em texto (base32) para entrada manual, exibido no passo de enroll. `null` na reexibição do `confirm` (o segredo pendente NÃO é regenerado). Ausente em `index`. |
|
|
110
|
-
* | `qrDataUrl` | `string \| null \| undefined` | QR code do `otpauth://` como data-URL (`<img src>`), no passo de enroll. `null` na reexibição do `confirm`. Ausente em `index`. |
|
|
111
|
-
* | `error` | `string \| undefined` | Mensagem de erro localizada (ex.: código TOTP inválido no `confirm`). Ausente quando não há erro. |
|
|
112
|
-
* | `messages` | `AuthMessages` | Catálogo de mensagens i18n. |
|
|
113
|
-
*
|
|
114
|
-
* ### Props da tela `account/confirm` (sudo — confirmar identidade)
|
|
115
|
-
*
|
|
116
|
-
* | Prop | Tipo | Descrição |
|
|
117
|
-
* |---------------|---------------------|-----------|
|
|
118
|
-
* | `csrfToken` | `string` | Token CSRF para o POST de cada método. |
|
|
119
|
-
* | `returnTo` | `string \| null` | Caminho interno de destino após confirmar (validado pelo servidor) ou `null`. |
|
|
120
|
-
* | `error` | `string \| null` | Flash de erro da última tentativa de confirmação ou `null`. |
|
|
121
|
-
* | `notice` | `string \| null` | Flash informativo (ex.: "link de confirmação enviado") ou `null`. |
|
|
122
|
-
* | `methods` | `Array<{ id: string; labelKey: string; kind: 'form' \| 'action' \| 'redirect' \| 'webauthn'; endpoint: string; fields?: Array<{ name: string; type: 'password' \| 'text'; labelKey: string }> }>` | Métodos de sudo disponíveis para a conta. A tela renderiza por `kind`; `endpoint` é o POST de verificação (`webauthn` pede options em `${endpoint}/options`). |
|
|
123
|
-
* | `preferredId` | `string \| null` | `id` do último método usado (destaque na UI) ou `null`. |
|
|
124
|
-
* | `messages` | `AuthMessages` | Catálogo de mensagens i18n. |
|
|
125
|
-
*
|
|
126
|
-
* ### Props da tela `account/email-confirmed` (terminal do link de troca de e-mail)
|
|
127
|
-
*
|
|
128
|
-
* | Prop | Tipo | Descrição |
|
|
129
|
-
* |------------|----------------|-----------|
|
|
130
|
-
* | `ok` | `boolean` | `true` quando o token era válido e o novo e-mail foi aplicado; `false` para token inválido/expirado ou store sem suporte. A tela mostra sucesso ou falha conforme o valor. |
|
|
131
|
-
* | `messages` | `AuthMessages` | Catálogo de mensagens i18n. |
|
|
85
|
+
* export default function Security(props: AccountSecurityProps) {
|
|
86
|
+
* return <form action="/account/security/password" method="post">…</form>
|
|
87
|
+
* }
|
|
88
|
+
* ```
|
|
132
89
|
*/
|
|
133
90
|
export declare function inertiaRenderer(opts: InertiaRendererOptions): (ctx: HttpContext, view: string, props: Record<string, unknown>) => Promise<any>;
|
|
@@ -29,77 +29,34 @@ async function resolveMessagesFromCtx(ctx) {
|
|
|
29
29
|
*
|
|
30
30
|
* ---
|
|
31
31
|
*
|
|
32
|
-
* ### Props
|
|
32
|
+
* ### Props das telas de conta — tipos exportados (fonte única)
|
|
33
33
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
34
|
+
* Um host que escreve as telas do console em React tipa cada página com os tipos
|
|
35
|
+
* de props exportados do pacote, em vez de copiar o shape à mão deste docblock —
|
|
36
|
+
* que já ficou desatualizado no passado. Cada tipo é a FONTE ÚNICA da verdade: os
|
|
37
|
+
* controllers satisfazem (`satisfies Omit<…, 'messages'>`) exatamente o mesmo
|
|
38
|
+
* tipo ao chamar `render()`, então divergência quebra o `tsc` do pacote. Ver
|
|
39
|
+
* `src/host/account_screen_props.ts`.
|
|
40
40
|
*
|
|
41
|
-
*
|
|
41
|
+
* | Tela | Tipo exportado |
|
|
42
|
+
* |---------------------------|----------------------------|
|
|
43
|
+
* | `account/login` | {@link AccountLoginProps} |
|
|
44
|
+
* | `account/security` | {@link AccountSecurityProps} |
|
|
45
|
+
* | `account/mfa` | {@link AccountMfaProps} |
|
|
46
|
+
* | `account/confirm` | {@link AccountConfirmProps} |
|
|
47
|
+
* | `account/email-confirmed` | {@link AccountEmailConfirmedProps} |
|
|
42
48
|
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* | `supported` | `boolean` | `false` quando o store não suporta o self-service de segurança (troca de senha/e-mail) — a tela deve degradar. |
|
|
47
|
-
* | `profileSupported` | `boolean` | `true` quando o store suporta editar nome/avatar (`updateProfile`). |
|
|
48
|
-
* | `avatarUploadSupported` | `boolean` | `true` quando algum backend (drive OU media) pode armazenar o upload de avatar. |
|
|
49
|
-
* | `email` | `string` | E-mail atual da conta (`''` se ausente). |
|
|
50
|
-
* | `name` | `string` | Nome atual da conta (`''` se ausente). |
|
|
51
|
-
* | `avatarUrl` | `string` | URL do avatar atual (`''` se ausente). |
|
|
52
|
-
* | `passwordChanged` | `string \| null` | Flash de sucesso da troca de senha (mensagem localizada) ou `null`. |
|
|
53
|
-
* | `emailChangeRequested` | `string \| null` | Flash: link de confirmação de troca de e-mail enviado (ou cancelamento) ou `null`. |
|
|
54
|
-
* | `emailChanged` | `string \| null` | Flash: troca de e-mail concluída ou `null`. |
|
|
55
|
-
* | `profileUpdated` | `string \| null` | Flash: perfil atualizado ou `null`. |
|
|
56
|
-
* | `error` | `string \| null` | Flash de erro de segurança (senha inválida, e-mail em uso, política violada) ou `null`. |
|
|
57
|
-
* | `trustedDevicesEnabled` | `boolean` | `true` quando o recurso de dispositivos confiáveis está ligado. |
|
|
58
|
-
* | `trustedDevicesRevoked` | `string \| null` | Flash: confiança deste navegador revogada ou `null`. |
|
|
59
|
-
* | `sessionsSupported` | `boolean` | `true` quando o adapter OIDC enumera as sessões ativas da conta. |
|
|
60
|
-
* | `sessions` | `Array<{ loginTs: string; browser: string; os: string; ip: string; location: string }>` | Sessões ativas da própria conta (vazio quando não suportado). `loginTs` é ISO ou `''`. |
|
|
61
|
-
* | `exportSupported` | `boolean` | Sempre `true` — export de dados (portabilidade/LGPD) disponível para a conta logada. |
|
|
62
|
-
* | `deletionSupported` | `boolean` | `true` quando o store suporta hard delete (danger zone). |
|
|
63
|
-
* | `deleteError` | `string \| null` | Flash de erro da confirmação de deleção ou `null`. |
|
|
64
|
-
* | `messages` | `AuthMessages` | Catálogo de mensagens i18n. |
|
|
49
|
+
* A prop `messages` (catálogo i18n {@link AuthMessages}) é injetada por este
|
|
50
|
+
* renderer como shared prop e faz parte de todos esses tipos — os controllers,
|
|
51
|
+
* que não a passam, satisfazem `Omit<…, 'messages'>`.
|
|
65
52
|
*
|
|
66
|
-
*
|
|
53
|
+
* ```tsx
|
|
54
|
+
* import type { AccountSecurityProps } from '@adonis-agora/authkit-server'
|
|
67
55
|
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
* | Prop | Tipo | Descrição |
|
|
73
|
-
* |---------------------|---------------------|-----------|
|
|
74
|
-
* | `csrfToken` | `string` | Token CSRF para os formulários de enroll/confirm/disable e passkeys. |
|
|
75
|
-
* | `enabled` | `boolean` | `true` quando o TOTP já está confirmado (habilitado) para a conta. |
|
|
76
|
-
* | `recoveryCodes` | `string[] \| null` | Códigos de recuperação recém-gerados (exibidos UMA vez após enroll/confirm) ou `null`. |
|
|
77
|
-
* | `passkeysSupported` | `boolean` | `true` quando o store persiste credenciais WebAuthn (passkeys). |
|
|
78
|
-
* | `passkeys` | `Array<{ id: string; label?: string; createdAt: string }>` | Passkeys cadastradas (vazio quando não suportado). `id` é base64url; `createdAt` é ISO. Presente em `index`. |
|
|
79
|
-
* | `enrolling` | `boolean \| undefined` | `true` no passo de enrollment (após `POST /mfa/enroll` e na reexibição do `confirm` com código inválido) — a tela mostra o QR/segredo e o campo de código. Ausente em `index`. |
|
|
80
|
-
* | `secret` | `string \| null \| undefined` | Segredo TOTP em texto (base32) para entrada manual, exibido no passo de enroll. `null` na reexibição do `confirm` (o segredo pendente NÃO é regenerado). Ausente em `index`. |
|
|
81
|
-
* | `qrDataUrl` | `string \| null \| undefined` | QR code do `otpauth://` como data-URL (`<img src>`), no passo de enroll. `null` na reexibição do `confirm`. Ausente em `index`. |
|
|
82
|
-
* | `error` | `string \| undefined` | Mensagem de erro localizada (ex.: código TOTP inválido no `confirm`). Ausente quando não há erro. |
|
|
83
|
-
* | `messages` | `AuthMessages` | Catálogo de mensagens i18n. |
|
|
84
|
-
*
|
|
85
|
-
* ### Props da tela `account/confirm` (sudo — confirmar identidade)
|
|
86
|
-
*
|
|
87
|
-
* | Prop | Tipo | Descrição |
|
|
88
|
-
* |---------------|---------------------|-----------|
|
|
89
|
-
* | `csrfToken` | `string` | Token CSRF para o POST de cada método. |
|
|
90
|
-
* | `returnTo` | `string \| null` | Caminho interno de destino após confirmar (validado pelo servidor) ou `null`. |
|
|
91
|
-
* | `error` | `string \| null` | Flash de erro da última tentativa de confirmação ou `null`. |
|
|
92
|
-
* | `notice` | `string \| null` | Flash informativo (ex.: "link de confirmação enviado") ou `null`. |
|
|
93
|
-
* | `methods` | `Array<{ id: string; labelKey: string; kind: 'form' \| 'action' \| 'redirect' \| 'webauthn'; endpoint: string; fields?: Array<{ name: string; type: 'password' \| 'text'; labelKey: string }> }>` | Métodos de sudo disponíveis para a conta. A tela renderiza por `kind`; `endpoint` é o POST de verificação (`webauthn` pede options em `${endpoint}/options`). |
|
|
94
|
-
* | `preferredId` | `string \| null` | `id` do último método usado (destaque na UI) ou `null`. |
|
|
95
|
-
* | `messages` | `AuthMessages` | Catálogo de mensagens i18n. |
|
|
96
|
-
*
|
|
97
|
-
* ### Props da tela `account/email-confirmed` (terminal do link de troca de e-mail)
|
|
98
|
-
*
|
|
99
|
-
* | Prop | Tipo | Descrição |
|
|
100
|
-
* |------------|----------------|-----------|
|
|
101
|
-
* | `ok` | `boolean` | `true` quando o token era válido e o novo e-mail foi aplicado; `false` para token inválido/expirado ou store sem suporte. A tela mostra sucesso ou falha conforme o valor. |
|
|
102
|
-
* | `messages` | `AuthMessages` | Catálogo de mensagens i18n. |
|
|
56
|
+
* export default function Security(props: AccountSecurityProps) {
|
|
57
|
+
* return <form action="/account/security/password" method="post">…</form>
|
|
58
|
+
* }
|
|
59
|
+
* ```
|
|
103
60
|
*/
|
|
104
61
|
export function inertiaRenderer(opts) {
|
|
105
62
|
const allowed = opts.views ? new Set(opts.views) : null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adonis-agora/authkit-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.50.0",
|
|
4
4
|
"description": "AdonisJS OIDC/OAuth2 provider (Identity Provider) toolkit: ejectable auth server with sessions, rate-limiting, MFA/TOTP, audit log, federated logout and OpenTelemetry metrics.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "dudousxd",
|
|
@@ -148,7 +148,7 @@
|
|
|
148
148
|
"react-error-boundary": "6.1.2",
|
|
149
149
|
"nuqs": "2.8.9",
|
|
150
150
|
"recharts": "3.8.1",
|
|
151
|
-
"@adonis-agora/authkit-react": "0.
|
|
151
|
+
"@adonis-agora/authkit-react": "0.17.0"
|
|
152
152
|
},
|
|
153
153
|
"scripts": {
|
|
154
154
|
"build": "node scripts/build_host_css.mjs && node scripts/build_webauthn.mjs && node scripts/build_ui.mjs && node -e \"const fs=require('node:fs');for(const d of ['build/stubs','build/host/views'])fs.rmSync(d,{recursive:true,force:true})\" && tsc && node -e \"require('node:fs').cpSync('src/host/assets','build/src/host/assets',{recursive:true})\" && node -e \"require('node:fs').cpSync('stubs','build/stubs',{recursive:true,filter:(s)=>!s.endsWith('.ts')})\" && node -e \"const fs=require('node:fs');if(fs.existsSync('assets'))fs.cpSync('assets','build/assets',{recursive:true})\" && node -e \"require('node:fs').cpSync('src/host/views','build/host/views',{recursive:true})\" && node -e \"const fs=require('node:fs');fs.mkdirSync('build/host/ui',{recursive:true});fs.readdirSync('src/host/ui').filter(f=>f.endsWith('.html')).forEach(f=>fs.copyFileSync('src/host/ui/'+f,'build/host/ui/'+f))\" && node -e \"require('node:fs').copyFileSync('commands/commands.json','build/commands/commands.json')\" && node -e \"const fs=require('node:fs');fs.mkdirSync('build/password',{recursive:true});fs.copyFileSync('src/password/common_passwords.txt','build/password/common_passwords.txt')\"",
|