@adonis-agora/authkit-server 0.35.0 → 0.37.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.
@@ -36,11 +36,19 @@
36
36
  class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm outline-none transition focus:border-gray-900 focus:ring-2 focus:ring-gray-900" />
37
37
  </div>
38
38
 
39
- <div class="mt-4">
40
- <label for="password" class="mb-1 block text-sm font-medium text-gray-700">{{ t('signup.password_label') }}</label>
41
- <input id="password" name="password" type="password" required minlength="8"
42
- class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm outline-none transition focus:border-gray-900 focus:ring-2 focus:ring-gray-900" />
43
- </div>
39
+ {{-- Cadastro passwordless: sem campo de senha. O login vem por magic link. --}}
40
+ @if(!passwordlessSignup)
41
+ <div class="mt-4">
42
+ <label for="password" class="mb-1 block text-sm font-medium text-gray-700">{{ t('signup.password_label') }}</label>
43
+ <input id="password" name="password" type="password" required minlength="8"
44
+ class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm outline-none transition focus:border-gray-900 focus:ring-2 focus:ring-gray-900" />
45
+ </div>
46
+ @end
47
+
48
+ {{-- Passwordless: confirmação de link enviado (anti-enumeração). --}}
49
+ @if(magicLinkSent)
50
+ <p class="mt-4 rounded-lg bg-green-50 px-3 py-2 text-sm text-green-700">{{ t('signup.magic_link_sent') }}</p>
51
+ @end
44
52
 
45
53
  {{-- Bot protection: container do widget (config-trusted; HTML do host renderizado raw). --}}
46
54
  @if(botProtection && botProtection.html)
package/build/index.d.ts CHANGED
@@ -13,8 +13,8 @@ export { registerOidcRoutes } from './src/register_routes.js';
13
13
  export type { AuthServerConfigInput, ResolvedServerConfig, DynamicRegistrationConfigInput, ResolvedDynamicRegistrationConfig, AdminConfigInput, ResolvedAdminConfig, AdminApiConfigInput, ResolvedAdminApiConfig, } from './src/define_config.js';
14
14
  export { resolveAdmin, resolveAdminApi, resolveWebauthn, resolveDynamicRegistration } from './src/define_config.js';
15
15
  export type { WebauthnConfigInput, ResolvedWebauthnConfig } from './src/define_config.js';
16
- export { resolvePasswordless, resolveLogin } from './src/define_config.js';
17
- export type { PasswordlessConfigInput, ResolvedPasswordlessConfig, LoginConfigInput, ResolvedLoginConfig, } from './src/define_config.js';
16
+ export { resolvePasswordless, resolveLogin, resolveAuthMethodsConfig } from './src/define_config.js';
17
+ export type { PasswordlessConfigInput, ResolvedPasswordlessConfig, AuthMethodsConfigInput, ResolvedAuthMethodsConfig, LoginConfigInput, ResolvedLoginConfig, } from './src/define_config.js';
18
18
  export { resolveTrustedDevices, isTrustedDeviceValid, buildTrustedDevicePayload, TRUSTED_DEVICE_COOKIE, } from './src/host/trusted_device.js';
19
19
  export type { TrustedDevicesConfigInput, ResolvedTrustedDevicesConfig, TrustedDevicePayload, } from './src/host/trusted_device.js';
20
20
  export { resolveBotProtection, botProtectionApplies, extractBotToken, verifyBotProtection, guardBotProtection, DEFAULT_BOT_TOKEN_FIELDS, } from './src/host/bot_protection.js';
@@ -105,8 +105,8 @@ export { buildKeysStatus, rotateNow } from './src/host/key_rotation_actions.js';
105
105
  export type { KeysStatus as ServerKeysStatus } from './src/host/key_rotation_actions.js';
106
106
  export { resolveEffectiveBotProtection, } from './src/host/bot_protection.js';
107
107
  export type { BotProtectionSetting } from './src/host/bot_protection.js';
