@dudousxd/adonis-authkit-server 0.2.0 → 0.4.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 (61) hide show
  1. package/build/commands/commands.json +28 -0
  2. package/build/commands/doctor.d.ts +10 -0
  3. package/build/commands/doctor.js +66 -0
  4. package/build/commands/rotate_keys.d.ts +10 -0
  5. package/build/commands/rotate_keys.js +53 -0
  6. package/build/host/views/account/email-confirmed.edge +15 -0
  7. package/build/host/views/account/security.edge +83 -0
  8. package/build/host/views/account/tokens.edge +7 -4
  9. package/build/host/views/admin/client_form.edge +83 -0
  10. package/build/host/views/admin/clients.edge +68 -3
  11. package/build/host/views/admin/sessions.edge +89 -0
  12. package/build/host/views/admin/users.edge +1 -0
  13. package/build/host/views/mfa-challenge.edge +29 -23
  14. package/build/index.d.ts +4 -3
  15. package/build/index.js +2 -2
  16. package/build/src/accounts/account_store.d.ts +46 -1
  17. package/build/src/accounts/account_store.js +4 -0
  18. package/build/src/accounts/lucid_store/core.d.ts +5 -4
  19. package/build/src/accounts/lucid_store/core.js +67 -2
  20. package/build/src/adapters/adapter_contract.d.ts +29 -0
  21. package/build/src/adapters/database_adapter.d.ts +12 -1
  22. package/build/src/adapters/database_adapter.js +24 -0
  23. package/build/src/adapters/redis_adapter.d.ts +14 -1
  24. package/build/src/adapters/redis_adapter.js +35 -0
  25. package/build/src/audit/audit_sink.d.ts +1 -1
  26. package/build/src/define_config.d.ts +102 -0
  27. package/build/src/define_config.js +46 -3
  28. package/build/src/doctor/checks.d.ts +51 -0
  29. package/build/src/doctor/checks.js +231 -0
  30. package/build/src/host/admin_clients_service.d.ts +65 -0
  31. package/build/src/host/admin_clients_service.js +143 -0
  32. package/build/src/host/admin_sessions_service.d.ts +63 -0
  33. package/build/src/host/admin_sessions_service.js +127 -0
  34. package/build/src/host/controllers/account_security_controller.d.ts +16 -0
  35. package/build/src/host/controllers/account_security_controller.js +119 -0
  36. package/build/src/host/controllers/account_session_controller.js +2 -1
  37. package/build/src/host/controllers/admin/admin_clients_controller.d.ts +17 -3
  38. package/build/src/host/controllers/admin/admin_clients_controller.js +158 -4
  39. package/build/src/host/controllers/admin/admin_sessions_controller.d.ts +14 -0
  40. package/build/src/host/controllers/admin/admin_sessions_controller.js +64 -0
  41. package/build/src/host/controllers/interaction_controller.d.ts +11 -0
  42. package/build/src/host/controllers/interaction_controller.js +49 -10
  43. package/build/src/host/default_mailer.d.ts +17 -0
  44. package/build/src/host/default_mailer.js +51 -0
  45. package/build/src/host/i18n.d.ts +80 -0
  46. package/build/src/host/i18n.js +86 -1
  47. package/build/src/host/login_notify.d.ts +20 -0
  48. package/build/src/host/login_notify.js +71 -0
  49. package/build/src/host/register_auth_host.js +20 -0
  50. package/build/src/host/validators.d.ts +32 -0
  51. package/build/src/host/validators.js +14 -0
  52. package/build/src/keys/keystore.d.ts +43 -0
  53. package/build/src/keys/keystore.js +74 -0
  54. package/build/src/provider/build_provider.js +23 -0
  55. package/build/src/provider/device_sources.d.ts +6 -0
  56. package/build/src/provider/device_sources.js +65 -0
  57. package/build/src/provider/interaction_actions.d.ts +6 -1
  58. package/build/src/provider/interaction_actions.js +9 -2
  59. package/build/src/provider/oidc_service.d.ts +15 -0
  60. package/build/src/provider/oidc_service.js +27 -0
  61. package/package.json +2 -2
@@ -14,20 +14,24 @@
14
14
  <p class="mt-4 text-sm text-red-600">{{ error }}</p>
15
15
  @end
16
16
 
17
- <div class="mt-6">
18
- <label for="code" class="mb-1 block text-sm font-medium text-gray-700">{{ t('mfa_challenge.code_label') }}</label>
19
- <input id="code" name="code" inputmode="numeric" autocomplete="one-time-code"
20
- pattern="[0-9]*" maxlength="6" autofocus
21
- class="w-full rounded-lg border border-gray-300 px-3 py-2 text-center text-lg tracking-[0.4em] outline-none transition focus:border-gray-900 focus:ring-2 focus:ring-gray-900" />
22
- </div>
17
+ {{-- Step-up sem MFA enrolado: bloqueia o login e instrui a configurar o MFA;
18
+ não renderiza o campo de código (não há 2º fator a desafiar). --}}
19
+ @if(!noEnrollment)
20
+ <div class="mt-6">
21
+ <label for="code" class="mb-1 block text-sm font-medium text-gray-700">{{ t('mfa_challenge.code_label') }}</label>
22
+ <input id="code" name="code" inputmode="numeric" autocomplete="one-time-code"
23
+ pattern="[0-9]*" maxlength="6" autofocus
24
+ class="w-full rounded-lg border border-gray-300 px-3 py-2 text-center text-lg tracking-[0.4em] outline-none transition focus:border-gray-900 focus:ring-2 focus:ring-gray-900" />
25
+ </div>
23
26
 
