@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.
- package/README.md +4 -4
- package/build/index.d.ts +2 -2
- package/build/index.js +1 -1
- package/build/providers/authkit_server_provider.d.ts +1 -12
- package/build/providers/authkit_server_provider.js +13 -0
- package/build/src/audit/audit_sink.d.ts +11 -2
- package/build/src/audit/audit_sink.js +104 -1
- package/build/src/define_config.d.ts +71 -9
- package/build/src/define_config.js +3 -0
- package/build/src/doctor/checks.d.ts +15 -3
- package/build/src/doctor/checks.js +43 -3
- package/build/src/host/admin_api/admin_orgs_service.d.ts +9 -2
- package/build/src/host/admin_api/admin_orgs_service.js +29 -16
- package/build/src/host/admin_api/api_orgs_controller.js +1 -1
- package/build/src/host/admin_api/dto.d.ts +1 -1
- package/build/src/host/admin_console/console_impersonation_controller.js +37 -3
- package/build/src/host/admin_console/console_orgs_controller.js +1 -1
- package/build/src/host/branding.d.ts +19 -0
- package/build/src/host/branding.js +25 -0
- package/build/src/host/config_locks.js +1 -1
- package/build/src/host/controllers/account_orgs_controller.js +21 -14
- package/build/src/host/default_mailer.d.ts +21 -0
- package/build/src/host/default_mailer.js +41 -0
- package/build/src/host/i18n.d.ts +10 -0
- package/build/src/host/i18n.js +15 -2
- package/build/src/host/impersonation.d.ts +23 -2
- package/build/src/host/impersonation.js +21 -5
- package/build/src/host/register_auth_host.d.ts +27 -6
- package/build/src/host/ui-dist/assets/{index-pAbBjdHC.js → index-6cE5JMyP.js} +1 -1
- package/build/src/host/ui-dist/index.html +1 -1
- package/build/src/observability/telescope/data_providers.js +4 -1
- package/build/src/provider/oidc_service.js +18 -6
- package/build/types.d.ts +21 -1
- 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/
|
|
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/
|
|
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/
|
|
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/
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
/**
|
|
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.
|
|
892
|
-
*
|
|
893
|
-
*
|
|
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.
|
|
898
|
-
*
|
|
899
|
-
*
|
|
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
|
|
917
|
-
* `auth_settings`, `auth_password_history
|
|
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
|
|
142
|
-
* -
|
|
143
|
-
* -
|
|
141
|
+
* Verifica o pepper de senha (`accountStore.password.pepper`).
|
|
142
|
+
* - Silencioso quando não há 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
|
|
577
|
-
* -
|
|
578
|
-
* -
|
|
576
|
+
* Verifica o pepper de senha (`accountStore.password.pepper`).
|
|
577
|
+
* - Silencioso quando não há 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
|
-
/**
|
|
113
|
-
|
|
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
|
-
/**
|
|
251
|
-
|
|
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
|
-
//
|
|
270
|
-
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
|
|
284
|
-
|
|
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:
|
|
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;
|