108
- export { SETTING_KEYS, resolveEffectiveRegistration, resolveEffectiveRequireVerifiedEmail, resolveEffectiveMaintenanceMode, } from './src/host/runtime_toggles.js';
109
- export type { SettingKey, RegistrationSetting, RequireVerifiedEmailSetting, MaintenanceModeSetting, ResolvedMaintenanceMode, } from './src/host/runtime_toggles.js';
108
+ export { SETTING_KEYS, resolveEffectiveRegistration, resolveEffectiveRequireVerifiedEmail, resolveEffectiveMaintenanceMode, resolveEffectiveAuthMethods, configLockedAuthMethods, } from './src/host/runtime_toggles.js';
109
+ export type { SettingKey, RegistrationSetting, RequireVerifiedEmailSetting, MaintenanceModeSetting, ResolvedMaintenanceMode, AuthMethodsSetting, ResolvedAuthMethods, AuthMethodsCapabilities, AuthMethodsConfigOverride, } from './src/host/runtime_toggles.js';
110
110
  export { resolveRegistration } from './src/define_config.js';
111
111
  export type { RegistrationConfigInput, ResolvedRegistrationConfig, } from './src/define_config.js';
112
112
  export { getAccountId, hasAccountSession, consoleLoginUrl, } from './src/host/console_session.js';
package/build/index.js CHANGED
@@ -9,7 +9,7 @@ export { withMfa } from './src/mixins/with_mfa.js';
9
9
  export { OidcService } from './src/provider/oidc_service.js';
10
10
  export { registerOidcRoutes } from './src/register_routes.js';
11
11
  export { resolveAdmin, resolveAdminApi, resolveWebauthn, resolveDynamicRegistration } from './src/define_config.js';
12
- export { resolvePasswordless, resolveLogin } from './src/define_config.js';
12
+ export { resolvePasswordless, resolveLogin, resolveAuthMethodsConfig } from './src/define_config.js';
13
13
  export { resolveTrustedDevices, isTrustedDeviceValid, buildTrustedDevicePayload, TRUSTED_DEVICE_COOKIE, } from './src/host/trusted_device.js';
14
14
  export { resolveBotProtection, botProtectionApplies, extractBotToken, verifyBotProtection, guardBotProtection, DEFAULT_BOT_TOKEN_FIELDS, } from './src/host/bot_protection.js';
15
15
  export { lucidAccountStore, appKeyEncrypter } from './src/accounts/lucid_account_store.js';
@@ -68,7 +68,7 @@ export { RuntimeSettings, supportsSettings } from './src/host/runtime_settings.j
68
68
  export { buildKeysStatus, rotateNow } from './src/host/key_rotation_actions.js';
69
69
  export { resolveEffectiveBotProtection, } from './src/host/bot_protection.js';
70
70
  // Runtime toggles (registration, require_verified_email, maintenance_mode).
71
- export { SETTING_KEYS, resolveEffectiveRegistration, resolveEffectiveRequireVerifiedEmail, resolveEffectiveMaintenanceMode, } from './src/host/runtime_toggles.js';
71
+ export { SETTING_KEYS, resolveEffectiveRegistration, resolveEffectiveRequireVerifiedEmail, resolveEffectiveMaintenanceMode, resolveEffectiveAuthMethods, configLockedAuthMethods, } from './src/host/runtime_toggles.js';
72
72
  export { resolveRegistration } from './src/define_config.js';
73
73
  export { getAccountId, hasAccountSession, consoleLoginUrl, } from './src/host/console_session.js';
74
74
  export { ACCOUNT_SESSION_KEY } from './src/host/middleware/account_auth.js';
@@ -322,12 +322,53 @@ export interface PasswordlessConfigInput {
322
322
  magicLink?: boolean;
323
323
  /** Liga o "entrar com passkey" antes da senha. Default: false. */
324
324
  passkeyFirst?: boolean;
325
+ /**
326
+ * Liga o cadastro público passwordless: o signup pede só e-mail + nome (sem
327
+ * senha), cria a conta com uma senha aleatória inutilizável e envia um magic
328
+ * link que finaliza o login (o mesmo fluxo do login por magic link). Default:
329
+ * false. Exige que o accountStore implemente {@link MagicLinkCapability}; sem
330
+ * ela o cadastro passwordless fica indisponível e o fluxo de senha segue valendo.
331
+ */
332
+ signup?: boolean;
325
333
  }
326
334
  export interface ResolvedPasswordlessConfig {
327
335
  magicLink: boolean;
328
336
  passkeyFirst: boolean;
337
+ signup: boolean;
329
338
  }
330
339
  export declare function resolvePasswordless(input?: PasswordlessConfigInput): ResolvedPasswordlessConfig;
