@adonis-agora/authkit-server 1.0.0-rc.16 → 1.0.0-rc.17

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 (42) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/build/index.d.ts +3 -3
  3. package/build/index.js +2 -1
  4. package/build/src/audit/audit_sink.d.ts +26 -19
  5. package/build/src/audit/lucid_audit_sink.js +99 -28
  6. package/build/src/commands/expire_scan_command.js +0 -2
  7. package/build/src/dev_tools/tools.js +10 -12
  8. package/build/src/host/account_export_service.js +0 -2
  9. package/build/src/host/admin_api/api_clients_controller.d.ts +6 -1
  10. package/build/src/host/admin_api/api_clients_controller.js +9 -5
  11. package/build/src/host/admin_api/api_misc_controller.d.ts +17 -7
  12. package/build/src/host/admin_api/api_misc_controller.js +10 -8
  13. package/build/src/host/admin_api/api_orgs_controller.d.ts +5 -1
  14. package/build/src/host/admin_api/api_orgs_controller.js +10 -8
  15. package/build/src/host/admin_api/api_users_controller.d.ts +15 -8
  16. package/build/src/host/admin_api/api_users_controller.js +11 -9
  17. package/build/src/host/admin_console/console_audit_controller.d.ts +14 -7
  18. package/build/src/host/admin_console/console_audit_controller.js +7 -8
  19. package/build/src/host/admin_console/console_clients_controller.d.ts +2 -1
  20. package/build/src/host/admin_console/console_clients_controller.js +5 -5
  21. package/build/src/host/admin_console/console_orgs_controller.d.ts +2 -1
  22. package/build/src/host/admin_console/console_orgs_controller.js +6 -2
  23. package/build/src/host/admin_console/console_overview_controller.d.ts +1 -1
  24. package/build/src/host/admin_console/console_overview_controller.js +4 -6
  25. package/build/src/host/admin_console/console_personal_agents_controller.d.ts +17 -20
  26. package/build/src/host/admin_console/console_personal_agents_controller.js +29 -8
  27. package/build/src/host/admin_console/console_sessions_controller.d.ts +11 -10
  28. package/build/src/host/admin_console/console_sessions_controller.js +41 -18
  29. package/build/src/host/admin_console/console_users_controller.d.ts +12 -8
  30. package/build/src/host/admin_console/console_users_controller.js +7 -12
  31. package/build/src/host/admin_stats_service.js +4 -4
  32. package/build/src/host/list_api.d.ts +118 -0
  33. package/build/src/host/list_api.js +236 -0
  34. package/build/src/host/login_attempt.js +0 -2
  35. package/build/src/host/login_notify.js +1 -2
  36. package/build/src/host/session_context.js +1 -2
  37. package/build/src/host/ui-dist/assets/index-SteA4yAA.js +67 -0
  38. package/build/src/host/ui-dist/index.html +1 -1
  39. package/build/src/pagination.d.ts +5 -31
  40. package/build/src/pagination.js +5 -12
  41. package/package.json +3 -2
  42. package/build/src/host/ui-dist/assets/index-Dq3dJVBv.js +0 -67
package/CHANGELOG.md CHANGED
@@ -1,5 +1,40 @@
1
1
  # @adonis-agora/authkit-server
2
2
 
