@adonis-agora/authkit-server 0.62.0 → 0.63.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/build/index.d.ts CHANGED
@@ -69,8 +69,8 @@ export type { AuthMessages, I18nConfig } from './src/host/i18n.js';
69
69
  export { BUILTIN_MESSAGES, DEFAULT_LOCALE, DEFAULT_MESSAGES, PT_BR_MESSAGES, resolveMessages, translate, } from './src/host/i18n.js';
70
70
  export type { ImpersonationClientLike, ImpersonationPanel } from './src/host/impersonation.js';
71
71
  export { buildImpersonationPanel } from './src/host/impersonation.js';
72
- export type { ImpersonationState, StartImpersonationParams, } from './src/host/impersonation_session.js';
73
- export { impersonationState, refreshAccessToken, rememberAccessToken, rememberRefreshToken, startImpersonation, stopImpersonation, } from './src/host/impersonation_session.js';
72
+ export type { ImpersonationStartErrorCode, ImpersonationState, StartImpersonationParams, StopImpersonationOptions, TokenExchangeResult, } from './src/host/impersonation_session.js';
73
+ export { ImpersonationStartError, impersonationState, refreshAccessToken, rememberAccessToken, rememberRefreshToken, startImpersonation, stopImpersonation, } from './src/host/impersonation_session.js';
74
74
  export type { KeysStatus as ServerKeysStatus } from './src/host/key_rotation_actions.js';
75
75
  export { buildKeysStatus, rotateNow } from './src/host/key_rotation_actions.js';
76
76
  export type { OidcRpGuardEvents, OidcRpGuardOptions } from './src/host/oidc_rp_guard.js';
package/build/index.js CHANGED
@@ -54,7 +54,7 @@ export { buildImpersonationPanel } from './src/host/impersonation.js';
54
54
  // Session impersonation — RP-side glue that routes through the IdP's RFC 8693
55
55
  // token-exchange (the IdP validates the admin role + audits). See
56
56
  // src/host/impersonation_session.ts.
57
- export { impersonationState, refreshAccessToken, rememberAccessToken, rememberRefreshToken, startImpersonation, stopImpersonation, } from './src/host/impersonation_session.js';
57
+ export { ImpersonationStartError, impersonationState, refreshAccessToken, rememberAccessToken, rememberRefreshToken, startImpersonation, stopImpersonation, } from './src/host/impersonation_session.js';
58
58
  // Key rotation actions — shared between the Admin REST API controller and the SDK embedded driver.
59
59
  export { buildKeysStatus, rotateNow } from './src/host/key_rotation_actions.js';
60
60
  // @adonisjs/auth integration (opt-in) — guard pra Relying Parties OIDC.
@@ -69,6 +69,29 @@ export interface TokenExchangeResult {
69
69
  /** `act.sub` (ator provado pelo IdP), quando o IdP informa. */
70
70
  actSub?: string;
71
71
  }
72
+ /**
73
+ * Códigos de falha do `startImpersonation` — o controller mapeia cada um numa
74
+ * mensagem amigável (sem vazar segredo), em vez de um genérico único que
75
+ * obriga adivinhar entre "token expirado", "sem refresh" e "IdP recusou".
76
+ */
77
+ export type ImpersonationStartErrorCode = 'already_active' | 'invalid_max_age' | 'no_admin_access_token' | 'no_account_session' | 'no_refresh_token' | 'refresh_rejected' | 'exchange_rejected';
78
+ /**
79
+ * Erro do `startImpersonation` com `code` machine-readable. `status` é o HTTP
80
+ * do token/refresh endpoint; `idpError` é o campo `error` do corpo
81
+ * (`invalid_grant`, `invalid_request`, …) — um código de allowlist, NUNCA
82
+ * tokens nem `error_description` (pode ecoar segredos). `cause` encadeia o
83
+ * erro original (ex.: o 400 do exchange que motivou a tentativa de refresh).
84
+ */
85
+ export declare class ImpersonationStartError extends Error {
86
+ readonly code: ImpersonationStartErrorCode;
87
+ readonly status?: number;
88
+ readonly idpError?: string;
89
+ constructor(code: ImpersonationStartErrorCode, message: string, options?: {
90
+ status?: number;
91
+ idpError?: string;
92
+ cause?: unknown;
93
+ });
94
+ }
72
95
  /**
73
96
  * Renova o access token do admin via refresh grant (RFC 6749 §6), usando o
74
97
  * refresh token guardado por `rememberRefreshToken`. Retorna o novo access
@@ -101,6 +124,12 @@ export declare function refreshAccessToken(ctx: HttpContext, params: Pick<StartI
101
124
  * encerrar continua explícito via `stopImpersonation`.
102
125
  * - Recusa (lança) se não houver access token do admin na sessão.
103
126
  * - Recusa (lança) `maxAge` não-positivo (fail-fast de misconfiguração).
127
+ *
128
+ * Toda falha é um `ImpersonationStartError` com `code` (`already_active`,
129
+ * `invalid_max_age`, `no_admin_access_token`, `no_account_session`,
130
+ * `no_refresh_token`, `refresh_rejected`, `exchange_rejected`) — o controller
131
+ * traduz cada um numa mensagem amigável. `status`/`idpError` carregam o HTTP
132
+ * e o `error` do IdP (códigos seguros, sem segredos) para o log server-side.
104
133
  */