340
+ /**
341
+ * Fixa métodos de login PELO ARQUIVO DE CONFIG, com PRIORIDADE sobre o runtime
342
+ * setting `auth_methods` (o inverso do default — normalmente o runtime manda).
343
+ *
344
+ * Cada campo declarado aqui é AUTORITATIVO: o console admin não sobrescreve em
345
+ * runtime (a UI mostra o toggle travado, "definido no config"). Campos omitidos
346
+ * continuam controlados pelo runtime → default por capability.
347
+ *
348
+ * Guards preservados no resolver:
349
+ * - Fixar um método em `true` só o liga se a CAPACIDADE existir (ex.: `magicLink: true`
350
+ * sem store capaz continua oculto). Fixar em `false` é sempre respeitado.
351
+ * - Fail-safe all-off: se pins + runtime zerarem TODOS os métodos, volta ao default
352
+ * (nunca trancar todo mundo pra fora).
353
+ *
354
+ * @example
355
+ * // Login sem senha (magic-link + passkey), travado pelo config — sem comando por ambiente:
356
+ * authMethods: { password: false }
357
+ */
358
+ export interface AuthMethodsConfigInput {
359
+ /** Fixa o login por senha. */
360
+ password?: boolean;
361
+ /** Fixa o login por magic link (respeitando a capacidade do store). */
362
+ magicLink?: boolean;
363
+ /** Fixa o "entrar com passkey" (respeitando a capacidade do store). */
364
+ passkey?: boolean;
365
+ /** Fixa o link "esqueci a senha" (só aparece com senha ligada, de qualquer forma). */
366
+ forgotPassword?: boolean;
367
+ }
368
+ /** authMethods resolvido: só os campos que o config FIXOU (os demais ficam a cargo do runtime). */
369
+ export type ResolvedAuthMethodsConfig = AuthMethodsConfigInput;
370
+ /** Normaliza o input: mantém só os campos booleanos declarados (os pins). */
371
+ export declare function resolveAuthMethodsConfig(input?: AuthMethodsConfigInput): ResolvedAuthMethodsConfig;
331
372
  /**
332
373
  * Política de login por senha/identidade. Hoje cobre `requireVerifiedEmail`:
333
374
  * quando ligado, TODO fluxo de login que materializa uma sessão (login por senha,
@@ -595,6 +636,12 @@ export interface AuthServerConfigInput {
595
636
  * desligados. Exigem as capacidades correspondentes no accountStore.
596
637
  */
597
638
  passwordless?: PasswordlessConfigInput;
639
+ /**
640
+ * Fixa métodos de login pelo arquivo de config, com PRIORIDADE sobre o runtime
641
+ * setting `auth_methods`. Cada campo declarado trava o método (o console admin não
642
+ * sobrescreve). Default: `{}` (tudo controlado pelo runtime). Ver {@link AuthMethodsConfigInput}.
643
+ */
644
+ authMethods?: AuthMethodsConfigInput;
598
645
  /**
599
646
  * Política de login (hoje: `requireVerifiedEmail`). Default: tudo desligado.
600
647
  * `requireVerifiedEmail` é capability-probed (precisa de `isEmailVerified` no store).
@@ -749,6 +796,8 @@ export interface ResolvedServerConfig {
749
796
  botProtection?: ResolvedBotProtectionConfig;
750
797
  /** Passwordless resolvido (default tudo desligado). */
751
798
  passwordless: ResolvedPasswordlessConfig;
799
+ /** Métodos de login fixados pelo config (prioridade sobre runtime; default `{}`). */
800
+ authMethods: ResolvedAuthMethodsConfig;
752
801
  /** Política de login resolvida (requireVerifiedEmail; default desligado). */
753
802
  login: ResolvedLoginConfig;
754
803
  /** Configuração de cadastro público resolvida (enabled; default true). */
@@ -92,8 +92,24 @@ export function resolvePasswordless(input) {
92
92
  return {
93
93
  magicLink: input?.magicLink ?? false,
94
94
  passkeyFirst: input?.passkeyFirst ?? false,
95
+ signup: input?.signup ?? false,
95
96
  };
96
97
  }
