@adonis-agora/authkit-server 0.61.3 → 0.62.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 (66) hide show
  1. package/build/commands/settings_get.js +1 -1
  2. package/build/commands/settings_set.js +1 -1
  3. package/build/commands/settings_unset.js +1 -1
  4. package/build/index.d.ts +99 -118
  5. package/build/index.js +75 -84
  6. package/build/src/accounts/lucid_store/core.js +1 -1
  7. package/build/src/accounts/lucid_store/mfa.d.ts +0 -12
  8. package/build/src/accounts/lucid_store/mfa.js +16 -16
  9. package/build/src/audit/audit_sink.d.ts +1 -1
  10. package/build/src/audit/audit_sink.js +3 -0
  11. package/build/src/commands/import_users.js +3 -1
  12. package/build/src/define_config.d.ts +2 -2
  13. package/build/src/host/account_api/account_api_controller.js +1 -1
  14. package/build/src/host/account_paths.d.ts +9 -9
  15. package/build/src/host/admin_api/api_orgs_controller.d.ts +10 -10
  16. package/build/src/host/admin_api/api_settings_controller.d.ts +3 -3
  17. package/build/src/host/admin_api/api_settings_controller.js +1 -1
  18. package/build/src/host/admin_api/api_users_controller.d.ts +3 -3
  19. package/build/src/host/admin_api/dto.d.ts +12 -13
  20. package/build/src/host/admin_console/console_orgs_controller.d.ts +9 -9
  21. package/build/src/host/admin_console/console_roles_controller.js +1 -1
  22. package/build/src/host/admin_console/console_settings_controller.d.ts +2 -2
  23. package/build/src/host/admin_console/console_settings_controller.js +1 -1
  24. package/build/src/host/admin_console/console_users_controller.d.ts +7 -7
  25. package/build/src/host/admin_validators.d.ts +39 -39
  26. package/build/src/host/bot_protection.d.ts +1 -1
  27. package/build/src/host/config_locks.d.ts +1 -1
  28. package/build/src/host/controllers/account_confirm_controller.js +1 -1
  29. package/build/src/host/controllers/account_security_controller.js +1 -1
  30. package/build/src/host/controllers/interaction_controller.js +2 -3
  31. package/build/src/host/durable/index.d.ts +2 -2
  32. package/build/src/host/durable/index.js +2 -2
  33. package/build/src/host/impersonation_session.d.ts +71 -7
  34. package/build/src/host/impersonation_session.js +117 -13
  35. package/build/src/host/login_methods_state.js +2 -5
  36. package/build/src/host/login_notify.js +1 -1
  37. package/build/src/host/oidc_rp_guard.d.ts +1 -1
  38. package/build/src/host/runtime_settings.js +1 -1
  39. package/build/src/host/runtime_toggles.d.ts +23 -23
  40. package/build/src/host/sudo/index.d.ts +1 -1
  41. package/build/src/host/ui-dist/assets/index-Dp__4wlB.js +155 -0
  42. package/build/src/host/ui-dist/index.html +1 -1
  43. package/build/src/host/validators.d.ts +15 -15
  44. package/build/src/observability/telescope/index.d.ts +2 -2
  45. package/build/src/observability/telescope/index.js +1 -1
  46. package/build/src/password/password_manager.js +1 -1
  47. package/build/src/provider/oidc_service.d.ts +1 -1
  48. package/build/stubs/ui/react/components/auth_shell.tsx +2 -7
  49. package/build/stubs/ui/react/pages/consent.tsx +1 -0
  50. package/build/stubs/ui/react/pages/forgot.tsx +1 -0
  51. package/build/stubs/ui/react/pages/login.tsx +1 -0
  52. package/build/stubs/ui/react/pages/mfa-challenge.tsx +1 -0
  53. package/build/stubs/ui/react/pages/reset.tsx +1 -0
  54. package/build/stubs/ui/react/pages/signup.tsx +1 -0
  55. package/build/stubs/ui/react/pages/verify-email.tsx +1 -0
  56. package/build/types.d.ts +1 -1
  57. package/package.json +32 -31
  58. package/stubs/ui/react/components/auth_shell.tsx +2 -7
  59. package/stubs/ui/react/pages/consent.tsx +1 -0
  60. package/stubs/ui/react/pages/forgot.tsx +1 -0
  61. package/stubs/ui/react/pages/login.tsx +1 -0
  62. package/stubs/ui/react/pages/mfa-challenge.tsx +1 -0
  63. package/stubs/ui/react/pages/reset.tsx +1 -0
  64. package/stubs/ui/react/pages/signup.tsx +1 -0
  65. package/stubs/ui/react/pages/verify-email.tsx +1 -0
  66. package/build/src/host/ui-dist/assets/index-Cb38N16r.js +0 -155