105
134
  export declare function startImpersonation(ctx: HttpContext, params: StartImpersonationParams): Promise<ImpersonationState>;
106
135
  /**
@@ -64,6 +64,42 @@ export function rememberAccessToken(ctx, accessToken) {
64
64
  export function rememberRefreshToken(ctx, refreshToken) {
65
65
  ctx.session.put(ADMIN_REFRESH_TOKEN_SESSION_KEY, refreshToken);
66
66
  }
67
+ /**
68
+ * Erro do `startImpersonation` com `code` machine-readable. `status` é o HTTP
69
+ * do token/refresh endpoint; `idpError` é o campo `error` do corpo
70
+ * (`invalid_grant`, `invalid_request`, …) — um código de allowlist, NUNCA
71
+ * tokens nem `error_description` (pode ecoar segredos). `cause` encadeia o
72
+ * erro original (ex.: o 400 do exchange que motivou a tentativa de refresh).
73
+ */
74
+ export class ImpersonationStartError extends Error {
75
+ code;
76
+ status;
77
+ idpError;
78
+ constructor(code, message, options) {
79
+ super(message, options?.cause !== undefined ? { cause: options.cause } : undefined);
80
+ this.name = 'ImpersonationStartError';
81
+ this.code = code;
82
+ if (options?.status !== undefined)
83
+ this.status = options.status;
84
+ if (options?.idpError !== undefined)
85
+ this.idpError = options.idpError;
86
+ }
87
+ }
88
+ /**
89
+ * Extrai o campo `error` do corpo de uma resposta de erro do token endpoint
90
+ * (`invalid_grant`, …). Tolerante: corpo ausente/fora de JSON/sem `error`
91
+ * string ⇒ `undefined`. Só o código — nunca `error_description` (pode ecoar
92
+ * segredos) nem o corpo cru.
93
+ */
94
+ async function readIdpErrorCode(res) {
95
+ try {
96
+ const data = (await res.json());
97
+ return typeof data?.error === 'string' && data.error.length > 0 ? data.error : undefined;
98
+ }
99
+ catch {
100
+ return undefined;
101
+ }
102
+ }
67
103
  /**
68
104
  * POST inline do RFC 8693 token-exchange. Inline (em vez de depender de
69
105
  * `@adonis-agora/authkit-client`) porque o client NÃO é dependência do server e
@@ -97,8 +133,10 @@ async function requestTokenExchange(params, subjectToken) {
97
133
  body: body.toString(),
98
134
  });
99
135
  if (!res.ok) {
100
- // NUNCA logamos tokens nem o corpo (pode ecoar segredos). Só o status.
101
- throw new Error(`Token exchange failed: ${res.status}`);
136
+ // NUNCA logamos tokens nem o corpo (pode ecoar segredos). Só o status +
137
+ // o código `error` do IdP (allowlist como `invalid_grant`).
138
+ const idpError = await readIdpErrorCode(res);
139
+ throw new ImpersonationStartError('exchange_rejected', `Token exchange failed: ${res.status}${idpError ? ` (${idpError})` : ''}`, { status: res.status, idpError });
102
140
  }
103
141
  const result = {};
104
142
  try {
@@ -171,23 +209,29 @@ export async function refreshAccessToken(ctx, params) {
171
209
  * encerrar continua explícito via `stopImpersonation`.
172
210
  * - Recusa (lança) se não houver access token do admin na sessão.
173
211
  * - Recusa (lança) `maxAge` não-positivo (fail-fast de misconfiguração).
212
+ *
213
+ * Toda falha é um `ImpersonationStartError` com `code` (`already_active`,
214
+ * `invalid_max_age`, `no_admin_access_token`, `no_account_session`,
215
+ * `no_refresh_token`, `refresh_rejected`, `exchange_rejected`) — o controller
216
+ * traduz cada um numa mensagem amigável. `status`/`idpError` carregam o HTTP
217
+ * e o `error` do IdP (códigos seguros, sem segredos) para o log server-side.
174
218
  */