98
+ /** Normaliza o input: mantém só os campos booleanos declarados (os pins). */
99
+ export function resolveAuthMethodsConfig(input) {
100
+ if (!input)
101
+ return {};
102
+ const out = {};
103
+ if (typeof input.password === "boolean")
104
+ out.password = input.password;
105
+ if (typeof input.magicLink === "boolean")
106
+ out.magicLink = input.magicLink;
107
+ if (typeof input.passkey === "boolean")
108
+ out.passkey = input.passkey;
109
+ if (typeof input.forgotPassword === "boolean")
110
+ out.forgotPassword = input.forgotPassword;
111
+ return out;
112
+ }
97
113
  export function resolveLogin(input) {
98
114
  return {
99
115
  requireVerifiedEmail: input?.requireVerifiedEmail ?? false,
@@ -334,6 +350,7 @@ export function defineConfig(config) {
334
350
  trustedDevices: resolveTrustedDevices(config.trustedDevices),
335
351
  botProtection: resolveBotProtection(config.botProtection),
336
352
  passwordless: resolvePasswordless(config.passwordless),
353
+ authMethods: resolveAuthMethodsConfig(config.authMethods),
337
354
  login: resolveLogin(config.login),
338
355
  registration: resolveRegistration(config.registration),
339
356
  accessTokens: resolveAccessTokens(config.issuer, config.accessTokens),
@@ -29,6 +29,10 @@ export function deriveLockedSettingKeys(config) {
29
29
  locked.push(key);
30
30
  };
31
31
  add(config.registration !== undefined, SETTING_KEYS.REGISTRATION);
32
+ // authMethods no config trava a key inteira `auth_methods`: os métodos passam a ser
33
+ // controlados pelo arquivo (via cfg.authMethods → configOverrides no resolver) e a UI
34
+ // desabilita os toggles com "definido via defineConfig".
35
+ add(config.authMethods !== undefined, SETTING_KEYS.AUTH_METHODS);
32
36
  add(config.login?.requireVerifiedEmail !== undefined, SETTING_KEYS.REQUIRE_VERIFIED_EMAIL);
33
37
  add(config.lockout !== undefined, SETTING_KEYS.LOCKOUT);
34
38
  add(config.rateLimit !== undefined, SETTING_KEYS.RATE_LIMIT);
@@ -87,6 +87,7 @@ export default class AuthInteractionController {
87
87
  configuredSocialProviders,
88
88
  magicLinkCapable: magicLinkCapableConfig,
89
89
  passkeyCapable: false, // passkey-first depends on account — resolved in step 2
90
+ configOverrides: cfg.authMethods,
90
91
  });
91
92
  if (!email) {
92
93
  // Step 1: identifier (email only).
@@ -1,6 +1,7 @@
1
1
  import '../augmentations.js';
2
2
  import type { HttpContext } from '@adonisjs/core/http';
3
3
  export default class AuthRegistrationController {
4
+ #private;
4
5
  /** GET /auth/interaction/:uid/signup — tela de cadastro (dentro do fluxo OIDC). */
5
6
  showSignup(ctx: HttpContext): Promise<any>;
6
7
  /** POST /auth/interaction/:uid/signup — cria o usuário e finaliza o login. */
@@ -1,7 +1,8 @@
1
1
  import '../augmentations.js';
2
2
  import { brandFor } from '../branding.js';
3
- import { signupValidator, forgotPasswordValidator, resetPasswordValidator } from '../validators.js';
4
- import { sendPasswordResetEmail, sendEmailVerificationEmail } from '../default_mailer.js';
3
+ import { randomBytes } from 'node:crypto';
4
+ import { signupValidator, passwordlessSignupValidator, forgotPasswordValidator, resetPasswordValidator, } from '../validators.js';
5
+ import { sendPasswordResetEmail, sendEmailVerificationEmail, sendMagicLinkEmail, } from '../default_mailer.js';
5
6
  import { translate } from '../i18n.js';
6
7
  import { PasswordPolicyError } from '../../password/password_manager.js';
7
8
  import { guardBotProtection, resolveEffectiveBotProtection } from '../bot_protection.js';
@@ -54,6 +55,7 @@ export default class AuthRegistrationController {
54
55
  uid: details.uid,
55
56
  csrfToken: ctx.request.csrfToken,
56
57
  brand,
58
+ passwordlessSignup: cfg.passwordless?.signup ?? false,
57
59
  botProtection: effectiveBot?.on.includes('signup') ? effectiveBot.widget : undefined,
58
60
  });
59
61
  }
@@ -102,6 +104,12 @@ export default class AuthRegistrationController {
102
104
  botProtection: effectiveBotSignup?.widget,
103
105
  });
104
106
  }