3
+ ## 1.0.0-rc.17
4
+
5
+ ### Major Changes
6
+
7
+ - [#484](https://github.com/DavideCarvalho/adonis-agora-authkit/pull/484) [`4a9eeb4`](https://github.com/DavideCarvalho/adonis-agora-authkit/commit/4a9eeb46782e78ad06ad0c2955e49398ab21bc7e) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - Lists use the `@adonis-agora/filter` wire format: `filter[field]=…`, `search`, `sort=-field`, then `page`/`size` or a cursor. A filter field, operator or sort a list does not support answers `400 { error: { code: 'invalid_list_query' } }`.
8
+
9
+ `@adonis-agora/authkit-server`:
10
+
11
+ - `GET {admin}/api/users` and Admin API `GET /users`: `?search&sort&page&size` (`sort` accepts `email` only) → `{ data, meta: { total, perPage, currentPage, lastPage, firstPage } }` (was `meta: { page, size, total }`).
12
+ - `GET {admin}/api/clients` and `GET /clients`: page with `?search&sort&page&size` (`sort`: `clientId`, `clientName`) → `{ data, meta, canList }`.
13
+ - `GET {admin}/api/orgs` and `GET /organizations`: page with `?search&sort&page&size` (`sort`: `name`, `slug`, `createdAt`, `memberCount`) → `{ data, meta }`.
14
+ - `GET {admin}/api/audit` and `GET /audit`: newest first, by cursor. `?filter[type]` (one or more), `?filter[subject]`, `after` / `before` + `first` / `last` → `{ data, meta: { nextCursor, prevCursor, hasNext, hasPrev } }`. `?type`, `?subject`, `page` and `size` are gone.
15
+ - `GET {admin}/api/sessions`: `?filter[accountId]&search&sort&page&size` (`sort`: `loginTs`, `email`; default `-loginTs`) → `{ data, meta, supported, truncated, grants }` (was `{ supported, truncated, sessions, grants }` with `?accountId`).
16
+ - `GET {admin}/api/personal-agents/connections`: `filter[protocol]`, `filter[clientId]`, `filter[accountId]` (were flat `?protocol=`…) → `{ data, meta }` with the offset meta above.
17
+ - `GET {admin}/api/overview`: `auditTotal` is `null` when the audit sink cannot count.
18
+ - `AuditSink.list` pages by cursor: it takes `{ type?: string | string[], subject?, size?, after?, before? }` and returns `{ data, nextCursor, prevCursor, hasNext, hasPrev }` (newest first). New optional `AuditSink.count({ type?, subject? })`. `lucidAuditSink` implements both (keyset on `createdAt`, `id`). Custom sinks with `list` must follow the new shape.
19
+ - The `authkit_list_audit` dev tool takes `cursor` (the previous result's `next`) instead of `page`, and no longer returns `total`.
20
+ - The `authkit_get_overview` dev tool returns `auditTotal: null` when the audit sink cannot count.
21
+ - Removed exports: `ListMeta`, `PaginatedResponse`, `parseListPage`, `parseListSize`. New exports: `listAccountsPage`, `listAuditPage`, `listClientsPage`, `listOrgsPage`, the list filter classes and `CountAuditParams`.
22
+ - Depends on `@adonis-agora/filter`.
23
+
24
+ `@adonis-agora/authkit-react`:
25
+
26
+ - `client.admin.users.list({ search?, sort?, page?, size? })`, `clients.list(params?)`, `orgs.list(params?)` return `OffsetPage<T>` (`meta: { total, perPage, currentPage, lastPage, firstPage }`).
27
+ - `client.admin.sessions.list({ accountId?, search?, sort?, page?, size? })` (was `list(accountId?)`) returns `AdminSessionListResult` (`data` instead of `sessions`).
28
+ - `client.admin.audit.list({ type?, subject?, after?, before?, first?, last? })` returns `CursorPageResponse<AuditEventEntry>`; `page` / `size` are gone.
29
+ - `useSessionsQueryOptions(params?)`, `useClientsQueryOptions(params?)` and `useOrgsQueryOptions(params?)` take the list params; `authkitKeys.admin.sessions/clients/orgs` take them too.
30
+ - `ListMeta` is removed; `OffsetPage`, `OffsetPageMeta`, `CursorPageResponse`, `CursorPageMeta`, `AdminListParams`, `CursorListParams`, `AdminSessionListParams` and `AdminSessionListResult` are exported. `AdminOverview.auditTotal` is `number | null`.
31
+
32
+ `@adonis-agora/authkit-sdk`:
33
+
34
+ - `users.list`, `clients.list` and `organizations.list` take `{ search?, sort?, page?, size? }` and return `OffsetPage<T>` (`clients.list` adds `canList`).
35
+ - `audit.list({ type?, subject?, after?, before?, first?, last? })` returns `CursorPageResponse<AuthkitAuditEvent>`.
36
+ - `ListMeta` is removed; `OffsetPage`, `CursorPageResponse`, `OffsetListParams`, `CursorListParams`, `ListClientsParams` and `ListOrganizationsParams` are exported.
37
+
3
38
  ## 1.0.0-rc.16
4
39
 
5
40
  ### Patch Changes
package/build/index.d.ts CHANGED
@@ -21,7 +21,7 @@ export { personalAgentAuth, personalAgentOf, personalAgentReceipt, personalAgent
21
21
  export type { BuiltinProtocolId, PactOptions, PersonalAgentProtocol, } from './src/agents/protocol.js';
22
22
  export { pact, pactProtocol } from './src/agents/protocol.js';
23
23
  export { type PersonalAgentRuntimeSource, personalAgentExtensionRuntime, } from './src/agents/runtime.js';
24
- export type { AuditEvent, AuditEventType, AuditPage, AuditSink, ListAuditParams, StoredAuditEvent, } from './src/audit/audit_sink.js';
24
+ export type { AuditEvent, AuditEventType, AuditPage, AuditSink, CountAuditParams, ListAuditParams, StoredAuditEvent, } from './src/audit/audit_sink.js';
25
25
  export { lucidAuditSink } from './src/audit/lucid_audit_sink.js';
26
26
  export type { ConsoleAuthorize, ConsoleAuthorizeResult, ConsoleAuthorizer, ConsoleAuthorizerClass, } from './src/console_authorize.js';
27
27
  export { authorizeByToken } from './src/console_authorize.js';
@@ -101,6 +101,7 @@ export type { ImpersonationStartErrorCode, ImpersonationState, StartImpersonatio
101
101
  export { ImpersonationStartError, impersonationState, refreshAccessToken, rememberAccessToken, rememberRefreshToken, startImpersonation, stopImpersonation, } from './src/host/impersonation_session.js';
102
102
  export type { KeysStatus as ServerKeysStatus } from './src/host/key_rotation_actions.js';
103
103
  export { buildKeysStatus, rotateNow } from './src/host/key_rotation_actions.js';
104
+ export { AuditListFilter, ConnectionListFilter, type ListQuery, listAccountsPage, listAuditPage, listClientsPage, listOrgsPage, NoListFilter, SessionListFilter, } from './src/host/list_api.js';
104
105
  export type { OidcBearerGuardEvents, OidcBearerGuardOptions, OidcBearerGuardPolicy, } from './src/host/oidc_bearer_guard.js';
105
106
  export { OidcBearerGuard, oidcBearerGuard } from './src/host/oidc_bearer_guard.js';
106
107
  export type { OidcRpGuardEvents, OidcRpGuardOptions } from './src/host/oidc_rp_guard.js';
@@ -144,8 +145,7 @@ export type { ProviderIdentityClass, ProviderIdentityRow, } from './src/mixins/w
144
145
  export { withProviderIdentity } from './src/mixins/with_provider_identity.js';
145
146
  export type { WebauthnCredentialClass, WebauthnCredentialRow, } from './src/mixins/with_webauthn_credential.js';
146
147
  export { withWebauthnCredential } from './src/mixins/with_webauthn_credential.js';
147
- export type { ListMeta, PaginatedResponse } from './src/pagination.js';
148
- export { ADMIN_LIST_DEFAULT_SIZE, ADMIN_LIST_HTTP_DEFAULT_SIZE, ADMIN_LIST_MAX_SIZE, clampListSize, LIST_FIRST_PAGE, normalizeListSize, parseListPage, parseListSize, resolveListPage, } from './src/pagination.js';
148
+ export { ADMIN_LIST_DEFAULT_SIZE, ADMIN_LIST_HTTP_DEFAULT_SIZE, ADMIN_LIST_MAX_SIZE, clampListSize, LIST_FIRST_PAGE, normalizeListSize, resolveListPage, } from './src/pagination.js';
149
149
  export type { LegacyPasswordVerifier, PasswordConfigInput, PasswordVerifyResult, } from './src/password/password_manager.js';
150
150
  export { PasswordManager, PasswordPolicyError, } from './src/password/password_manager.js';
151
151
  export type { PasswordPolicyViolation, ResolvedPasswordConfig, ResolvedPasswordPolicy, } from './src/password/policy.js';
package/build/index.js CHANGED
@@ -73,6 +73,7 @@ export { buildImpersonationPanel } from './src/host/impersonation.js';
73
73
  export { ImpersonationStartError, impersonationState, refreshAccessToken, rememberAccessToken, rememberRefreshToken, startImpersonation, stopImpersonation, } from './src/host/impersonation_session.js';
74
74
  // Key rotation actions — shared between the Admin REST API controller and the SDK embedded driver.
75
75
  export { buildKeysStatus, rotateNow } from './src/host/key_rotation_actions.js';
76
+ export { AuditListFilter, ConnectionListFilter, listAccountsPage, listAuditPage, listClientsPage, listOrgsPage, NoListFilter, SessionListFilter, } from './src/host/list_api.js';
76
77
  // @adonisjs/auth integration (opt-in) — guard de access token (Authorization:
77
78
  // Bearer) pra APIs chamadas por apps nativos (RFC 8252), SPAs e serviços. Par do
78
79
  // oidcRpGuard para clientes sem sessão web; verifica in-process (issuer embarcado)
@@ -126,7 +127,7 @@ export { withCredentials } from './src/mixins/with_credentials.js';
126
127
  export { withPersonalAccessToken } from './src/mixins/with_personal_access_token.js';
127
128
  export { withProviderIdentity } from './src/mixins/with_provider_identity.js';
128
129
  export { withWebauthnCredential } from './src/mixins/with_webauthn_credential.js';
129
- export { ADMIN_LIST_DEFAULT_SIZE, ADMIN_LIST_HTTP_DEFAULT_SIZE, ADMIN_LIST_MAX_SIZE, clampListSize, LIST_FIRST_PAGE, normalizeListSize, parseListPage, parseListSize, resolveListPage, } from './src/pagination.js';
130
+ export { ADMIN_LIST_DEFAULT_SIZE, ADMIN_LIST_HTTP_DEFAULT_SIZE, ADMIN_LIST_MAX_SIZE, clampListSize, LIST_FIRST_PAGE, normalizeListSize, resolveListPage, } from './src/pagination.js';
130
131
  // Gerência de senha: lazy rehash + legacy verifier, política e checagem de vazamento.
131
132
  export { PasswordManager, PasswordPolicyError, } from './src/password/password_manager.js';
132
133
  export { checkPasswordPolicy, DEFAULT_PWNED_TIMEOUT_MS, policyViolationParams, } from './src/password/policy.js';
@@ -43,31 +43,36 @@ export interface StoredAuditEvent extends AuditEvent {
43
43
  createdAt: Date | string | null;
44
44
  }
45
45
  /**
46
- * Filtros de listagem do log de auditoria (console admin).
46
+ * What {@link AuditSink.list} returns: one page of the log, NEWEST FIRST, read by cursor.
47
47
  *
48
- * O par `{ page, size }` espelha INTENCIONALMENTE o shape de paginação por
49
- * offset de `@adonis-agora/filter` (`FilterInput.page`/`.size`), para manter a
50
- * mesma interface em todo o ecossistema `@adonis-agora/*`. É um casamento
51
- * ESTRUTURAL: não há dependência de `@adonis-agora/filter` aqui.
48
+ * The log is append-only and grows without bound, so it pages by an opaque cursor rather than
49
+ * by page number: `after` = the `nextCursor` of the previous page (older events), `before` =
50
+ * the `prevCursor` of a page (newer events). The cursor's content is the sink's business; the
51
+ * caller hands back what the sink returned.
52
52
  */
53
53
  export interface ListAuditParams {
54
- /** Página (1-based). Default: 1. */
55
- page?: number;
56
- /**
57
- * Itens por página. Default: 20; limitado a `ADMIN_LIST_MAX_SIZE` (200).
58
- *
59
- * Renomeado de `limit` para `size` para casar com `@adonis-agora/filter`.
60
- */
61
- size?: number;
62
- /** Filtra por tipo de evento exato. */
63
- type?: string;
64
- /** Filtra pelo subject (accountId) do evento. */
54
+ /** Only events of this type (or of any of these types). */
55
+ type?: string | readonly string[];
56
+ /** Only events about this account (`accountId`). */
65
57
  subject?: string;
58
+ /** Events per page. Default 20. */
59
+ size?: number;
60
+ /** The page after this cursor (older events). Wins over `before`. */
61
+ after?: string;
62
+ /** The page before this cursor (newer events). */
63
+ before?: string;
66
64
  }
67
- /** Página de eventos + total absoluto. */
65
+ /** The filters of {@link AuditSink.count}. */
66
+ export type CountAuditParams = Pick<ListAuditParams, 'type' | 'subject'>;
67
+ /** One page of events, newest first, and the cursors around it. */
68
68
  export interface AuditPage {
69
69
  data: StoredAuditEvent[];
70
- total: number;
70
+ /** Cursor of the next (older) page, `null` on the last one. */
71
+ nextCursor: string | null;
72
+ /** Cursor of the previous (newer) page, `null` on the first one. */
73
+ prevCursor: string | null;
74
+ hasNext: boolean;
75
+ hasPrev: boolean;
71
76
  }
72
77
  /**
73
78
  * Sink plugável de auditoria. Implementações devem ser best-effort: `record`
@@ -79,8 +84,10 @@ export interface AuditPage {
79
84
  */
80
85
  export interface AuditSink {
81
86
  record(event: AuditEvent): Promise<void>;
82
- /** Lista eventos paginados (opcional — só sinks que suportam consulta). */
87
+ /** One page of events, newest first (optional: only sinks that can be queried). */
83
88
  list?(params: ListAuditParams): Promise<AuditPage>;
89
+ /** How many events match (optional; the admin overview shows it when present). */
90
+ count?(params: CountAuditParams): Promise<number>;
84
91
  /**
85
92
  * Anonimiza (LGPD/GDPR) os eventos de uma conta SEM apagar o histórico: remove
86
93
  * os identificadores pessoais (`email`, `ip`) e re-escreve o `accountId` para um
@@ -1,5 +1,58 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { ADMIN_LIST_HTTP_DEFAULT_SIZE, normalizeListSize, resolveListPage } from '../pagination.js';
2
+ import { decodeCursor, encodeCursor } from '@adonis-agora/filter';
3
+ import { DateTime } from 'luxon';
4
+ import { ADMIN_LIST_HTTP_DEFAULT_SIZE, normalizeListSize } from '../pagination.js';
5
+ /** Narrows an audit query to the requested type(s) and subject. */
6
+ function filtered(q, params) {
7
+ const types = params.type === undefined ? [] : [].concat(params.type);
8
+ if (types.length === 1)
9
+ q.where('type', types[0]);
10
+ else if (types.length > 1)
11
+ q.whereIn('type', types);
12
+ if (params.subject)
13
+ q.where('accountId', params.subject);
14
+ return q;
15
+ }
16
+ /** A Lucid `createdAt` (luxon), a `Date` or an ISO string, as an ISO string. */
17
+ function isoOf(value) {
18
+ if (value === null || value === undefined)
19
+ return null;
20
+ if (typeof value.toISO === 'function')
21
+ return value.toISO();
22
+ if (value instanceof Date)
23
+ return value.toISOString();
24
+ return String(value);
25
+ }
26
+ function toStored(row) {
27
+ return {
28
+ id: String(row.id),
29
+ type: row.type,
30
+ accountId: row.accountId ?? null,
31
+ email: row.email ?? null,
32
+ clientId: row.clientId ?? null,
33
+ actorId: row.actorId ?? null,
34
+ ip: row.ip ?? null,
35
+ metadata: row.metadata ?? undefined,
36
+ createdAt: isoOf(row.createdAt),
37
+ };
38
+ }
39
+ /** The row's position in the log (`createdAt`, primary key), as an opaque cursor. */
40
+ function encodeAuditCursor(row) {
41
+ return encodeCursor([isoOf(row.createdAt), row.$primaryKeyValue ?? row.id]);
42
+ }
43
+ /** A cursor back to `{ at, id }`; `null` for anything this sink did not produce. */
44
+ function decodeAuditCursor(cursor) {
45
+ const values = decodeCursor(cursor);
46
+ if (!values || values.length !== 2 || typeof values[0] !== 'string')
47
+ return null;
48
+ const at = DateTime.fromISO(values[0]);
49
+ if (!at.isValid)
50
+ return null;
51
+ const id = values[1];
52
+ if (typeof id !== 'string' && typeof id !== 'number')
53
+ return null;
54
+ return { at, id };
55
+ }
3
56
  /**
4
57
  * Pseudônimo estável para um accountId anonimizado: `anon:<sha256(accountId)[:16]>`.
5
58
  * Mantém os eventos de uma conta deletada correlacionáveis ENTRE SI (mesmo
@@ -36,35 +89,53 @@ export function lucidAuditSink(Model) {
36
89
  }
37
90
  },
38
91
  async list(params) {
39
- const page = resolveListPage(params.page);
92
+ // The primary key breaks ties between events written in the same instant.
93
+ const pk = Model.primaryKey ?? 'id';
40
94
  const size = normalizeListSize(params.size, ADMIN_LIST_HTTP_DEFAULT_SIZE);
41
- const base = () => {
42
- const q = Model.query();
43
- if (params.type)
44
- q.where('type', params.type);
45
- if (params.subject)
46
- q.where('accountId', params.subject);
47
- return q;
95
+ const after = params.after ? decodeAuditCursor(params.after) : null;
96
+ const before = !after && params.before ? decodeAuditCursor(params.before) : null;
97
+ const backward = before !== null;
98
+ const seek = after ?? before;
99
+ // Newest first: (createdAt, id) DESC. Going backward the seek and the order flip, and the
100
+ // rows are flipped back below. One extra row tells whether a further page exists.
101
+ const q = filtered(Model.query(), params);
102
+ if (seek) {
103
+ const op = backward ? '>' : '<';
104
+ // Formatted the way Lucid writes `createdAt` for this dialect: nested `where` callbacks
105
+ // do not convert a luxon value, and the comparison must be against the stored form.
106
+ const format = q.client?.dialect?.dateTimeFormat;
107
+ const at = format ? seek.at.toFormat(format) : seek.at.toJSDate();
108
+ q.where((w) => {
109
+ w.where('createdAt', op, at).orWhere((eq) => {
110
+ eq.where('createdAt', at).where(pk, op, seek.id);
111
+ });
112
+ });
113
+ }
114
+ const dir = backward ? 'asc' : 'desc';
115
+ const rows = await q
116
+ .orderBy('createdAt', dir)
117
+ .orderBy(pk, dir)
118
+ .limit(size + 1);
119
+ const hasMore = rows.length > size;
120
+ let page = hasMore ? rows.slice(0, size) : rows;
121
+ if (backward)
122
+ page = page.slice().reverse();
123
+ const data = page.map(toStored);
124
+ const first = page[0];
125
+ const last = page[page.length - 1];
126
+ const hasNext = backward ? seek !== null : hasMore;
127
+ const hasPrev = backward ? hasMore : seek !== null;
128
+ return {
129
+ data,
130
+ nextCursor: hasNext && last ? encodeAuditCursor(last) : null,
131
+ prevCursor: hasPrev && first ? encodeAuditCursor(first) : null,
132
+ hasNext,
133
+ hasPrev,
48
134
  };
49
- const countResult = await base().count('* as total');
50
- const total = Number(countResult[0]?.$extras?.total ?? 0);
51
- const rows = await base()
52
- .orderBy('createdAt', 'desc')
53
- .offset((page - 1) * size)
54
- .limit(size);
55
- const data = rows.map((row) => ({
56
- id: String(row.id),
57
- type: row.type,
58
- accountId: row.accountId ?? null,
59
- email: row.email ?? null,
60
- clientId: row.clientId ?? null,
61
- actorId: row.actorId ?? null,
62
- ip: row.ip ?? null,
63
- metadata: row.metadata ?? undefined,
64
- // createdAt é um luxon DateTime no Lucid; normaliza para ISO string.
65
- createdAt: row.createdAt?.toISO?.() ?? row.createdAt ?? null,
66
- }));
67
- return { data, total };
135
+ },
136
+ async count(params) {
137
+ const result = await filtered(Model.query(), params).count('* as total');
138
+ return Number(result[0]?.$extras?.total ?? 0);
68
139
  },
69
140
  /**
70
141
  * Anonimiza (LGPD/GDPR) as linhas de audit cujo subject (`accountId`) OU ator
@@ -150,7 +150,6 @@ async function getLastLoginMs(audit, accountId) {
150
150
  const result = await audit.list({
151
151
  type: 'login.success',
152
152
  subject: accountId,
153
- page: LIST_FIRST_PAGE,
154
153
  size: 1,
155
154
  });
156
155
  if (result.data.length === 0)
@@ -174,7 +173,6 @@ async function wasWarnedRecently(audit, accountId, warnDays) {
174
173
  const result = await audit.list({
175
174
  type: 'account.expiration_warned',
176
175
  subject: accountId,
177
- page: LIST_FIRST_PAGE,
178
176
  size: 1,
179
177
  });
180
178
  if (result.data.length === 0)
@@ -98,9 +98,8 @@ export function authkitDevTools(deps = {}) {
98
98
  const stats = await computeAdminStats(cfg, sessions(svc));
99
99
  const clientSvc = clients(svc);
100
100
  const clientsCount = clientSvc.canList ? (await clientSvc.list()).length : 0;
101
- const recent = typeof cfg.audit?.list === 'function'
102
- ? await cfg.audit.list({ page: LIST_FIRST_PAGE, size: 5 })
103
- : null;
101
+ const recent = typeof cfg.audit?.list === 'function' ? await cfg.audit.list({ size: 5 }) : null;
102
+ const auditTotal = typeof cfg.audit?.count === 'function' ? await cfg.audit.count({}) : null;
104
103
  return {
105
104
  usersTotal: stats.totalUsers,
106
105
  activeSessions: stats.activeSessions,
@@ -112,7 +111,7 @@ export function authkitDevTools(deps = {}) {
112
111
  windowDays: stats.windowDays,
113
112
  auditSupported: stats.auditSupported,
114
113
  clientsCount,
115
- auditTotal: recent?.total ?? 0,
114
+ auditTotal,
116
115
  recentEvents: (recent?.data ?? []).map(auditDto),
117
116
  };
118
117
  },
@@ -290,13 +289,13 @@ export function authkitDevTools(deps = {}) {
290
289
  {
291
290
  name: 'authkit_list_audit',
292
291
  kind: 'read',
293
- description: 'The audit log, newest first: event type, account, actor, client, IP, metadata, time. Filter by `type` (e.g. "login.failure") and `subject` (an account id). Returns { supported, items, total, next }.',
292
+ description: 'The audit log, newest first: event type, account, actor, client, IP, metadata, time. Filter by `type` (e.g. "login.failure") and `subject` (an account id). Pass the `next` of a result as `cursor` for the following (older) page. Returns { supported, items, next }.',
294
293
  input: {
295
294
  type: 'object',
296
295
  properties: {
297
296
  type: { type: 'string', description: 'Event type, e.g. "login.success".' },
298
297
  subject: { type: 'string', description: 'Account id the events are about.' },
299
- page,
298
+ cursor: { type: 'string', description: 'The `next` of the previous page.' },
300
299
  limit,
301
300
  },
302
301
  },
@@ -304,20 +303,19 @@ export function authkitDevTools(deps = {}) {
304
303
  const cfg = (await service(call.ctx)).config;
305
304
  const sink = cfg.audit;
306
305
  if (!sink || typeof sink.list !== 'function') {
307
- return { supported: false, items: [], total: 0, next: null };
306
+ return { supported: false, items: [], next: null };
308
307
  }
309
- const p = pageOf(input);
308
+ const cursor = text(input, 'cursor');
310
309
  const result = await sink.list({
311
- page: p.page,
312
- size: p.limit,
310
+ size: pageOf(input).limit,
313
311
  type: text(input, 'type'),
314
312
  subject: text(input, 'subject'),
313
+ ...(cursor !== undefined && { after: cursor }),
315
314
  });
316
315
  return {
317
316
  supported: true,
318
317
  items: result.data.map(auditDto),
319
- total: result.total,
320
- next: nextPage(p.page, p.limit, result.total),
318
+ next: result.hasNext ? result.nextCursor : null,
321
319
  };
322
320
  },
323
321
  },
@@ -1,5 +1,4 @@
1
1
  import { supportsOrganizations, supportsPasskeys, supportsProviderIdentity, } from '../accounts/account_store.js';
2
- import { LIST_FIRST_PAGE } from '../pagination.js';
3
2
  import { AdminSessionsService } from './admin_sessions_service.js';
4
3
  /** Limite de eventos de audit incluídos no export (evita payloads gigantes). */
5
4
  const AUDIT_EXPORT_SIZE = 1000;
@@ -103,7 +102,6 @@ export class AccountExportService {
103
102
  try {
104
103
  const page = await cfg.audit.list({
105
104
  subject: accountId,
106
- page: LIST_FIRST_PAGE,
107
105
  size: AUDIT_EXPORT_SIZE,
108
106
  });
109
107
  auditLog = page.data.map((e) => ({
@@ -7,7 +7,12 @@ import type { HttpContext } from '@adonisjs/core/http';
7
7
  */
8
8
  export default class ApiClientsController {
9
9
  #private;
10
- index(ctx: HttpContext): Promise<{
10
+ /**
11
+ * GET /clients — `?search=&sort=clientId|clientName&page=&size=` →
12
+ * `{ data, meta: { total, perPage, currentPage, lastPage, firstPage }, canList }`.
13
+ */
14
+ index(ctx: HttpContext): Promise<void | {
15
+ meta: import("@adonis-agora/filter").OffsetPageMeta;
11
16
  data: {
12
17
  clientId: string;
13
18
  confidential: boolean;
@@ -2,6 +2,7 @@ import '../augmentations.js';
2
2
  import { AdminClientsService } from '../admin_clients_service.js';
3
3
  import { clientCreateInput, clientInputValidator, clientPartialInput, } from '../admin_validators.js';
4
4
  import { ClientMetadataError } from '../client_metadata.js';
5
+ import { listClientsPage, withListQuery } from '../list_api.js';
5
6
  import { apiError, clientDto, createdClientDto } from './dto.js';
6
7
  /** Resolve o serviço (== OidcService) + o AdminClientsService. */
7
8
  async function clientsService(ctx) {
@@ -21,13 +22,16 @@ function invalidMetadata(ctx, err) {
21
22
  * em create/regenerate. Audita create/update/delete.
22
23
  */
23
24
  export default class ApiClientsController {
25
+ /**
26
+ * GET /clients — `?search=&sort=clientId|clientName&page=&size=` →
27
+ * `{ data, meta: { total, perPage, currentPage, lastPage, firstPage }, canList }`.
28
+ */
24
29
  async index(ctx) {
25
30
  const { svc } = await clientsService(ctx);
26
- if (!svc.canList) {
27
- return { data: [], canList: false };
28
- }
29
- const clients = await svc.list();
30
- return { data: clients.map(clientDto), canList: true };
31
+ return withListQuery(ctx, async () => {
32
+ const page = await listClientsPage(svc.canList ? await svc.list() : [], ctx.request.qs());
33
+ return { ...page, data: page.data.map(clientDto), canList: svc.canList };
34
+ });
31
35
  }
32
36
  async show(ctx) {
33
37
  const { svc } = await clientsService(ctx);
@@ -5,14 +5,24 @@ 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 — `{ meta: { page, size, total }, data }` (501 JSON quando o sink não consulta). */
8
+ /**
9
+ * GET /audit — newest first, `?filter[type]=&filter[subject]=&after=&before=&first=&last=` →
10
+ * `{ data, meta: { nextCursor, prevCursor, hasNext, hasPrev } }` (501 JSON when the sink
11
+ * cannot be queried).
12
+ */
9
13
  audit(ctx: HttpContext): Promise<void | {
10
- meta: {
11
- page: number;
12
- size: number;
13
- total: any;
14
- };
15
- data: any;
14
+ meta: import("@adonis-agora/filter").CursorPageMeta;
15
+ data: {
16
+ id: string;
17
+ type: "account.deleted" | "account.expiration_warned" | "account.expired_login_blocked" | "account.exported" | "account.extension.action" | "account.extension.redirect" | "account.locked" | "account.signed_out_all" | "agent.allowed" | "agent.blocked" | "agent.delegation_approved" | "agent.delegation_denied" | "agent.grant_revoked" | "agent.grant_revoked_by_admin" | "agent.registry_removed" | "bot_protection.rejected" | "client.created" | "client.deleted" | "client.disabled" | "client.enabled" | "client.secret_regenerated" | "client.updated" | "email.change_requested" | "email.changed" | "email_change.cancelled" | "email_change.confirmed" | "email_change.requested" | "email_verification.consumed" | "email_verification.issued" | "grant.revoked_by_user" | "impersonation" | "impersonation.panel_viewed" | "impersonation.stopped" | "keys.rotated" | "login.failure" | "login.magic_link_sent" | "login.new_device" | "login.new_ip_notified" | "login.otp_failed" | "login.otp_invalidated" | "login.otp_sent" | "login.otp_verified" | "login.success" | "maintenance.disabled" | "maintenance.enabled" | "mfa.disabled" | "mfa.enabled" | "mfa.recovery_codes_regenerated" | "organization.created" | "organization.deactivated" | "organization.deleted" | "organization.invitation_accepted" | "organization.invitation_revoked" | "organization.invitation_sent" | "organization.member_added" | "organization.member_removed" | "organization.member_role_changed" | "organization.member_role_updated" | "organization.switched" | "organization.updated" | "otp.locked" | "otp.unlock_failed" | "otp.unlocked" | "passkey.registered" | "passkey.removed" | "password.changed" | "password.expired_change_forced" | "password.rehashed" | "password_reset.consumed" | "password_reset.issued" | "pat.issued" | "pat.revoked" | "pat.used" | "profile.updated" | "roles_catalog.updated" | "security_notice.sent" | "service_tenant.token_issued" | "session.revoked" | "session.revoked_all" | "session.single_enforced" | "settings.updated" | "signup" | "sudo.confirmed" | "trusted_device.revoked" | "user.created" | "user.deleted" | "user.disabled" | "user.enabled" | "user.password_reset_sent";
18
+ accountId: string | null;
19
+ email: string | null;
20
+ clientId: string | null;
21
+ actorId: string | null;
22
+ ip: string | null;
23
+ metadata: Record<string, unknown> | null;
24
+ createdAt: string | null;
25
+ }[];
16
26
  }>;
17
27
  /** GET /stats — métricas-resumo do IdP (totais + MAU + séries de 30 dias). */
18
28
  stats(ctx: HttpContext): Promise<import("../admin_stats_service.js").AdminStats>;
@@ -1,8 +1,8 @@
1
1
  import '../augmentations.js';
2
- import { ADMIN_LIST_HTTP_DEFAULT_SIZE, parseListPage, parseListSize } from '../../pagination.js';
3
2
  import { AdminSessionsService } from '../admin_sessions_service.js';
4
3
  import { computeAdminStats } from '../admin_stats_service.js';
5
4
  import { tokenVerifyValidator } from '../admin_validators.js';
5
+ import { listAuditPage, withListQuery } from '../list_api.js';
6
6
  import { apiError, auditDto } from './dto.js';
7
7
  import { TokenVerifyService } from './token_verify_service.js';
8
8
  /**
@@ -10,7 +10,11 @@ import { TokenVerifyService } from './token_verify_service.js';
10
10
  * introspecção genérica de token (`POST /tokens/verify`).
11
11
  */
12
12
  export default class ApiMiscController {
13
- /** GET /audit — `{ meta: { page, size, total }, data }` (501 JSON quando o sink não consulta). */
13
+ /**
14
+ * GET /audit — newest first, `?filter[type]=&filter[subject]=&after=&before=&first=&last=` →
15
+ * `{ data, meta: { nextCursor, prevCursor, hasNext, hasPrev } }` (501 JSON when the sink
16
+ * cannot be queried).
17
+ */
14
18
  async audit(ctx) {
15
19
  const service = await ctx.containerResolver.make('authkit.server');
16
20
  const cfg = service.config;
@@ -20,12 +24,10 @@ export default class ApiMiscController {
20
24
  .status(501)
21
25
  .send(apiError('not_implemented', 'O sink de auditoria configurado não suporta consulta.'));
22
26
  }
23
- const page = parseListPage(ctx.request.input('page'));
24
- const size = parseListSize(ctx.request.input('size'), ADMIN_LIST_HTTP_DEFAULT_SIZE);
25
- const type = ctx.request.input('type')?.trim() || undefined;
26
- const subject = ctx.request.input('subject')?.trim() || undefined;
27
- const result = await sink.list({ page, size, type, subject });
28
- return { meta: { page, size, total: result.total }, data: result.data.map(auditDto) };
27
+ return withListQuery(ctx, async () => {
28
+ const page = await listAuditPage(sink, ctx.request.qs());
29
+ return { ...page, data: page.data.map(auditDto) };
30
+ });
29
31
  }
30
32
  /** GET /stats — métricas-resumo do IdP (totais + MAU + séries de 30 dias). */
31
33
  async stats(ctx) {
@@ -5,8 +5,12 @@ import type { HttpContext } from '@adonisjs/core/http';
5
5
  * Todas as rotas ficam sob `/api/authkit/v1/organizations`.
6
6
  */
7
7
  export default class ApiOrgsController {
8
- /** GET /organizations — lista todas as orgs com contagem de membros. */
8
+ /**
9
+ * GET /organizations — `?search=&sort=name|slug|createdAt|memberCount&page=&size=` →
10
+ * `{ data, meta: { total, perPage, currentPage, lastPage, firstPage } }`, with member counts.
11
+ */
9
12
  index(ctx: HttpContext): Promise<void | {
13
+ meta: import("@adonis-agora/filter").OffsetPageMeta;
10
14
  data: {
11
15
  id: string;
12
16
  name: string;
@@ -1,5 +1,6 @@
1
1
  import '../augmentations.js';
2
2
  import { orgAddMemberValidator, orgCreateValidator, orgInvitationValidator, orgMemberRoleValidator, orgUpdateValidator, } from '../admin_validators.js';
3
+ import { listOrgsPage, withListQuery } from '../list_api.js';
3
4
  import { authkitOrigin } from '../origin.js';
4
5
  import { resolveRuntimeSettings } from '../runtime_settings.js';
5
6
  import { AdminOrgsService } from './admin_orgs_service.js';
@@ -20,19 +21,20 @@ function notSupported(ctx) {
20
21
  * Todas as rotas ficam sob `/api/authkit/v1/organizations`.
21
22
  */
22
23
  export default class ApiOrgsController {
23
- /** GET /organizations — lista todas as orgs com contagem de membros. */
24
+ /**
25
+ * GET /organizations — `?search=&sort=name|slug|createdAt|memberCount&page=&size=` →
26
+ * `{ data, meta: { total, perPage, currentPage, lastPage, firstPage } }`, with member counts.
27
+ */
24
28
  async index(ctx) {
25
29
  const { cfg, service } = await ctxBits(ctx);
26
30
  const svc = new AdminOrgsService(cfg, service);
27
31
  const result = await svc.listOrgs();
28
- if (!Array.isArray(result)) {
29
- if (result.reason === 'not_supported')
30
- return notSupported(ctx);
31
- }
32
- const orgs = result;
33
- if (!Array.isArray(orgs))
32
+ if (!Array.isArray(result))
34
33
  return notSupported(ctx);
35
- return { data: orgs.map(orgDto) };
34
+ return withListQuery(ctx, async () => {
35
+ const page = await listOrgsPage(result, ctx.request.qs());
36
+ return { ...page, data: page.data.map(orgDto) };
37
+ });
36
38
  }
37
39
  /** POST /organizations — cria uma org. Body: { name, slug, ownerAccountId, logoUrl? } */
38
40
  async store(ctx) {
@@ -6,14 +6,21 @@ import type { HttpContext } from '@adonisjs/core/http';
6
6
  */
7
7
  export default class ApiUsersController {
8
8
  #private;
9
- /** GET /users — `{ meta: { page, size, total }, data }`, com busca por e-mail. */
10
- index(ctx: HttpContext): Promise<{
11
- meta: {
12
- page: number;
13
- size: number;
14
- total: any;
15
- };
16
- data: any[];
9
+ /**
10
+ * GET /users — `?search=&sort=email&page=&size=` →
11
+ * `{ data, meta: { total, perPage, currentPage, lastPage, firstPage } }`.
12
+ */
13
+ index(ctx: HttpContext): Promise<void | {
14
+ meta: import("@adonis-agora/filter").OffsetPageMeta;
15
+ data: {
16
+ id: string;
17
+ email: string;
18
+ name: string | null;
19
+ avatarUrl: string | null;
20
+ globalRoles: string[];
21
+ disabled: boolean;
22
+ roles: string[];
23
+ }[];
17
24
  }>;
18
25
  /** GET /users/:id */
19
26
  show(ctx: HttpContext): Promise<void | {