@adonis-agora/authkit-server 0.63.0 → 0.64.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.
@@ -4,6 +4,24 @@ export type OidcAdapterClass = new (name: string) => OidcAdapter;
4
4
  export interface AdapterFactory {
5
5
  resolver(app: ApplicationService): Promise<OidcAdapterClass>;
6
6
  }
7
+ /**
8
+ * Modelos do oidc-provider de vida curta, amarrados ao ciclo da sessão de
9
+ * login (nomes exatos que o provider passa ao adapter). Perder qualquer um
10
+ * deles custa no máximo um relogin — nunca um outage — então são os
11
+ * candidatos ao adapter da `session:` (Redis). Todo modelo fora daqui
12
+ * (`Client` em particular — sem ele nem a tela de login abre) fica no
13
+ * adapter default (durável).
14
+ */
15
+ export declare const SESSION_SCOPED_MODELS: ReadonlySet<string>;
16
+ /**
17
+ * Escolhe a classe de adapter pro nome do modelo: session-scoped vai pra
18
+ * `sessionClass`, o resto pra `defaultClass`. Pura (sem I/O) de propósito —
19
+ * o dispatcher do provider (`build_provider.ts`) e os serviços que instanciam
20
+ * adapter na mão (`AdminSessionsService`, account API) usam a MESMA regra.
21
+ * Sem `session.adapter` configurado as duas classes são a mesma (back-compat:
22
+ * tudo continua onde está hoje).
23
+ */
24
+ export declare function pickModelAdapterClass(model: string, defaultClass: OidcAdapterClass, sessionClass: OidcAdapterClass): OidcAdapterClass;
7
25
  export interface RedisAdapterConfig {
8
26
  /** nome da conexão do @adonisjs/redis */
9
27
  connection: string;
@@ -1,5 +1,40 @@
1
1
  import { DatabaseAdapter } from './database_adapter.js';
2
2
  import { RedisAdapter } from './redis_adapter.js';
3
+ /**
4
+ * Modelos do oidc-provider de vida curta, amarrados ao ciclo da sessão de
5
+ * login (nomes exatos que o provider passa ao adapter). Perder qualquer um
6
+ * deles custa no máximo um relogin — nunca um outage — então são os
7
+ * candidatos ao adapter da `session:` (Redis). Todo modelo fora daqui
8
+ * (`Client` em particular — sem ele nem a tela de login abre) fica no
9
+ * adapter default (durável).
10
+ */
11
+ export const SESSION_SCOPED_MODELS = new Set([
12
+ 'Session',
13
+ 'Interaction',
14
+ 'Grant',
15
+ 'AccessToken',
16
+ 'RefreshToken',
17
+ 'AuthorizationCode',
18
+ 'DeviceCode',
19
+ 'PushedAuthorizationRequest',
20
+ 'BackchannelAuthenticationRequest',
21
+ 'ClientCredentials',
22
+ 'ReplayDetection',
23
+ // `RegistrationAccessToken`/`InitialAccessToken` ficam FORA de propósito:
24
+ // gerenciam o `Client` (rotação/reconfiguração) e precisam morar junto dele
25
+ // no backend durável — senão um flush do Redis orfana o gerenciamento.
26
+ ]);
27
+ /**
28
+ * Escolhe a classe de adapter pro nome do modelo: session-scoped vai pra
29
+ * `sessionClass`, o resto pra `defaultClass`. Pura (sem I/O) de propósito —
30
+ * o dispatcher do provider (`build_provider.ts`) e os serviços que instanciam
31
+ * adapter na mão (`AdminSessionsService`, account API) usam a MESMA regra.
32
+ * Sem `session.adapter` configurado as duas classes são a mesma (back-compat:
33
+ * tudo continua onde está hoje).
34
+ */
35
+ export function pickModelAdapterClass(model, defaultClass, sessionClass) {
36
+ return SESSION_SCOPED_MODELS.has(model) ? sessionClass : defaultClass;
37
+ }
3
38
  export const adapters = {
4
39
  /**
5
40
  * Factory para o adapter Redis. O consumidor precisa ter o @adonisjs/redis
@@ -706,6 +706,30 @@ export interface ResolvedInteractionRecoveryConfig {
706
706
  export interface AuthServerConfigInput {
707
707
  issuer: string;
708
708
  adapter: AdapterFactory;
709
+ /**
710
+ * Override do adapter pros modelos de vida curta (sessão e órbita dela —
711
+ * ver `SESSION_SCOPED_MODELS` em `adapters/factory.ts`). O `adapter` de cima
712
+ * continua sendo o default de TUDO (back-compat: sem `session`, nada muda);
713
+ * com `session.adapter`, os modelos session-scoped passam a usar ele e o
714
+ * resto (`Client` em particular) fica no `adapter`.
715
+ *
716
+ * O split recomendado: sessão no Redis (efêmero, TTL nativo, mesmo destino
717
+ * da sessão do app) e o resto no banco (durável — sem o `Client` nem a tela
718
+ * de login abre). Mesma connection Redis pros dois níveis de sessão é a
719
+ * recomendação (prefixos já namespaciam; destinos diferentes criam
720
+ * meio-logado e dois domínios de falha no logout), mas a lib não trava isso.
721
+ *
722
+ * ```ts
723
+ * defineConfig({
724
+ * adapter: adapters.database({ connection: 'auth' }),
725
+ * session: { adapter: adapters.redis({ connection: 'main' }) },
726
+ * // ...
727
+ * })
728
+ * ```
729
+ */
730
+ session?: {
731
+ adapter?: AdapterFactory;
732
+ };
709
733
  /**
710
734
  * Clientes OIDC pré-carregados no provider ao subir. Útil para testes e
711
735
  * migrações pontuais. Para uso em produção, gerencie clients via console admin
@@ -1041,6 +1065,11 @@ export interface AuthServerConfigInput {
1041
1065
  export interface ResolvedServerConfig {
1042
1066
  issuer: string;
1043
1067
  AdapterClass: OidcAdapterClass;
1068
+ /**
1069
+ * Adapter dos modelos session-scoped (`SESSION_SCOPED_MODELS`). Sem
1070
+ * `session.adapter` no input é a MESMA classe do `AdapterClass`.
1071
+ */
1072
+ SessionAdapterClass: OidcAdapterClass;
1044
1073
  clients: ClientConfig[];
1045
1074
  jwks: {
1046
1075
  keys: Record<string, any>[];
@@ -243,6 +243,11 @@ export function jwksAutoFallbackWarning(storePath) {
243
243
  export function defineConfig(config) {
244
244
  return configProvider.create(async (app) => {
245
245
  const AdapterClass = await config.adapter.resolver(app);
246
+ // `session.adapter` ausente ⇒ mesma classe do default (back-compat: o
247
+ // dispatcher vira identidade e tudo continua no adapter único de hoje).
248
+ const SessionAdapterClass = config.session?.adapter
249
+ ? await config.session.adapter.resolver(app)
250
+ : AdapterClass;
246
251
  // `jwks: 'auto'` → resolve env-aware: AUTHKIT_JWKS inline, senão managed em arquivo.
247
252
  const jwksConfig = config.jwks === 'auto'
248
253
  ? process.env.AUTHKIT_JWKS
@@ -323,6 +328,7 @@ export function defineConfig(config) {
323
328
  return {
324
329
  issuer: config.issuer,
325
330
  AdapterClass,
331
+ SessionAdapterClass,
326
332
  clients: config.clients ?? [],
327
333
  jwks: jwks,
328
334
  jwksConfig,
@@ -495,9 +495,10 @@ export default class AccountApiController {
495
495
  if (!target) {
496
496
  return ctx.response.notFound(apiErr('not_found', 'Session not found.'));
497
497
  }
498
- // Revoga via adapter diretamente.
499
- const AdapterClass = service.config.AdapterClass;
500
- const sessionAdapter = new AdapterClass('Session');
498
+ // Revoga via adapter diretamente (o da sessão — `Session` é
499
+ // session-scoped e pode viver em backend distinto do default).
500
+ const SessionAdapterClass = service.config.SessionAdapterClass ?? service.config.AdapterClass;
501
+ const sessionAdapter = new SessionAdapterClass('Session');
501
502
  await sessionAdapter.destroy(sessionId);
502
503
  await cfg.audit?.record({
503
504
  type: 'session.revoked',
@@ -19,8 +19,9 @@ export class AdminClientsService {
19
19
  #adapter;
20
20
  constructor(oidc) {
21
21
  this.oidc = oidc;
22
- // O AdapterClass é o MESMO que o provider usa; instanciamos o model 'Client'
23
- // para ler/gravar os mesmos artefatos que o oidc-provider persiste.
22
+ // `Client` NÃO é session-scoped: fica sempre no `AdapterClass` (default),
23
+ // os mesmos artefatos que o oidc-provider persiste — mesmo com `session:`
24
+ // configurado, clients continuam no backend durável.
24
25
  this.#adapter = new oidc.config.AdapterClass('Client');
25
26
  }
26
27
  /** Indica se o adapter suporta enumeração (capacidade opcional). */
@@ -1,8 +1,10 @@
1
1
  import { getBootedApp } from '../../services/booted_app.js';
2
+ import { pickModelAdapterClass } from '../adapters/factory.js';
2
3
  /**
3
- * Serviço de inspeção/revogação das SESSÕES e GRANTS ativos de uma conta,
4
- * persistidos pelo oidc-provider via o MESMO `AdapterClass` (mesmo padrão do
5
- * {@link AdminClientsService}). Encapsula:
4
+ * Serviço de inspeção/revogação das SESSÕES e GRANTS ativos de uma conta.
5
+ * Lê/escreve via `pickModelAdapterClass` (a MESMA regra do provider): com
6
+ * `session:` configurado, Session/Grant/tokens saem do adapter da sessão, não
7
+ * do default. Encapsula:
6
8
  * - a enumeração via a capacidade opcional `list` do adapter (degrada quando
7
9
  * ausente, igual ao CRUD de clients);
8
10
  * - a destruição das sessões + grants da conta. Destruir um grant CASCATEIA a
@@ -16,16 +18,21 @@ import { getBootedApp } from '../../services/booted_app.js';
16
18
  const GLOBAL_SESSION_LIMIT = 500;
17
19
  export class AdminSessionsService {
18
20
  #AdapterClass;
21
+ #SessionAdapterClass;
19
22
  #accountStore;
20
23
  /** Conexão Lucid das tabelas authkit (schema `auth`) — onde vive auth_session_revocations. */
21
24
  #schemaConnection;
22
25
  constructor(oidc) {
23
26
  this.#AdapterClass = oidc.config.AdapterClass;
27
+ // `Session`/`Grant`/tokens vivem no adapter da sessão quando `session:`
28
+ // está configurado (default: o mesmo do `AdapterClass`). `??` cobre
29
+ // configs resolvidas por versões antigas do `defineConfig`.
30
+ this.#SessionAdapterClass = oidc.config.SessionAdapterClass ?? oidc.config.AdapterClass;
24
31
  this.#accountStore = oidc.config.accountStore;
25
32
  this.#schemaConnection = oidc.config.schema?.connection;
26
33
  }
27
34
  #adapter(model) {
28
- return new this.#AdapterClass(model);
35
+ return new (pickModelAdapterClass(model, this.#AdapterClass, this.#SessionAdapterClass))(model);
29
36
  }
30
37
  /**
31
38
  * Grava uma revogação por `sub` na tabela compartilhada `auth_session_revocations`,
@@ -1,4 +1,5 @@
1
1
  import * as oidc from 'oidc-provider';
2
+ import { pickModelAdapterClass } from '../adapters/factory.js';
2
3
  import { createDeviceSources } from './device_sources.js';
3
4
  import { createLogoutSources } from './logout_sources.js';
4
5
  /** Atualiza o holder mutável do TTL de sessão com os valores da setting. */
@@ -92,7 +93,12 @@ export function buildProvider(config, options, sessionTtlHolder, tokenTtlHolder)
92
93
  }
93
94
  : {};
94
95
  const provider = new oidc.Provider(config.issuer, {
95
- adapter: config.AdapterClass,
96
+ // Dispatcher por modelo (suportado pelo oidc-provider: `Adapter` aceita
97
+ // função `(name) => adapter` além de classe). Session-scoped vai pro
98
+ // `SessionAdapterClass`, o resto pro `AdapterClass` — mesma regra de
99
+ // `pickModelAdapterClass`, usada também pelos serviços que instanciam
100
+ // adapter na mão. Sem `session.adapter`, as duas classes são a mesma.
101
+ adapter: ((name) => new (pickModelAdapterClass(name, config.AdapterClass, config.SessionAdapterClass))(name)),
96
102
  clients: config.clients.map((c) => ({
97
103
  client_id: c.clientId,
98
104
  client_secret: c.clientSecret,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adonis-agora/authkit-server",
3
- "version": "0.63.0",
3
+ "version": "0.64.0",
4
4
  "description": "AdonisJS OIDC/OAuth2 provider (Identity Provider) toolkit: ejectable auth server with sessions, rate-limiting, MFA/TOTP, audit log, federated logout and OpenTelemetry metrics.",
5
5
  "license": "MIT",
6
6
  "author": "dudousxd",