175
219
  export async function startImpersonation(ctx, params) {
176
220
  if (ctx.session.get(IMPERSONATOR_SESSION_KEY)) {
177
- throw new Error('Impersonation already active; stop the current one before starting another');
221
+ throw new ImpersonationStartError('already_active', 'Impersonation already active; stop the current one before starting another');
178
222
  }
179
223
  if (params.maxAge !== undefined && !(params.maxAge > 0)) {
180
- throw new Error('Invalid maxAge: must be a positive number of seconds');
224
+ throw new ImpersonationStartError('invalid_max_age', 'Invalid maxAge: must be a positive number of seconds');
181
225
  }
182
226
  const adminAccessToken = ctx.session.get(ADMIN_ACCESS_TOKEN_SESSION_KEY);
183
227
  if (!adminAccessToken) {
184
- throw new Error('No admin access token in session; call rememberAccessToken after login');
228
+ throw new ImpersonationStartError('no_admin_access_token', 'No admin access token in session; call rememberAccessToken after login');
185
229
  }
186
230
  const impersonatorId = ctx.session.get(ACCOUNT_SESSION_KEY);
187
231
  if (!impersonatorId) {
188
232
  // Não há admin logado para impersonar como — sem identidade para restaurar
189
233
  // depois. Recusa antes de qualquer chamada/mutação.
190
- throw new Error('No account session; log in as the admin before impersonating');
234
+ throw new ImpersonationStartError('no_account_session', 'No account session; log in as the admin before impersonating');
191
235
  }
192
236
  // O IdP é o gatekeeper: lança se o admin não puder personificar. Chamado ANTES
193
237
  // de qualquer mutação de sessão — em caso de erro nada é trocado.
@@ -197,12 +241,26 @@ export async function startImpersonation(ctx, params) {
197
241
  }
198
242
  catch (err) {
199
243
  // Access token expirado (4xx do exchange): renova via refresh token e tenta
200
- // de novo. Sem refresh token / refresh falho → propaga o erro original.
201
- if (!(err instanceof Error) || !/exchange failed: 4\d\d/.test(err.message))
244
+ // de novo. Sem refresh token / refresh falho → erro TIPADO (o controller
245
+ // distingue "entre de novo" de "IdP recusou"), com o 4xx original em `cause`.
246
+ const status = err instanceof ImpersonationStartError ? err.status : undefined;
247
+ const isExpiredToken = err instanceof Error && status !== undefined && status >= 400 && status < 500;
248
+ if (!isExpiredToken)
202
249
  throw err;
250
+ const hasRefreshToken = Boolean(ctx.session.get(ADMIN_REFRESH_TOKEN_SESSION_KEY));
251
+ if (!hasRefreshToken) {
252
+ throw new ImpersonationStartError('no_refresh_token', `Admin access token expired (Token exchange failed: ${status}${err instanceof ImpersonationStartError && err.idpError ? ` (${err.idpError})` : ''}) and no refresh token in session; log in again`, {
253
+ status,
254
+ ...(err instanceof ImpersonationStartError && err.idpError
255
+ ? { idpError: err.idpError }
256
+ : {}),
257
+ cause: err,
258
+ });
259
+ }
203
260
  const refreshed = await refreshAccessToken(ctx, params);
204
- if (!refreshed)
205
- throw err;
261
+ if (!refreshed) {
262
+ throw new ImpersonationStartError('refresh_rejected', 'Admin access token expired and refresh token was rejected; log in again', { status, cause: err });
263
+ }
206
264
  rememberAccessToken(ctx, refreshed.accessToken);
207
265
  if (refreshed.refreshToken)
208
266
  rememberRefreshToken(ctx, refreshed.refreshToken);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adonis-agora/authkit-server",
3
- "version": "0.62.0",
3
+ "version": "0.63.0",
4
4
  "description": "AdonisJS OIDC/OAuth2 provider (Identity Provider) toolkit: ejectable auth server with sessions, rate-limiting, MFA/TOTP, audit log, federated logout and OpenTelemetry metrics.",
5
5
  "license": "MIT",
6
6
  "author": "dudousxd",