24
- <button type="submit"
25
- class="mt-6 w-full rounded-lg bg-gray-900 py-2.5 text-sm font-semibold text-white transition hover:opacity-90">
26
- {{ t('mfa_challenge.submit') }}
27
- </button>
27
+ <button type="submit"
28
+ class="mt-6 w-full rounded-lg bg-gray-900 py-2.5 text-sm font-semibold text-white transition hover:opacity-90">
29
+ {{ t('mfa_challenge.submit') }}
30
+ </button>
31
+ @end
28
32
  </form>
29
33
 
30
- @if(passkeyAvailable)
34
+ @if(passkeyAvailable && !noEnrollment)
31
35
  {{-- Passkey como alternativa ao código TOTP. O form é submetido por página
32
36
  inteira (não fetch) para que o 303 de volta ao client navegue o browser. --}}
33
37
  <form id="passkey-form" method="POST" action="/auth/interaction/{{ uid }}/passkey/verify" class="mt-4">
@@ -41,18 +45,20 @@
41
45
  <p id="passkey-error" class="mt-3 hidden text-sm text-red-600">{{ t('mfa_challenge.passkey_error') }}</p>
42
46
  @end
43
47
 
44
- <details class="mt-6 text-sm text-gray-600">
45
- <summary class="cursor-pointer hover:underline">{{ t('mfa_challenge.recovery_summary') }}</summary>
46
- <form method="POST" action="/auth/interaction/{{ uid }}/mfa" class="mt-3">
47
- <input type="hidden" name="_csrf" value="{{ csrfToken }}">
48
- <input name="recoveryCode" placeholder="xxxxx-xxxxx"
49
- class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm outline-none focus:border-gray-900" />
50
- <button type="submit"
51
- class="mt-3 w-full rounded-lg border border-gray-300 py-2.5 text-sm font-semibold text-gray-700 transition hover:bg-gray-50">
52
- {{ t('mfa_challenge.recovery_submit') }}
53
- </button>
54
- </form>
55
- </details>
48
+ @if(!noEnrollment)
49
+ <details class="mt-6 text-sm text-gray-600">
50
+ <summary class="cursor-pointer hover:underline">{{ t('mfa_challenge.recovery_summary') }}</summary>
51
+ <form method="POST" action="/auth/interaction/{{ uid }}/mfa" class="mt-3">
52
+ <input type="hidden" name="_csrf" value="{{ csrfToken }}">
53
+ <input name="recoveryCode" placeholder="xxxxx-xxxxx"
54
+ class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm outline-none focus:border-gray-900" />
55
+ <button type="submit"
56
+ class="mt-3 w-full rounded-lg border border-gray-300 py-2.5 text-sm font-semibold text-gray-700 transition hover:bg-gray-50">
57
+ {{ t('mfa_challenge.recovery_submit') }}
58
+ </button>
59
+ </form>
60
+ </details>
61
+ @end
56
62
  </div>
57
63
 
58
64
  @if(passkeyAvailable)
package/build/index.d.ts CHANGED
@@ -10,8 +10,8 @@ export { resolveAdmin, resolveWebauthn, resolveDynamicRegistration } from './src
10
10
  export type { WebauthnConfigInput, ResolvedWebauthnConfig } from './src/define_config.js';
11
11
  export { lucidAccountStore } from './src/accounts/lucid_account_store.js';
12
12
  export type { LucidAccountStoreOptions, AccountSecretEncrypter, } from './src/accounts/lucid_account_store.js';
13
- export type { AccountStore, CoreAccountStore, AdminCapability, MfaCapability, WebauthnCapability, ProviderIdentityCapability, AuthAccount, CreateAccountInput, LinkProviderIdentityInput, ListAccountsParams, Paginated, PasskeySummary, } from './src/accounts/account_store.js';
14
- export { supportsMfa, supportsPasskeys, supportsProviderIdentity, } from './src/accounts/account_store.js';
13
+ export type { AccountStore, CoreAccountStore, AdminCapability, MfaCapability, WebauthnCapability, ProviderIdentityCapability, AccountSecurityCapability, AuthAccount, CreateAccountInput, LinkProviderIdentityInput, ListAccountsParams, Paginated, PasskeySummary, } from './src/accounts/account_store.js';
14
+ export { supportsMfa, supportsPasskeys, supportsProviderIdentity, supportsAccountSecurity, } from './src/accounts/account_store.js';
15
15
  export { withProviderIdentity } from './src/mixins/with_provider_identity.js';
16
16
  export type { ProviderIdentityRow, ProviderIdentityClass, } from './src/mixins/with_provider_identity.js';
17
17
  export { withWebauthnCredential } from './src/mixins/with_webauthn_credential.js';
