@dudousxd/adonis-authkit-server 0.4.0 → 0.6.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.
Files changed (50) hide show
  1. package/README.md +23 -2
  2. package/build/host/views/account/apps.edge +58 -0
  3. package/build/host/views/account/security.edge +53 -0
  4. package/build/host/views/account/tokens.edge +1 -0
  5. package/build/host/views/admin/users.edge +62 -2
  6. package/build/host/views/login.edge +55 -0
  7. package/build/host/views/mfa-challenge.edge +12 -0
  8. package/build/index.d.ts +9 -3
  9. package/build/index.js +5 -2
  10. package/build/src/accounts/account_store.d.ts +80 -2
  11. package/build/src/accounts/account_store.js +12 -0
  12. package/build/src/accounts/lucid_account_store.js +8 -0
  13. package/build/src/accounts/lucid_store/core.d.ts +2 -2
  14. package/build/src/accounts/lucid_store/core.js +33 -0
  15. package/build/src/accounts/lucid_store/mfa.js +4 -1
  16. package/build/src/accounts/lucid_store/status_profile.d.ts +21 -0
  17. package/build/src/accounts/lucid_store/status_profile.js +66 -0
  18. package/build/src/audit/audit_sink.d.ts +1 -1
  19. package/build/src/define_config.d.ts +53 -0
  20. package/build/src/define_config.js +14 -1
  21. package/build/src/doctor/checks.js +32 -32
  22. package/build/src/events/dispatcher.d.ts +45 -0
  23. package/build/src/events/dispatcher.js +92 -0
  24. package/build/src/host/admin_sessions_service.d.ts +8 -0
  25. package/build/src/host/admin_sessions_service.js +19 -0
  26. package/build/src/host/controllers/account_apps_controller.d.ts +15 -0
  27. package/build/src/host/controllers/account_apps_controller.js +61 -0
  28. package/build/src/host/controllers/account_mfa_controller.js +6 -2
  29. package/build/src/host/controllers/account_security_controller.d.ts +9 -0
  30. package/build/src/host/controllers/account_security_controller.js +52 -2
  31. package/build/src/host/controllers/account_session_controller.js +3 -1
  32. package/build/src/host/controllers/admin/admin_users_controller.d.ts +13 -0
  33. package/build/src/host/controllers/admin/admin_users_controller.js +133 -0
  34. package/build/src/host/controllers/interaction_controller.d.ts +32 -0
  35. package/build/src/host/controllers/interaction_controller.js +175 -8
  36. package/build/src/host/default_mailer.d.ts +8 -0
  37. package/build/src/host/default_mailer.js +81 -19
  38. package/build/src/host/email_templates.d.ts +4 -0
  39. package/build/src/host/email_templates.js +5 -2
  40. package/build/src/host/i18n.d.ts +395 -11
  41. package/build/src/host/i18n.js +433 -12
  42. package/build/src/host/login_attempt.d.ts +1 -0
  43. package/build/src/host/login_attempt.js +11 -0
  44. package/build/src/host/register_auth_host.js +18 -1
  45. package/build/src/host/trusted_device.d.ts +61 -0
  46. package/build/src/host/trusted_device.js +65 -0
  47. package/build/src/host/validators.d.ts +35 -0
  48. package/build/src/host/validators.js +14 -0
  49. package/build/src/observability/metrics_controller.js +4 -4
  50. package/package.json +1 -1
