@adonis-agora/authkit-server 0.90.2 → 1.0.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/CHANGELOG.md +3637 -0
  2. package/build/commands/clients_create.d.ts +1 -0
  3. package/build/commands/clients_create.js +15 -2
  4. package/build/commands/commands.json +7 -0
  5. package/build/host/ui/admin.html +1 -1
  6. package/build/host/views/account/apps.edge +8 -3
  7. package/build/host/views/partials/styles.edge +1 -1
  8. package/build/index.d.ts +3 -3
  9. package/build/index.js +3 -5
  10. package/build/src/accounts/lucid_account_store.d.ts +0 -18
  11. package/build/src/accounts/lucid_account_store.js +7 -48
  12. package/build/src/accounts/lucid_store/mfa.js +1 -1
  13. package/build/src/accounts/lucid_store/organizations.d.ts +1 -1
  14. package/build/src/accounts/lucid_store/password_hygiene.d.ts +2 -3
  15. package/build/src/accounts/lucid_store/password_hygiene.js +2 -3
  16. package/build/src/accounts/lucid_store/shared.d.ts +0 -7
  17. package/build/src/accounts/lucid_store/shared.js +0 -14
  18. package/build/src/accounts/lucid_stores.d.ts +1 -1
  19. package/build/src/adapters/redis_adapter.js +12 -2
  20. package/build/src/define_config.d.ts +6 -12
  21. package/build/src/define_config.js +3 -2
  22. package/build/src/doctor/checks.d.ts +3 -4
  23. package/build/src/doctor/checks.js +5 -7
  24. package/build/src/host/account_api/account_api_controller.d.ts +2 -1
  25. package/build/src/host/account_api/account_api_controller.js +5 -11
  26. package/build/src/host/admin_api/api_clients_controller.d.ts +9 -3
  27. package/build/src/host/admin_api/dto.d.ts +3 -1
  28. package/build/src/host/admin_api/dto.js +3 -1
  29. package/build/src/host/admin_clients_service.d.ts +13 -5
  30. package/build/src/host/admin_clients_service.js +18 -5
  31. package/build/src/host/admin_console/admin_shell_controller.d.ts +16 -1
  32. package/build/src/host/admin_console/admin_shell_controller.js +21 -7
  33. package/build/src/host/admin_console/console_clients_controller.d.ts +21 -2
  34. package/build/src/host/admin_console/console_clients_controller.js +9 -0
  35. package/build/src/host/admin_console/console_impersonation_controller.js +3 -8
  36. package/build/src/host/admin_console/console_overview_controller.d.ts +2 -2
  37. package/build/src/host/admin_console/console_overview_controller.js +4 -2
  38. package/build/src/host/admin_validators.d.ts +15 -11
  39. package/build/src/host/admin_validators.js +16 -9
  40. package/build/src/host/client_names.d.ts +9 -2
  41. package/build/src/host/client_names.js +11 -3
  42. package/build/src/host/consent_params.d.ts +7 -0
  43. package/build/src/host/consent_params.js +27 -0
  44. package/build/src/host/controllers/account_apps_controller.js +4 -3
  45. package/build/src/host/controllers/account_confirm_controller.d.ts +0 -7
  46. package/build/src/host/controllers/account_confirm_controller.js +0 -6
  47. package/build/src/host/controllers/account_orgs_controller.d.ts +0 -50
  48. package/build/src/host/controllers/account_orgs_controller.js +7 -113
  49. package/build/src/host/controllers/account_security_controller.js +7 -11
  50. package/build/src/host/controllers/agent_consent_controller.js +23 -5
  51. package/build/src/host/controllers/interaction_controller.js +6 -2
  52. package/build/src/host/controllers/poppy_consent_controller.js +33 -7
  53. package/build/src/host/controllers/registration_controller.js +7 -2
  54. package/build/src/host/i18n.d.ts +0 -4
  55. package/build/src/host/i18n.js +0 -6
  56. package/build/src/host/impersonation.d.ts +7 -15
  57. package/build/src/host/impersonation.js +7 -16
  58. package/build/src/host/interaction_recovery.js +4 -1
  59. package/build/src/host/login_attempt.js +5 -1
  60. package/build/src/host/middleware/account_auth.js +6 -0
  61. package/build/src/host/rate_limit.js +5 -1
  62. package/build/src/host/register_auth_host.d.ts +6 -0
  63. package/build/src/host/register_auth_host.js +32 -8
  64. package/build/src/host/renderers/inertia_renderer.d.ts +9 -21
  65. package/build/src/host/renderers/inertia_renderer.js +5 -6
  66. package/build/src/host/screen_props.d.ts +309 -0
  67. package/build/src/host/screen_props.js +11 -0
  68. package/build/src/host/sudo_mode.d.ts +5 -11
  69. package/build/src/host/sudo_mode.js +5 -11
  70. package/build/src/host/ui-dist/assets/index-D0EIeyKK.js +67 -0
  71. package/build/src/host/ui-dist/index.html +1 -1
  72. package/build/src/password/password_manager.js +5 -0
  73. package/build/src/provider/build_provider.js +0 -34
  74. package/build/src/provider/oidc_service.d.ts +0 -2
  75. package/build/src/provider/oidc_service.js +1 -12
  76. package/build/src/provider/token_exchange.d.ts +1 -3
  77. package/build/src/provider/token_exchange.js +1 -1
  78. package/build/stubs/config/authkit_react.stub +3 -3
  79. package/package.json +9 -8
  80. package/skills/authkit-interactions/SKILL.md +32 -5
  81. package/stubs/config/authkit_react.stub +3 -3
  82. package/build/src/commands/import_clients.d.ts +0 -40
  83. package/build/src/commands/import_clients.js +0 -81
  84. package/build/src/host/ui-dist/assets/index-DUVBDAyh.js +0 -67
  85. package/build/src/mixins/with_mfa.d.ts +0 -35
  86. package/build/src/mixins/with_mfa.js +0 -21