@@ -31,7 +31,8 @@ export type { I18nConfig, AuthMessages } from './src/host/i18n.js';
31
31
  export type { AuthHostRenderer, AuthSocialConfig } from './src/define_config.js';
32
32
  export { registerAuthHost } from './src/host/register_auth_host.js';
33
33
  export type { AuthHostOptions } from './src/host/register_auth_host.js';
34
- export { resolveRateLimit } from './src/define_config.js';
34
+ export { resolveRateLimit, resolveNotifications } from './src/define_config.js';
35
+ export type { NotificationsConfigInput, ResolvedNotificationsConfig, } from './src/define_config.js';
35
36
  export type { RateLimitConfigInput, RateLimitBucket, ResolvedRateLimitConfig, } from './src/define_config.js';
36
37
  export { createAuthThrottles } from './src/host/rate_limit.js';
37
38
  export type { AuthThrottles, ThrottleMiddleware } from './src/host/rate_limit.js';
package/build/index.js CHANGED
@@ -7,7 +7,7 @@ export { OidcService } from './src/provider/oidc_service.js';
7
7
  export { registerOidcRoutes } from './src/register_routes.js';
8
8
  export { resolveAdmin, resolveWebauthn, resolveDynamicRegistration } from './src/define_config.js';
9
9
  export { lucidAccountStore } from './src/accounts/lucid_account_store.js';
10
- export { supportsMfa, supportsPasskeys, supportsProviderIdentity, } from './src/accounts/account_store.js';
10
+ export { supportsMfa, supportsPasskeys, supportsProviderIdentity, supportsAccountSecurity, } from './src/accounts/account_store.js';
11
11
  export { withProviderIdentity } from './src/mixins/with_provider_identity.js';
12
12
  export { withWebauthnCredential } from './src/mixins/with_webauthn_credential.js';
13
13
  export { lucidPatStore } from './src/pat/lucid_pat_store.js';
@@ -19,7 +19,7 @@ export { edgeRenderer } from './src/host/renderers/edge_renderer.js';
19
19
  export { brandFor, isFirstParty } from './src/host/branding.js';
20
20
  export { resolveMessages, translate, DEFAULT_MESSAGES, DEFAULT_LOCALE } from './src/host/i18n.js';
21
21
  export { registerAuthHost } from './src/host/register_auth_host.js';
22
- export { resolveRateLimit } from './src/define_config.js';
22
+ export { resolveRateLimit, resolveNotifications } from './src/define_config.js';
23
23
  export { createAuthThrottles } from './src/host/rate_limit.js';
24
24
  /**
25
25
  * Configure hook + stubsRoot resolvidos pelo `node ace configure @dudousxd/adonis-authkit-server`.
@@ -83,6 +83,49 @@ export interface AdminCapability {
83
83
  /** Substitui as roles globais de uma conta. */
84
84
  setGlobalRoles(accountId: string, roles: string[]): Promise<void>;
85
85
  }
86
+ /**
87
+ * Self-service de segurança da conta (console de conta): trocar a senha e o
88
+ * e-mail (com confirmação no NOVO endereço). É uma CAPACIDADE opcional — stores
89
+ * sem suporte omitem os métodos e a UI esconde a seção correspondente.
90
+ *
91
+ * A troca de e-mail usa um token de confirmação que viaja para o NOVO endereço
92
+ * ({@link requestEmailChange}) e é consumido por {@link confirmEmailChange}. O
93
+ * store default (Lucid) reaproveita a coluna `emailVerificationToken` codificando
94
+ * um payload `ec:<email>:<token>` — assim NÃO exige migração nova (ver
95
+ * `lucid_store/core.ts`). O tradeoff é que um token de verificação de cadastro e
96
+ * um de troca de e-mail não coexistem (mesma coluna); na prática são fluxos
97
+ * distintos no tempo.
98
+ */
99
+ export interface AccountSecurityCapability {
100
+ /**
101
+ * Define uma nova senha para a conta (após o controller confirmar a senha ATUAL
102
+ * via {@link CoreAccountStore.verifyCredentials}). Retorna false se a conta não
103
+ * existe.
104
+ */
105
+ changePassword(accountId: string, newPassword: string): Promise<boolean>;
106
+ /**
107
+ * Inicia a troca de e-mail: gera um token de confirmação para o `newEmail` e o
108
+ * persiste. Retorna o token + a conta, ou null se a conta não existe OU se o
109
+ * `newEmail` já pertence a outra conta.
110
+ */
111
+ requestEmailChange(accountId: string, newEmail: string): Promise<{
112
+ token: string;
113
+ account: AuthAccount;
114
+ newEmail: string;
115
+ } | null>;
116
+ /**
117
+ * Confirma a troca de e-mail consumindo o token (single-use). Em caso de
118
+ * sucesso aplica o novo e-mail, marca-o como verificado e limpa o token.
119
+ * Retorna `{ ok: true, account, newEmail }` ou `{ ok: false }`.
120
+ */
121
+ confirmEmailChange(token: string): Promise<{
122
+ ok: true;
123
+ account: AuthAccount;
124
+ newEmail: string;
125
+ } | {
126
+ ok: false;
127
+ }>;
128
+ }
86
129
  /**
87
130
  * Account linking por identidade de provider (Google, GitHub, …).
88
131
  * `(provider, providerUserId)` é a chave estável vinda do provider OAuth — não
@@ -184,10 +227,12 @@ export interface WebauthnCapability {
184
227
  * presentes-mas-lançando). Use os type guards {@link supportsMfa},
185
228
  * {@link supportsPasskeys}, {@link supportsProviderIdentity} para estreitar.
186
229
  */
