@adonis-agora/authkit-server 0.57.0 → 0.58.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 (34) hide show
  1. package/README.md +4 -4
  2. package/build/index.d.ts +2 -2
  3. package/build/index.js +1 -1
  4. package/build/providers/authkit_server_provider.d.ts +1 -12
  5. package/build/providers/authkit_server_provider.js +13 -0
  6. package/build/src/audit/audit_sink.d.ts +11 -2
  7. package/build/src/audit/audit_sink.js +104 -1
  8. package/build/src/define_config.d.ts +71 -9
  9. package/build/src/define_config.js +3 -0
  10. package/build/src/doctor/checks.d.ts +15 -3
  11. package/build/src/doctor/checks.js +43 -3
  12. package/build/src/host/admin_api/admin_orgs_service.d.ts +9 -2
  13. package/build/src/host/admin_api/admin_orgs_service.js +29 -16
  14. package/build/src/host/admin_api/api_orgs_controller.js +1 -1
  15. package/build/src/host/admin_api/dto.d.ts +1 -1
  16. package/build/src/host/admin_console/console_impersonation_controller.js +37 -3
  17. package/build/src/host/admin_console/console_orgs_controller.js +1 -1
  18. package/build/src/host/branding.d.ts +19 -0
  19. package/build/src/host/branding.js +25 -0
  20. package/build/src/host/config_locks.js +1 -1
  21. package/build/src/host/controllers/account_orgs_controller.js +21 -14
  22. package/build/src/host/default_mailer.d.ts +21 -0
  23. package/build/src/host/default_mailer.js +41 -0
  24. package/build/src/host/i18n.d.ts +10 -0
  25. package/build/src/host/i18n.js +15 -2
  26. package/build/src/host/impersonation.d.ts +23 -2
  27. package/build/src/host/impersonation.js +21 -5
  28. package/build/src/host/register_auth_host.d.ts +27 -6
  29. package/build/src/host/ui-dist/assets/{index-pAbBjdHC.js → index-6cE5JMyP.js} +1 -1
  30. package/build/src/host/ui-dist/index.html +1 -1
  31. package/build/src/observability/telescope/data_providers.js +4 -1
  32. package/build/src/provider/oidc_service.js +18 -6
  33. package/build/types.d.ts +21 -1
  34. package/package.json +6 -6
package/README.md CHANGED
@@ -31,7 +31,7 @@ node ace add @adonis-agora/authkit-server
31
31
  # ou: pnpm add @adonis-agora/authkit-server && node ace configure @adonis-agora/authkit-server
32
32
  ```
33
33
 
34
- O `configure` publica `config/authkit_server.ts`, o model `app/models/auth_user.ts`,
34
+ O `configure` publica `config/authkit.ts`, o model `app/models/auth_user.ts`,
35
35
  o controller de interactions (`app/controllers/auth_interaction_controller.ts`) e
36
36
  registra o provider.
37
37
 
@@ -89,7 +89,7 @@ Em todos os presets o controller ejetado é **casca**: a lógica vive em
89
89
  `details(ctx)`, `login(ctx, { email, password })` e `consent(ctx)`. Você edita só a parte de
90
90
  render/redirect.
91
91
 
92
- Quem decide se as credenciais valem é o `verifyCredentials` do `config/authkit_server.ts`
92
+ Quem decide se as credenciais valem é o `verifyCredentials` do `config/authkit.ts`
93
93
  — é o que o `service.interactions.login` chama. O default consulta o `AuthUser` por e-mail e
94
94
  usa `verifyPassword`; sobrescreva para plugar sua própria base de usuários.
95
95
 
@@ -105,7 +105,7 @@ router.post('/auth/interaction/:uid/consent', [AuthInteractionController, 'conse
105
105
 
106
106
  ## Persistência
107
107
 
108
- Escolha o backend no `config/authkit_server.ts`:
108
+ Escolha o backend no `config/authkit.ts`:
109
109
  - `adapters.redis({ connection })` — requer `@adonisjs/redis` configurado.
110
110
  - `adapters.database({ connection? })` — Lucid; rode a migração `authkit_oidc_payloads`.
111
111
 
@@ -116,7 +116,7 @@ de resolve) e as expõe de forma opt-in.
116
116
 
117
117
  ### Configuração
118
118
 
119
- No `config/authkit_server.ts`, use a chave `observability`:
119
+ No `config/authkit.ts`, use a chave `observability`:
120
120
 
121
121
  ```ts
