@adonis-agora/authkit-server 0.65.3 → 0.66.1

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 (44) hide show
  1. package/build/host/views/account/apps.edge +2 -2
  2. package/build/host/views/account/confirm.edge +1 -1
  3. package/build/host/views/account/login.edge +2 -2
  4. package/build/host/views/account/mfa.edge +5 -5
  5. package/build/host/views/account/security.edge +17 -17
  6. package/build/host/views/account/tokens.edge +3 -3
  7. package/build/host/views/forgot.edge +1 -1
  8. package/build/host/views/login.edge +2 -2
  9. package/build/host/views/maintenance.edge +1 -1
  10. package/build/host/views/mfa-challenge.edge +2 -2
  11. package/build/host/views/otp-unlock.edge +1 -1
  12. package/build/host/views/partials/styles.edge +2 -1
  13. package/build/host/views/reset.edge +1 -1
  14. package/build/host/views/signup.edge +3 -3
  15. package/build/index.d.ts +2 -0
  16. package/build/index.js +1 -0
  17. package/build/src/accounts/account_store.d.ts +14 -3
  18. package/build/src/accounts/lucid_store/core.js +5 -4
  19. package/build/src/audit/audit_sink.d.ts +14 -3
  20. package/build/src/audit/lucid_audit_sink.js +5 -4
  21. package/build/src/commands/expire_scan_command.js +9 -8
  22. package/build/src/host/account_export_service.js +4 -3
  23. package/build/src/host/admin_api/admin_orgs_service.js +6 -5
  24. package/build/src/host/admin_api/admin_users_service.d.ts +3 -3
  25. package/build/src/host/admin_api/admin_users_service.js +8 -7
  26. package/build/src/host/admin_api/api_misc_controller.d.ts +6 -4
  27. package/build/src/host/admin_api/api_misc_controller.js +6 -5
  28. package/build/src/host/admin_api/api_users_controller.d.ts +6 -4
  29. package/build/src/host/admin_api/api_users_controller.js +6 -6
  30. package/build/src/host/admin_console/console_audit_controller.d.ts +6 -4
  31. package/build/src/host/admin_console/console_audit_controller.js +6 -5
  32. package/build/src/host/admin_console/console_overview_controller.js +4 -1
  33. package/build/src/host/admin_console/console_users_controller.d.ts +6 -4
  34. package/build/src/host/admin_console/console_users_controller.js +6 -6
  35. package/build/src/host/admin_stats_service.js +7 -5
  36. package/build/src/host/assets/webauthn.js +2 -2
  37. package/build/src/host/login_attempt.js +3 -2
  38. package/build/src/host/login_notify.js +3 -2
  39. package/build/src/host/session_context.js +3 -2
  40. package/build/src/host/ui-dist/assets/{index-Dp__4wlB.js → index-CkCQ94la.js} +3 -3
  41. package/build/src/host/ui-dist/index.html +1 -1
  42. package/build/src/pagination.d.ts +77 -0
  43. package/build/src/pagination.js +66 -0
  44. package/package.json +14 -14
@@ -29,6 +29,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
29
29
  };
30
30
  import { RuntimeSettings } from '../host/runtime_settings.js';
31
31
  import { resolveEffectiveAccountExpiration } from '../host/runtime_toggles.js';
32
+ import { ADMIN_LIST_DEFAULT_SIZE, LIST_FIRST_PAGE } from '../pagination.js';
32
33
  import { resolveAuthkitConfig } from './resolve_config.js';
33
34
  // ---------------------------------------------------------------------------
34
35
  // Core logic (pura, testável sem Ace)
@@ -76,10 +77,10 @@ export async function runExpireScan(app, opts) {
76
77
  let warnedCount = 0;
77
78
  let dedupedCount = 0;
78
79
  let scanned = 0;
79
- let page = 1;
80
- const limit = 100;
80
+ let page = LIST_FIRST_PAGE;
81
+ const size = ADMIN_LIST_DEFAULT_SIZE;
81
82
  while (true) {
82
- const batch = await accountStore.listAccounts({ page, limit });
83
+ const batch = await accountStore.listAccounts({ page, size });
83
84
  if (batch.data.length === 0)
84
85
  break;
85
86
  for (const account of batch.data) {
@@ -127,7 +128,7 @@ export async function runExpireScan(app, opts) {
127
128
  }
128
129
  }
129
130
  }
130
- if (batch.data.length < limit)
131
+ if (batch.data.length < size)
131
132
  break;
132
133
  page++;
133
134
  }