@@ -0,0 +1,61 @@
1
+ import '../augmentations.js';
2
+ import { ACCOUNT_SESSION_KEY } from '../middleware/account_auth.js';
3
+ import { AdminSessionsService } from '../admin_sessions_service.js';
4
+ /**
5
+ * Self-service de consentimento ("apps com acesso") no console de conta. Lista os
6
+ * Grants da própria conta agrupados por client (resolvendo o nome do client da
7
+ * config estática ou do payload do adapter) e permite revogar o acesso de um
8
+ * client (destrói os grants + AT/RT daquele client). Degrada graciosamente quando
9
+ * o adapter OIDC não enumera (`list`), espelhando o console admin.
10
+ */
11
+ export default class AccountAppsController {
12
+ /** GET /account/apps — lista os apps com acesso (grants) da conta logada. */
13
+ async index(ctx) {
14
+ const service = await ctx.containerResolver.make('authkit.server');
15
+ const cfg = service.config;
16
+ const render = cfg.render;
17
+ const accountId = ctx.session.get(ACCOUNT_SESSION_KEY);
18
+ const sessions = new AdminSessionsService(service);
19
+ const supported = sessions.canList;
20
+ const grantList = supported ? await sessions.listGrants(accountId) : [];
21
+ // Resolve o nome amigável do client: clientId é o fallback (config estática não
22
+ // carrega um display name).
23
+ const nameOf = (clientId) => clientId ?? '';
24
+ const revoked = ctx.session.flashMessages.get('appRevoked');
25
+ return render(ctx, 'account/apps', {
26
+ csrfToken: ctx.request.csrfToken,
27
+ supported,
28
+ revoked: revoked ?? null,
29
+ apps: grantList
30
+ .filter((g) => !!g.clientId)
31
+ .map((g) => ({
32
+ clientId: g.clientId,
33
+ name: nameOf(g.clientId),
34
+ accessTokens: g.accessTokens,
35
+ refreshTokens: g.refreshTokens,
36
+ })),
37
+ });
38
+ }
39
+ /** POST /account/apps/:clientId/revoke — revoga o acesso de um client. */
40
+ async revoke(ctx) {
41
+ const service = await ctx.containerResolver.make('authkit.server');
42
+ const cfg = service.config;
43
+ const accountId = ctx.session.get(ACCOUNT_SESSION_KEY);
44
+ const clientId = ctx.request.param('clientId');
45
+ const sessions = new AdminSessionsService(service);
46
+ const result = await sessions.revokeClientGrants(accountId, clientId);
47
+ await cfg.audit?.record({
48
+ type: 'grant.revoked_by_user',
49
+ accountId,
50
+ clientId,
51
+ ip: ctx.request.ip?.() ?? null,
52
+ metadata: {
53
+ grants: result.grants,
54
+ accessTokens: result.accessTokens,
55
+ refreshTokens: result.refreshTokens,
56
+ },
57
+ });
58
+ ctx.session.flash('appRevoked', cfg.messages['account.apps.revoked'] ?? 'account.apps.revoked');
59
+ return ctx.response.redirect('/account/apps');
60
+ }
61
+ }
@@ -39,7 +39,9 @@ export default class AccountMfaController {
39
39
  const userId = ctx.session.get(ACCOUNT_SESSION_KEY);
40
40
  const generated = await cfg.accountStore.generatePasskeyRegistrationOptions?.(userId);
41
41
  if (!generated) {
42
- return ctx.response.notFound({ message: 'Passkeys indisponíveis' });
42
+ return ctx.response.notFound({
43
+ message: translate(cfg.messages, 'errors.passkeys_unavailable'),
44
+ });
43
45
  }
44
46
  ctx.session.put(PASSKEY_REG_CHALLENGE_KEY, generated.challenge);
45
47
  return generated.options;
@@ -54,7 +56,9 @@ export default class AccountMfaController {
54
56
  const userId = ctx.session.get(ACCOUNT_SESSION_KEY);
55
57
  const challenge = ctx.session.get(PASSKEY_REG_CHALLENGE_KEY);
56
58
  if (!challenge) {
57
- return ctx.response.badRequest({ message: 'Desafio expirado' });
59
+ return ctx.response.badRequest({
60
+ message: translate(cfg.messages, 'errors.challenge_expired'),
61
+ });
58
62
  }
59
63
  const body = ctx.request.input('response', ctx.request.body());
60
64
  const ok = (await cfg.accountStore.verifyPasskeyRegistration?.(userId, body, challenge)) ?? false;
@@ -9,6 +9,15 @@ import type { HttpContext } from '@adonisjs/core/http';
9
9
  */
10
10
  export default class AccountSecurityController {
11
11
  index(ctx: HttpContext): Promise<any>;
12
+ /**
13
+ * POST /account/security/trusted-devices/revoke
14
+ * Limpa o cookie de dispositivo confiável DESTE navegador (o MFA volta a ser
15
+ * exigido aqui). Revogação global por-dispositivo não existe sem estado
16
+ * server-side; re-enrolar o MFA invalida a confiança em TODOS os dispositivos.
17
+ */
18
+ revokeTrustedDevices(ctx: HttpContext): Promise<void>;
19
+ /** POST /account/security/profile — atualiza nome + avatar do próprio perfil. */
20
+ updateProfile(ctx: HttpContext): Promise<void>;
12
21
  changePassword(ctx: HttpContext): Promise<void>;
13
22
  changeEmail(ctx: HttpContext): Promise<void>;
14
23
  /** GET /account/email/confirm?token=... — consome o token e aplica o novo e-mail. */
@@ -1,9 +1,10 @@
1
1
  import '../augmentations.js';
2
2
  import { ACCOUNT_SESSION_KEY } from '../middleware/account_auth.js';
3
- import { supportsAccountSecurity } from '../../accounts/account_store.js';
4
- import { changePasswordValidator, changeEmailValidator } from '../validators.js';
3
+ import { supportsAccountSecurity, supportsProfile } from '../../accounts/account_store.js';
4
+ import { changePasswordValidator, changeEmailValidator, updateProfileValidator } from '../validators.js';
5
5
  import { sendEmailChangeConfirmationEmail } from '../default_mailer.js';
6
6
  import { translate } from '../i18n.js';
7
+ import { TRUSTED_DEVICE_COOKIE } from '../trusted_device.js';
7
8
  /**
8
9
  * Self-service de segurança da conta (console de conta): trocar a senha e o
9
10
  * e-mail. A troca de senha exige a senha ATUAL (verifyCredentials). A troca de
@@ -21,13 +22,62 @@ export default class AccountSecurityController {
21
22
  return render(ctx, 'account/security', {
22
23
  csrfToken: ctx.request.csrfToken,
23
24
  supported: supportsAccountSecurity(cfg.accountStore),
25
+ profileSupported: supportsProfile(cfg.accountStore),
24
26
  email: account?.email ?? '',
27
+ name: account?.name ?? '',
28
+ avatarUrl: account?.avatarUrl ?? '',
25
29
  passwordChanged: ctx.session.flashMessages.get('passwordChanged') ?? null,
26
30
  emailChangeRequested: ctx.session.flashMessages.get('emailChangeRequested') ?? null,
27
31
  emailChanged: ctx.session.flashMessages.get('emailChanged') ?? null,
32
+ profileUpdated: ctx.session.flashMessages.get('profileUpdated') ?? null,
28
33
  error: ctx.session.flashMessages.get('securityError') ?? null,
34
+ trustedDevicesEnabled: cfg.trustedDevices.enabled,
35
+ trustedDevicesRevoked: ctx.session.flashMessages.get('trustedDevicesRevoked') ?? null,
29
36
  });
30
37
  }
38
+ /**
39
+ * POST /account/security/trusted-devices/revoke
40
+ * Limpa o cookie de dispositivo confiável DESTE navegador (o MFA volta a ser
41
+ * exigido aqui). Revogação global por-dispositivo não existe sem estado
42
+ * server-side; re-enrolar o MFA invalida a confiança em TODOS os dispositivos.
43
+ */
44
+ async revokeTrustedDevices(ctx) {
45
+ const service = await ctx.containerResolver.make('authkit.server');
46
+ const cfg = service.config;
47
+ ctx.response.clearCookie(TRUSTED_DEVICE_COOKIE);
48
+ const userId = ctx.session.get(ACCOUNT_SESSION_KEY);
49
+ await cfg.audit?.record({
50
+ type: 'trusted_device.revoked',
51
+ accountId: userId,
52
+ ip: ctx.request.ip?.() ?? null,
53
+ });
54
+ ctx.session.flash('trustedDevicesRevoked', translate(cfg.messages, 'account.security.trusted_devices_revoked'));
55
+ return ctx.response.redirect('/account/security');
56
+ }
57
+ /** POST /account/security/profile — atualiza nome + avatar do próprio perfil. */
58
+ async updateProfile(ctx) {
59
+ const service = await ctx.containerResolver.make('authkit.server');
60
+ const cfg = service.config;
61
+ const store = cfg.accountStore;
62
+ const userId = ctx.session.get(ACCOUNT_SESSION_KEY);
63
+ if (!supportsProfile(store)) {
64
+ return ctx.response.redirect('/account/security');
65
+ }
66
+ const { name, avatarUrl } = await ctx.request.validateUsing(updateProfileValidator);
67
+ // Campos ausentes no form viram string vazia (limpa o valor); enviamos null
68
+ // para limpar, ou o valor trimado.
69
+ await store.updateProfile(userId, {
70
+ name: name ?? null,
71
+ avatarUrl: avatarUrl ?? null,
72
+ });
73
+ await cfg.audit?.record({
74
+ type: 'profile.updated',
75
+ accountId: userId,
76
+ ip: ctx.request.ip?.() ?? null,
77
+ });
78
+ ctx.session.flash('profileUpdated', translate(cfg.messages, 'account.profile.updated'));
79
+ return ctx.response.redirect('/account/security');
80
+ }
31
81
  async changePassword(ctx) {
32
82
  const service = await ctx.containerResolver.make('authkit.server');
33
83
  const cfg = service.config;
@@ -28,7 +28,9 @@ export default class AccountSessionController {
28
28
  ? translate(cfg.messages, 'errors.account_locked', {
29
29
  seconds: result.retryAfterSec ?? 0,
30
30
  })
31
- : translate(cfg.messages, 'errors.invalid_credentials'),
31
+ : result.disabled
32
+ ? translate(cfg.messages, 'errors.account_disabled')
33
+ : translate(cfg.messages, 'errors.invalid_credentials'),
32
34
  });
33
35
  }
34
36
  const acc = result.account;
@@ -6,6 +6,19 @@ import type { HttpContext } from '@adonisjs/core/http';
6
6
  * vírgula no formulário e normalizadas aqui.
7
7
  */
8
8
  export default class AdminUsersController {
9
+ #private;
9
10
  index(ctx: HttpContext): Promise<any>;
11
+ /**
12
+ * POST /admin/users — cria uma conta. Se `password` for informado, a conta já
13
+ * nasce com senha. Senão, emite um token de reset e envia o e-mail (o usuário
14
+ * define a própria senha) — fluxo "create + invite". Audita `user.created`.
15
+ */
16
+ store(ctx: HttpContext): Promise<void>;
17
+ /** POST /admin/users/:id/reset-password — emite token de reset + envia e-mail. */
18
+ resetPassword(ctx: HttpContext): Promise<void>;
19
+ /** POST /admin/users/:id/disable — desabilita a conta (bloqueia login). */
20
+ disable(ctx: HttpContext): Promise<void>;
21
+ /** POST /admin/users/:id/enable — reabilita a conta. */
22
+ enable(ctx: HttpContext): Promise<void>;
10
23
  updateRoles(ctx: HttpContext): Promise<void>;
11
24
  }
@@ -1,4 +1,9 @@
1
1
  import '../../augmentations.js';
2
+ import { randomBytes } from 'node:crypto';
3
+ import { supportsAccountStatus } from '../../../accounts/account_store.js';
4
+ import { ACCOUNT_SESSION_KEY } from '../../middleware/account_auth.js';
5
+ import { adminCreateUserValidator } from '../../validators.js';
6
+ import { sendPasswordResetEmail } from '../../default_mailer.js';
2
7
  const PAGE_SIZE = 20;
3
8
  /**
4
9
  * Gestão de usuários do IdP: listagem paginada com busca por e-mail e edição das
@@ -14,21 +19,149 @@ export default class AdminUsersController {
14
19
  const page = Math.max(1, Number.parseInt(ctx.request.input('page', '1'), 10) || 1);
15
20
  const result = await cfg.accountStore.listAccounts({ search, page, limit: PAGE_SIZE });
16
21
  const totalPages = Math.max(1, Math.ceil(result.total / PAGE_SIZE));
22
+ const store = cfg.accountStore;
23
+ const statusSupported = supportsAccountStatus(store);
24
+ // Resolve o estado de disabled por conta (só quando suportado).
25
+ const disabledMap = new Map();
26
+ if (statusSupported) {
27
+ for (const u of result.data) {
28
+ disabledMap.set(u.id, await store.isDisabled(u.id));
29
+ }
30
+ }
17
31
  return render(ctx, 'admin/users', {
18
32
  csrfToken: ctx.request.csrfToken,
19
33
  search,
20
34
  page,
21
35
  totalPages,
22
36
  total: result.total,
37
+ statusSupported,
38
+ created: ctx.session.flashMessages.get('userCreated') ?? null,
39
+ resetSent: ctx.session.flashMessages.get('resetSent') ?? null,
40
+ statusChanged: ctx.session.flashMessages.get('statusChanged') ?? null,
41
+ error: ctx.session.flashMessages.get('usersError') ?? null,
23
42
  users: result.data.map((u) => ({
24
43
  id: u.id,
25
44
  email: u.email,
26
45
  name: u.name ?? '',
27
46
  roles: u.globalRoles ?? [],
28
47
  rolesText: (u.globalRoles ?? []).join(', '),
48
+ disabled: disabledMap.get(u.id) ?? false,
29
49
  })),
30
50
  });
31
51
  }
52
+ /**
53
+ * POST /admin/users — cria uma conta. Se `password` for informado, a conta já
54
+ * nasce com senha. Senão, emite um token de reset e envia o e-mail (o usuário
55
+ * define a própria senha) — fluxo "create + invite". Audita `user.created`.
56
+ */
57
+ async store(ctx) {
58
+ const service = await ctx.containerResolver.make('authkit.server');
59
+ const cfg = service.config;
60
+ const store = cfg.accountStore;
61
+ const actorId = ctx.session.get(ACCOUNT_SESSION_KEY) ?? null;
62
+ const ip = ctx.request.ip?.() ?? null;
63
+ const { email, name, password } = await ctx.request.validateUsing(adminCreateUserValidator);
64
+ const existing = await store.findByEmail(email);
65
+ if (existing) {
66
+ ctx.session.flash('usersError', cfg.messages['errors.email_taken'] ?? 'errors.email_taken');
67
+ return ctx.response.redirect('/admin/users');
68
+ }
69
+ // Sem senha informada: cria com uma senha aleatória forte (descartável) e
70
+ // dispara o fluxo de reset para o usuário definir a sua.
71
+ const initialPassword = password ?? randomBytes(24).toString('hex');
72
+ const account = await store.create({ email, password: initialPassword, fullName: name ?? null });
73
+ await cfg.audit?.record({
74
+ type: 'user.created',
75
+ accountId: account.id,
76
+ email,
77
+ actorId,
78
+ ip,
79
+ metadata: { invited: !password },
80
+ });
81
+ if (!password) {
82
+ await this.#sendResetEmail(ctx, cfg, email);
83
+ }
84
+ ctx.session.flash('userCreated', cfg.messages['admin.users.created'] ?? 'admin.users.created');
85
+ return ctx.response.redirect('/admin/users');
86
+ }
87
+ /** POST /admin/users/:id/reset-password — emite token de reset + envia e-mail. */
88
+ async resetPassword(ctx) {
89
+ const service = await ctx.containerResolver.make('authkit.server');
90
+ const cfg = service.config;
91
+ const store = cfg.accountStore;
92
+ const actorId = ctx.session.get(ACCOUNT_SESSION_KEY) ?? null;
93
+ const ip = ctx.request.ip?.() ?? null;
94
+ const accountId = ctx.request.param('id');
95
+ const account = await store.findById(accountId);
96
+ if (account) {
97
+ await this.#sendResetEmail(ctx, cfg, account.email);
98
+ await cfg.audit?.record({
99
+ type: 'user.password_reset_sent',
100
+ accountId,
101
+ email: account.email,
102
+ actorId,
103
+ ip,
104
+ });
105
+ }
106
+ ctx.session.flash('resetSent', cfg.messages['admin.users.reset_sent'] ?? 'admin.users.reset_sent');
107
+ return this.#redirectBack(ctx);
108
+ }
109
+ /** POST /admin/users/:id/disable — desabilita a conta (bloqueia login). */
110
+ async disable(ctx) {
111
+ return this.#toggleStatus(ctx, true);
112
+ }
113
+ /** POST /admin/users/:id/enable — reabilita a conta. */
114
+ async enable(ctx) {
115
+ return this.#toggleStatus(ctx, false);
116
+ }
117
+ async #toggleStatus(ctx, disable) {
118
+ const service = await ctx.containerResolver.make('authkit.server');
119
+ const cfg = service.config;
120
+ const store = cfg.accountStore;
121
+ const actorId = ctx.session.get(ACCOUNT_SESSION_KEY) ?? null;
122
+ const ip = ctx.request.ip?.() ?? null;
123
+ const accountId = ctx.request.param('id');
124
+ if (supportsAccountStatus(store)) {
125
+ if (disable)
126
+ await store.disableAccount(accountId);
127
+ else
128
+ await store.enableAccount(accountId);
129
+ await cfg.audit?.record({
130
+ type: disable ? 'user.disabled' : 'user.enabled',
131
+ accountId,
132
+ actorId,
133
+ ip,
134
+ });
135
+ ctx.session.flash('statusChanged', cfg.messages[disable ? 'admin.users.disabled' : 'admin.users.enabled'] ??
136
+ (disable ? 'admin.users.disabled' : 'admin.users.enabled'));
137
+ }
138
+ return this.#redirectBack(ctx);
139
+ }
140
+ /** Emite o token de reset e dispara o e-mail (hook do config tem prioridade). */
141
+ async #sendResetEmail(ctx, cfg, email) {
142
+ const issued = await cfg.accountStore.issuePasswordResetToken(email);
143
+ if (!issued)
144
+ return;
145
+ const origin = `${ctx.request.protocol()}://${ctx.request.host()}`;
146
+ const resetUrl = `${origin}/auth/reset-password?token=${encodeURIComponent(issued.token)}`;
147
+ if (cfg.mail?.onPasswordReset) {
148
+ await cfg.mail.onPasswordReset({ email, resetUrl, token: issued.token });
149
+ }
150
+ else {
151
+ await sendPasswordResetEmail(ctx, { email, resetUrl });
152
+ }
153
+ }
154
+ #redirectBack(ctx) {
155
+ const search = ctx.request.input('search', '').trim();
156
+ const page = Math.max(1, Number.parseInt(ctx.request.input('page', '1'), 10) || 1);
157
+ const qs = new URLSearchParams();
158
+ if (search)
159
+ qs.set('search', search);
160
+ if (page > 1)
161
+ qs.set('page', String(page));
162
+ const query = qs.toString();
163
+ return ctx.response.redirect(`/admin/users${query ? `?${query}` : ''}`);
164
+ }
32
165
  async updateRoles(ctx) {
33
166
  const service = await ctx.containerResolver.make('authkit.server');
34
167
  const cfg = service.config;
@@ -32,6 +32,38 @@ export default class AuthInteractionController {
32
32
  private stepUpExtra;
33
33
  /** true se o store suporta passkeys E a conta tem ao menos uma registrada. */
34
34
  private hasPasskeys;
35
+ /**
36
+ * Lê o cookie de dispositivo confiável (encriptado, appKey-backed) e valida que
37
+ * pertence a `accountId`, não expirou e é posterior ao último (re)enrollment de
38
+ * MFA. Step-up NÃO chama isto (força sempre o MFA). Best-effort: qualquer erro de
39
+ * leitura → não confiável.
40
+ */
41
+ private checkTrustedDevice;
42
+ /**
43
+ * Se o checkbox "confiar neste dispositivo" foi marcado E o mecanismo está ligado,
44
+ * grava o cookie encriptado de confiança para a conta (skip MFA por N dias).
45
+ */
46
+ private maybeTrustDevice;
47
+ /**
48
+ * accountId para uma cerimônia de passkey no login. Prioriza o accountId pendente
49
+ * do MFA (passkey como 2º fator). Quando ausente e o passkey-first está ligado,
50
+ * resolve a conta pelo e-mail guardado na sessão (passkey ANTES da senha) — só se
51
+ * a conta existe E tem ao menos uma passkey.
52
+ */
53
+ private resolvePasskeyAccountId;
54
+ /**
55
+ * POST /auth/interaction/:uid/magic
56
+ * Magic link: lê o e-mail da sessão (passwordless.magicLink ligado), emite um
57
+ * token de uso único e dispara o e-mail. SEMPRE renderiza "link enviado",
58
+ * independentemente de a conta existir (anti-enumeração). Throttled como o login.
59
+ */
60
+ magicLinkRequest(ctx: HttpContext): Promise<any>;
61
+ /**
62
+ * GET /auth/interaction/:uid/magic?token=...
63
+ * Consome o magic link. Em sucesso finaliza o login (amr `['email']`). Token
64
+ * inválido/expirado volta ao início do login.
65
+ */
66
+ magicLinkConsume(ctx: HttpContext): Promise<void>;
35
67
  /**
36
68
  * POST /auth/interaction/:uid/passkey/options
37
69
  * Gera as opções de autenticação por passkey para o accountId pendente do MFA,