187
- export type AccountStore = CoreAccountStore & Partial<MfaCapability & WebauthnCapability & ProviderIdentityCapability>;
230
+ export type AccountStore = CoreAccountStore & Partial<MfaCapability & WebauthnCapability & ProviderIdentityCapability & AccountSecurityCapability>;
188
231
  /** Type guard: o store implementa a capacidade de MFA / TOTP. */
189
232
  export declare function supportsMfa(store: AccountStore): store is AccountStore & MfaCapability;
190
233
  /** Type guard: o store implementa a capacidade de passkeys / WebAuthn. */
191
234
  export declare function supportsPasskeys(store: AccountStore): store is AccountStore & WebauthnCapability;
192
235
  /** Type guard: o store implementa account linking por identidade de provider. */
193
236
  export declare function supportsProviderIdentity(store: AccountStore): store is AccountStore & ProviderIdentityCapability;
237
+ /** Type guard: o store implementa o self-service de segurança (senha/e-mail). */
238
+ export declare function supportsAccountSecurity(store: AccountStore): store is AccountStore & AccountSecurityCapability;
@@ -10,3 +10,7 @@ export function supportsPasskeys(store) {
10
10
  export function supportsProviderIdentity(store) {
11
11
  return typeof store.findByProviderIdentity === 'function';
12
12
  }
13
+ /** Type guard: o store implementa o self-service de segurança (senha/e-mail). */
14
+ export function supportsAccountSecurity(store) {
15
+ return typeof store.changePassword === 'function';
16
+ }
@@ -1,8 +1,9 @@
1
- import type { CoreAccountStore } from '../account_store.js';
1
+ import type { AccountSecurityCapability, CoreAccountStore } from '../account_store.js';
2
2
  import type { LucidStoreContext } from './shared.js';
3
3
  /**
4
4
  * Núcleo SEMPRE presente do {@link CoreAccountStore} sobre um model Lucid:
5
- * identidade, cadastro, reset de senha, verificação de e-mail e administração
6
- * (listagem paginada + roles globais).
5
+ * identidade, cadastro, reset de senha, verificação de e-mail, administração
6
+ * (listagem paginada + roles globais) e o self-service de segurança
7
+ * ({@link AccountSecurityCapability}: trocar senha/e-mail).
7
8
  */
8
- export declare function buildCore(ctx: LucidStoreContext): CoreAccountStore;
9
+ export declare function buildCore(ctx: LucidStoreContext): CoreAccountStore & AccountSecurityCapability;
@@ -1,9 +1,12 @@
1
1
  import { randomBytes } from 'node:crypto';
2
2
  import { DateTime } from 'luxon';
3
+ /** Prefixo do token de troca de e-mail (reaproveita a coluna emailVerificationToken). */
4
+ const EMAIL_CHANGE_PREFIX = 'ec:';
3
5
  /**
4
6
  * Núcleo SEMPRE presente do {@link CoreAccountStore} sobre um model Lucid:
5
- * identidade, cadastro, reset de senha, verificação de e-mail e administração
6
- * (listagem paginada + roles globais).
7
+ * identidade, cadastro, reset de senha, verificação de e-mail, administração
8
+ * (listagem paginada + roles globais) e o self-service de segurança
9
+ * ({@link AccountSecurityCapability}: trocar senha/e-mail).
7
10
  */
8
11
  export function buildCore(ctx) {
9
12
  const { Model, toAccount } = ctx;
@@ -66,6 +69,10 @@ export function buildCore(ctx) {
66
69
  async consumeEmailVerificationToken(token) {
67
70
  if (!token)
68
71
  return false;
72
+ // Tokens de troca de e-mail (`ec:`) NÃO são verificações de cadastro — só o
73
+ // fluxo de confirmEmailChange pode consumi-los.
74
+ if (token.startsWith(EMAIL_CHANGE_PREFIX))
75
+ return false;
69
76
  const row = await Model.query().where('emailVerificationToken', token).first();
70
77
  if (!row)
71
78
  return false;
@@ -104,5 +111,63 @@ export function buildCore(ctx) {
104
111
  row.globalRoles = roles;
105
112
  await row.save();
106
113
  },
114
+ // ----- Self-service de segurança (console de conta) -----
115
+ async changePassword(accountId, newPassword) {
116
+ const row = await Model.find(accountId);
117
+ if (!row)
118
+ return false;
119
+ // O hash acontece no @beforeSave do mixin withAuthUser ao detectar $dirty.password.
120
+ row.password = newPassword;
121
+ await row.save();
122
+ return true;
123
+ },
124
+ async requestEmailChange(accountId, newEmail) {
125
+ const row = await Model.find(accountId);
126
+ if (!row)
127
+ return null;
128
+ // Não permite tomar um e-mail já usado por OUTRA conta.
129
+ const taken = await Model.query().where('email', newEmail).first();
130
+ if (taken && taken.id !== row.id)
131
+ return null;
132
+ // Token = `ec:<base64url(newEmail)>:<random>`. Reaproveita a coluna
133
+ // emailVerificationToken (sem migração nova); o prefixo `ec:` distingue do
134
+ // token de verificação de cadastro. O e-mail viaja codificado no próprio
135
+ // token, então não precisamos de coluna extra para o "pending email".
136
+ const encodedEmail = Buffer.from(newEmail, 'utf8').toString('base64url');
137
+ const token = `${EMAIL_CHANGE_PREFIX}${encodedEmail}:${randomBytes(24).toString('hex')}`;
138
+ row.emailVerificationToken = token;
139
+ await row.save();
140
+ return { token, account: toAccount(row), newEmail };
141
+ },
142
+ async confirmEmailChange(token) {
143
+ if (!token || !token.startsWith(EMAIL_CHANGE_PREFIX))
144
+ return { ok: false };
145
+ const parts = token.split(':');
146
+ // Forma esperada: ['ec', '<b64email>', '<random>']
147
+ if (parts.length !== 3)
148
+ return { ok: false };
149
+ let newEmail;
150
+ try {
151
+ newEmail = Buffer.from(parts[1], 'base64url').toString('utf8');
152
+ }
153
+ catch {
154
+ return { ok: false };
155
+ }
156
+ if (!newEmail)
157
+ return { ok: false };
158
+ const row = await Model.query().where('emailVerificationToken', token).first();
159
+ if (!row)
160
+ return { ok: false };
161
+ // Defesa contra corrida: o e-mail pode ter sido tomado entre o pedido e a
162
+ // confirmação por outra conta.
163
+ const taken = await Model.query().where('email', newEmail).first();
164
+ if (taken && taken.id !== row.id)
165
+ return { ok: false };
166
+ row.email = newEmail;
167
+ row.emailVerifiedAt = DateTime.now();
168
+ row.emailVerificationToken = null;
169
+ await row.save();
170
+ return { ok: true, account: toAccount(row), newEmail };
171
+ },
107
172
  };
108
173
  }
@@ -6,6 +6,16 @@ export interface OidcPayload {
6
6
  uid?: string;
7
7
  consumed?: unknown;
8
8
  }
9
+ /** Um client OIDC enumerado do adapter (id + payload de metadata persistido). */
10
+ export interface EnumeratedClient {
11
+ clientId: string;
12
+ payload: Record<string, unknown>;
13
+ }
14
+ /** Um artefato OIDC enumerado de um model qualquer (id + payload persistido). */
15
+ export interface EnumeratedArtifact {
16
+ id: string;
17
+ payload: Record<string, unknown>;
18
+ }
9
19
  /** Contrato que o oidc-provider espera de um adapter (um por model). */
10
20
  export interface OidcAdapter {
11
21
  upsert(id: string, payload: OidcPayload, expiresIn: number): Promise<void>;
@@ -15,4 +25,23 @@ export interface OidcAdapter {
15
25
  consume(id: string): Promise<void>;
16
26
  destroy(id: string): Promise<void>;
17
27
  revokeByGrantId(grantId: string): Promise<void>;
28
+ /**
29
+ * Enumera os artefatos do model deste adapter — usado SÓ para o model `Client`
30
+ * pelo console admin, para listar clients persistidos (registro dinâmico/CRUD).
31
+ * Capacidade OPCIONAL (estilo `AuditSink.list`): adapters que não conseguem
32
+ * enumerar de forma barata omitem o método e a UI degrada graciosamente.
33
+ *
34
+ * @deprecated Use {@link list} (genérico). Mantido por compat: quando presente,
35
+ * delega para `list()` (o adapter é sempre instanciado com `model = 'Client'`).
36
+ */
37
+ listClients?(): Promise<EnumeratedClient[]>;
38
+ /**
39
+ * Enumeração GENÉRICA dos artefatos do model deste adapter (id + payload). Usada
40
+ * pelo console admin para listar `Client` (CRUD) e `Session`/`Grant`/tokens
41
+ * (sessões ativas + revogação). Capacidade OPCIONAL (estilo `AuditSink.list`):
42
+ * adapters que não conseguem enumerar de forma barata omitem o método e a UI
43
+ * degrada graciosamente. O adapter já é escopado a UM model na construção
44
+ * (`new AdapterClass(model)`), então não recebe parâmetro.
45
+ */
46
+ list?(): Promise<EnumeratedArtifact[]>;
18
47
  }
@@ -1,5 +1,5 @@
1
1
  import type { Database } from '@adonisjs/lucid/database';
2
- import type { OidcAdapter, OidcPayload } from './adapter_contract.js';
2
+ import type { EnumeratedArtifact, EnumeratedClient, OidcAdapter, OidcPayload } from './adapter_contract.js';
3
3
  export declare class DatabaseAdapter implements OidcAdapter {
4
4
  #private;
5
5
  private name;
@@ -12,4 +12,15 @@ export declare class DatabaseAdapter implements OidcAdapter {
12
12
  consume(id: string): Promise<void>;
13
13
  destroy(id: string): Promise<void>;
14
14
  revokeByGrantId(grantId: string): Promise<void>;
15
+ /**
16
+ * Enumeração genérica dos artefatos do model deste adapter (id + payload).
17
+ * Filtra por `model_name = this.name` e descarta linhas expiradas. Usada pelo
18
+ * console admin para listar `Client` (CRUD) e `Session`/`Grant`/tokens.
19
+ */
20
+ list(): Promise<EnumeratedArtifact[]>;
21
+ /**
22
+ * Compat: enumera os clients persistidos. Delega para {@link list} (o adapter é
23
+ * instanciado com `model_name = 'Client'`), reprojetando `id` → `clientId`.
24
+ */
25
+ listClients(): Promise<EnumeratedClient[]>;
15
26
  }
@@ -60,4 +60,28 @@ export class DatabaseAdapter {
60
60
  async revokeByGrantId(grantId) {
61
61
  await this.db.query().from(TABLE).where('grant_id', grantId).delete();
62
62
  }
63
+ /**
64
+ * Enumeração genérica dos artefatos do model deste adapter (id + payload).
65
+ * Filtra por `model_name = this.name` e descarta linhas expiradas. Usada pelo
66
+ * console admin para listar `Client` (CRUD) e `Session`/`Grant`/tokens.
67
+ */
68
+ async list() {
69
+ const rows = await this.#query().orderBy('id', 'asc');
70
+ const now = Date.now();
71
+ const result = [];
72
+ for (const row of rows) {
73
+ if (row.expires_at && new Date(row.expires_at).getTime() <= now)
74
+ continue;
75
+ result.push({ id: row.id, payload: JSON.parse(row.payload) });
76
+ }
77
+ return result;
78
+ }
79
+ /**
80
+ * Compat: enumera os clients persistidos. Delega para {@link list} (o adapter é
81
+ * instanciado com `model_name = 'Client'`), reprojetando `id` → `clientId`.
82
+ */
83
+ async listClients() {
84
+ const rows = await this.list();
85
+ return rows.map((r) => ({ clientId: r.id, payload: r.payload }));
86
+ }
63
87
  }
@@ -1,5 +1,5 @@
1
1
  import type { Redis } from 'ioredis';
2
- import type { OidcAdapter, OidcPayload } from './adapter_contract.js';
2
+ import type { EnumeratedArtifact, EnumeratedClient, OidcAdapter, OidcPayload } from './adapter_contract.js';
3
3
  export declare class RedisAdapter implements OidcAdapter {
4
4
  #private;
5
5
  private name;
@@ -13,4 +13,17 @@ export declare class RedisAdapter implements OidcAdapter {
13
13
  consume(id: string): Promise<void>;
14
14
  destroy(id: string): Promise<void>;
15
15
  revokeByGrantId(grantId: string): Promise<void>;
16
+ /**
17
+ * Enumeração genérica dos artefatos do model deste adapter via SCAN sobre o
18
+ * prefixo de chave do model (`<prefix>:<name>:*`). É limpo porque cada artefato
19
+ * é uma chave única já namespaceada por `prefix` + `name`; SCAN é não-bloqueante
20
+ * (cursor) ao contrário de KEYS. Usado pelo console admin (`Client`, `Session`,
21
+ * `Grant`, tokens).
22
+ */
23
+ list(): Promise<EnumeratedArtifact[]>;
24
+ /**
25
+ * Compat: enumera os clients persistidos. Delega para {@link list} (o adapter é
26
+ * instanciado com `name = 'Client'`), reprojetando `id` → `clientId`.
27
+ */
28
+ listClients(): Promise<EnumeratedClient[]>;
16
29
  }
@@ -92,4 +92,39 @@ export class RedisAdapter {
92
92
  multi.del(gk);
93
93
  await multi.exec();
94
94
  }
95
+ /**
96
+ * Enumeração genérica dos artefatos do model deste adapter via SCAN sobre o
97
+ * prefixo de chave do model (`<prefix>:<name>:*`). É limpo porque cada artefato
98
+ * é uma chave única já namespaceada por `prefix` + `name`; SCAN é não-bloqueante
99
+ * (cursor) ao contrário de KEYS. Usado pelo console admin (`Client`, `Session`,
100
+ * `Grant`, tokens).
101
+ */
102
+ async list() {
103
+ const prefix = `${this.prefix}:${this.name}:`;
104
+ const result = [];
105
+ let cursor = '0';
106
+ do {
107
+ const [next, keys] = await this.redis.scan(cursor, 'MATCH', `${prefix}*`, 'COUNT', 100);
108
+ cursor = next;
109
+ for (const key of keys) {
110
+ const data = await this.redis.get(key);
111
+ if (!data)
112
+ continue;
113
+ result.push({
114
+ id: key.slice(prefix.length),
115
+ payload: JSON.parse(data),
116
+ });
117
+ }
118
+ } while (cursor !== '0');
119
+ result.sort((a, b) => a.id.localeCompare(b.id));
120
+ return result;
121
+ }
122
+ /**
123
+ * Compat: enumera os clients persistidos. Delega para {@link list} (o adapter é
124
+ * instanciado com `name = 'Client'`), reprojetando `id` → `clientId`.
125
+ */
126
+ async listClients() {
127
+ const rows = await this.list();
128
+ return rows.map((r) => ({ clientId: r.id, payload: r.payload }));
129
+ }
95
130
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Tipos de eventos de auditoria relevantes para segurança emitidos pelo IdP.
3
3
  */
4
- export type AuditEventType = 'login.success' | 'login.failure' | 'signup' | 'password_reset.issued' | 'password_reset.consumed' | 'pat.issued' | 'pat.revoked' | 'pat.used' | 'impersonation' | 'mfa.enabled' | 'mfa.disabled' | 'account.locked' | 'passkey.registered' | 'passkey.removed' | 'email_verification.issued' | 'email_verification.consumed';
4
+ export type AuditEventType = 'login.success' | 'login.failure' | 'signup' | 'password_reset.issued' | 'password_reset.consumed' | 'pat.issued' | 'pat.revoked' | 'pat.used' | 'impersonation' | 'mfa.enabled' | 'mfa.disabled' | 'account.locked' | 'passkey.registered' | 'passkey.removed' | 'email_verification.issued' | 'email_verification.consumed' | 'client.created' | 'client.updated' | 'client.deleted' | 'session.revoked_all' | 'password.changed' | 'email.change_requested' | 'email.changed' | 'login.new_ip_notified';
5
5
  /**
6
6
  * Evento de auditoria a registrar. O timestamp é definido pelo sink (não aqui).
7
7
  */
@@ -89,6 +89,22 @@ export interface ResolvedLockoutConfig {
89
89
  store?: string;
90
90
  }
91
91
  export declare function resolveLockout(input?: LockoutConfigInput): ResolvedLockoutConfig;
92
+ /**
93
+ * Notificações de segurança por e-mail (best-effort, fire-and-forget). Hoje cobre
94
+ * o alerta de NOVO acesso (login de um IP nunca visto antes para a conta).
95
+ */
96
+ export interface NotificationsConfigInput {
97
+ /**
98
+ * Envia um e-mail "novo acesso à sua conta" quando um login bem-sucedido vem de
99
+ * um IP sem `login.success` anterior para a conta. Default: true. A checagem do
100
+ * histórico usa o `audit.list` (degrada para no-op se o sink não consulta).
101
+ */
102
+ newLoginEmail?: boolean;
103
+ }
104
+ export interface ResolvedNotificationsConfig {
105
+ newLoginEmail: boolean;
106
+ }
107
+ export declare function resolveNotifications(input?: NotificationsConfigInput): ResolvedNotificationsConfig;
92
108
  /**
93
109
  * Registro dinâmico de clients (OIDC Dynamic Client Registration — RFC 7591).
94
110
  *
@@ -123,6 +139,72 @@ export interface ResolvedDynamicRegistrationConfig {
123
139
  * (RFC 7591) — `management: true` com `enabled: false` é um erro de configuração.
124
140
  */
125
141
  export declare function resolveDynamicRegistration(input?: DynamicRegistrationConfigInput): ResolvedDynamicRegistrationConfig;
142
+ /**
143
+ * Device Authorization Grant (RFC 8628). Quando habilitado, o oidc-provider expõe
144
+ * o `device_authorization_endpoint` (`/device/auth`) e a tela de verificação de
145
+ * user-code (`/device`). O grant `urn:ietf:params:oauth:grant-type:device_code`
146
+ * deve ser concedido ao client (lista `grants`) para o fluxo funcionar.
147
+ */
148
+ export interface DeviceFlowConfigInput {
149
+ /** Liga o Device Authorization Grant. Default: false. */
150
+ enabled: boolean;
151
+ }
152
+ export interface ResolvedDeviceFlowConfig {
153
+ enabled: boolean;
154
+ }
155
+ export declare function resolveDeviceFlow(input?: DeviceFlowConfigInput): ResolvedDeviceFlowConfig;
156
+ /**
157
+ * DPoP — Demonstrating Proof of Possession (RFC 9449). Quando habilitado, o
158
+ * oidc-provider aceita DPoP proofs e emite tokens sender-constrained
159
+ * (`token_type: DPoP`, com `cnf.jkt`). A discovery passa a anunciar
160
+ * `dpop_signing_alg_values_supported`. Os resolvers do authkit-client aceitam o
161
+ * token via introspecção (a cnf viaja no resultado) — a geração de provas DPoP no
162
+ * client está fora de escopo (documentado como trabalho futuro).
163
+ */
164
+ export interface DpopConfigInput {
165
+ /** Liga o DPoP. Default: false. */
166
+ enabled: boolean;
167
+ }
168
+ export interface ResolvedDpopConfig {
169
+ enabled: boolean;
170
+ }
171
+ export declare function resolveDpop(input?: DpopConfigInput): ResolvedDpopConfig;
172
+ /**
173
+ * PAR — Pushed Authorization Requests (RFC 9126). Quando habilitado, o
174
+ * oidc-provider expõe o `pushed_authorization_request_endpoint` (`/request`): o
175
+ * client POSTa os parâmetros de authorize e recebe um `request_uri` opaco para
176
+ * usar no `/auth`. Com `requirePushedAuthorizationRequests`, o `/auth` SÓ aceita
177
+ * requests via `request_uri` (parâmetros inline são rejeitados).
178
+ */
179
+ export interface ParConfigInput {
180
+ /** Liga o PAR. Default: false. */
181
+ enabled: boolean;
182
+ /** Exige que TODO authorize venha via request_uri do PAR. Default: false. */
183
+ requirePushedAuthorizationRequests?: boolean;
184
+ }
185
+ export interface ResolvedParConfig {
186
+ enabled: boolean;
187
+ requirePushedAuthorizationRequests: boolean;
188
+ }
189
+ export declare function resolvePar(input?: ParConfigInput): ResolvedParConfig;
190
+ /**
191
+ * Step-up authentication via `acr_values` (MVP pragmático de MFA por requisição).
192
+ * Quando o client solicita `acr_values` contendo `mfaAcr`, o login EXIGE o 2º
193
+ * fator: contas com MFA enrolado passam pelo desafio (o `acr` do id_token vira
194
+ * `mfaAcr` e `amr` recebe `['mfa', método]`); contas SEM MFA enrolado têm o login
195
+ * bloqueado naquela requisição com a instrução de configurar MFA no console.
196
+ */
197
+ export interface StepUpConfigInput {
198
+ /** Lista de acr_values anunciados como suportados (discovery). */
199
+ acrValues?: string[];
200
+ /** O acr que dispara a exigência de MFA. Default: 'urn:authkit:mfa'. */
201
+ mfaAcr?: string;
202
+ }
203
+ export interface ResolvedStepUpConfig {
204
+ acrValues: string[];
205
+ mfaAcr: string;
206
+ }
207
+ export declare function resolveStepUp(input?: StepUpConfigInput): ResolvedStepUpConfig;
126
208
  /**
127
209
  * Console admin opt-in do IdP (B6). Quando habilitado, monta o grupo `/admin/*`
128
210
  * (dashboard, usuários/papéis, clients, audit) atrás de um guard que exige sessão
@@ -193,6 +275,8 @@ export interface AuthServerConfigInput {
193
275
  rateLimit?: RateLimitConfigInput;
194
276
  /** Bloqueio progressivo de conta por email em falhas repetidas. Default: ligado (no-op sem limiter). */
195
277
  lockout?: LockoutConfigInput;
278
+ /** Notificações de segurança por e-mail (alerta de novo acesso). Default: ligado. */
279
+ notifications?: NotificationsConfigInput;
196
280
  /** Hooks de e-mail (reset de senha / verificação). Opcional — fallback de log em dev. */
197
281
  mail?: MailHooks;
198
282
  /** Sink de auditoria (best-effort). Opcional — quando ausente, auditoria é no-op. */
@@ -213,6 +297,14 @@ export interface AuthServerConfigInput {
213
297
  * os clients registrados são persistidos pelo mesmo adapter OIDC.
214
298
  */
215
299
  dynamicRegistration?: DynamicRegistrationConfigInput;
300
+ /** Device Authorization Grant (RFC 8628). Default: desligado. */
301
+ deviceFlow?: DeviceFlowConfigInput;
302
+ /** DPoP — sender-constrained tokens (RFC 9449). Default: desligado. */
303
+ dpop?: DpopConfigInput;
304
+ /** Pushed Authorization Requests (RFC 9126). Default: desligado. */
305
+ par?: ParConfigInput;
306
+ /** Step-up auth via acr_values (MFA por requisição). Default: vazio (só o mfaAcr derivado). */
307
+ stepUp?: StepUpConfigInput;
216
308
  /**
217
309
  * Console admin do IdP (B6). Default: desligado. Quando ligado, o host também
218
310
  * deve passar `admin: true` em {@link AuthHostOptions} no registro de rotas
@@ -250,12 +342,22 @@ export interface ResolvedServerConfig {
250
342
  rateLimit: ResolvedRateLimitConfig;
251
343
  /** Bloqueio progressivo de conta resolvido (sempre presente; default ligado). */
252
344
  lockout: ResolvedLockoutConfig;
345
+ /** Notificações de segurança resolvidas (sempre presente; default ligado). */
346
+ notifications: ResolvedNotificationsConfig;
253
347
  mail?: MailHooks;
254
348
  audit?: AuditSink;
255
349
  mfaIssuer: string;
256
350
  /** RP de WebAuthn resolvido (sempre presente; derivado do issuer por default). */
257
351
  webauthn: ResolvedWebauthnConfig;
258
352
  dynamicRegistration: ResolvedDynamicRegistrationConfig;
353
+ /** Device Authorization Grant resolvido (default desligado). */
354
+ deviceFlow: ResolvedDeviceFlowConfig;
355
+ /** DPoP resolvido (default desligado). */
356
+ dpop: ResolvedDpopConfig;
357
+ /** PAR resolvido (default desligado). */
358
+ par: ResolvedParConfig;
359
+ /** Step-up auth resolvido (mfaAcr sempre presente). */
360
+ stepUp: ResolvedStepUpConfig;
259
361
  /** Console admin resolvido (sempre presente; default desligado). */
260
362
  admin: ResolvedAdminConfig;
261
363
  /** Catálogo de mensagens ativo (locale resolvido), pronto para os renderers. */