@@ -157,8 +158,8 @@ async function getLastLoginMs(audit, accountId) {
157
158
  const result = await audit.list({
158
159
  type: 'login.success',
159
160
  subject: accountId,
160
- page: 1,
161
- limit: 1,
161
+ page: LIST_FIRST_PAGE,
162
+ size: 1,
162
163
  });
163
164
  if (result.data.length === 0)
164
165
  return null;
@@ -181,8 +182,8 @@ async function wasWarnedRecently(audit, accountId, warnDays) {
181
182
  const result = await audit.list({
182
183
  type: 'account.expiration_warned',
183
184
  subject: accountId,
184
- page: 1,
185
- limit: 1,
185
+ page: LIST_FIRST_PAGE,
186
+ size: 1,
186
187
  });
187
188
  if (result.data.length === 0)
188
189
  return false;
@@ -1,7 +1,8 @@
1
1
  import { supportsOrganizations, supportsPasskeys, supportsProviderIdentity, } from '../accounts/account_store.js';
2
+ import { LIST_FIRST_PAGE } from '../pagination.js';
2
3
  import { AdminSessionsService } from './admin_sessions_service.js';
3
4
  /** Limite de eventos de audit incluídos no export (evita payloads gigantes). */
4
- const AUDIT_EXPORT_LIMIT = 1000;
5
+ const AUDIT_EXPORT_SIZE = 1000;
5
6
  /**
6
7
  * Monta o pacote de dados de uma conta para portabilidade (LGPD/GDPR). NUNCA
7
8
  * inclui segredos: hash de senha, segredo TOTP, recovery codes, chave pública de
@@ -102,8 +103,8 @@ export class AccountExportService {
102
103
  try {
103
104
  const page = await cfg.audit.list({
104
105
  subject: accountId,
105
- page: 1,
106
- limit: AUDIT_EXPORT_LIMIT,
106
+ page: LIST_FIRST_PAGE,
107
+ size: AUDIT_EXPORT_SIZE,
107
108
  });
108
109
  auditLog = page.data.map((e) => ({
109
110
  type: e.type,
@@ -1,4 +1,5 @@
1
1
  import { supportsOrganizations } from '../../accounts/account_store.js';
2
+ import { ADMIN_LIST_DEFAULT_SIZE, LIST_FIRST_PAGE } from '../../pagination.js';
2
3
  import { accountPath } from '../account_paths.js';
3
4
  import { sendOrgInvitationEmail } from '../default_mailer.js';
4
5
  import { isRoleInCatalog, resolveRoleCatalogList } from '../runtime_toggles.js';
@@ -50,14 +51,14 @@ export class AdminOrgsService {
50
51
  // O store não tem listAllOrgs — acumulamos via memberships de todas as contas.
51
52
  // Para admin, fazemos um listAccounts full e coletamos todas as orgs únicas.
52
53
  // Mais eficiente: usa a tabela de organizations diretamente pelo listOrgMembers
53
- // mas o store não expõe listAllOrgs. Usamos hack: listAccounts com limit alto,
54
+ // mas o store não expõe listAllOrgs. Usamos hack: listAccounts com size alto,
54
55
  // coletamos orgs via listOrgsForAccount por conta.
55
56
  // Para evitar N+1 excessivo, usamos uma abordagem "seen" de IDs.
56
57
  const seen = new Map();
57
- let page = 1;
58
- const limit = 100;
58
+ let page = LIST_FIRST_PAGE;
59
+ const size = ADMIN_LIST_DEFAULT_SIZE;
59
60
  while (true) {
60
- const result = await store.listAccounts({ page, limit });
61
+ const result = await store.listAccounts({ page, size });
61
62
  for (const account of result.data) {
62
63
  const orgs = await store.listOrgsForAccount(account.id);
63
64
  for (const org of orgs) {
@@ -68,7 +69,7 @@ export class AdminOrgsService {
68
69
  }
69
70
  }
70
71
  }
71
- if (result.data.length < limit)
72
+ if (result.data.length < size)
72
73
  break;
73
74
  page++;
74
75
  }
@@ -121,12 +121,12 @@ export declare class AdminUsersService {
121
121
  * conta é contada no máximo uma vez via {@link hasAdminRole}).
122
122
  *
123
123
  * Terminadores honestos (sem número mágico): a paginação para quando
124
- * - a página veio incompleta (`data.length < pageSize`) → última página, OU
124
+ * - a página veio incompleta (`data.length < size`) → última página, OU
125
125
  * - a página veio vazia (`data.length === 0`) → sem mais dados, OU
126
- * - já cobrimos o total reportado (`page * pageSize >= total`).
126
+ * - já cobrimos o total reportado (`page * size >= total`).
127
127
  * Uma página COMPLETA com `total` ainda maior continua paginando. Isto encerra
128
128
  * de forma garantida para qualquer store que devolva páginas finitas (a última
129
- * página é, por definição, menor que `pageSize` ou vazia).
129
+ * página é, por definição, menor que `size` ou vazia).
130
130
  */
131
131
  private countAdminsByScan;
132
132
  /**
@@ -1,5 +1,6 @@
1
1
  import { randomBytes } from 'node:crypto';
2
2
  import { supportsAccountDeletion, supportsAccountStatus, supportsCountByGlobalRole, supportsProfile, } from '../../accounts/account_store.js';
3
+ import { ADMIN_LIST_DEFAULT_SIZE, LIST_FIRST_PAGE } from '../../pagination.js';
3
4
  import { PasswordPolicyError } from '../../password/password_manager.js';
4
5
  import { AccountDeletionService } from '../account_deletion_service.js';
5
6
  import { sendPasswordResetEmail } from '../default_mailer.js';
@@ -179,28 +180,28 @@ export class AdminUsersService {
179
180
  * conta é contada no máximo uma vez via {@link hasAdminRole}).
180
181
  *
181
182
  * Terminadores honestos (sem número mágico): a paginação para quando
182
- * - a página veio incompleta (`data.length < pageSize`) → última página, OU
183
+ * - a página veio incompleta (`data.length < size`) → última página, OU
183
184
  * - a página veio vazia (`data.length === 0`) → sem mais dados, OU
184
- * - já cobrimos o total reportado (`page * pageSize >= total`).
185
+ * - já cobrimos o total reportado (`page * size >= total`).
185
186
  * Uma página COMPLETA com `total` ainda maior continua paginando. Isto encerra
186
187
  * de forma garantida para qualquer store que devolva páginas finitas (a última
187
- * página é, por definição, menor que `pageSize` ou vazia).
188
+ * página é, por definição, menor que `size` ou vazia).
188
189
  */
189
190
  async countAdminsByScan() {
190
191
  const store = this.cfg.accountStore;
191
- const pageSize = 100;
192
- let page = 1;
192
+ const size = ADMIN_LIST_DEFAULT_SIZE;
193
+ let page = LIST_FIRST_PAGE;
193
194
  let count = 0;
194
195
  while (true) {
195
196
  const { data, total } = await store.listAccounts({
196
197
  page,
197
- limit: pageSize,
198
+ size,
198
199
  });
199
200
  for (const acc of data) {
200
201
  if (this.hasAdminRole(acc.globalRoles ?? []))
201
202
  count++;
202
203
  }
203
- if (data.length < pageSize || page * pageSize >= total)
204
+ if (data.length < size || page * size >= total)
204
205
  break;
205
206
  page++;
206
207
  }
@@ -5,12 +5,14 @@ import type { HttpContext } from '@adonisjs/core/http';
5
5
  * introspecção genérica de token (`POST /tokens/verify`).
6
6
  */
7
7
  export default class ApiMiscController {
8
- /** GET /audit — listagem paginada (501 JSON quando o sink não consulta). */
8
+ /** GET /audit — `{ meta: { page, size, total }, data }` (501 JSON quando o sink não consulta). */
9
9
  audit(ctx: HttpContext): Promise<void | {
10
+ meta: {
11
+ page: number;
12
+ size: number;
13
+ total: any;
14
+ };
10
15
  data: any;
11
- total: any;
12
- page: number;
13
- limit: number;
14
16
  }>;
15
17
  /** GET /stats — métricas-resumo do IdP (totais + MAU + séries de 30 dias). */
16
18
  stats(ctx: HttpContext): Promise<import("../admin_stats_service.js").AdminStats>;
@@ -1,4 +1,5 @@
1
1
  import '../augmentations.js';
2
+ import { ADMIN_LIST_HTTP_DEFAULT_SIZE, parseListPage, parseListSize } from '../../pagination.js';
2
3
  import { AdminSessionsService } from '../admin_sessions_service.js';
3
4
  import { computeAdminStats } from '../admin_stats_service.js';
4
5
  import { tokenVerifyValidator } from '../admin_validators.js';
@@ -9,7 +10,7 @@ import { TokenVerifyService } from './token_verify_service.js';
9
10
  * introspecção genérica de token (`POST /tokens/verify`).
10
11
  */
11
12
  export default class ApiMiscController {
12
- /** GET /audit — listagem paginada (501 JSON quando o sink não consulta). */
13
+ /** GET /audit — `{ meta: { page, size, total }, data }` (501 JSON quando o sink não consulta). */
13
14
  async audit(ctx) {
14
15
  const service = await ctx.containerResolver.make('authkit.server');
15
16
  const cfg = service.config;
@@ -19,12 +20,12 @@ export default class ApiMiscController {
19
20
  .status(501)
20
21
  .send(apiError('not_implemented', 'O sink de auditoria configurado não suporta consulta.'));
21
22
  }
22
- const page = Math.max(1, Number.parseInt(ctx.request.input('page', '1'), 10) || 1);
23
- const limit = Math.max(1, Math.min(100, Number.parseInt(ctx.request.input('limit', '20'), 10) || 20));
23
+ const page = parseListPage(ctx.request.input('page'));
24
+ const size = parseListSize(ctx.request.input('size'), ADMIN_LIST_HTTP_DEFAULT_SIZE);
24
25
  const type = ctx.request.input('type')?.trim() || undefined;
25
26
  const subject = ctx.request.input('subject')?.trim() || undefined;
26
- const result = await sink.list({ page, limit, type, subject });
27
- return { data: result.data.map(auditDto), total: result.total, page, limit };
27
+ const result = await sink.list({ page, size, type, subject });
28
+ return { meta: { page, size, total: result.total }, data: result.data.map(auditDto) };
28
29
  }
29
30
  /** GET /stats — métricas-resumo do IdP (totais + MAU + séries de 30 dias). */
30
31
  async stats(ctx) {
@@ -6,12 +6,14 @@ import type { HttpContext } from '@adonisjs/core/http';
6
6
  */
7
7
  export default class ApiUsersController {
8
8
  #private;
9
- /** GET /users — listagem paginada com busca por e-mail. */
9
+ /** GET /users — `{ meta: { page, size, total }, data }`, com busca por e-mail. */
10
10
  index(ctx: HttpContext): Promise<{
11
+ meta: {
12
+ page: number;
13
+ size: number;
14
+ total: any;
15
+ };
11
16
  data: any[];
12
- total: any;
13
- page: number;
14
- limit: number;
15
17
  }>;
16
18
  /** GET /users/:id */
17
19
  show(ctx: HttpContext): Promise<void | {
@@ -1,11 +1,11 @@
1
1
  import '../augmentations.js';
2
+ import { ADMIN_LIST_HTTP_DEFAULT_SIZE, parseListPage, parseListSize } from '../../pagination.js';
2
3
  import { AdminSessionsService } from '../admin_sessions_service.js';
3
4
  import { adminUserCreateValidator, adminUserUpdateValidator } from '../admin_validators.js';
4
5
  import { resolveRuntimeSettings } from '../runtime_settings.js';
5
6
  import { enrichSessionsWithContext } from '../session_context.js';
6
7
  import { AdminUsersService } from './admin_users_service.js';
7
8
  import { apiError, grantDto, sessionDto, userDto } from './dto.js';
8
- const PAGE_SIZE = 20;
9
9
  /**
10
10
  * Lê a config + monta o actor `admin-api` para auditoria. O `actorId` recebe o
11
11
  * id NÃO-SENSÍVEL da API key que autenticou (anexado pelo `adminApiGuard` em
@@ -27,16 +27,16 @@ async function ctxBits(ctx) {
27
27
  * envelope `{ error: { code, message } }`. Toda escrita audita com `actor: 'admin-api'`.
28
28
  */
29
29
  export default class ApiUsersController {
30
- /** GET /users — listagem paginada com busca por e-mail. */
30
+ /** GET /users — `{ meta: { page, size, total }, data }`, com busca por e-mail. */
31
31
  async index(ctx) {
32
32
  const { cfg } = await ctxBits(ctx);
33
33
  const search = ctx.request.input('search', '').trim();
34
- const page = Math.max(1, Number.parseInt(ctx.request.input('page', '1'), 10) || 1);
35
- const limit = Math.max(1, Math.min(100, Number.parseInt(ctx.request.input('limit', String(PAGE_SIZE)), 10) || PAGE_SIZE));
36
- const result = await cfg.accountStore.listAccounts({ search, page, limit });
34
+ const page = parseListPage(ctx.request.input('page'));
35
+ const size = parseListSize(ctx.request.input('size'), ADMIN_LIST_HTTP_DEFAULT_SIZE);
36
+ const result = await cfg.accountStore.listAccounts({ search, page, size });
37
37
  const users = new AdminUsersService(cfg);
38
38
  const data = await Promise.all(result.data.map(async (u) => userDto(u, await users.isDisabled(u.id))));
39
- return { data, total: result.total, page, limit };
39
+ return { meta: { page, size, total: result.total }, data };
40
40
  }
41
41
  /** GET /users/:id */
42
42
  async show(ctx) {
@@ -3,15 +3,17 @@ import type { HttpContext } from '@adonisjs/core/http';
3
3
  /**
4
4
  * Endpoints JSON do log de auditoria do console admin React.
5
5
  *
6
- * GET {prefix}/api/audit?type=&page=&limit= → listagem paginada
6
+ * GET {prefix}/api/audit?type=&page=&size= → `{ meta: { page, size, total }, data }`
7
7
  *
8
8
  * 404 honesto (`capability_unsupported`) quando o sink não suporta consulta.
9
9
  */
10
10
  export default class ConsoleAuditController {
11
11
  index(ctx: HttpContext): Promise<void | {
12
+ meta: {
13
+ page: number;
14
+ size: number;
15
+ total: any;
16
+ };
12
17
  data: any;
13
- total: any;
14
- page: number;
15
- limit: number;
16
18
  }>;
17
19
  }
@@ -1,9 +1,10 @@
1
1
  import '../augmentations.js';
2
+ import { ADMIN_LIST_HTTP_DEFAULT_SIZE, parseListPage, parseListSize } from '../../pagination.js';
2
3
  import { apiError, auditDto } from '../admin_api/dto.js';
3
4
  /**
4
5
  * Endpoints JSON do log de auditoria do console admin React.
5
6
  *
6
- * GET {prefix}/api/audit?type=&page=&limit= → listagem paginada
7
+ * GET {prefix}/api/audit?type=&page=&size= → `{ meta: { page, size, total }, data }`
7
8
  *
8
9
  * 404 honesto (`capability_unsupported`) quando o sink não suporta consulta.
9
10
  */
@@ -15,11 +16,11 @@ export default class ConsoleAuditController {
15
16
  if (!sink || typeof sink.list !== 'function') {
16
17
  return ctx.response.notFound(apiError('capability_unsupported', 'O sink de auditoria configurado não suporta consulta.'));
17
18
  }
18
- const page = Math.max(1, Number.parseInt(ctx.request.input('page', '1'), 10) || 1);
19
- const limit = Math.max(1, Math.min(100, Number.parseInt(ctx.request.input('limit', '20'), 10) || 20));
19
+ const page = parseListPage(ctx.request.input('page'));
20
+ const size = parseListSize(ctx.request.input('size'), ADMIN_LIST_HTTP_DEFAULT_SIZE);
20
21
  const type = ctx.request.input('type')?.trim() || undefined;
21
22
  const subject = ctx.request.input('subject')?.trim() || undefined;
22
- const result = await sink.list({ page, limit, type, subject });
23
- return { data: result.data.map(auditDto), total: result.total, page, limit };
23
+ const result = await sink.list({ page, size, type, subject });
24
+ return { meta: { page, size, total: result.total }, data: result.data.map(auditDto) };
24
25
  }
25
26
  }
@@ -1,4 +1,5 @@
1
1
  import '../augmentations.js';
2
+ import { LIST_FIRST_PAGE } from '../../pagination.js';
2
3
  import { AdminSessionsService } from '../admin_sessions_service.js';
3
4
  import { computeAdminStats } from '../admin_stats_service.js';
4
5
  /**
@@ -17,7 +18,9 @@ export default class ConsoleOverviewController {
17
18
  const sessions = new AdminSessionsService(service);
18
19
  const stats = await computeAdminStats(cfg, sessions);
19
20
  const clientsCount = cfg.clients.length;
20
- const recentResult = typeof cfg.audit?.list === 'function' ? await cfg.audit.list({ page: 1, limit: 5 }) : null;
21
+ const recentResult = typeof cfg.audit?.list === 'function'
22
+ ? await cfg.audit.list({ page: LIST_FIRST_PAGE, size: 5 })
23
+ : null;
21
24
  return {
22
25
  usersTotal: stats.totalUsers,
23
26
  activeSessions: stats.activeSessions,
@@ -3,7 +3,7 @@ import type { HttpContext } from '@adonisjs/core/http';
3
3
  /**
4
4
  * Endpoints JSON de usuários do console admin React.
5
5
  *
6
- * GET {prefix}/api/users?search=&page=&perPage= → lista paginada + roles
6
+ * GET {prefix}/api/users?search=&page=&size= → `{ meta: { page, size, total }, data }`
7
7
  * GET {prefix}/api/users/:id → detalhe + sessões + identidades + MFA status
8
8
  * POST {prefix}/api/users → criar usuário
9
9
  * PATCH {prefix}/api/users/:id/roles → substituir roles globais
@@ -20,10 +20,12 @@ export default class ConsoleUsersController {
20
20
  #private;
21
21
  /** GET {prefix}/api/users */
22
22
  index(ctx: HttpContext): Promise<{
23
+ meta: {
24
+ page: number;
25
+ size: number;
26
+ total: any;
27
+ };
23
28
  data: any[];
24
- total: any;
25
- page: number;
26
- perPage: number;
27
29
  }>;
28
30
  /** GET {prefix}/api/users/:id */
29
31
  show(ctx: HttpContext): Promise<void | {
@@ -1,5 +1,6 @@
1
1
  import '../augmentations.js';
2
2
  import { supportsAccountDeletion, supportsAccountStatus } from '../../accounts/account_store.js';
3
+ import { ADMIN_LIST_HTTP_DEFAULT_SIZE, parseListPage, parseListSize } from '../../pagination.js';
3
4
  import { ACCOUNT_SESSION_KEY } from '../account_session_key.js';
4
5
  import { AdminUsersService } from '../admin_api/admin_users_service.js';
5
6
  import { apiError, grantDto, sessionDto, userDto } from '../admin_api/dto.js';
@@ -9,7 +10,6 @@ import { resolveRuntimeSettings } from '../runtime_settings.js';
9
10
  import { resolveEffectiveRolesCatalog } from '../runtime_toggles.js';
10
11
  import { enrichSessionsWithContext } from '../session_context.js';
11
12
  import { requireSudo } from '../sudo_mode.js';
12
- const PAGE_SIZE = 20;
13
13
  /**
14
14
  * Gate de sudo (M9) para ações destrutivas do console (delete user, disable,
15
15
  * reset-password): exige confirmação de identidade RECENTE do próprio admin
@@ -34,7 +34,7 @@ async function gateSudo(ctx) {
34
34
  /**
35
35
  * Endpoints JSON de usuários do console admin React.
36
36
  *
37
- * GET {prefix}/api/users?search=&page=&perPage= → lista paginada + roles
37
+ * GET {prefix}/api/users?search=&page=&size= → `{ meta: { page, size, total }, data }`
38
38
  * GET {prefix}/api/users/:id → detalhe + sessões + identidades + MFA status
39
39
  * POST {prefix}/api/users → criar usuário
40
40
  * PATCH {prefix}/api/users/:id/roles → substituir roles globais
@@ -53,16 +53,16 @@ export default class ConsoleUsersController {
53
53
  const service = await ctx.containerResolver.make('authkit.server');
54
54
  const cfg = service.config;
55
55
  const search = ctx.request.input('search', '').trim();
56
- const page = Math.max(1, Number.parseInt(ctx.request.input('page', '1'), 10) || 1);
57
- const perPage = Math.max(1, Math.min(100, Number.parseInt(ctx.request.input('perPage', String(PAGE_SIZE)), 10) || PAGE_SIZE));
56
+ const page = parseListPage(ctx.request.input('page'));
57
+ const size = parseListSize(ctx.request.input('size'), ADMIN_LIST_HTTP_DEFAULT_SIZE);
58
58
  const result = await cfg.accountStore.listAccounts({
59
59
  search,
60
60
  page,
61
- limit: perPage,
61
+ size,
62
62
  });
63
63
  const users = new AdminUsersService(cfg);
64
64
  const data = await Promise.all(result.data.map(async (u) => userDto(u, await users.isDisabled(u.id))));
65
- return { data, total: result.total, page, perPage };
65
+ return { meta: { page, size, total: result.total }, data };
66
66
  }
67
67
  /** GET {prefix}/api/users/:id */
68
68
  async show(ctx) {
@@ -1,3 +1,4 @@
1
+ import { ADMIN_LIST_MAX_SIZE, LIST_FIRST_PAGE } from '../pagination.js';
1
2
  /** Janela de agregação (dias). */
2
3
  const WINDOW_DAYS = 30;
3
4
  /**
@@ -19,7 +20,8 @@ const MAX_EVENTS = 10000;
19
20
  * nativa e expor um `stats()` próprio no futuro.
20
21
  */
21
22
  export async function computeAdminStats(cfg, sessionsService) {
22
- const totalUsers = (await cfg.accountStore.listAccounts({ page: 1, limit: 1 })).total;
23
+ const totalUsers = (await cfg.accountStore.listAccounts({ page: LIST_FIRST_PAGE, size: 1 }))
24
+ .total;
23
25
  const activeSessions = sessionsService.canList
24
26
  ? await sessionsService.countActiveSessions()
25
27
  : null;
@@ -65,10 +67,10 @@ export async function computeAdminStats(cfg, sessionsService) {
65
67
  */
66
68
  async function loadEventsInWindow(audit, type, windowStartMs) {
67
69
  const out = [];
68
- const limit = 200;
69
- let page = 1;
70
+ const size = ADMIN_LIST_MAX_SIZE;
71
+ let page = LIST_FIRST_PAGE;
70
72
  while (out.length < MAX_EVENTS) {
71
- const result = await audit.list({ type, page, limit });
73
+ const result = await audit.list({ type, page, size });
72
74
  if (result.data.length === 0)
73
75
  break;
74
76
  let allBelowWindow = true;
@@ -86,7 +88,7 @@ async function loadEventsInWindow(audit, type, windowStartMs) {
86
88
  // Página inteira anterior à janela → não há mais nada relevante (ordem desc).
87
89
  if (allBelowWindow)
88
90
  break;
89
- if (result.data.length < limit)
91
+ if (result.data.length < size)
90
92
  break;
91
93
  page += 1;
92
94
  }
@@ -1,2 +1,2 @@
1
- function l(e){let t=new Uint8Array(e),r="";for(let n of t)r+=String.fromCharCode(n);return btoa(r).replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,"")}function h(e){let t=e.replace(/-/g,"+").replace(/_/g,"/"),r=(4-t.length%4)%4,i=t.padEnd(t.length+r,"="),n=atob(i),c=new ArrayBuffer(n.length),f=new Uint8Array(c);for(let a=0;a<n.length;a++)f[a]=n.charCodeAt(a);return c}function p(){return C.stubThis(globalThis?.PublicKeyCredential!==void 0&&typeof globalThis.PublicKeyCredential=="function")}var C={stubThis:e=>e};function R(e){let{id:t}=e;return{...e,id:h(t),transports:e.transports}}function w(e){return e==="localhost"||/^((xn--[a-z0-9-]+|[a-z0-9]+(-[a-z0-9]+)*)\.)+([a-z]{2,}|xn--[a-z0-9-]+)$/i.test(e)}var o=class extends Error{constructor({message:t,code:r,cause:i,name:n}){super(t,{cause:i}),Object.defineProperty(this,"code",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),this.name=n??i.name,this.code=r}};function I({error:e,options:t}){let{publicKey:r}=t;if(!r)throw Error("options was missing required publicKey property");if(e.name==="AbortError"){if(t.signal instanceof AbortSignal)return new o({message:"Registration ceremony was sent an abort signal",code:"ERROR_CEREMONY_ABORTED",cause:e})}else if(e.name==="ConstraintError"){if(r.authenticatorSelection?.requireResidentKey===!0)return new o({message:"Discoverable credentials were required but no available authenticator supported it",code:"ERROR_AUTHENTICATOR_MISSING_DISCOVERABLE_CREDENTIAL_SUPPORT",cause:e});if(t.mediation==="conditional"&&r.authenticatorSelection?.userVerification==="required")return new o({message:"User verification was required during automatic registration but it could not be performed",code:"ERROR_AUTO_REGISTER_USER_VERIFICATION_FAILURE",cause:e});if(r.authenticatorSelection?.userVerification==="required")return new o({message:"User verification was required but no available authenticator supported it",code:"ERROR_AUTHENTICATOR_MISSING_USER_VERIFICATION_SUPPORT",cause:e})}else{if(e.name==="InvalidStateError")return new o({message:"The authenticator was previously registered",code:"ERROR_AUTHENTICATOR_PREVIOUSLY_REGISTERED",cause:e});if(e.name==="NotAllowedError")return new o({message:e.message,code:"ERROR_PASSTHROUGH_SEE_CAUSE_PROPERTY",cause:e});if(e.name==="NotSupportedError")return r.pubKeyCredParams.filter(n=>n.type==="public-key").length===0?new o({message:'No entry in pubKeyCredParams was of type "public-key"',code:"ERROR_MALFORMED_PUBKEYCREDPARAMS",cause:e}):new o({message:"No available authenticator supported any of the specified pubKeyCredParams algorithms",code:"ERROR_AUTHENTICATOR_NO_SUPPORTED_PUBKEYCREDPARAMS_ALG",cause:e});if(e.name==="SecurityError"){let i=globalThis.location.hostname;if(w(i)){if(r.rp.id!==i)return new o({message:`The RP ID "${r.rp.id}" is invalid for this domain`,code:"ERROR_INVALID_RP_ID",cause:e})}else return new o({message:`${globalThis.location.hostname} is an invalid domain`,code:"ERROR_INVALID_DOMAIN",cause:e})}else if(e.name==="TypeError"){if(r.user.id.byteLength<1||r.user.id.byteLength>64)return new o({message:"User ID was not between 1 and 64 characters",code:"ERROR_INVALID_USER_ID_LENGTH",cause:e})}else if(e.name==="UnknownError")return new o({message:"The authenticator was unable to process the specified options, or could not create a new credential",code:"ERROR_AUTHENTICATOR_GENERAL_ERROR",cause:e})}return e}var S=class{constructor(){Object.defineProperty(this,"controller",{enumerable:!0,configurable:!0,writable:!0,value:void 0})}createNewAbortSignal(){if(this.controller){let r=new Error("Cancelling existing WebAuthn API call for new one");r.name="AbortError",this.controller.abort(r)}let t=new AbortController;return this.controller=t,t.signal}cancelCeremony(){if(this.controller){let t=new Error("Manually cancelling existing WebAuthn API call");t.name="AbortError",this.controller.abort(t),this.controller=void 0}}},A=new S;var x=["cross-platform","platform"];function E(e){if(e&&!(x.indexOf(e)<0))return e}async function N(e){!e.optionsJSON&&e.challenge&&(console.warn("startRegistration() was not called correctly. It will try to continue with the provided options, but this call should be refactored to use the expected call structure instead. See https://simplewebauthn.dev/docs/packages/browser#typeerror-cannot-read-properties-of-undefined-reading-challenge for more information."),e={optionsJSON:e});let{optionsJSON:t,useAutoRegister:r=!1}=e;if(!p())throw new Error("WebAuthn is not supported in this browser");let i={...t,challenge:h(t.challenge),user:{...t.user,id:h(t.user.id)},excludeCredentials:t.excludeCredentials?.map(R)},n={};r&&(n.mediation="conditional"),n.publicKey=i,n.signal=A.createNewAbortSignal();let c;try{c=await navigator.credentials.create(n)}catch(u){throw I({error:u,options:n})}if(!c)throw new Error("Registration was not completed");let{id:f,rawId:a,response:s,type:y}=c,d;typeof s.getTransports=="function"&&(d=s.getTransports());let g;if(typeof s.getPublicKeyAlgorithm=="function")try{g=s.getPublicKeyAlgorithm()}catch(u){O("getPublicKeyAlgorithm()",u)}let b;if(typeof s.getPublicKey=="function")try{let u=s.getPublicKey();u!==null&&(b=l(u))}catch(u){O("getPublicKey()",u)}let m;if(typeof s.getAuthenticatorData=="function")try{m=l(s.getAuthenticatorData())}catch(u){O("getAuthenticatorData()",u)}return{id:f,rawId:l(a),response:{attestationObject:l(s.attestationObject),clientDataJSON:l(s.clientDataJSON),transports:d,publicKeyAlgorithm:g,publicKey:b,authenticatorData:m},type:y,clientExtensionResults:c.getClientExtensionResults(),authenticatorAttachment:E(c.authenticatorAttachment)}}function O(e,t){console.warn(`The browser extension that intercepted this WebAuthn API call incorrectly implemented ${e}. You should report this error to them.
2
- `,t)}function _(){if(!p())return T.stubThis(new Promise(t=>t(!1)));let e=globalThis.PublicKeyCredential;return e?.isConditionalMediationAvailable===void 0?T.stubThis(new Promise(t=>t(!1))):T.stubThis(e.isConditionalMediationAvailable())}var T={stubThis:e=>e};function P({error:e,options:t}){let{publicKey:r}=t;if(!r)throw Error("options was missing required publicKey property");if(e.name==="AbortError"){if(t.signal instanceof AbortSignal)return new o({message:"Authentication ceremony was sent an abort signal",code:"ERROR_CEREMONY_ABORTED",cause:e})}else{if(e.name==="NotAllowedError")return new o({message:e.message,code:"ERROR_PASSTHROUGH_SEE_CAUSE_PROPERTY",cause:e});if(e.name==="SecurityError"){let i=globalThis.location.hostname;if(w(i)){if(r.rpId!==i)return new o({message:`The RP ID "${r.rpId}" is invalid for this domain`,code:"ERROR_INVALID_RP_ID",cause:e})}else return new o({message:`${globalThis.location.hostname} is an invalid domain`,code:"ERROR_INVALID_DOMAIN",cause:e})}else if(e.name==="UnknownError")return new o({message:"The authenticator was unable to process the specified options, or could not create a new assertion signature",code:"ERROR_AUTHENTICATOR_GENERAL_ERROR",cause:e})}return e}async function D(e){!e.optionsJSON&&e.challenge&&(console.warn("startAuthentication() was not called correctly. It will try to continue with the provided options, but this call should be refactored to use the expected call structure instead. See https://simplewebauthn.dev/docs/packages/browser#typeerror-cannot-read-properties-of-undefined-reading-challenge for more information."),e={optionsJSON:e});let{optionsJSON:t,useBrowserAutofill:r=!1,verifyBrowserAutofillInput:i=!0}=e;if(!p())throw new Error("WebAuthn is not supported in this browser");let n;t.allowCredentials?.length!==0&&(n=t.allowCredentials?.map(R));let c={...t,challenge:h(t.challenge),allowCredentials:n},f={};if(r){if(!await _())throw Error("Browser does not support WebAuthn autofill");if(document.querySelectorAll("input[autocomplete$='webauthn']").length<1&&i)throw Error('No <input> with "webauthn" as the only or last value in its `autocomplete` attribute was detected');f.mediation="conditional",c.allowCredentials=[]}f.publicKey=c,f.signal=A.createNewAbortSignal();let a;try{a=await navigator.credentials.get(f)}catch(m){throw P({error:m,options:f})}if(!a)throw new Error("Authentication was not completed");let{id:s,rawId:y,response:d,type:g}=a,b;return d.userHandle&&(b=l(d.userHandle)),{id:s,rawId:l(y),response:{authenticatorData:l(d.authenticatorData),clientDataJSON:l(d.clientDataJSON),signature:l(d.signature),userHandle:b},type:g,clientExtensionResults:a.getClientExtensionResults(),authenticatorAttachment:E(a.authenticatorAttachment)}}function v(){return p()?PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable():new Promise(e=>e(!1))}export{A as WebAuthnAbortService,o as WebAuthnError,p as browserSupportsWebAuthn,_ as browserSupportsWebAuthnAutofill,v as platformAuthenticatorIsAvailable,D as startAuthentication,N as startRegistration};
1
+ function l(e){let t=new Uint8Array(e),r="";for(let i of t)r+=String.fromCharCode(i);return btoa(r).replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,"")}function b(e){let t=e.replace(/-/g,"+").replace(/_/g,"/"),r=(4-t.length%4)%4,o=t.padEnd(t.length+r,"="),i=atob(o),u=new ArrayBuffer(i.length),f=new Uint8Array(u);for(let a=0;a<i.length;a++)f[a]=i.charCodeAt(a);return u}function d(){return O.stubThis(globalThis?.PublicKeyCredential!==void 0&&typeof globalThis.PublicKeyCredential=="function")}var O={stubThis:e=>e};function y(e){let{id:t}=e;return{...e,id:b(t),transports:e.transports,type:e.type}}function g(e){return e==="localhost"||/^((xn--[a-z0-9-]+|[a-z0-9]+(-[a-z0-9]+)*)\.)+([a-z]{2,}|xn--[a-z0-9-]+)$/i.test(e)}var n=class extends Error{constructor({message:t,code:r,cause:o,name:i}){super(t,{cause:o}),Object.defineProperty(this,"code",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),this.name=i??o.name,this.code=r}};function P({error:e,options:t}){let{publicKey:r}=t;if(!r)throw Error("options was missing required publicKey property");if(e.name==="AbortError"){if(t.signal instanceof AbortSignal)return new n({message:"Registration ceremony was sent an abort signal",code:"ERROR_CEREMONY_ABORTED",cause:e})}else if(e.name==="ConstraintError"){if(r.authenticatorSelection?.requireResidentKey===!0)return new n({message:"Discoverable credentials were required but no available authenticator supported it",code:"ERROR_AUTHENTICATOR_MISSING_DISCOVERABLE_CREDENTIAL_SUPPORT",cause:e});if(t.mediation==="conditional"&&r.authenticatorSelection?.userVerification==="required")return new n({message:"User verification was required during automatic registration but it could not be performed",code:"ERROR_AUTO_REGISTER_USER_VERIFICATION_FAILURE",cause:e});if(r.authenticatorSelection?.userVerification==="required")return new n({message:"User verification was required but no available authenticator supported it",code:"ERROR_AUTHENTICATOR_MISSING_USER_VERIFICATION_SUPPORT",cause:e})}else{if(e.name==="InvalidStateError")return new n({message:"The authenticator was previously registered",code:"ERROR_AUTHENTICATOR_PREVIOUSLY_REGISTERED",cause:e});if(e.name==="NotAllowedError")return new n({message:e.message,code:"ERROR_PASSTHROUGH_SEE_CAUSE_PROPERTY",cause:e});if(e.name==="NotSupportedError")return r.pubKeyCredParams.filter(i=>i.type==="public-key").length===0?new n({message:'No entry in pubKeyCredParams was of type "public-key"',code:"ERROR_MALFORMED_PUBKEYCREDPARAMS",cause:e}):new n({message:"No available authenticator supported any of the specified pubKeyCredParams algorithms",code:"ERROR_AUTHENTICATOR_NO_SUPPORTED_PUBKEYCREDPARAMS_ALG",cause:e});if(e.name==="SecurityError"){let o=globalThis.location.hostname;if(g(o)){if(r.rp.id!==o)return new n({message:`The RP ID "${r.rp.id}" is invalid for this domain`,code:"ERROR_INVALID_RP_ID",cause:e})}else return new n({message:`${globalThis.location.hostname} is an invalid domain`,code:"ERROR_INVALID_DOMAIN",cause:e})}else if(e.name==="TypeError"){if(r.user.id.byteLength<1||r.user.id.byteLength>64)return new n({message:"User ID was not between 1 and 64 characters",code:"ERROR_INVALID_USER_ID_LENGTH",cause:e})}else if(e.name==="UnknownError")return new n({message:"The authenticator was unable to process the specified options, or could not create a new credential",code:"ERROR_AUTHENTICATOR_GENERAL_ERROR",cause:e})}return e}var C=class{constructor(){Object.defineProperty(this,"controller",{enumerable:!0,configurable:!0,writable:!0,value:void 0})}createNewAbortSignal(){if(this.controller){let r=new Error("Cancelling existing WebAuthn API call for new one");r.name="AbortError",this.controller.abort(r)}let t=new AbortController;return this.controller=t,t.signal}cancelCeremony(){if(this.controller){let t=new Error("Manually cancelling existing WebAuthn API call");t.name="AbortError",this.controller.abort(t),this.controller=void 0}}},A=new C;var D=["cross-platform","platform"];function R(e){if(e&&!(D.indexOf(e)<0))return e}async function N(e){!e.optionsJSON&&e.challenge&&(console.warn("startRegistration() was not called correctly. It will try to continue with the provided options, but this call should be refactored to use the expected call structure instead. See https://simplewebauthn.dev/docs/packages/browser#typeerror-cannot-read-properties-of-undefined-reading-challenge for more information."),e={optionsJSON:e});let{optionsJSON:t,useAutoRegister:r=!1}=e;if(!d())throw new Error("WebAuthn is not supported in this browser");let o={...t,challenge:b(t.challenge),user:{...t.user,id:b(t.user.id)},excludeCredentials:t.excludeCredentials?.map(y)},i={};r&&(i.mediation="conditional"),i.publicKey=o,i.signal=A.createNewAbortSignal();let u;try{u=await navigator.credentials.create(i)}catch(c){throw P({error:c,options:i})}if(!u)throw new Error("Registration was not completed");let{id:f,rawId:a,response:s,type:E}=u,p;typeof s.getTransports=="function"&&(p=s.getTransports());let w;if(typeof s.getPublicKeyAlgorithm=="function")try{w=s.getPublicKeyAlgorithm()}catch(c){I("getPublicKeyAlgorithm()",c)}let m;if(typeof s.getPublicKey=="function")try{let c=s.getPublicKey();c!==null&&(m=l(c))}catch(c){I("getPublicKey()",c)}let h;if(typeof s.getAuthenticatorData=="function")try{h=l(s.getAuthenticatorData())}catch(c){I("getAuthenticatorData()",c)}return{id:f,rawId:l(a),response:{attestationObject:l(s.attestationObject),clientDataJSON:l(s.clientDataJSON),transports:p,publicKeyAlgorithm:w,publicKey:m,authenticatorData:h},type:E,clientExtensionResults:u.getClientExtensionResults(),authenticatorAttachment:R(u.authenticatorAttachment)}}function I(e,t){console.warn(`The browser extension that intercepted this WebAuthn API call incorrectly implemented ${e}. You should report this error to them.
2
+ `,t)}function T(){if(!d())return S.stubThis(new Promise(t=>t(!1)));let e=globalThis.PublicKeyCredential;return e?.isConditionalMediationAvailable===void 0?S.stubThis(new Promise(t=>t(!1))):S.stubThis(e.isConditionalMediationAvailable())}var S={stubThis:e=>e};function _({error:e,options:t}){let{publicKey:r}=t;if(!r)throw Error("options was missing required publicKey property");if(e.name==="AbortError"){if(t.signal instanceof AbortSignal)return new n({message:"Authentication ceremony was sent an abort signal",code:"ERROR_CEREMONY_ABORTED",cause:e})}else{if(e.name==="NotAllowedError")return new n({message:e.message,code:"ERROR_PASSTHROUGH_SEE_CAUSE_PROPERTY",cause:e});if(e.name==="SecurityError"){let o=globalThis.location.hostname;if(g(o)){if(r.rpId!==o)return new n({message:`The RP ID "${r.rpId}" is invalid for this domain`,code:"ERROR_INVALID_RP_ID",cause:e})}else return new n({message:`${globalThis.location.hostname} is an invalid domain`,code:"ERROR_INVALID_DOMAIN",cause:e})}else if(e.name==="UnknownError")return new n({message:"The authenticator was unable to process the specified options, or could not create a new assertion signature",code:"ERROR_AUTHENTICATOR_GENERAL_ERROR",cause:e})}return e}async function U(e){!e.optionsJSON&&e.challenge&&(console.warn("startAuthentication() was not called correctly. It will try to continue with the provided options, but this call should be refactored to use the expected call structure instead. See https://simplewebauthn.dev/docs/packages/browser#typeerror-cannot-read-properties-of-undefined-reading-challenge for more information."),e={optionsJSON:e});let{optionsJSON:t,useBrowserAutofill:r=!1,verifyBrowserAutofillInput:o=!0}=e;if(!d())throw new Error("WebAuthn is not supported in this browser");let i;t.allowCredentials?.length!==0&&(i=t.allowCredentials?.map(y));let u={...t,challenge:b(t.challenge),allowCredentials:i},f={};if(r){if(!await T())throw Error("Browser does not support WebAuthn autofill");if(document.querySelectorAll("input[autocomplete$='webauthn']").length<1&&o)throw Error('No <input> with "webauthn" as the only or last value in its `autocomplete` attribute was detected');f.mediation="conditional",u.allowCredentials=[]}f.publicKey=u,f.signal=A.createNewAbortSignal();let a;try{a=await navigator.credentials.get(f)}catch(h){throw _({error:h,options:f})}if(!a)throw new Error("Authentication was not completed");let{id:s,rawId:E,response:p,type:w}=a,m;return p.userHandle&&(m=l(p.userHandle)),{id:s,rawId:l(E),response:{authenticatorData:l(p.authenticatorData),clientDataJSON:l(p.clientDataJSON),signature:l(p.signature),userHandle:m},type:w,clientExtensionResults:a.getClientExtensionResults(),authenticatorAttachment:R(a.authenticatorAttachment)}}function x(){return d()?PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable():new Promise(e=>e(!1))}export{A as WebAuthnAbortService,n as WebAuthnError,d as browserSupportsWebAuthn,T as browserSupportsWebAuthnAutofill,x as platformAuthenticatorIsAvailable,U as startAuthentication,N as startRegistration};
@@ -1,4 +1,5 @@
1
1
  import { supportsAccountStatus, supportsEmailVerificationStatus, supportsPasswordExpiration, } from '../accounts/account_store.js';
2
+ import { LIST_FIRST_PAGE } from '../pagination.js';
2
3
  import { createAccountLockout } from './account_lockout.js';
3
4
  import { resolveEffectiveAccountExpiration, resolveEffectiveLockout, resolveEffectivePasswordExpiration, resolveEffectiveRequireVerifiedEmail, resolveEffectiveRequireVerifiedEmailFull, } from './runtime_toggles.js';
4
5
  /**
@@ -100,8 +101,8 @@ export async function isAccountExpired(audit, accountId, settings, logger) {
100
101
  const result = await audit.list({
101
102
  type: 'login.success',
102
103
  subject: accountId,
103
- page: 1,
104
- limit: 1,
104
+ page: LIST_FIRST_PAGE,
105
+ size: 1,
105
106
  });
106
107
  if (result.data.length === 0) {
107
108
  // Nunca logou → considera ativa (conta nova, não inativa).
@@ -1,3 +1,4 @@
1
+ import { ADMIN_LIST_MAX_SIZE, LIST_FIRST_PAGE } from '../pagination.js';
1
2
  import { sendNewDeviceLoginEmail, sendNewLoginEmail } from './default_mailer.js';
2
3
  import { isTrustedDeviceValid, TRUSTED_DEVICE_COOKIE } from './trusted_device.js';
3
4
  /**
@@ -124,8 +125,8 @@ async function maybeNotifyNewLogin(ctx, cfg, data) {
124
125
  const page = await cfg.audit.list({
125
126
  type: 'login.success',
126
127
  subject: accountId,
127
- page: 1,
128
- limit: 200,
128
+ page: LIST_FIRST_PAGE,
129
+ size: ADMIN_LIST_MAX_SIZE,
129
130
  });
130
131
  const sameIpCount = page.data.filter((e) => e.ip === ip).length;
131
132
  // > 1 significa que já havia um login.success deste IP antes do atual → não é novo.
@@ -1,3 +1,4 @@
1
+ import { ADMIN_LIST_MAX_SIZE, LIST_FIRST_PAGE } from '../pagination.js';
1
2
  import { resolveGeoSafe } from './geo.js';
2
3
  import { parseUserAgent } from './user_agent.js';
3
4
  /**
@@ -30,8 +31,8 @@ export async function enrichSessionsWithContext(cfg, accountId, sessions) {
30
31
  const page = await cfg.audit.list({
31
32
  type: 'login.success',
32
33
  subject: accountId,
33
- page: 1,
34
- limit: 200,
34
+ page: LIST_FIRST_PAGE,
35
+ size: ADMIN_LIST_MAX_SIZE,
35
36
  });
36
37
  const events = page.data.map((e) => ({
37
38
  ts: toEpochSeconds(e.createdAt),