@@ -18,16 +18,13 @@ export interface ImpersonationPanel {
18
18
  curl: string;
19
19
  }
20
20
  /**
21
- * Um client candidato a hospedar o token-exchange. Modelado estruturalmente para
22
- * aceitar tanto os clients ESTÁTICOS do config (`ResolvedServerConfig['clients']`,
23
- * que trazem o secret) quanto os clients de RUNTIME lidos do adapter pelo
24
- * `AdminClientsService` (que NÃO trazem o secret — ele não é recuperável).
21
+ * Um client candidato a hospedar o token-exchange, na forma que o
22
+ * `AdminClientsService` lê do adapter (sem o secret — ele não é recuperável).
25
23
  */
26
24
  export interface ImpersonationClientLike {
27
25
  clientId: string;
28
- clientSecret?: string;
29
- grants?: string[];
30
- /** Runtime clients reportam confidencialidade sem expor o secret. */
26
+ grantTypes?: string[];
27
+ /** Client confidencial: o curl sai com o placeholder do secret. */
31
28
  confidential?: boolean;
32
29
  }
33
30
  /**
@@ -35,12 +32,7 @@ export interface ImpersonationClientLike {
35
32
  * client candidato tem o grant token-exchange habilitado (sem ele o fluxo não
36
33
  * funciona) — a UI então mostra a instrução de habilitar o grant.
37
34
  *
38
- * ⚠️ `clients` é OBRIGATÓRIO na prática. `cfg.clients` é sempre `[]` num host
39
- * real: clients são 100% de runtime (console admin / Admin API / `authkit:clients:create`),
40
- * e o config resolvido nunca os carrega — é o que o `checkClients` do doctor
41
- * afirma. Passar só o config faria o painel devolver `null` SEMPRE, que foi
42
- * exatamente o bug: o botão do console nunca teve um client para oferecer.
43
- * O fallback para `cfg.clients` existe só para hosts que ainda declaram clients
44
- * estáticos e para os testes que os declaram.
35
+ * `clients` são os clients do adapter (console admin / Admin API /
36
+ * `authkit:clients:create`), por exemplo `new AdminClientsService(service).list()`.
45
37
  */
46
- export declare function buildImpersonationPanel(cfg: Pick<ResolvedServerConfig, 'issuer' | 'clients'>, targetId: string, clients?: readonly ImpersonationClientLike[]): ImpersonationPanel | null;
38
+ export declare function buildImpersonationPanel(cfg: Pick<ResolvedServerConfig, 'issuer'>, targetId: string, clients: readonly ImpersonationClientLike[]): ImpersonationPanel | null;
@@ -7,27 +7,18 @@ const SECRET_PLACEHOLDER = '<CLIENT_SECRET>';
7
7
  * client candidato tem o grant token-exchange habilitado (sem ele o fluxo não
8
8
  * funciona) — a UI então mostra a instrução de habilitar o grant.
9
9
  *
10
- * ⚠️ `clients` é OBRIGATÓRIO na prática. `cfg.clients` é sempre `[]` num host
11
- * real: clients são 100% de runtime (console admin / Admin API / `authkit:clients:create`),
12
- * e o config resolvido nunca os carrega — é o que o `checkClients` do doctor
13
- * afirma. Passar só o config faria o painel devolver `null` SEMPRE, que foi
14
- * exatamente o bug: o botão do console nunca teve um client para oferecer.
15
- * O fallback para `cfg.clients` existe só para hosts que ainda declaram clients
16
- * estáticos e para os testes que os declaram.
10
+ * `clients` são os clients do adapter (console admin / Admin API /
11
+ * `authkit:clients:create`), por exemplo `new AdminClientsService(service).list()`.
17
12
  */