122
122
  observability: {
package/build/index.d.ts CHANGED
@@ -49,7 +49,7 @@ export type { AuthkitScreen } from './src/host/renderers/inertia_renderer.js';
49
49
  export type { InertiaRendererOptions } from './src/host/renderers/inertia_renderer.js';
50
50
  export type { AccountLoginProps, AccountSecurityProps, AccountMfaProps, AccountConfirmProps, AccountConfirmMethod, AccountEmailConfirmedProps, } from './src/host/account_screen_props.js';
51
51
  export { edgeRenderer } from './src/host/renderers/edge_renderer.js';
52
- export { brandFor, isFirstParty } from './src/host/branding.js';
52
+ export { brandFor, isFirstParty, isFirstPartyClient } from './src/host/branding.js';
53
53
  export type { BrandingConfig, ClientBrand } from './src/host/branding.js';
54
54
  export { resolveMessages, translate, DEFAULT_MESSAGES, PT_BR_MESSAGES, BUILTIN_MESSAGES, DEFAULT_LOCALE, } from './src/host/i18n.js';
55
55
  export type { I18nConfig, AuthMessages } from './src/host/i18n.js';
@@ -105,7 +105,7 @@ export { computeAdminStats } from './src/host/admin_stats_service.js';
105
105
  export type { AdminStats, DailyPoint } from './src/host/admin_stats_service.js';
106
106
  export { barChartSvg } from './src/host/svg_chart.js';
107
107
  export { buildImpersonationPanel } from './src/host/impersonation.js';
108
- export type { ImpersonationPanel } from './src/host/impersonation.js';
108
+ export type { ImpersonationPanel, ImpersonationClientLike } from './src/host/impersonation.js';
109
109
  export { ACTIVE_ORG_COOKIE, ACTIVE_ORG_COOKIE_TTL, encodeActiveOrgCookie, decodeActiveOrgCookie, readActiveOrgFromKoaCtx, } from './src/host/active_org_cookie.js';
110
110
  export { resolveOrganizations } from './src/define_config.js';
111
111
  export type { OrganizationsConfigInput, ResolvedOrganizationsConfig, } from './src/define_config.js';
package/build/index.js CHANGED
@@ -30,7 +30,7 @@ export { withAuditLog } from './src/mixins/with_audit_log.js';
30
30
  export { composeAuditSink, resolveEvents, buildWebhookBody, signWebhookBody, } from './src/events/dispatcher.js';
31
31
  export { inertiaRenderer } from './src/host/renderers/inertia_renderer.js';
32
32
  export { edgeRenderer } from './src/host/renderers/edge_renderer.js';
33
- export { brandFor, isFirstParty } from './src/host/branding.js';
33
+ export { brandFor, isFirstParty, isFirstPartyClient } from './src/host/branding.js';
34
34
  export { resolveMessages, translate, DEFAULT_MESSAGES, PT_BR_MESSAGES, BUILTIN_MESSAGES, DEFAULT_LOCALE, } from './src/host/i18n.js';
35
35
  export { registerAuthHost } from './src/host/register_auth_host.js';
36
36
  export { POLICY_ROUTE_OPTIONS } from './src/host/config_locks.js';
@@ -1,16 +1,5 @@
1
- import type { MetricsRecorder } from '@adonis-agora/authkit-core';
1
+ import '../types.js';
2
2
  import type { ApplicationService } from '@adonisjs/core/types';
3
- import type { AccountStore } from '../src/accounts/account_store.js';
4
- import type { PatStore } from '../src/pat/pat_store.js';
5
- import { OidcService } from '../src/provider/oidc_service.js';
6
- declare module '@adonisjs/core/types' {
7
- interface ContainerBindings {
8
- 'authkit.server': OidcService;
9
- 'authkit.metrics': MetricsRecorder;
10
- 'authkit.accountStore': AccountStore;
11
- 'authkit.patStore': PatStore;
12
- }
13
- }
14
3
  export default class AuthkitServerProvider {
15
4
  #private;
16
5
  protected app: ApplicationService;
@@ -1,3 +1,16 @@
1
+ // Side-effect import, and it is load-bearing: the `ContainerBindings`
2
+ // augmentation for this package's four bindings lives in the root `types.ts`,
3
+ // and this is the module that carries it into a host app's declaration graph —
4
+ // every host registers this provider in `adonisrc.ts`.
5
+ //
6
+ // The declaration used to be duplicated here as well. One copy typed the hosts
7
+ // and the other backed the `./types` subpath, and the two had drifted: this one
8
+ // declared four bindings, `types.ts` declared one. There is a single table now.
9
+ //
10
+ // It must be a bare side-effect import — the `import type {} from` form is erased
11
+ // from the emitted `.d.ts` and would drop the augmentation. `build/types.js`
12
+ // compiles down to `export {}`, so the runtime cost is nil.
13
+ import '../types.js';
1
14
  import { existsSync } from 'node:fs';
2
15
  import { fileURLToPath } from 'node:url';
3
16
  import { configProvider } from '@adonisjs/core';
@@ -1,7 +1,16 @@
1
1
  /**
2
- * Tipos de eventos de auditoria relevantes para segurança emitidos pelo IdP.
2
+ * Tipos de eventos de auditoria emitidos pelo IdP, como VALOR em runtime.
3
+ *
4
+ * A união {@link AuditEventType} é DERIVADA desta lista, e não escrita à mão,
5
+ * porque uma união escrita à mão não tem como ser conferida contra o que o
6
+ * código de fato emite — foi assim que cinco eventos passaram a existir sem
7
+ * estar no tipo. Com a lista em runtime, `tests/audit/audit_event_drift.spec.ts`
8
+ * varre os `audit.record({ type: ... })` do `src/` e falha quando um evento
9
+ * emitido não está aqui.
3
10
  */
4
- export type AuditEventType = 'login.success' | 'login.failure' | 'signup' | 'password_reset.issued' | 'password_reset.consumed' | 'pat.issued' | 'pat.revoked' | 'pat.used' | 'impersonation' | 'impersonation.started' | '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' | 'password.rehashed' | 'email.change_requested' | 'email.changed' | 'login.new_ip_notified' | 'login.new_device' | 'login.otp_sent' | 'login.otp_verified' | 'login.otp_failed' | 'login.otp_invalidated' | 'bot_protection.rejected' | 'grant.revoked_by_user' | 'user.created' | 'user.password_reset_sent' | 'user.disabled' | 'user.enabled' | 'user.deleted' | 'profile.updated' | 'account.deleted' | 'account.exported' | 'keys.rotated' | 'organization.created' | 'organization.updated' | 'organization.deleted' | 'organization.member_added' | 'organization.member_removed' | 'organization.member_role_changed' | 'organization.member_role_updated' | 'organization.switched' | 'organization.deactivated' | 'organization.invitation_sent' | 'organization.invitation_accepted' | 'organization.invitation_revoked' | 'email_change.requested' | 'email_change.confirmed' | 'email_change.cancelled' | 'security_notice.sent' | 'settings.updated' | 'maintenance.enabled' | 'maintenance.disabled' | 'trusted_device.revoked' | 'password.expired_change_forced' | 'otp.locked' | 'otp.unlocked' | 'otp.unlock_failed' | 'sudo.confirmed' | 'session.single_enforced' | 'account.expired_login_blocked' | 'account.expiration_warned';
11
+ export declare const AUDIT_EVENT_TYPES: readonly ["login.success", "login.failure", "signup", "password_reset.issued", "password_reset.consumed", "pat.issued", "pat.revoked", "pat.used", "impersonation", "impersonation.panel_viewed", "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", "password.rehashed", "email.change_requested", "email.changed", "login.new_ip_notified", "login.new_device", "login.otp_sent", "login.otp_verified", "login.otp_failed", "login.otp_invalidated", "bot_protection.rejected", "grant.revoked_by_user", "user.created", "user.password_reset_sent", "user.disabled", "user.enabled", "user.deleted", "profile.updated", "account.deleted", "account.exported", "keys.rotated", "organization.created", "organization.updated", "organization.deleted", "organization.member_added", "organization.member_removed", "organization.member_role_changed", "organization.member_role_updated", "organization.switched", "organization.deactivated", "organization.invitation_sent", "organization.invitation_accepted", "organization.invitation_revoked", "email_change.requested", "email_change.confirmed", "email_change.cancelled", "security_notice.sent", "settings.updated", "maintenance.enabled", "maintenance.disabled", "trusted_device.revoked", "password.expired_change_forced", "otp.locked", "otp.unlocked", "otp.unlock_failed", "sudo.confirmed", "session.single_enforced", "account.expired_login_blocked", "account.expiration_warned", "login.magic_link_sent", "session.revoked", "account.signed_out_all", "client.secret_regenerated", "roles_catalog.updated"];
12
+ /** Tipos de eventos de auditoria relevantes para segurança emitidos pelo IdP. */
13
+ export type AuditEventType = (typeof AUDIT_EVENT_TYPES)[number];
5
14
  /**
6
15
  * Evento de auditoria a registrar. O timestamp é definido pelo sink (não aqui).
7
16
  */
@@ -1 +1,104 @@
1
- export {};
1
+ /**
2
+ * Tipos de eventos de auditoria emitidos pelo IdP, como VALOR em runtime.
3
+ *
4
+ * A união {@link AuditEventType} é DERIVADA desta lista, e não escrita à mão,
5
+ * porque uma união escrita à mão não tem como ser conferida contra o que o
6
+ * código de fato emite — foi assim que cinco eventos passaram a existir sem
7
+ * estar no tipo. Com a lista em runtime, `tests/audit/audit_event_drift.spec.ts`
8
+ * varre os `audit.record({ type: ... })` do `src/` e falha quando um evento
9
+ * emitido não está aqui.
10
+ */
11
+ export const AUDIT_EVENT_TYPES = [
12
+ 'login.success',
13
+ 'login.failure',
14
+ 'signup',
15
+ 'password_reset.issued',
16
+ 'password_reset.consumed',
17
+ 'pat.issued',
18
+ 'pat.revoked',
19
+ 'pat.used',
20
+ // O exchange RFC 8693 de fato aconteceu: uma identidade foi assumida.
21
+ 'impersonation',
22
+ // O admin abriu o painel do console e revelou os parâmetros do exchange.
23
+ // NÃO é uma impersonação: nada foi assumido ainda, e o exchange pode nunca
24
+ // acontecer. Manter os dois separados é o que impede a trilha de auditoria de
25
+ // afirmar uma impersonação que só foi consultada.
26
+ 'impersonation.panel_viewed',
27
+ 'mfa.enabled',
28
+ 'mfa.disabled',
29
+ 'account.locked',
30
+ 'passkey.registered',
31
+ 'passkey.removed',
32
+ 'email_verification.issued',
33
+ 'email_verification.consumed',
34
+ 'client.created',
35
+ 'client.updated',
36
+ 'client.deleted',
37
+ 'session.revoked_all',
38
+ 'password.changed',
39
+ 'password.rehashed',
40
+ 'email.change_requested',
41
+ 'email.changed',
42
+ 'login.new_ip_notified',
43
+ 'login.new_device',
44
+ // Login por OTP (código digitável)
45
+ 'login.otp_sent',
46
+ 'login.otp_verified',
47
+ 'login.otp_failed',
48
+ 'login.otp_invalidated',
49
+ 'bot_protection.rejected',
50
+ 'grant.revoked_by_user',
51
+ 'user.created',
52
+ 'user.password_reset_sent',
53
+ 'user.disabled',
54
+ 'user.enabled',
55
+ 'user.deleted',
56
+ 'profile.updated',
57
+ 'account.deleted',
58
+ 'account.exported',
59
+ 'keys.rotated',
60
+ 'organization.created',
61
+ 'organization.updated',
62
+ 'organization.deleted',
63
+ 'organization.member_added',
64
+ 'organization.member_removed',
65
+ 'organization.member_role_changed',
66
+ 'organization.member_role_updated',
67
+ 'organization.switched',
68
+ 'organization.deactivated',
69
+ 'organization.invitation_sent',
70
+ 'organization.invitation_accepted',
71
+ 'organization.invitation_revoked',
72
+ // Email change (verified flow)
73
+ 'email_change.requested',
74
+ 'email_change.confirmed',
75
+ 'email_change.cancelled',
76
+ // Security notices
77
+ 'security_notice.sent',
78
+ // Settings
79
+ 'settings.updated',
80
+ 'maintenance.enabled',
81
+ 'maintenance.disabled',
82
+ 'trusted_device.revoked',
83
+ // Password hygiene
84
+ 'password.expired_change_forced',
85
+ // OTP lockout
86
+ 'otp.locked',
87
+ 'otp.unlocked',
88
+ 'otp.unlock_failed',
89
+ // Sudo mode
90
+ 'sudo.confirmed',
91
+ // Session
92
+ 'session.single_enforced',
93
+ // Account expiration
94
+ 'account.expired_login_blocked',
95
+ 'account.expiration_warned',
96
+ // Emitidos pelo host-kit e antes AUSENTES desta lista — o switch exaustivo
97
+ // de um consumidor os perdia silenciosamente. Ver `audit_event_drift.spec.ts`,
98
+ // que varre os call sites e falha se a divergência voltar.
99
+ 'login.magic_link_sent',
100
+ 'session.revoked',
101
+ 'account.signed_out_all',
102
+ 'client.secret_regenerated',
103
+ 'roles_catalog.updated',
104
+ ];
@@ -87,7 +87,14 @@ export interface MailHooks {
87
87
  userAgent?: string | null;
88
88
  timestamp: string;
89
89
  }) => Promise<void>;
90
- /** Disparado ao criar um convite de organização. */
90
+ /**
91
+ * Disparado ao criar um convite de organização.
92
+ *
93
+ * OPTIONAL, like every other mail hook here: without it the host-kit itself
94
+ * sends the invitation email through the default mailer (`@adonisjs/mail`),
95
+ * branded and translated. Best-effort — a delivery failure never breaks the
96
+ * invitation.
97
+ */
91
98
  onOrgInvitation?: (data: {
92
99
  email: string;
93
100
  invitationId: string;
@@ -131,6 +138,19 @@ export interface MailHooks {
131
138
  unlockUrl: string;
132
139
  token: string;
133
140
  }) => Promise<void>;
141
+ /**
142
+ * Disparado pelo `authkit:expire-scan` quando uma conta está prestes a ser
143
+ * desativada por inatividade. Substitui o e-mail default de aviso quando
144
+ * fornecido. Best-effort, fire-and-forget; deduplicado pela janela de
145
+ * `warnDays` da setting `account_expiration`, então cada conta recebe no
146
+ * máximo um aviso por janela.
147
+ */
148
+ onAccountExpirationWarning?: (data: {
149
+ /** E-mail da conta que será desativada. */
150
+ email: string;
151
+ /** Dias restantes até a desativação. */
152
+ expiresInDays: number;
153
+ }) => Promise<void>;
134
154
  /**
135
155
  * Disparado após um evento de segurança (senha alterada, MFA habilitado/desabilitado,
136
156
  * passkey adicionada/removida, e-mail alterado). Substitui o e-mail default quando
@@ -776,6 +796,28 @@ export interface AuthServerConfigInput {
776
796
  interactionRecovery?: InteractionRecoveryConfigInput;
777
797
  /** Configuração de branding por cliente. */
778
798
  branding?: BrandingConfig;
799
+ /**
800
+ * Allowlist dos clients FIRST-PARTY: quem recebe as claims de AUTORIZAÇÃO
801
+ * (`<globalRolesClaim>`, `org_id`, `org_slug`, `org_role`) nos tokens.
802
+ *
803
+ * Ausente (default) → TODO client registrado é first-party. Isso NÃO é
804
+ * "qualquer um vê as roles": a claim está amarrada ao escopo `roles` (ver
805
+ * `claims` em `provider/build_provider.ts`), então o client precisa (a) ter
806
+ * sido registrado por um admin e (b) pedir `scope=roles`. A allowlist é uma
807
+ * terceira barreira, opcional, para quem hospeda clients de terceiros.
808
+ *
809
+ * Declarada → só os clients listados recebem; os demais nunca, nem pedindo
810
+ * `scope=roles`.
811
+ *
812
+ * Por que existe separado de `branding.firstParty`: aquele campo vive num
813
+ * bloco de TEMA. Quem não personalizava a aparência não declarava `branding`,
814
+ * e o gate lia isso como "ninguém é first-party" — todo RP recebia
815
+ * `globalRoles: []` enquanto o console admin seguia funcionando (ele lê roles
816
+ * da sessão, não do token). Uma decisão de autorização não pode depender de o
817
+ * host ter ou não escolhido uma cor. `branding.firstParty` continua sendo lido
818
+ * como fallback para quem já o declarou.
819
+ */
820
+ firstPartyClients?: string[];
779
821
  /** Internacionalização das telas. Default: pt-BR embutido (zero config). */
780
822
  i18n?: I18nConfig;
781
823
  /** Configuração de providers sociais. */
@@ -888,15 +930,21 @@ export interface AuthServerConfigInput {
888
930
  */
889
931
  accessTokens?: AccessTokensConfig;
890
932
  /**
891
- * Console admin do IdP (B6). Default: desligado. Quando ligado, o host também
892
- * deve passar `admin: true` em {@link AuthHostOptions} no registro de rotas
893
- * (a montagem das rotas acontece antes do config resolver).
933
+ * Console admin do IdP (B6). Default: desligado.
934
+ *
935
+ * Declarar esta chave TRAVA o liga/desliga: `registerAuthHost(router, { admin })`
936
+ * passa a ser ignorado (ver `deriveLockedRouteOptions`). Não repita o
937
+ * `enabled` no `start/routes.ts` — no máximo passe `admin: { prefix }`, que é
938
+ * estrutural e continua valendo.
894
939
  */
895
940
  admin?: AdminConfigInput;
896
941
  /**
897
- * Admin REST API (R6). Default: desligada. Quando ligada, o host também deve
898
- * passar `adminApi: true` em {@link AuthHostOptions} no registro de rotas (a
899
- * montagem das rotas acontece antes do config resolver). Autenticação por API key.
942
+ * Admin REST API (R6). Default: desligada. Autenticação por API key.
943
+ *
944
+ * Declarar esta chave TRAVA o liga/desliga: `registerAuthHost(router, { adminApi })`
945
+ * passa a ser ignorado (ver `deriveLockedRouteOptions`). Não repita o
946
+ * `enabled` no `start/routes.ts` — no máximo passe `adminApi: { prefix }`, que
947
+ * é estrutural e continua valendo.
900
948
  */
901
949
  adminApi?: AdminApiConfigInput;
902
950
  /**
@@ -913,8 +961,16 @@ export interface AuthServerConfigInput {
913
961
  */
914
962
  resolveGeo?: ResolveGeo;
915
963
  /**
916
- * Gestão automática do schema das tabelas do authkit (`authkit_oidc_payloads`,
917
- * `auth_settings`, `auth_password_history` e as três de organizations).
964
+ * Gestão automática do schema das OITO tabelas do authkit:
965
+ * `authkit_oidc_payloads`, `auth_settings`, `auth_password_history`,
966
+ * `auth_mfa`, `auth_session_revocations` e as três de organizations
967
+ * (`auth_organizations`, `auth_organization_members`,
968
+ * `auth_organization_invitations`). Ver `TABLES` em `schema/ensure.ts` — a
969
+ * lista aqui existe para o leitor, mas quem manda é aquele array.
970
+ *
971
+ * FORA desta gestão: `authkit_keystore`, criada sob demanda pelo
972
+ * `LucidKeystoreVault` na primeira escrita, e as tabelas do MODEL do host
973
+ * (`auth_users` etc.), que são migrations do app.
918
974
  *
919
975
  * - `autoManage` (default `true`): no boot, cria as tabelas que faltam e
920
976
  * adiciona colunas novas (aditivo — nunca dropa nem altera tipos).
@@ -1011,6 +1067,12 @@ export interface ResolvedServerConfig {
1011
1067
  /** Recuperação de sessão de interaction perdida resolvida (default `{ mode: 'screen' }`). */
1012
1068
  interactionRecovery: ResolvedInteractionRecoveryConfig;
1013
1069
  branding?: BrandingConfig;
1070
+ /**
1071
+ * Allowlist resolvida de clients first-party, ou `undefined` quando o host não
1072
+ * declarou nenhuma — caso em que todo client registrado é first-party. Ver
1073
+ * {@link AuthServerConfigInput.firstPartyClients}.
1074
+ */
1075
+ firstPartyClients?: string[];
1014
1076
  social?: AuthSocialConfig;
1015
1077
  patIntrospectionSecret?: string;
1016
1078
  rateLimit: ResolvedRateLimitConfig;
@@ -360,6 +360,9 @@ export function defineConfig(config) {
360
360
  redirectTo: config.interactionRecovery?.redirectTo,
361
361
  },
362
362
  branding: config.branding,
363
+ // `firstPartyClients` manda; `branding.firstParty` é o fallback de
364
+ // back-compat. Nenhum dos dois declarado → `undefined` = sem allowlist.
365
+ firstPartyClients: config.firstPartyClients ?? config.branding?.firstParty,
363
366
  social: config.social,
364
367
  patIntrospectionSecret: config.patIntrospectionSecret,
365
368
  rateLimit: resolveRateLimit(config.rateLimit),
@@ -138,9 +138,11 @@ export declare function checkEmailChange(input: DoctorInput): Finding | null;
138
138
  */
139
139
  export declare function checkSecurityNotifications(input: DoctorInput): Finding | null;
140
140
  /**
141
- * Verifica a capability de histórico de senhas.
142
- * - Informa se a tabela `auth_password_history` está presente.
143
- * - Avisa quando pepper está configurado como string (não array) rotation recomendada.
141
+ * Verifica o pepper de senha (`accountStore.password.pepper`).
142
+ * - Silencioso quando não pepper configurado.
143
+ * - Reporta o pepper como string e sugere a forma `[novo, antigo]`, que permite
144
+ * rotação sem downtime (lazy re-hash no verify).
145
+ * - Reporta quantos peppers a rotação tem quando já é array.
144
146
  */
145
147
  export declare function checkPasswordPepper(input: DoctorInput): Finding | null;
146
148
  /**
@@ -186,6 +188,16 @@ export declare function checkAccountExpiration(input: DoctorInput): Finding | nu
186
188
  * Informativo: se webauthn não estiver configurado, o autofill nunca será exibido.
187
189
  */
188
190
  export declare function checkPasskeyAutofill(input: DoctorInput): Finding | null;
191
+ /**
192
+ * Allowlist de clients first-party — quem recebe as claims de autorização
193
+ * (`<globalRolesClaim>`, `org_id`, `org_slug`, `org_role`).
194
+ *
195
+ * Informativa quando declarada. `warn` quando NÃO declarada, porque aí todo
196
+ * client registrado recebe as claims ao pedir `scope=roles` — o que é o default
197
+ * certo (o oposto apagava as roles de todo RP), mas um host que registra clients
198
+ * de TERCEIROS precisa saber que existe uma lista para fechar.
199
+ */
200
+ export declare function checkFirstPartyClients(input: DoctorInput): Finding | null;
189
201
  /**
190
202
  * Finding da idade da chave de assinatura managed. `ageDays === null` (sem
191
203
  * keystore em arquivo/cofre) → no-op `ok`. Acima de `maxAgeDays` → `warn`.
@@ -573,9 +573,11 @@ export function checkSecurityNotifications(input) {
573
573
  return null; // Silencioso quando tudo está ok (não-obrigatório).
574
574
  }
575
575
  /**
576
- * Verifica a capability de histórico de senhas.
577
- * - Informa se a tabela `auth_password_history` está presente.
578
- * - Avisa quando pepper está configurado como string (não array) rotation recomendada.
576
+ * Verifica o pepper de senha (`accountStore.password.pepper`).
577
+ * - Silencioso quando não pepper configurado.
578
+ * - Reporta o pepper como string e sugere a forma `[novo, antigo]`, que permite
579
+ * rotação sem downtime (lazy re-hash no verify).
580
+ * - Reporta quantos peppers a rotação tem quando já é array.
579
581
  */
580
582
  export function checkPasswordPepper(input) {
581
583
  const store = input.authkitConfig?.accountStore;
@@ -780,6 +782,41 @@ export function checkPasskeyAutofill(input) {
780
782
  'Requires browsers with PublicKeyCredential.isConditionalMediationAvailable() support.',
781
783
  };
782
784
  }
785
+ /**
786
+ * Allowlist de clients first-party — quem recebe as claims de autorização
787
+ * (`<globalRolesClaim>`, `org_id`, `org_slug`, `org_role`).
788
+ *
789
+ * Informativa quando declarada. `warn` quando NÃO declarada, porque aí todo
790
+ * client registrado recebe as claims ao pedir `scope=roles` — o que é o default
791
+ * certo (o oposto apagava as roles de todo RP), mas um host que registra clients
792
+ * de TERCEIROS precisa saber que existe uma lista para fechar.
793
+ */
794
+ export function checkFirstPartyClients(input) {
795
+ const cfg = input.authkitConfig;
796
+ if (!cfg)
797
+ return null;
798
+ const allowlist = cfg.firstPartyClients;
799
+ if (allowlist === undefined) {
800
+ return {
801
+ level: 'warn',
802
+ message: 'firstPartyClients is not declared: every registered client that requests `scope=roles` ' +
803
+ 'receives the roles and organization claims. That is the default on purpose — the claims ' +
804
+ 'are already gated by client registration and by the requested scope. Declare ' +
805
+ '`firstPartyClients: [...]` in config/authkit.ts to restrict them to your own apps.',
806
+ };
807
+ }
808
+ if (allowlist.length === 0) {
809
+ return {
810
+ level: 'ok',
811
+ message: 'firstPartyClients is an empty list: NO client receives the roles/organization claims. ' +
812
+ 'Remove the key entirely if you meant "no restriction".',
813
+ };
814
+ }
815
+ return {
816
+ level: 'ok',
817
+ message: `firstPartyClients: ${allowlist.length} client(s) receive the roles/organization claims (${allowlist.join(', ')}).`,
818
+ };
819
+ }
783
820
  /**
784
821
  * Finding da idade da chave de assinatura managed. `ageDays === null` (sem
785
822
  * keystore em arquivo/cofre) → no-op `ok`. Acima de `maxAgeDays` → `warn`.
@@ -875,6 +912,9 @@ export function runAllChecks(input) {
875
912
  const passkeyAutofill = checkPasskeyAutofill(input);
876
913
  if (passkeyAutofill)
877
914
  findings.push(passkeyAutofill);
915
+ const firstPartyClients = checkFirstPartyClients(input);
916
+ if (firstPartyClients)
917
+ findings.push(firstPartyClients);
878
918
  return findings;
879
919
  }
880
920
  /** Há algum finding de nível 'error'? (define o exit code). */
@@ -1,3 +1,4 @@
1
+ import type { HttpContext } from '@adonisjs/core/http';
1
2
  import type { OrgInvitation, OrgMember, OrgSummary } from '../../accounts/account_store.js';
2
3
  import type { ResolvedServerConfig } from '../../define_config.js';
3
4
  import type { SettingsCapability } from '../runtime_settings.js';
@@ -109,8 +110,14 @@ export declare class AdminOrgsService {
109
110
  ok: false;
110
111
  reason: 'member_not_found';
111
112
  }>;
112
- /** Cria um convite por e-mail. Dispara o mail hook quando configurado. */
113
- createInvitation(orgId: string, input: CreateInvitationInput, actor: AdminActor, origin: string, settings?: SettingsCapability | null): Promise<{
113
+ /**
114
+ * Cria um convite por e-mail. Dispara o mail hook quando configurado.
115
+ *
116
+ * `ctx` is optional only for back-compat with callers that have no request in
117
+ * hand; when given (both HTTP call sites do), the library default invitation
118
+ * email is sent whenever the host defines no `mail.onOrgInvitation` hook.
119
+ */
120
+ createInvitation(orgId: string, input: CreateInvitationInput, actor: AdminActor, origin: string, settings?: SettingsCapability | null, ctx?: HttpContext): Promise<{
114
121
  ok: true;
115
122
  invitation: OrgInvitation;
116
123
  token: string;
@@ -1,5 +1,6 @@
1
1
  import { supportsOrganizations } from '../../accounts/account_store.js';
2
2
  import { accountPath } from '../account_paths.js';
3
+ import { sendOrgInvitationEmail } from '../default_mailer.js';
3
4
  import { isRoleInCatalog, resolveRoleCatalogList } from '../runtime_toggles.js';
4
5
  /**
5
6
  * Lógica de gestão de organizações compartilhada entre o console admin (HTML)
@@ -247,8 +248,14 @@ export class AdminOrgsService {
247
248
  });
248
249
  return { ok: true };
249
250
  }
250
- /** Cria um convite por e-mail. Dispara o mail hook quando configurado. */
251
- async createInvitation(orgId, input, actor, origin, settings = null) {
251
+ /**
252
+ * Cria um convite por e-mail. Dispara o mail hook quando configurado.
253
+ *
254
+ * `ctx` is optional only for back-compat with callers that have no request in
255
+ * hand; when given (both HTTP call sites do), the library default invitation
256
+ * email is sent whenever the host defines no `mail.onOrgInvitation` hook.
257
+ */
258
+ async createInvitation(orgId, input, actor, origin, settings = null, ctx) {
252
259
  const store = this.cfg.accountStore;
253
260
  if (!supportsOrganizations(store))
254
261
  return { ok: false, reason: 'not_supported' };
@@ -266,24 +273,30 @@ export class AdminOrgsService {
266
273
  invitedBy: actor.actorId ?? 'admin',
267
274
  ttlHours: this.cfg.organizations.invitationTtlHours,
268
275
  });
269
- // Dispara mail hook (best-effort)
270
- if (this.cfg.mail?.onOrgInvitation) {
276
+ // Sends the invitation email (best-effort). The host hook wins when present;
277
+ // otherwise the host-kit sends its own branded/translated email, like every
278
+ // other email of the library. Delivery NEVER breaks invitation creation.
279
+ try {
271
280
  const acceptUrl = `${origin}${accountPath('orgs')}/invitations/${token}/accept`;
272
- try {
273
- await this.cfg.mail.onOrgInvitation({
274
- email: input.email,
275
- invitationId: invitation.id,
276
- orgName: org.name,
277
- orgSlug: org.slug,
278
- role: input.role,
279
- acceptUrl,
280
- token,
281
- });
281
+ const payload = {
282
+ email: input.email,
283
+ invitationId: invitation.id,
284
+ orgName: org.name,
285
+ orgSlug: org.slug,
286
+ role: input.role,
287
+ acceptUrl,
288
+ token,
289
+ };
290
+ if (this.cfg.mail?.onOrgInvitation) {
291
+ await this.cfg.mail.onOrgInvitation(payload);
282
292
  }
283
- catch {
284
- // best-effort
293
+ else if (ctx) {
294
+ await sendOrgInvitationEmail(ctx, payload);
285
295
  }
286
296
  }
297
+ catch {
298
+ // best-effort
299
+ }
287
300
  await this.cfg.audit?.record({
288
301
  type: 'organization.invitation_sent',
289
302
  actorId: actor.actorId,
@@ -160,7 +160,7 @@ export default class ApiOrgsController {
160
160
  const orgId = ctx.request.param('id');
161
161
  const { email, role } = await ctx.request.validateUsing(orgInvitationValidator);
162
162
  const origin = authkitOrigin(cfg);
163
- const result = await svc.createInvitation(orgId, { email, role: role ?? 'member' }, actor, origin, await resolveRuntimeSettings(ctx));
163
+ const result = await svc.createInvitation(orgId, { email, role: role ?? 'member' }, actor, origin, await resolveRuntimeSettings(ctx), ctx);
164
164
  if (!result.ok) {
165
165
  if (result.reason === 'not_supported')
166
166
  return notSupported(ctx);
@@ -49,7 +49,7 @@ export declare function grantDto(grant: AdminGrant): {
49
49
  };
50
50
  export declare function auditDto(event: StoredAuditEvent): {
51
51
  id: string;
52
- type: import("../../audit/audit_sink.js").AuditEventType;
52
+ type: "login.success" | "login.failure" | "signup" | "password_reset.issued" | "password_reset.consumed" | "pat.issued" | "pat.revoked" | "pat.used" | "impersonation" | "impersonation.panel_viewed" | "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" | "password.rehashed" | "email.change_requested" | "email.changed" | "login.new_ip_notified" | "login.new_device" | "login.otp_sent" | "login.otp_verified" | "login.otp_failed" | "login.otp_invalidated" | "bot_protection.rejected" | "grant.revoked_by_user" | "user.created" | "user.password_reset_sent" | "user.disabled" | "user.enabled" | "user.deleted" | "profile.updated" | "account.deleted" | "account.exported" | "keys.rotated" | "organization.created" | "organization.updated" | "organization.deleted" | "organization.member_added" | "organization.member_removed" | "organization.member_role_changed" | "organization.member_role_updated" | "organization.switched" | "organization.deactivated" | "organization.invitation_sent" | "organization.invitation_accepted" | "organization.invitation_revoked" | "email_change.requested" | "email_change.confirmed" | "email_change.cancelled" | "security_notice.sent" | "settings.updated" | "maintenance.enabled" | "maintenance.disabled" | "trusted_device.revoked" | "password.expired_change_forced" | "otp.locked" | "otp.unlocked" | "otp.unlock_failed" | "sudo.confirmed" | "session.single_enforced" | "account.expired_login_blocked" | "account.expiration_warned" | "login.magic_link_sent" | "session.revoked" | "account.signed_out_all" | "client.secret_regenerated" | "roles_catalog.updated";
53
53
  accountId: string | null;
54
54
  email: string | null;
55
55
  clientId: string | null;