107
+ // Cadastro passwordless (config): só e-mail + nome. Cria conta com senha random
108
+ // inutilizável e envia um magic link — mesmo fluxo do login por magic link.
109
+ if (cfg.passwordless?.signup &&
110
+ typeof cfg.accountStore.issueMagicLinkToken === 'function') {
111
+ return this.#passwordlessSignup(ctx, { cfg, brand, details });
112
+ }
105
113
  const data = await ctx.request.validateUsing(signupValidator);
106
114
  const accountStore = cfg.accountStore;
107
115
  const existing = await accountStore.findByEmail(data.email);
@@ -183,6 +191,55 @@ export default class AuthRegistrationController {
183
191
  ctx.logger.error({ err: error, email: data.email }, 'authkit: falha ao enviar verificação de e-mail');
184
192
  }
185
193
  }
194
+ /**
195
+ * Cadastro passwordless: valida e-mail + nome, cria a conta (senha random
196
+ * inutilizável) se ainda não existe, emite um magic link e o envia. Sempre
197
+ * responde "link enviado" (anti-enumeração), exista a conta ou não. Consumir o
198
+ * link finaliza o login pelo fluxo de magic link já existente (GET /magic).
199
+ */
200
+ async #passwordlessSignup(ctx, deps) {
201
+ const { cfg, brand, details } = deps;
202
+ const render = cfg.render;
203
+ const accountStore = cfg.accountStore;
204
+ const uid = ctx.request.param('uid');
205
+ const data = await ctx.request.validateUsing(passwordlessSignupValidator);
206
+ // Cria a conta se ainda não existe. Senha random inutilizável: o login é 100%
207
+ // passwordless (mesmo precedente das contas criadas por identidade social).
208
+ const existing = await accountStore.findByEmail(data.email);
209
+ if (!existing) {
210
+ const created = await accountStore.create({
211
+ email: data.email,
212
+ fullName: data.fullName,
213
+ password: randomBytes(24).toString('hex'),
214
+ });
215
+ await cfg.audit?.record({
216
+ type: 'signup',
217
+ accountId: created.id,
218
+ email: data.email,
219
+ ip: ctx.request.ip?.() ?? null,
220
+ clientId: details.params.client_id ?? null,
221
+ });
222
+ }
223
+ // Emite + envia o magic link (mesma construção do login por magic link).
224
+ const issued = await accountStore.issueMagicLinkToken(data.email);
225
+ if (issued) {
226
+ const origin = `${ctx.request.protocol()}://${ctx.request.host()}`;
227
+ const magicUrl = `${origin}/auth/interaction/${uid}/magic?token=${encodeURIComponent(issued.token)}`;
228
+ if (cfg.mail?.onMagicLink) {
229
+ await cfg.mail.onMagicLink({ email: data.email, magicUrl, token: issued.token });
230
+ }
231
+ else {
232
+ await sendMagicLinkEmail(ctx, { email: data.email, magicUrl });
233
+ }
234
+ }
235
+ // Resposta uniforme: "enviamos um link" (não vaza existência da conta).
236
+ return render(ctx, 'signup', {
237
+ uid,
238
+ csrfToken: ctx.request.csrfToken,
239
+ brand,
240
+ magicLinkSent: true,
241
+ });
242
+ }
186
243
  /** GET /auth/forgot-password — tela standalone. */