18
13
  export function buildImpersonationPanel(cfg, targetId, clients) {
19
- const candidates = clients && clients.length > 0 ? clients : (cfg.clients ?? []);
20
- const client = candidates.find((c) => (c.grants ?? []).includes(TOKEN_EXCHANGE));
14
+ const client = clients.find((c) => (c.grantTypes ?? []).includes(TOKEN_EXCHANGE));
21
15
  if (!client)
22
16
  return null;
23
17
  const tokenEndpoint = `${cfg.issuer.replace(/\/+$/, '')}/token`;
24
- // Confidencial = tem secret conhecido, ou o adapter marcou como confidencial.
25
- // No segundo caso o secret NÃO é recuperável (o adapter guarda o hash/valor
26
- // opaco e a lib mostra o secret uma única vez na criação), então o curl sai
27
- // com um placeholder em vez de mentir com uma string vazia.
28
- const confidential = client.confidential ?? client.clientSecret !== undefined;
29
- const auth = confidential
30
- ? ` -u '${client.clientId}:${client.clientSecret ?? SECRET_PLACEHOLDER}' \\\n`
18
+ // O secret NÃO é recuperável (a lib mostra o secret uma única vez na criação),
19
+ // então o curl de um client confidencial sai com um placeholder.
20
+ const auth = client.confidential
21
+ ? ` -u '${client.clientId}:${SECRET_PLACEHOLDER}' \\\n`
31
22
  : ` -d 'client_id=${client.clientId}' \\\n`;
32
23
  const curl = `curl -X POST '${tokenEndpoint}' \\\n${auth} -d 'grant_type=${TOKEN_EXCHANGE}' \\\n -d 'subject_token=<ADMIN_ACCESS_TOKEN>' \\\n -d 'subject_token_type=${ACCESS_TOKEN_TYPE}' \\\n -d 'requested_subject=${targetId}' \\\n -d 'scope=openid profile email'`;
33
24
  return {
@@ -87,7 +87,10 @@ export async function recoverLostInteraction(ctx) {
87
87
  return ctx.response.redirect(loginUrl);
88
88
  }
89
89
  ctx.response.status(400);
90
- const body = await render(ctx, 'session-expired', { loginUrl, brand });
90
+ const body = await render(ctx, 'session-expired', {
91
+ loginUrl,
92
+ brand,
93
+ });
91
94
  // ESCREVE o body na resposta. Diferente do dispatch normal de rota — onde o
92
95
  // `useReturnValue`/`canWriteResponseBody` do http-server envia o valor
93
96
  // retornado — o caminho de exception handler DESCARTA o retorno de
@@ -231,7 +231,11 @@ export async function attemptPasswordLogin(cfg, input) {
231
231
  if (lock.locked) {
232
232
  return { ok: false, locked: true, retryAfterSec: lock.retryAfterSec };
233
233
  }
234
- const account = await cfg.accountStore.verifyCredentials(email, password);
234
+ // A form posted without a password (e-mail only, typical when password login is
235
+ // off) is a failed attempt like a wrong password, never a crash in the hasher.
236
+ const account = typeof password === 'string' && password !== ''
237
+ ? await cfg.accountStore.verifyCredentials(email, password)
238
+ : null;
235
239
  if (!account) {
236
240
  // `clientId` só entra no evento quando o fluxo o fornece (interaction).
237
241
  await cfg.audit?.record(input.clientId !== undefined
@@ -7,6 +7,12 @@ export default class AccountAuthMiddleware {
7
7
  async handle(ctx, next) {
8
8
  // Sessão do console — ou, com `accountSession.acceptIdpSession`, a do IdP (SSO).
9
9
  if (!(await ensureConsoleSession(ctx))) {
10
+ // JSON callers get a status, not an HTML login page.
11
+ if (ctx.request?.accepts?.(['html', 'json']) === 'json') {
12
+ return ctx.response
13
+ .status(401)
14
+ .send({ error: { code: 'unauthenticated', message: 'Sign in to continue.' } });
15
+ }
10
16
  // Destino configurável (`accountLoginUrl`): default `/account/login`.
11
17
  return ctx.response.redirect(getAccountLoginUrl());
12
18
  }
@@ -159,7 +159,11 @@ export function withFriendly429(throttle) {
159
159
  ctx.response.status(429);
160
160
  if (retryAfter)
161
161
  ctx.response.header('Retry-After', String(retryAfter));
162
- const body = await render(ctx, 'throttled', { brand, retryAfter, retryUrl });
162
+ const body = await render(ctx, 'throttled', {
163
+ brand,
164
+ retryAfter,
165
+ retryUrl,
166
+ });
163
167
  const res = ctx.response;
164
168
  if (body !== undefined && !res.hasLazyBody && body !== ctx.response) {
165
169
  res.send(body);
@@ -5,6 +5,12 @@ import type { PolicyRouteOption } from './config_locks.js';
5
5
  import type { SudoMethod } from './sudo/types.js';
6
6
  /** Chave da sessão Adonis que registra o timestamp da última atividade (idle timeout). */
7
7
  export declare const ACCOUNT_LAST_SEEN_KEY = "authkit_last_seen";
8
+ /**
9
+ * `true` when the caller wants JSON (an `Accept` that prefers `application/json`,
10
+ * as `fetch` from the React client sends): a JSON API answers it with a status,
11
+ * not with a redirect to an HTML login page.
12
+ */
13
+ export declare function wantsJson(ctx: any): boolean;
8
14
  /**
9
15
  * Guard do console admin (B6). Como o `accountGuard`, é uma closure inline (forma
10
16
  * confiável do `.use()` num grupo). Exige:
@@ -119,6 +119,28 @@ function isPoppyBrowser(ctx) {
119
119
  const marker = ctx.session?.get?.(POPPY_WEB_SESSION_KEY);
120
120
  return typeof marker === 'object' && marker !== null && typeof marker.sessionId === 'string';
121
121
  }
122
+ /**
123
+ * `true` when the caller wants JSON (an `Accept` that prefers `application/json`,
124
+ * as `fetch` from the React client sends): a JSON API answers it with a status,
125
+ * not with a redirect to an HTML login page.
126
+ */
127
+ export function wantsJson(ctx) {
128
+ try {
129
+ return ctx.request?.accepts?.(['html', 'json']) === 'json';
130
+ }
131
+ catch {
132
+ return false;
133
+ }
134
+ }
135
+ /** 401 for a JSON caller, otherwise the redirect to the login. */
136
+ function unauthenticated(ctx, redirectTo) {
137
+ if (wantsJson(ctx)) {
138
+ return ctx.response
139
+ .status(401)
140
+ .send({ error: { code: 'unauthenticated', message: 'Sign in to continue.' } });
141
+ }
142
+ return redirectExact(ctx.response, redirectTo);
143
+ }
122
144
  const accountGuard = async (ctx, next) => {
123
145
  // O navegador de um personal agent Poppy (§5) não entra no console de conta nem
124
146
  // nas telas de consentimento: a sessão é do agente, não do usuário.
@@ -126,12 +148,12 @@ const accountGuard = async (ctx, next) => {
126
148
  return ctx.response.status(403).send('');
127
149
  // Sessão do console — ou, com `accountSession.acceptIdpSession`, a do IdP (SSO).
128
150
  if (!(await ensureConsoleSession(ctx))) {
129
- return redirectExact(ctx.response, buildLoginRedirect(ctx));
151
+ return unauthenticated(ctx, buildLoginRedirect(ctx));
130
152
  }
131
153
  // Idle timeout: encerra e redireciona com query param de motivo.
132
154
  const idleExpired = await checkAndRefreshIdle(ctx);
133
155
  if (idleExpired) {
134
- return redirectExact(ctx.response, buildLoginRedirect(ctx, 'reason=idle'));
156
+ return unauthenticated(ctx, buildLoginRedirect(ctx, 'reason=idle'));
135
157
  }
136
158
  return next();
137
159
  };
@@ -162,12 +184,12 @@ export const adminGuard = async (ctx, next) => {
162
184
  const accountId = ctx.session?.get(ACCOUNT_SESSION_KEY);
163
185
  if (!accountId) {
164
186
  // `/account/login` é sempre o login da conta — NÃO muda com o prefixo admin.
165
- return redirectExact(ctx.response, buildLoginRedirect(ctx));
187
+ return unauthenticated(ctx, buildLoginRedirect(ctx));
166
188
  }
167
189
  // Idle timeout: também protege o console admin.
168
190
  const idleExpired = await checkAndRefreshIdle(ctx);
169
191
  if (idleExpired) {
170
- return redirectExact(ctx.response, buildLoginRedirect(ctx, 'reason=idle'));
192
+ return unauthenticated(ctx, buildLoginRedirect(ctx, 'reason=idle'));
171
193
  }
172
194
  const allowed = cfg.admin.roles;
173
195
  const account = await cfg.accountStore.findById(accountId);
@@ -179,6 +201,11 @@ export const adminGuard = async (ctx, next) => {
179
201
  if (!isAdmin) {
180
202
  // Evita vazar a existência do console admin: redireciona para o accountHome
181
203
  // (sem mostrar a URL do admin).
204
+ if (wantsJson(ctx)) {
205
+ return ctx.response
206
+ .status(403)
207
+ .send({ error: { code: 'forbidden', message: 'Admin role required.' } });
208
+ }
182
209
  return ctx.response.redirect(accountHome(cfg));
183
210
  }
184
211
  return next();
@@ -770,10 +797,6 @@ export function registerAuthHost(router, opts = {}) {
770
797
  C.accountOrgs,
771
798
  'revokeInvitation',
772
799
  ]);
773
- // JSON endpoints for React hooks (authkit-react).
774
- router.get(`${orgsPath}/json`, [C.accountOrgs, 'listJson']);
775
- router.get(`${orgsPath}/invitations/json`, [C.accountOrgs, 'listInvitationsJson']);
776
- router.get(`${orgsPath}/:id/json`, [C.accountOrgs, 'showJson']);
777
800
  }
778
801
  // ─── Account self-service JSON API (authkit-react TanStack hooks) ─────
779
802
  // ⚠️ ORDER MATTERS: fixed-segment routes before parameterised ones.
@@ -925,6 +948,7 @@ export function registerAuthHost(router, opts = {}) {
925
948
  // Clients OIDC.
926
949
  router.get(`${ap}/api/clients`, [C.consoleClients, 'index']);
927
950
  router.post(`${ap}/api/clients`, [C.consoleClients, 'store']);
951
+ router.get(`${ap}/api/clients/:id`, [C.consoleClients, 'show']);
928
952
  router.patch(`${ap}/api/clients/:id`, [C.consoleClients, 'update']);
929
953
  router.delete(`${ap}/api/clients/:id`, [C.consoleClients, 'destroy']);
930
954
  router.post(`${ap}/api/clients/:id/regenerate-secret`, [
@@ -1,4 +1,5 @@
1
1
  import type { HttpContext } from '@adonisjs/core/http';
2
+ import type { AuthkitScreen } from '../screen_props.js';
2
3
  /**
3
4
  * Opções do renderer Inertia/React.
4
5
  */
@@ -11,18 +12,10 @@ export interface InertiaRendererOptions {
11
12
  /**
12
13
  * Lista de nomes de view que o host criou como páginas React (allowlist).
13
14
  *
14
- * Quando fornecida, **somente** as views listadas passam pelo Inertia; qualquer
15
- * outra view (não listada, ou prefixada com `admin/`) recai silenciosamente no
16
- * renderer Edge built-in da lib — sem erro 500.
17
- *
18
- * ### ⚠️ Atenção: sem `views`, qualquer tela nova da lib quebra hosts SSR
19
- *
20
- * Se `views` for omitido, o comportamento legado é preservado (todas as views
21
- * são enviadas ao Inertia) para compatibilidade retroativa. Nesse modo, qualquer
22
- * tela nova adicionada pela lib gerará um erro SSR no host ("Cannot read
23
- * properties of undefined (reading 'default')") porque o host não terá criado a
24
- * página React correspondente. **Recomenda-se sempre fornecer `views`** com
25
- * exatamente as páginas que o scaffold (`node ace configure`) gerou.
15
+ * **Somente** as views listadas passam pelo Inertia; qualquer outra view (não
16
+ * listada, ou prefixada com `admin/`) é renderizada pelo renderer Edge built-in
17
+ * da lib. Liste exatamente as páginas que o scaffold (`node ace configure`)
18
+ * gerou, mais as que você criar.
26
19
  *
27
20
  * A ordem do array é irrelevante — `views` é tratado como um *conjunto*
28
21
  * (allowlist). Os nomes têm autocomplete via {@link AuthkitScreen}.
@@ -38,14 +31,9 @@ export interface InertiaRendererOptions {
38
31
  * ],
39
32
  * })
40
33
  */
41
- views?: AuthkitScreen[];
34
+ views: readonly AuthkitScreen[];
42
35
  }
43
- /**
44
- * Nomes das telas conhecidas da lib — dá autocomplete no array `views`.
45
- * O `(string & {})` mantém o tipo aberto: telas custom (ou de versões mais
46
- * novas da lib) continuam aceitas sem erro de compilação.
47
- */
48
- export type AuthkitScreen = 'login' | 'signup' | 'consent' | 'forgot' | 'reset' | 'verify-email' | 'mfa-challenge' | 'otp-unlock' | 'maintenance' | 'session-expired' | 'account/login' | 'account/tokens' | 'account/mfa' | 'account/security' | 'account/apps' | 'account/orgs' | 'account/confirm' | 'account/email-confirmed' | (string & {});
36
+ export type { AuthkitScreen } from '../screen_props.js';
49
37
  /**
50
38
  * Renderer do seam para hosts Inertia/React.
51
39
  *
@@ -53,8 +41,8 @@ export type AuthkitScreen = 'login' | 'signup' | 'consent' | 'forgot' | 'reset'
53
41
  * (O console admin não passa por aqui: é a SPA self-contained da lib,
54
42
  * servida pelo shell controller.)
55
43
  *
56
- * Quando `views` é fornecido (allowlist), apenas as views listadas vão ao Inertia;
57
- * qualquer outra view recai no Edge renderer built-in sem erro.
44
+ * Apenas as views listadas em `views` (allowlist) vão ao Inertia; qualquer outra
45
+ * view é renderizada pelo Edge renderer built-in.
58
46
  *
59
47
  * ---
60
48
  *
@@ -24,8 +24,8 @@ async function resolveMessagesFromCtx(ctx) {
24
24
  * (O console admin não passa por aqui: é a SPA self-contained da lib,
25
25
  * servida pelo shell controller.)
26
26
  *
27
- * Quando `views` é fornecido (allowlist), apenas as views listadas vão ao Inertia;
28
- * qualquer outra view recai no Edge renderer built-in sem erro.
27
+ * Apenas as views listadas em `views` (allowlist) vão ao Inertia; qualquer outra
28
+ * view é renderizada pelo Edge renderer built-in.
29
29
  *
30
30
  * ---
31
31
  *
@@ -59,11 +59,10 @@ async function resolveMessagesFromCtx(ctx) {
59
59
  * ```
60
60
  */
61
61
  export function inertiaRenderer(opts) {
62
- const allowed = opts.views ? new Set(opts.views) : null;
62
+ const allowed = new Set(opts.views);
63
63
  return async (ctx, view, props) => {
64
- // Allowlist: se `views` foi fornecido e a view não está na lista,
65
- // usa o fallback Edge silenciosamente (evita SSR crash por página inexistente no host).
66
- if (allowed !== null && !allowed.has(view)) {
64
+ // Allowlist: view fora da lista vai ao Edge (o host não tem a página React).
65
+ if (!allowed.has(view)) {
67
66
  return renderEdgeView(ctx, view, props);
68
67
  }
69
68
  // Happy path: view vai ao Inertia.
@@ -0,0 +1,309 @@
1
+ /**
2
+ * Props of every screen authkit-server renders through `cfg.render` — what an
3
+ * `inertiaRenderer` page (or a custom renderer) receives for each view.
4
+ *
5
+ * `AuthkitScreenProps['<view>']` is the shape a page gets, `messages` included
6
+ * (the renderer adds it). The controllers call `cfg.render` with
7
+ * {@link ScreenRenderProps} (the same shape without `messages`), and `render` is
8
+ * typed per view, so a controller that drifts from these types fails the
9
+ * package's `tsc`.
10
+ */
11
+ import type { OrgInvitation, OrgMember, OrgSummary } from '../accounts/account_store.js';
12
+ import type { PatRecord } from '../pat/pat_store.js';
13
+ import type { AccountConfirmProps, AccountEmailConfirmedProps, AccountLoginProps, AccountMfaProps, AccountSecurityProps } from './account_screen_props.js';
14
+ import type { BotProtectionWidget } from './bot_protection.js';
15
+ import type { ClientBrand } from './branding.js';
16
+ import type { CustomMfaDescriptor } from './custom_mfa.js';
17
+ import type { AuthMessages } from './i18n.js';
18
+ import type { LastLoginMethod } from './last_login_method.js';
19
+ import type { MagicChannelProp } from './login_channel.js';
20
+ import type { ResolvedAuthMethods, ResolvedOrganizationsPolicySetting } from './runtime_toggles.js';
21
+ /** What every screen receives from the renderer. */
22
+ interface WithMessages {
23
+ /** The i18n catalog for the request's locale (added by the renderer). */
24
+ messages: AuthMessages;
25
+ }
26
+ /** The account a login step is about (step 2+), or `null`. */
27
+ export interface LoginAccount {
28
+ fullName: string | null;
29
+ globalRoles: string[];
30
+ }
31
+ /**
32
+ * `login` — the OIDC interaction's login screen. `step` drives the sub-view:
33
+ * `identifier` (ask for the e-mail), `password`, or `password_expired`.
34
+ */
35
+ export interface LoginProps extends WithMessages {
36
+ uid: string;
37
+ csrfToken: string;
38
+ brand?: ClientBrand;
39
+ step?: 'identifier' | 'password' | 'password_expired';
40
+ email?: string;
41
+ account?: LoginAccount | null;
42
+ error?: string | null;
43
+ /** `true` shows the link to sign up. */
44
+ registrationEnabled?: boolean;
45
+ /** Login methods allowed now (global settings × the account's preference). */
46
+ authMethods?: ResolvedAuthMethods;
47
+ magicLinkAvailable?: boolean;
48
+ otpEnabled?: boolean;
49
+ /** The method this device signed in with last time, for the "last used" badge. */
50
+ lastUsedLoginMethod?: LastLoginMethod;
51
+ passkeyFirstAvailable?: boolean;
52
+ botProtection?: BotProtectionWidget;
53
+ rememberEnabled?: boolean;
54
+ rememberDays?: number;
55
+ /** `true` after a magic link / code was sent. */
56
+ magicLinkSent?: boolean;
57
+ /** Which part of the "sent" state to show; `none` hides both. */
58
+ magicChannel?: MagicChannelProp | 'none';
59
+ /** Error of the one-time code field. */
60
+ otpError?: string;
61
+ }
62
+ /** `signup` — the interaction's registration screen. */
63
+ export interface SignupProps extends WithMessages {
64
+ uid: string;
65
+ csrfToken: string;
66
+ brand?: ClientBrand;
67
+ error?: string;
68
+ registrationDisabled?: boolean;
69
+ registrationDisabledMessage?: string;
70
+ /** `true` when signup sends a magic link instead of asking for a password. */
71
+ passwordlessSignup?: boolean;
72
+ botProtection?: BotProtectionWidget;
73
+ /** `true` after the passwordless signup link was sent. */
74
+ magicLinkSent?: boolean;
75
+ otpEnabled?: boolean;
76
+ }
77
+ /** `forgot` — request a password reset e-mail. */
78
+ export interface ForgotProps extends WithMessages {
79
+ csrfToken: string;
80
+ error?: string;
81
+ botProtection?: BotProtectionWidget;
82
+ /** `true` after the e-mail was sent. */
83
+ sent?: boolean;
84
+ }
85
+ /** `reset` — choose a new password from the e-mailed link. */
86
+ export interface ResetProps extends WithMessages {
87
+ token: string;
88
+ csrfToken: string;
89
+ error?: string;
90
+ /** `true` once the password was changed. */
91
+ done?: boolean;
92
+ }
93
+ /** `verify-email` — result of the e-mail verification link. */
94
+ export interface VerifyEmailProps extends WithMessages {
95
+ verified: boolean;
96
+ }
97
+ /** A host-registered second factor, as the `mfa-challenge` screen lists it. */
98
+ export type MfaChallengeCustomMethod = CustomMfaDescriptor & {
99
+ beginUrl: string;
100
+ verifyUrl: string;
101
+ started: boolean;
102
+ };
103
+ /** `mfa-challenge` — the second factor of a login. */
104
+ export interface MfaChallengeProps extends WithMessages {
105
+ uid: string;
106
+ csrfToken: string;
107
+ brand?: ClientBrand;
108
+ error?: string;
109
+ totpAvailable?: boolean;
110
+ passkeyAvailable?: boolean;
111
+ /** MFA is required but the account has no factor enrolled. */
112
+ noEnrollment?: boolean;
113
+ trustedDevicesEnabled?: boolean;
114
+ trustedDeviceDays?: number;
115
+ /** Too many wrong codes: the code field is locked for now. */
116
+ otpLocked?: boolean;
117
+ customMfaMethods?: MfaChallengeCustomMethod[];
118
+ completedMfaMethods?: string[];
119
+ requiredMfaFactors?: number;
120
+ }
121
+ /** A scope on the consent screen. */
122
+ export interface ConsentScope {
123
+ id: string;
124
+ label: string;
125
+ }
126
+ /**
127
+ * The authorization request behind a consent, as the provider parsed it. Only
128
+ * `client_id` is always there: device-flow and CIBA requests carry no
129
+ * `redirect_uri` / `response_type`, and plain OAuth requests may omit `scope`.
130
+ * Every other request parameter (`audience`, `resource`, `claims`, …) keeps its
131
+ * raw value under its own name.
132
+ */
133
+ export type ConsentRequestParams = {
134
+ client_id: string;
135
+ redirect_uri?: string;
136
+ response_type?: string;
137
+ scope?: string;
138
+ state?: string;
139
+ prompt?: string;
140
+ nonce?: string;
141
+ code_challenge?: string;
142
+ code_challenge_method?: string;
143
+ } & Record<string, unknown>;
144
+ /** `consent` — the user grants the client the requested scopes. */
145
+ export interface ConsentProps extends WithMessages {
146
+ uid: string;
147
+ csrfToken: string;
148
+ brand?: ClientBrand;
149
+ /** The authorization request parameters. */
150
+ params: ConsentRequestParams;
151
+ /** The client's registered `client_name`, or its `client_id`. */
152
+ clientName: string;
153
+ /** `clientName`, HTML-escaped (Edge raw output). */
154
+ clientNameHtml: string;
155
+ scopes: ConsentScope[];
156
+ }
157
+ /** `maintenance` — maintenance mode is on. */
158
+ export interface MaintenanceProps extends WithMessages {
159
+ uid?: string;
160
+ csrfToken: string;
161
+ brand?: ClientBrand;
162
+ message: string;
163
+ /** Admins may still sign in. */
164
+ adminLoginAllowed?: boolean;
165
+ adminLoginNote?: string;
166
+ }
167
+ /** `otp-unlock` — result of the "unlock my code login" link. */
168
+ export interface OtpUnlockProps extends WithMessages {
169
+ ok: boolean;
170
+ expired?: boolean;
171
+ }
172
+ /** `session-expired` — the interaction expired or was lost. */
173
+ export interface SessionExpiredProps extends WithMessages {
174
+ /** Where the "sign in again" button goes (`interactionRecovery.redirectTo`). */
175
+ loginUrl: string;
176
+ brand?: ClientBrand;
177
+ }
178
+ /** `throttled` — the request hit a rate limit. */
179
+ export interface ThrottledProps extends WithMessages {
180
+ brand?: ClientBrand;
181
+ /** Seconds until a retry is allowed, or `null` when unknown. */
182
+ retryAfter: number | null;
183
+ retryUrl: string;
184
+ }
185
+ /** The agent asking for access, on `agents/consent`. */
186
+ export interface AgentConsentRequest {
187
+ /** Device-flow user code, when the agent uses it. */
188
+ userCode?: string;
189
+ agentName: string | null;
190
+ agentOrigin: string | null;
191
+ logoUri?: string | null;
192
+ scopes: Array<{
193
+ id: string;
194
+ description: string;
195
+ }>;
196
+ /** Extra hidden form fields to send back. */
197
+ hidden?: Array<{
198
+ name: string;
199
+ value: string;
200
+ }>;
201
+ }
202
+ /** `agents/consent` — the user approves a personal agent's request. */
203
+ export interface AgentsConsentProps extends WithMessages {
204
+ csrfToken: string;
205
+ /** The form's POST target. */
206
+ action: string;
207
+ /** `null` while the user still has to enter a user code. */
208
+ request: AgentConsentRequest | null;
209
+ account: {
210
+ email: string | null;
211
+ } | null;
212
+ /** i18n key of the error to show, or `null`. */
213
+ error: string | null;
214
+ }
215
+ /** `agents/done` — outcome of a personal agent consent. */
216
+ export interface AgentsDoneProps extends WithMessages {
217
+ status: 'approved' | 'denied' | 'expired' | 'invalid';
218
+ agentName: string | null;
219
+ scopes: Array<{
220
+ id: string;
221
+ description: string;
222
+ }>;
223
+ }
224
+ /** `account/apps` — apps and personal agents with access to the account. */
225
+ export interface AccountAppsProps extends WithMessages {
226
+ csrfToken: string;
227
+ supported: boolean;
228
+ /** Personal agents with a delegation, or `null` when the feature is off. */
229
+ agents: Array<{
230
+ id: string;
231
+ name: string;
232
+ scopes: Array<{
233
+ id: string;
234
+ description: string;
235
+ }>;
236
+ }> | null;
237
+ /** Flash with the revoked app, or `null`. */
238
+ revoked: string | null;
239
+ apps: Array<{
240
+ clientId: string;
241
+ name: string;
242
+ /** The client's registered `logo_uri`, or `null`. */
243
+ logoUrl: string | null;
244
+ accessTokens: number;
245
+ refreshTokens: number;
246
+ }>;
247
+ }
248
+ /** `account/tokens` — personal access tokens. */
249
+ export interface AccountTokensProps extends WithMessages {
250
+ csrfToken: string;
251
+ /** The token just created, shown once, or `null`. */
252
+ createdToken: string | null;
253
+ tokens: Array<Pick<PatRecord, 'id' | 'name' | 'scopes' | 'audience' | 'lastUsedAt' | 'createdAt'>>;
254
+ }
255
+ /** An organization on `account/orgs`. */
256
+ export type AccountOrg = OrgSummary & {
257
+ role: string;
258
+ members: OrgMember[];
259
+ canManage: boolean;
260
+ isActive: boolean;
261
+ };
262
+ /**
263
+ * `account/orgs` — the user's organizations. With `subview: 'accept-invitation'`
264
+ * it is the invitation acceptance screen (`invitation`, `token`).
265
+ */
266
+ export interface AccountOrgsProps extends WithMessages {
267
+ csrfToken: string;
268
+ supported?: boolean;
269
+ orgs?: AccountOrg[];
270
+ pendingInvitations?: Array<OrgInvitation & {
271
+ orgName: string;
272
+ }>;
273
+ allowSelfCreate?: ResolvedOrganizationsPolicySetting['allowSelfCreate'];
274
+ availableRoles?: ResolvedOrganizationsPolicySetting['roles'];
275
+ subview?: 'accept-invitation';
276
+ invitation?: OrgInvitation | null;
277
+ token?: string;
278
+ }
279
+ /** Props of every screen, by view name. */
280
+ export interface AuthkitScreenProps {
281
+ login: LoginProps;
282
+ signup: SignupProps;
283
+ consent: ConsentProps;
284
+ forgot: ForgotProps;
285
+ reset: ResetProps;
286
+ 'verify-email': VerifyEmailProps;
287
+ 'mfa-challenge': MfaChallengeProps;
288
+ 'otp-unlock': OtpUnlockProps;
289
+ maintenance: MaintenanceProps;
290
+ 'session-expired': SessionExpiredProps;
291
+ throttled: ThrottledProps;
292
+ 'agents/consent': AgentsConsentProps;
293
+ 'agents/done': AgentsDoneProps;
294
+ 'account/login': AccountLoginProps;
295
+ 'account/tokens': AccountTokensProps;
296
+ 'account/mfa': AccountMfaProps;
297
+ 'account/security': AccountSecurityProps;
298
+ 'account/apps': AccountAppsProps;
299
+ 'account/orgs': AccountOrgsProps;
300
+ 'account/confirm': AccountConfirmProps;
301
+ 'account/email-confirmed': AccountEmailConfirmedProps;
302
+ }
303
+ /** Every screen authkit-server renders. */
304
+ export type AuthkitScreen = keyof AuthkitScreenProps;
305
+ /** What a controller hands `cfg.render` for a view: the page props without `messages`. */
306
+ export type ScreenRenderProps<View extends AuthkitScreen> = Omit<AuthkitScreenProps[View], 'messages'>;
307
+ /** `cfg.render`, typed per view. */
308
+ export type AuthkitScreenRenderer = <View extends AuthkitScreen>(ctx: import('@adonisjs/core/http').HttpContext, view: View, props: ScreenRenderProps<View>) => unknown;
309
+ export {};
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Props of every screen authkit-server renders through `cfg.render` — what an
3
+ * `inertiaRenderer` page (or a custom renderer) receives for each view.
4
+ *
5
+ * `AuthkitScreenProps['<view>']` is the shape a page gets, `messages` included
6
+ * (the renderer adds it). The controllers call `cfg.render` with
7
+ * {@link ScreenRenderProps} (the same shape without `messages`), and `render` is
8
+ * typed per view, so a controller that drifts from these types fails the
9
+ * package's `tsc`.
10
+ */
11
+ export {};