@@ -98,7 +98,7 @@ export interface BotProtectionWidget {
98
98
  * - `g-recaptcha-response` (Google reCAPTCHA)
99
99
  * - `authkit-bot-token` (genérico, p/ widgets custom)
100
100
  */
101
- export declare const DEFAULT_BOT_TOKEN_FIELDS: readonly ["cf-turnstile-response", "h-captcha-response", "g-recaptcha-response", "authkit-bot-token"];
101
+ export declare const DEFAULT_BOT_TOKEN_FIELDS: readonly ['cf-turnstile-response', 'h-captcha-response', 'g-recaptcha-response', 'authkit-bot-token'];
102
102
  /** Config de entrada do bot protection (em `config/authkit.ts`). */
103
103
  export interface BotProtectionConfigInput {
104
104
  /**
@@ -27,7 +27,7 @@ export declare function deriveLockedSettingKeys(config: Record<string, any>): st
27
27
  * (onde as rotas moram). Ver `AuthHostOptions` e o docblock de
28
28
  * `registerAuthHost`.
29
29
  */
30
- export declare const POLICY_ROUTE_OPTIONS: readonly ["social", "rateLimit", "sudoMethods", "admin", "adminApi"];
30
+ export declare const POLICY_ROUTE_OPTIONS: readonly ['social', 'rateLimit', 'sudoMethods', 'admin', 'adminApi'];
31
31
  /** Uma opção de política de `AuthHostOptions`. */
32
32
  export type PolicyRouteOption = (typeof POLICY_ROUTE_OPTIONS)[number];
33
33
  /**
@@ -8,7 +8,7 @@
8
8
  * A tela está atrás do `accountGuard` (requer sessão de conta ativa).
9
9
  */
10
10
  import '../augmentations.js';
11
- import { LAST_METHOD_SESSION_KEY, configuredSudoMethods, isSudoMethodMounted, resolveAvailableMethods, sudoContextFrom, } from '../sudo/runtime.js';
11
+ import { configuredSudoMethods, isSudoMethodMounted, LAST_METHOD_SESSION_KEY, resolveAvailableMethods, sudoContextFrom, } from '../sudo/runtime.js';
12
12
  /**
13
13
  * Reexport de compatibilidade. O construtor canônico do `SudoContext` vive em
14
14
  * `sudo/runtime.ts` (é runtime do SPI, não detalhe da tela); este caminho
@@ -9,7 +9,7 @@ import { ACCOUNT_SESSION_KEY } from '../account_session_key.js';
9
9
  import { AdminSessionsService } from '../admin_sessions_service.js';
10
10
  import { syncAdonisAuthLogout } from '../adonis_auth_sync.js';
11
11
  import { AvatarUploadError, isAvatarUploadSupported, storeAvatar } from '../avatar_storage.js';
12
- import { sendEmailChangeConfirmationEmail, sendEmailChangeNoticeEmail, sendEmailChangedCompletedEmail, } from '../default_mailer.js';
12
+ import { sendEmailChangeConfirmationEmail, sendEmailChangedCompletedEmail, sendEmailChangeNoticeEmail, } from '../default_mailer.js';
13
13
  import { translate } from '../i18n.js';
14
14
  import { authkitOrigin } from '../origin.js';
15
15
  import { resolveRuntimeSettings } from '../runtime_settings.js';
@@ -3,8 +3,7 @@ import { supportsLoginMethodsPreference, supportsMagicLink, supportsOtpLogin, su
3
3
  import { AdminSessionsService } from '../admin_sessions_service.js';
4
4
  import { guardBotProtection, resolveEffectiveBotProtection } from '../bot_protection.js';
5
5
  import { brandFor, isFirstParty } from '../branding.js';
6
- import { sendMagicLinkEmail } from '../default_mailer.js';
7
- import { sendOtpUnlockEmail } from '../default_mailer.js';
6
+ import { sendMagicLinkEmail, sendOtpUnlockEmail } from '../default_mailer.js';
8
7
  import { translate } from '../i18n.js';
9
8
  import { assertLoginAllowed, attemptPasswordLogin, isEmailUnverifiedBlock, } from '../login_attempt.js';
10
9
  import { magicChannelProp, normalizeLoginChannel } from '../login_channel.js';
@@ -13,7 +12,7 @@ import { authkitOrigin } from '../origin.js';
13
12
  import { createOtpLockout, generateOtpUnlockToken, rawToDbOtpUnlockToken, resolveEffectiveOtpLockout, } from '../otp_lockout.js';
14
13
  import { resolveRuntimeSettingsOrNoop } from '../runtime_settings.js';
15
14
  import { resolveEffectiveAuthMethods, resolveEffectiveMaintenanceMode, resolveEffectiveRegistration, resolveEffectiveSessionPolicy, } from '../runtime_toggles.js';
16
- import { TRUSTED_DEVICE_COOKIE, buildTrustedDevicePayload, isTrustedDeviceValid, } from '../trusted_device.js';
15
+ import { buildTrustedDevicePayload, isTrustedDeviceValid, TRUSTED_DEVICE_COOKIE, } from '../trusted_device.js';
17
16
  import { normalizeUserLoginMethods, resolveEffectiveUserLoginMethods, } from '../user_login_methods.js';
18
17
  /**
19
18
  * Chave i18n do erro de status de conta (disabled/expired) compartilhada pelos
@@ -29,8 +29,8 @@
29
29
  import type { DeletionActor } from '../account_deletion_service.js';
30
30
  import { type AccountDeleteWorkflowInput } from './account_deletion_workflow.js';
31
31
  import { type AccountExportWorkflowInput } from './account_export_workflow.js';
32
- export { defineAccountDeletionWorkflow, ACCOUNT_DELETE_WORKFLOW, type AccountDeleteWorkflowInput, type AccountDeletionWorkflowDeps, type DurableStepCtx, type WorkflowBody, } from './account_deletion_workflow.js';
33
- export { defineAccountExportWorkflow, ACCOUNT_EXPORT_WORKFLOW, type AccountExportWorkflowInput, type AccountExportWorkflowResult, type AccountExportWorkflowDeps, type PersistArtifact, type DeliverArtifact, } from './account_export_workflow.js';
32
+ export { ACCOUNT_DELETE_WORKFLOW, type AccountDeleteWorkflowInput, type AccountDeletionWorkflowDeps, type DurableStepCtx, defineAccountDeletionWorkflow, type WorkflowBody, } from './account_deletion_workflow.js';
33
+ export { ACCOUNT_EXPORT_WORKFLOW, type AccountExportWorkflowDeps, type AccountExportWorkflowInput, type AccountExportWorkflowResult, type DeliverArtifact, defineAccountExportWorkflow, type PersistArtifact, } from './account_export_workflow.js';
34
34
  /**
35
35
  * Superfície mínima do `WorkflowEngine` que o enqueue usa (`start` idempotente por
36
36
  * run-id). Tipada estruturalmente p/ NÃO acoplar o build ao peer opcional.
@@ -28,8 +28,8 @@
28
28
  */
29
29
  import { ACCOUNT_DELETE_WORKFLOW, } from './account_deletion_workflow.js';
30
30
  import { ACCOUNT_EXPORT_WORKFLOW, } from './account_export_workflow.js';
31
- export { defineAccountDeletionWorkflow, ACCOUNT_DELETE_WORKFLOW, } from './account_deletion_workflow.js';
32
- export { defineAccountExportWorkflow, ACCOUNT_EXPORT_WORKFLOW, } from './account_export_workflow.js';
31
+ export { ACCOUNT_DELETE_WORKFLOW, defineAccountDeletionWorkflow, } from './account_deletion_workflow.js';
32
+ export { ACCOUNT_EXPORT_WORKFLOW, defineAccountExportWorkflow, } from './account_export_workflow.js';
33
33
  /**
34
34
  * Resolve o `WorkflowEngine` do container — SÓ no caminho durável. Importa
35
35
  * `@adonis-agora/durable` dinamicamente (peer opcional), então o barrel principal
@@ -1,4 +1,5 @@
1
1
  import type { HttpContext } from '@adonisjs/core/http';
2
+ import type { AuditSink } from '../audit/audit_sink.js';
2
3
  /**
3
4
  * Guarda o access token do admin na sessão (chame no callback OIDC do RP, logo
4
5
  * após o login). Necessário porque o token-exchange exige o access token do admin
@@ -25,6 +26,18 @@ export interface StartImpersonationParams {
25
26
  tokenEndpoint?: string;
26
27
  scope?: string;
27
28
  fetchImpl?: typeof fetch;
29
+ /**
30
+ * Vida máxima da impersonation em SEGUNDOS, contada do start. Quando
31
+ * definido, `impersonationState().active` passa a `false` após o prazo
32
+ * (expiração preguiçosa — nenhum timer roda; a leitura decide) e o stop
33
+ * continua restaurando o admin normalmente.
34
+ *
35
+ * OPCIONAL e ausente por default: sem ele a impersonation dura até o stop
36
+ * explícito / logout / fim da sessão (comportamento histórico — um default
37
+ * com prazo faria a impersonation "parar sozinha" para quem não configurou
38
+ * nada). Deve ser > 0.
39
+ */
40
+ maxAge?: number;
28
41
  }
29
42
  export interface ImpersonationState {
30
43
  active: boolean;
@@ -32,6 +45,29 @@ export interface ImpersonationState {
32
45
  targetId?: string;
33
46
  /** O admin real (impersonator), quando `active`. */
34
47
  impersonatorId?: string;
48
+ /** Id único desta impersonation (gerado no start). */
49
+ impersonationId?: string;
50
+ /** Epoch ms do start. */
51
+ startedAt?: number;
52
+ /** Epoch ms da expiração — só quando `maxAge` foi configurado. */
53
+ expiresAt?: number;
54
+ /**
55
+ * `act.sub` devolvido pelo token-exchange (o ator PROVADO pelo IdP, que
56
+ * deve coincidir com `impersonatorId`).
57
+ */
58
+ actSub?: string;
59
+ /**
60
+ * `expires_in` (segundos) do token trocado — INFORMATIVO: é a vida do token
61
+ * do lado do IdP, NÃO impõe expiração na sessão (só `maxAge` faz isso).
62
+ */
63
+ exchangeExpiresIn?: number;
64
+ }
65
+ /** Metadados aproveitados da resposta do token-exchange (só o que não é segredo). */
66
+ export interface TokenExchangeResult {
67
+ /** `expires_in` (s) do token trocado, quando o IdP informa. */
68
+ expiresIn?: number;
69
+ /** `act.sub` (ator provado pelo IdP), quando o IdP informa. */
70
+ actSub?: string;
35
71
  }
36
72
  /**
37
73
  * Renova o access token do admin via refresh grant (RFC 6749 §6), usando o
@@ -53,20 +89,48 @@ export declare function refreshAccessToken(ctx: HttpContext, params: Pick<StartI
53
89
  * impersonator (= `account_user_id` atual), regenera a sessão (anti-fixation) e
54
90
  * seta `account_user_id = targetId`.
55
91
  *
92
+ * Além da troca de identidade, registra a impersonation: um id único
93
+ * (`impersonationId`), o instante do start e — só com `maxAge` configurado —
94
+ * a expiração, mais os metadados não-secretos do exchange (`actSub`,
95
+ * `exchangeExpiresIn`). Devolve o estado criado (o mesmo shape de
96
+ * `impersonationState`).
97
+ *
56
98
  * - Se o exchange falhar, LANÇA e NÃO troca NADA na sessão.
57
- * - Recusa (lança) se já houver impersonation ativa (pare a atual antes).
99
+ * - Recusa (lança) se já houver impersonation ativa (pare a atual antes) —
100
+ * inclusive expirada pelo prazo: expirar só apaga o `active` da leitura,
101
+ * encerrar continua explícito via `stopImpersonation`.
58
102
  * - Recusa (lança) se não houver access token do admin na sessão.
103
+ * - Recusa (lança) `maxAge` não-positivo (fail-fast de misconfiguração).
59
104
  */
60
- export declare function startImpersonation(ctx: HttpContext, params: StartImpersonationParams): Promise<void>;
105
+ export declare function startImpersonation(ctx: HttpContext, params: StartImpersonationParams): Promise<ImpersonationState>;
61
106
  /**
62
107
  * Estado da impersonation pra UI (ex.: banner). `active` quando há um impersonator
63
- * guardado na sessão.
108
+ * guardado na sessão E a impersonation não expirou (expiração preguiçosa: com
109
+ * `maxAge` configurado no start, passado o prazo a leitura devolve
110
+ * `active: false` — mas os dados seguem na sessão até o `stopImpersonation`
111
+ * explícito, que continua restaurando o admin normalmente).
112
+ *
113
+ * O shape evita keys com valor `undefined` (construção condicional): `{ active:
114
+ * false }` puro quando nunca houve impersonation.
64
115
  */
65
116
  export declare function impersonationState(ctx: HttpContext): ImpersonationState;
117
+ /** Opções do `stopImpersonation`. */
118
+ export interface StopImpersonationOptions {
119
+ /**
120
+ * Sink para auditar o encerramento (`impersonation.stopped`, com o
121
+ * `impersonationId`). Sem ele, o stop só mexe na sessão — o start segue
122
+ * auditado pelo IdP, mas o fim da impersonation não aparece na trilha.
123
+ */
124
+ audit?: AuditSink;
125
+ /** IP a registrar no evento de auditoria (o helper não lê o request). */
126
+ ip?: string | null;
127
+ }
66
128
  /**
67
- * Encerra a impersonation: restaura `account_user_id = impersonator`, remove a
68
- * key de impersonation e regenera a sessão (anti-fixation). No-op quando não há
69
- * impersonation ativa.
129
+ * Encerra a impersonation: restaura `account_user_id = impersonator`, remove
130
+ * TODAS as keys de sessão de impersonation (id, tempos, prova do exchange) e
131
+ * regenera a sessão (anti-fixation). Devolve o estado encerrado (com o
132
+ * `impersonationId`, mesmo que já expirado pelo prazo) ou `null` quando não
133
+ * havia nenhuma ativa (no-op — sem audit, sem regenerate).
70
134
  *
71
135
  * O `admin_access_token` (a credencial do ADMIN usada como `subject_token` do
72
136
  * token-exchange) é PRESERVADO: ele é do admin — não do alvo — e o admin segue
@@ -75,4 +139,4 @@ export declare function impersonationState(ctx: HttpContext): ImpersonationState
75
139
  * session). O token segue curto (TTL do access token do RP) e o logout do app
76
140
  * (`ctx.session.clear()` em `AuthRpController.logout`) continua limpando tudo.
77
141
  */
78
- export declare function stopImpersonation(ctx: HttpContext): Promise<void>;
142
+ export declare function stopImpersonation(ctx: HttpContext, options?: StopImpersonationOptions): Promise<ImpersonationState | null>;
@@ -1,3 +1,4 @@
1
+ import { randomUUID } from 'node:crypto';
1
2
  import { ACCOUNT_SESSION_KEY } from './account_session_key.js';
2
3
  /**
3
4
  * Ergonômico de SESSÃO de browser no RP para "personificar" (impersonate) um
@@ -22,6 +23,16 @@ const ACCESS_TOKEN_TYPE = 'urn:ietf:params:oauth:token-type:access_token';
22
23
  * implementação, leia via `impersonationState`.
23
24
  */
24
25
  const IMPERSONATOR_SESSION_KEY = 'impersonator_user_id';
26
+ /**
27
+ * Keys de sessão do registro da impersonation (id + tempos + prova do
28
+ * exchange). Internas pelo mesmo motivo da key do impersonator: o contrato
29
+ * público é `impersonationState` / o retorno de `startImpersonation`.
30
+ */
31
+ const IMPERSONATION_ID_SESSION_KEY = 'impersonation_id';
32
+ const IMPERSONATION_STARTED_AT_SESSION_KEY = 'impersonation_started_at';
33
+ const IMPERSONATION_EXPIRES_AT_SESSION_KEY = 'impersonation_expires_at';
34
+ const IMPERSONATION_ACT_SESSION_KEY = 'impersonation_act_sub';
35
+ const IMPERSONATION_EXCHANGE_EXPIRES_IN_SESSION_KEY = 'impersonation_exchange_expires_in';
25
36
  /**
26
37
  * Key de sessão que guarda o access token do admin, necessário como
27
38
  * `subject_token` do token-exchange. Interna: NÃO exporte o literal.
@@ -59,6 +70,13 @@ export function rememberRefreshToken(ctx, refreshToken) {
59
70
  * adicioná-la inverteria a direção do grafo de pacotes (server = IdP toolkit). São
60
71
  * ~12 linhas; testável via `fetchImpl`. Lança se o IdP não responder 2xx (é o
61
72
  * gatekeeper: não-admin / token expirado ⇒ erro ⇒ a sessão não é tocada).
73
+ *
74
+ * Em sucesso devolve os metadados NÃO-secretos da resposta (`expires_in` e
75
+ * `act.sub`) — o access token trocado é DESCARTADO de propósito: é uma
76
+ * credencial bearer do alvo e nada no fluxo o consome depois do start
77
+ * (o app age como o alvo via troca de sessão, não via bearer). NUNCA guarde
78
+ * esse token na sessão. O parse do corpo é tolerante: IdP que responde 2xx
79
+ * sem JSON válido ainda conta como exchange OK, só sem metadados.
62
80
  */
63
81
  async function requestTokenExchange(params, subjectToken) {
64
82
  const body = new URLSearchParams({
@@ -82,6 +100,21 @@ async function requestTokenExchange(params, subjectToken) {
82
100
  // NUNCA logamos tokens nem o corpo (pode ecoar segredos). Só o status.
83
101
  throw new Error(`Token exchange failed: ${res.status}`);
84
102
  }
103
+ const result = {};
104
+ try {
105
+ const data = (await res.json());
106
+ if (typeof data?.expires_in === 'number' && Number.isFinite(data.expires_in)) {
107
+ result.expiresIn = data.expires_in;
108
+ }
109
+ const act = data?.act;
110
+ if (act && typeof act.sub === 'string' && act.sub.length > 0) {
111
+ result.actSub = act.sub;
112
+ }
113
+ }
114
+ catch {
115
+ // Corpo fora do JSON esperado: exchange valeu, metadados ficam ausentes.
116
+ }
117
+ return result;
85
118
  }
86
119
  /**
87
120
  * Renova o access token do admin via refresh grant (RFC 6749 §6), usando o
@@ -126,14 +159,26 @@ export async function refreshAccessToken(ctx, params) {
126
159
  * impersonator (= `account_user_id` atual), regenera a sessão (anti-fixation) e
127
160
  * seta `account_user_id = targetId`.
128
161
  *
162
+ * Além da troca de identidade, registra a impersonation: um id único
163
+ * (`impersonationId`), o instante do start e — só com `maxAge` configurado —
164
+ * a expiração, mais os metadados não-secretos do exchange (`actSub`,
165
+ * `exchangeExpiresIn`). Devolve o estado criado (o mesmo shape de
166
+ * `impersonationState`).
167
+ *
129
168
  * - Se o exchange falhar, LANÇA e NÃO troca NADA na sessão.
130
- * - Recusa (lança) se já houver impersonation ativa (pare a atual antes).
169
+ * - Recusa (lança) se já houver impersonation ativa (pare a atual antes) —
170
+ * inclusive expirada pelo prazo: expirar só apaga o `active` da leitura,
171
+ * encerrar continua explícito via `stopImpersonation`.
131
172
  * - Recusa (lança) se não houver access token do admin na sessão.
173
+ * - Recusa (lança) `maxAge` não-positivo (fail-fast de misconfiguração).
132
174
  */
133
175
  export async function startImpersonation(ctx, params) {
134
176
  if (ctx.session.get(IMPERSONATOR_SESSION_KEY)) {
135
177
  throw new Error('Impersonation already active; stop the current one before starting another');
136
178
  }
179
+ if (params.maxAge !== undefined && !(params.maxAge > 0)) {
180
+ throw new Error('Invalid maxAge: must be a positive number of seconds');
181
+ }
137
182
  const adminAccessToken = ctx.session.get(ADMIN_ACCESS_TOKEN_SESSION_KEY);
138
183
  if (!adminAccessToken) {
139
184
  throw new Error('No admin access token in session; call rememberAccessToken after login');
@@ -146,8 +191,9 @@ export async function startImpersonation(ctx, params) {
146
191
  }
147
192
  // O IdP é o gatekeeper: lança se o admin não puder personificar. Chamado ANTES
148
193
  // de qualquer mutação de sessão — em caso de erro nada é trocado.
194
+ let exchange;
149
195
  try {
150
- await requestTokenExchange(params, adminAccessToken);
196
+ exchange = await requestTokenExchange(params, adminAccessToken);
151
197
  }
152
198
  catch (err) {
153
199
  // Access token expirado (4xx do exchange): renova via refresh token e tenta
@@ -160,11 +206,14 @@ export async function startImpersonation(ctx, params) {
160
206
  rememberAccessToken(ctx, refreshed.accessToken);
161
207
  if (refreshed.refreshToken)
162
208
  rememberRefreshToken(ctx, refreshed.refreshToken);
163
- await requestTokenExchange(params, refreshed.accessToken);
209
+ exchange = await requestTokenExchange(params, refreshed.accessToken);
164
210
  }
165
211
  // Anti-fixation: rotaciona o id da sessão (mantém os dados) antes de gravar a
166
212
  // nova identidade. Mesmo padrão do consumidor real no RP.
167
213
  await ctx.session.regenerate();
214
+ const impersonationId = randomUUID();
215
+ const startedAt = Date.now();
216
+ const expiresAt = params.maxAge !== undefined ? startedAt + params.maxAge * 1000 : undefined;
168
217
  // ESCALAÇÃO DE PRIVILÉGIO (fechada por vinculação): trocar a conta aqui NÃO
169
218
  // pode carregar junto o sudo que o admin confirmou sobre a PRÓPRIA conta —
170
219
  // senão ele entraria personificando já com a graça aberta sobre a conta
@@ -173,24 +222,61 @@ export async function startImpersonation(ctx, params) {
173
222
  // `isSudoActive` a recusa sozinho assim que `ACCOUNT_SESSION_KEY` muda. A
174
223
  // garantia é estrutural — vale para qualquer troca de conta futura, sem
175
224
  // depender de um `forget` lembrado em cada nova transição.
225
+ ctx.session.put(IMPERSONATION_ID_SESSION_KEY, impersonationId);
226
+ ctx.session.put(IMPERSONATION_STARTED_AT_SESSION_KEY, startedAt);
227
+ if (expiresAt !== undefined)
228
+ ctx.session.put(IMPERSONATION_EXPIRES_AT_SESSION_KEY, expiresAt);
229
+ if (exchange.actSub)
230
+ ctx.session.put(IMPERSONATION_ACT_SESSION_KEY, exchange.actSub);
231
+ if (exchange.expiresIn !== undefined) {
232
+ ctx.session.put(IMPERSONATION_EXCHANGE_EXPIRES_IN_SESSION_KEY, exchange.expiresIn);
233
+ }
176
234
  ctx.session.put(IMPERSONATOR_SESSION_KEY, impersonatorId);
177
235
  ctx.session.put(ACCOUNT_SESSION_KEY, params.targetId);
236
+ return impersonationState(ctx);
178
237
  }
179
238
  /**
180
239
  * Estado da impersonation pra UI (ex.: banner). `active` quando há um impersonator
181
- * guardado na sessão.
240
+ * guardado na sessão E a impersonation não expirou (expiração preguiçosa: com
241
+ * `maxAge` configurado no start, passado o prazo a leitura devolve
242
+ * `active: false` — mas os dados seguem na sessão até o `stopImpersonation`
243
+ * explícito, que continua restaurando o admin normalmente).
244
+ *
245
+ * O shape evita keys com valor `undefined` (construção condicional): `{ active:
246
+ * false }` puro quando nunca houve impersonation.
182
247
  */
183
248
  export function impersonationState(ctx) {
184
249
  const impersonatorId = ctx.session.get(IMPERSONATOR_SESSION_KEY);
185
250
  if (!impersonatorId)
186
251
  return { active: false };
187
252
  const targetId = ctx.session.get(ACCOUNT_SESSION_KEY);
188
- return { active: true, targetId, impersonatorId };
253
+ const state = { active: true, targetId, impersonatorId };
254
+ const impersonationId = ctx.session.get(IMPERSONATION_ID_SESSION_KEY);
255
+ if (impersonationId)
256
+ state.impersonationId = impersonationId;
257
+ const startedAt = ctx.session.get(IMPERSONATION_STARTED_AT_SESSION_KEY);
258
+ if (typeof startedAt === 'number')
259
+ state.startedAt = startedAt;
260
+ const expiresAt = ctx.session.get(IMPERSONATION_EXPIRES_AT_SESSION_KEY);
261
+ if (typeof expiresAt === 'number')
262
+ state.expiresAt = expiresAt;
263
+ const actSub = ctx.session.get(IMPERSONATION_ACT_SESSION_KEY);
264
+ if (actSub)
265
+ state.actSub = actSub;
266
+ const exchangeExpiresIn = ctx.session.get(IMPERSONATION_EXCHANGE_EXPIRES_IN_SESSION_KEY);
267
+ if (typeof exchangeExpiresIn === 'number')
268
+ state.exchangeExpiresIn = exchangeExpiresIn;
269
+ if (state.expiresAt !== undefined && Date.now() > state.expiresAt) {
270
+ state.active = false;
271
+ }
272
+ return state;
189
273
  }
190
274
  /**
191
- * Encerra a impersonation: restaura `account_user_id = impersonator`, remove a
192
- * key de impersonation e regenera a sessão (anti-fixation). No-op quando não há
193
- * impersonation ativa.
275
+ * Encerra a impersonation: restaura `account_user_id = impersonator`, remove
276
+ * TODAS as keys de sessão de impersonation (id, tempos, prova do exchange) e
277
+ * regenera a sessão (anti-fixation). Devolve o estado encerrado (com o
278
+ * `impersonationId`, mesmo que já expirado pelo prazo) ou `null` quando não
279
+ * havia nenhuma ativa (no-op — sem audit, sem regenerate).
194
280
  *
195
281
  * O `admin_access_token` (a credencial do ADMIN usada como `subject_token` do
196
282
  * token-exchange) é PRESERVADO: ele é do admin — não do alvo — e o admin segue
@@ -199,10 +285,10 @@ export function impersonationState(ctx) {
199
285
  * session). O token segue curto (TTL do access token do RP) e o logout do app
200
286
  * (`ctx.session.clear()` em `AuthRpController.logout`) continua limpando tudo.
201
287
  */
202
- export async function stopImpersonation(ctx) {
203
- const impersonatorId = ctx.session.get(IMPERSONATOR_SESSION_KEY);
204
- if (!impersonatorId)
205
- return;
288
+ export async function stopImpersonation(ctx, options) {
289
+ const stopped = impersonationState(ctx);
290
+ if (!stopped.impersonatorId)
291
+ return null;
206
292
  await ctx.session.regenerate();
207
293
  // Simétrico ao `startImpersonation`: o sudo obtido ENQUANTO personificava
208
294
  // ficaria valendo sobre a conta do admin ao voltar. A vinculação corta isso —
@@ -211,6 +297,24 @@ export async function stopImpersonation(ctx) {
211
297
  // Nota: se o admin tinha sudo sobre a própria conta ANTES de personificar e a
212
298
  // graça ainda não venceu, ele volta valendo. Correto e intencional: é a
213
299
  // confirmação dele, sobre a conta dele, dentro da janela dele.
214
- ctx.session.put(ACCOUNT_SESSION_KEY, impersonatorId);
300
+ ctx.session.put(ACCOUNT_SESSION_KEY, stopped.impersonatorId);
215
301
  ctx.session.forget(IMPERSONATOR_SESSION_KEY);
302
+ ctx.session.forget(IMPERSONATION_ID_SESSION_KEY);
303
+ ctx.session.forget(IMPERSONATION_STARTED_AT_SESSION_KEY);
304
+ ctx.session.forget(IMPERSONATION_EXPIRES_AT_SESSION_KEY);
305
+ ctx.session.forget(IMPERSONATION_ACT_SESSION_KEY);
306
+ ctx.session.forget(IMPERSONATION_EXCHANGE_EXPIRES_IN_SESSION_KEY);
307
+ await options?.audit?.record({
308
+ type: 'impersonation.stopped',
309
+ accountId: stopped.targetId ?? null,
310
+ actorId: stopped.impersonatorId,
311
+ ip: options?.ip ?? null,
312
+ metadata: {
313
+ impersonationId: stopped.impersonationId ?? null,
314
+ startedAt: stopped.startedAt ?? null,
315
+ stoppedAt: Date.now(),
316
+ expired: !stopped.active,
317
+ },
318
+ });
319
+ return stopped;
216
320
  }
@@ -1,8 +1,5 @@
1
- import { supportsMagicLink } from '../accounts/account_store.js';
2
- import { supportsPasskeys } from '../accounts/account_store.js';
3
- import { supportsLoginMethodsPreference } from '../accounts/account_store.js';
4
- import { resolveEffectiveAuthMethods } from './runtime_toggles.js';
5
- import { configLockedAuthMethods } from './runtime_toggles.js';
1
+ import { supportsLoginMethodsPreference, supportsMagicLink, supportsPasskeys, } from '../accounts/account_store.js';
2
+ import { configLockedAuthMethods, resolveEffectiveAuthMethods } from './runtime_toggles.js';
6
3
  import { normalizeUserLoginMethods, resolveEffectiveUserLoginMethods, } from './user_login_methods.js';
7
4
  /**
8
5
  * Métodos globais efetivos + pins de config — resolução compartilhada entre o
@@ -1,5 +1,5 @@
1
1
  import { sendNewDeviceLoginEmail, sendNewLoginEmail } from './default_mailer.js';
2
- import { TRUSTED_DEVICE_COOKIE, isTrustedDeviceValid } from './trusted_device.js';
2
+ import { isTrustedDeviceValid, TRUSTED_DEVICE_COOKIE } from './trusted_device.js';
3
3
  /**
4
4
  * Centraliza o pós-login bem-sucedido: registra o evento `login.success` e dispara
5
5
  * (best-effort) o alerta de NOVO acesso quando o IP nunca foi visto para a conta.
@@ -75,7 +75,7 @@ export type OidcRpGuardEvents<User> = {
75
75
  export declare class OidcRpGuard<UserProvider extends SessionUserProviderContract<unknown>> implements GuardContract<RealUser<UserProvider>> {
76
76
  #private;
77
77
  [symbols.GUARD_KNOWN_EVENTS]: OidcRpGuardEvents<RealUser<UserProvider>>;
78
- driverName: "oidc_rp";
78
+ driverName: 'oidc_rp';
79
79
  authenticationAttempted: boolean;
80
80
  isAuthenticated: boolean;
81
81
  isLoggedOut: boolean;
@@ -50,7 +50,7 @@
50
50
  * );
51
51
  * ```
52
52
  */
53
- import { SettingLockedError, isSettingLocked } from './config_locks.js';
53
+ import { isSettingLocked, SettingLockedError } from './config_locks.js';
54
54
  /**
55
55
  * Type guard: o objeto (store ou serviço) expõe SettingsCapability?
56
56
  */
@@ -17,29 +17,29 @@ import type { SettingsCapability } from './runtime_settings.js';
17
17
  * que qualquer code-path possa importar a constante e não depender de strings.
18
18
  */
19
19
  export declare const SETTING_KEYS: {
20
- readonly BOT_PROTECTION: "bot_protection";
21
- readonly REGISTRATION: "registration";
22
- readonly REQUIRE_VERIFIED_EMAIL: "require_verified_email";
23
- readonly MAINTENANCE_MODE: "maintenance_mode";
24
- readonly AUTH_METHODS: "auth_methods";
25
- readonly EMAIL_CHANGE: "email_change";
26
- readonly SECURITY_NOTIFICATIONS: "security_notifications";
27
- readonly PASSWORD_HISTORY: "password_history";
28
- readonly PASSWORD_EXPIRATION: "password_expiration";
29
- readonly SESSION_POLICY: "session_policy";
30
- readonly LOCKOUT: "lockout";
31
- readonly RATE_LIMIT: "rate_limit";
32
- readonly PASSWORD_POLICY: "password_policy";
33
- readonly NOTIFICATIONS: "notifications";
34
- readonly TRUSTED_DEVICES: "trusted_devices";
35
- readonly TOKEN_TTL: "token_ttl";
36
- readonly ADMIN_IMPERSONATION: "admin_impersonation";
37
- readonly ORGANIZATIONS_POLICY: "organizations_policy";
38
- readonly ROLES_CATALOG: "roles_catalog";
39
- readonly OTP_LOCKOUT: "otp_lockout";
40
- readonly SUDO_MODE: "sudo_mode";
41
- readonly ACCOUNT_EXPIRATION: "account_expiration";
42
- readonly KEY_ROTATION: "key_rotation";
20
+ readonly BOT_PROTECTION: 'bot_protection';
21
+ readonly REGISTRATION: 'registration';
22
+ readonly REQUIRE_VERIFIED_EMAIL: 'require_verified_email';
23
+ readonly MAINTENANCE_MODE: 'maintenance_mode';
24
+ readonly AUTH_METHODS: 'auth_methods';
25
+ readonly EMAIL_CHANGE: 'email_change';
26
+ readonly SECURITY_NOTIFICATIONS: 'security_notifications';
27
+ readonly PASSWORD_HISTORY: 'password_history';
28
+ readonly PASSWORD_EXPIRATION: 'password_expiration';
29
+ readonly SESSION_POLICY: 'session_policy';
30
+ readonly LOCKOUT: 'lockout';
31
+ readonly RATE_LIMIT: 'rate_limit';
32
+ readonly PASSWORD_POLICY: 'password_policy';
33
+ readonly NOTIFICATIONS: 'notifications';
34
+ readonly TRUSTED_DEVICES: 'trusted_devices';
35
+ readonly TOKEN_TTL: 'token_ttl';
36
+ readonly ADMIN_IMPERSONATION: 'admin_impersonation';
37
+ readonly ORGANIZATIONS_POLICY: 'organizations_policy';
38
+ readonly ROLES_CATALOG: 'roles_catalog';
39
+ readonly OTP_LOCKOUT: 'otp_lockout';
40
+ readonly SUDO_MODE: 'sudo_mode';
41
+ readonly ACCOUNT_EXPIRATION: 'account_expiration';
42
+ readonly KEY_ROTATION: 'key_rotation';
43
43
  };
44
44
  export type SettingKey = (typeof SETTING_KEYS)[keyof typeof SETTING_KEYS];
45
45
  /**
@@ -46,10 +46,10 @@ export declare const sudoMethods: {
46
46
  oidcStepUp: typeof oidcStepUp;
47
47
  magicLink: typeof magicLink;
48
48
  };
49
- export type { SudoMethod, SudoContext, SudoMethodDescriptor, SudoRouteHelpers } from './types.js';
50
49
  /**
51
50
  * Montagem do `SudoContext` a partir do `HttpContext`. Reexportado aqui por
52
51
  * simetria com `sudoMethods` e os tipos: quem escreve um método (ou a rota de
53
52
  * callback do `oidcStepUp`) precisa dos três.
54
53
  */
55
54
  export { sudoContextFrom } from './runtime.js';
55
+ export type { SudoContext, SudoMethod, SudoMethodDescriptor, SudoRouteHelpers } from './types.js';