187
244
  async showForgot(ctx) {
188
245
  const service = await ctx.containerResolver.make('authkit.server');
@@ -201,6 +258,7 @@ export default class AuthRegistrationController {
201
258
  const authMethods = await resolveEffectiveAuthMethods(runtimeSettings, {
202
259
  configuredSocialProviders: cfg.social?.providers ?? [],
203
260
  magicLinkCapable: cfg.passwordless?.magicLink && typeof cfg.accountStore.issueMagicLinkToken === 'function',
261
+ configOverrides: cfg.authMethods,
204
262
  });
205
263
  if (!authMethods.forgotPassword) {
206
264
  return ctx.response.notFound({ error: translate(cfg.messages, 'errors.not_found') });
@@ -229,6 +287,7 @@ export default class AuthRegistrationController {
229
287
  const authMethods = await resolveEffectiveAuthMethods(runtimeSettings, {
230
288
  configuredSocialProviders: cfg.social?.providers ?? [],
231
289
  magicLinkCapable: cfg.passwordless?.magicLink && typeof cfg.accountStore.issueMagicLinkToken === 'function',
290
+ configOverrides: cfg.authMethods,
232
291
  });
233
292
  if (!authMethods.forgotPassword) {
234
293
  return ctx.response.notFound({ error: translate(cfg.messages, 'errors.not_found') });
@@ -48,6 +48,7 @@ export declare const DEFAULT_MESSAGES: {
48
48
  "account.login.idle_timeout": string;
49
49
  "login.magic_link_button": string;
50
50
  "login.magic_link_sent": string;
51
+ "signup.magic_link_sent": string;
51
52
  "login.passkey_button": string;
52
53
  "signup.page_title": string;
53
54
  "signup.title": string;
@@ -734,6 +735,7 @@ export declare const PT_BR_MESSAGES: {
734
735
  "account.login.idle_timeout": string;
735
736
  "login.magic_link_button": string;
736
737
  "login.magic_link_sent": string;
738
+ "signup.magic_link_sent": string;
737
739
  "login.passkey_button": string;
738
740
  "signup.page_title": string;
739
741
  "signup.title": string;
@@ -39,6 +39,7 @@ export const DEFAULT_MESSAGES = {
39
39
  // Passwordless (login).
40
40
  "login.magic_link_button": "Email me a login link",
41
41
  "login.magic_link_sent": "If the account exists, we sent you a login link.",
42
+ "signup.magic_link_sent": "Check your email — we sent you a link to finish creating your account.",
42
43
  "login.passkey_button": "Sign in with a passkey",
43
44
  // Tela de cadastro (signup).
44
45
  "signup.page_title": "Create account",
@@ -803,6 +804,7 @@ export const PT_BR_MESSAGES = {
803
804
  // Passwordless (login).
804
805
  "login.magic_link_button": "Me envie um link de login",
805
806
  "login.magic_link_sent": "Se a conta existir, enviamos um link de login.",
807
+ "signup.magic_link_sent": "Enviamos um link para o seu e-mail. Abra-o para concluir o cadastro.",
806
808
  "login.passkey_button": "Entrar com passkey",
807
809
  // Tela de cadastro (signup).
808
810
  "signup.page_title": "Criar conta",
@@ -202,7 +202,27 @@ export interface AuthMethodsCapabilities {
202
202
  magicLinkCapable?: boolean;
203
203
  /** Passkey-first disponível (config.passwordless.passkeyFirst && store tem passkeys). */
204
204
  passkeyCapable?: boolean;
205
+ /**
206
+ * Pins vindos do ARQUIVO DE CONFIG (`cfg.authMethods`): têm PRIORIDADE sobre o
207
+ * runtime setting `auth_methods`. Cada campo presente sobrescreve o valor resolvido
208
+ * do setting. Ligar respeita as capacidades (magicLink/passkey só ligam se capable);
209
+ * desligar é sempre respeitado. Campos ausentes seguem controlados pelo runtime.
210
+ */
211
+ configOverrides?: AuthMethodsConfigOverride;
205
212
  }
213
+ /** Métodos que o config pode FIXAR (prioridade sobre runtime). Só os campos declarados travam. */
214
+ export interface AuthMethodsConfigOverride {
215
+ password?: boolean;
216
+ magicLink?: boolean;
217
+ passkey?: boolean;
218
+ forgotPassword?: boolean;
219
+ }
220
+ /**
221
+ * Quais métodos estão TRAVADOS pelo config (para a UI admin mostrar o toggle
222
+ * desabilitado com "definido no arquivo de config"). É só a lista de campos
223
+ * booleanos presentes em `configOverrides`.
224
+ */
225
+ export declare function configLockedAuthMethods(configOverrides?: AuthMethodsConfigOverride): Array<keyof AuthMethodsConfigOverride>;
206
226
  /**
207
227
  * Resolve os métodos de autenticação efetivos combinando config estático + setting
208
228
  * persistida em runtime + capabilities.
@@ -127,6 +127,22 @@ export async function resolveEffectiveMaintenanceMode(settings) {
127
127
  message: typeof setting.message === 'string' ? setting.message : undefined,
128
128
  };
129
129
  }
130
+ /**
131
+ * Quais métodos estão TRAVADOS pelo config (para a UI admin mostrar o toggle
132
+ * desabilitado com "definido no arquivo de config"). É só a lista de campos
133
+ * booleanos presentes em `configOverrides`.
134
+ */
135
+ export function configLockedAuthMethods(configOverrides) {
136
+ if (!configOverrides)
137
+ return [];
138
+ const keys = [
139
+ 'password',
140
+ 'magicLink',
141
+ 'passkey',
142
+ 'forgotPassword',
143
+ ];
144
+ return keys.filter((k) => typeof configOverrides[k] === 'boolean');
145
+ }
130
146
  /**
131
147
  * Resolve os métodos de autenticação efetivos combinando config estático + setting
132
148
  * persistida em runtime + capabilities.
@@ -143,7 +159,7 @@ export async function resolveEffectiveMaintenanceMode(settings) {
143
159
  * 6. Qualquer erro DB → defaults derivados do config (fail-safe total).
144
160
  */
145
161
  export async function resolveEffectiveAuthMethods(settings, capabilities = {}) {
146
- const { configuredSocialProviders = [], magicLinkCapable = false, passkeyCapable = false, } = capabilities;
162
+ const { configuredSocialProviders = [], magicLinkCapable = false, passkeyCapable = false, configOverrides, } = capabilities;
147
163
  // Defaults derivados do config/capabilities.
148
164
  const configDefaults = {
149
165
  password: true,
@@ -153,47 +169,55 @@ export async function resolveEffectiveAuthMethods(settings, capabilities = {}) {
153
169
  forgotPassword: true,
154
170
  passkeyAutofill: passkeyCapable, // on por default quando passkey disponível
155
171
  };
172
+ // 1) Runtime setting sobre os defaults (setting ausente/inválida → só defaults).
156
173
  const raw = await settings.getSetting(SETTING_KEYS.AUTH_METHODS);
157
- if (raw === null || raw === undefined)
158
- return configDefaults;
159
- if (typeof raw !== 'object' || Array.isArray(raw))
160
- return configDefaults; // shape inválida
161
- const s = raw;
162
- // Aplica campo a campo, com fallback ao default quando o campo está ausente.
163
- const passwordEnabled = typeof s.password === 'boolean' ? s.password : configDefaults.password;
164
- const magicLinkEnabled = typeof s.magicLink === 'boolean' ? s.magicLink : configDefaults.magicLink;
165
- const passkeyEnabled = typeof s.passkey === 'boolean' ? s.passkey : configDefaults.passkey;
174
+ const s = raw !== null && raw !== undefined && typeof raw === 'object' && !Array.isArray(raw)
175
+ ? raw
176
+ : {};
177
+ let password = typeof s.password === 'boolean' ? s.password : configDefaults.password;
178
+ let magicLink = typeof s.magicLink === 'boolean' ? s.magicLink : configDefaults.magicLink;
179
+ let passkey = typeof s.passkey === 'boolean' ? s.passkey : configDefaults.passkey;
166
180
  // Social: interseção com os providers do config (setting não pode ligar o que não existe).
167
- let socialEnabled;
168
- if (Array.isArray(s.social)) {
169
- socialEnabled = s.social.filter((p) => configuredSocialProviders.includes(p));
170
- }
171
- else {
172
- socialEnabled = configDefaults.social;
173
- }
174
- // forgotPassword: derivado automaticamente — sem senha NUNCA existe esqueci-senha.
175
- const forgotPasswordEnabled = passwordEnabled && (typeof s.forgotPassword === 'boolean' ? s.forgotPassword : true);
181
+ const social = Array.isArray(s.social)
182
+ ? s.social.filter((p) => configuredSocialProviders.includes(p))
183
+ : configDefaults.social;
184
+ // 2) Pins do config: PRIORIDADE sobre o setting. Ligar respeita a capacidade;
185
+ // desligar sempre vale. `password` não tem guard de capacidade.
186
+ if (configOverrides) {
187
+ if (typeof configOverrides.password === 'boolean')
188
+ password = configOverrides.password;
189
+ if (typeof configOverrides.magicLink === 'boolean')
190
+ magicLink = configOverrides.magicLink && magicLinkCapable;
191
+ if (typeof configOverrides.passkey === 'boolean')
192
+ passkey = configOverrides.passkey && passkeyCapable;
193
+ }
194
+ // forgotPassword: derivado — sem senha NUNCA existe esqueci-senha. O pin/setting só
195
+ // pode DESLIGAR (nunca ligar sem senha). Precedência: pin do config > setting > default(on).
196
+ const forgotPasswordRequested = typeof configOverrides?.forgotPassword === 'boolean'
197
+ ? configOverrides.forgotPassword
198
+ : typeof s.forgotPassword === 'boolean'
199
+ ? s.forgotPassword
200
+ : true;
201
+ const forgotPassword = password && forgotPasswordRequested;
176
202
  // passkeyAutofill: só faz sentido quando passkey está ligado.
177
- const passkeyAutofillEnabled = passkeyEnabled &&
203
+ const passkeyAutofill = passkey &&
178
204
  (typeof s.passkeyAutofill === 'boolean' ? s.passkeyAutofill : configDefaults.passkeyAutofill);
179
205
  const resolved = {
180
- password: passwordEnabled,
181
- magicLink: magicLinkEnabled,
182
- passkey: passkeyEnabled,
183
- social: socialEnabled,
184
- forgotPassword: forgotPasswordEnabled,
185
- passkeyAutofill: passkeyAutofillEnabled,
206
+ password,
207
+ magicLink,
208
+ passkey,
209
+ social,
210
+ forgotPassword,
211
+ passkeyAutofill,
186
212
  };
187
- // FAIL-SAFE all-off: se todos ficaram false/vazio, volta ao config derivado.
188
- const allOff = !resolved.password &&
189
- !resolved.magicLink &&
190
- !resolved.passkey &&
191
- resolved.social.length === 0;
213
+ // FAIL-SAFE all-off: se pins + setting zerarem TODOS os métodos, volta ao config derivado
214
+ // (nunca deixar a tela sem método / trancar todo mundo pra fora).
215
+ const allOff = !resolved.password && !resolved.magicLink && !resolved.passkey && resolved.social.length === 0;
192
216
  if (allOff) {
193
217
  // eslint-disable-next-line no-console
194
- console.warn('[authkit] auth_methods setting deixou todos os métodos desligados — ' +
218
+ console.warn('[authkit] auth_methods (config pins + setting) deixou todos os métodos desligados — ' +
195
219
  'revertendo para os defaults do config (fail-safe). ' +
196
- 'Verifique a setting auth_methods no console admin.');
220
+ 'Verifique cfg.authMethods e a setting auth_methods no console admin.');
197
221
  return configDefaults;
198
222
  }
199
223
  return resolved;
@@ -1,3 +1,17 @@
1
+ /** Cadastro passwordless: só e-mail + nome (sem senha). O login vem por magic link. */
2
+ export declare const passwordlessSignupValidator: import("@vinejs/vine").VineValidator<import("@vinejs/vine").VineObject<{
3
+ email: import("@vinejs/vine").VineString;
4
+ fullName: import("@vinejs/vine").VineString;
5
+ }, {
6
+ email: string;
7
+ fullName: string;
8
+ }, {
9
+ email: string;
10
+ fullName: string;
11
+ }, {
12
+ email: string;
13
+ fullName: string;
14
+ }>, Record<string, any> | undefined>;
1
15
  export declare const signupValidator: import("@vinejs/vine").VineValidator<import("@vinejs/vine").VineObject<{
2
16
  email: import("@vinejs/vine").VineString;
3
17
  fullName: import("@vinejs/vine").VineString;
@@ -1,4 +1,9 @@
1
1
  import vine from '@vinejs/vine';
2
+ /** Cadastro passwordless: só e-mail + nome (sem senha). O login vem por magic link. */
3
+ export const passwordlessSignupValidator = vine.compile(vine.object({
4
+ email: vine.string().trim().email().normalizeEmail(),
5
+ fullName: vine.string().trim().minLength(2).maxLength(255),
6
+ }));
2
7
  export const signupValidator = vine.compile(vine.object({
3
8
  email: vine.string().trim().email().normalizeEmail(),
4
9
  fullName: vine.string().trim().minLength(2).maxLength(255),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adonis-agora/authkit-server",
3
- "version": "0.35.0",
3
+ "version": "0.37.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",
@@ -140,7 +140,7 @@
140
140
  "react-error-boundary": "6.1.2",
141
141
  "nuqs": "2.8.9",
142
142
  "recharts": "3.8.1",
143
- "@adonis-agora/authkit-react": "0.13.1"
143
+ "@adonis-agora/authkit-react": "0.16.0"
144
144
  },
145
145
  "scripts": {
146
146
  "build": "node scripts/build_host_css.mjs && node scripts/build_ui.mjs && tsc && 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')\"",