@adonis-agora/authkit-server 1.0.0-rc.11 → 1.0.0-rc.12

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 +20 -0
  2. package/README.md +1 -1
  3. package/build/index.d.ts +2 -1
  4. package/build/index.js +1 -0
  5. package/build/providers/authkit_server_provider.js +15 -4
  6. package/build/src/accounts/lucid_account_store.js +4 -3
  7. package/build/src/accounts/lucid_store/mfa.js +1 -1
  8. package/build/src/accounts/lucid_store/organization_models.d.ts +9 -5
  9. package/build/src/accounts/lucid_store/organization_models.js +16 -12
  10. package/build/src/accounts/lucid_store/password_hygiene.js +4 -2
  11. package/build/src/accounts/lucid_store/shared.d.ts +6 -1
  12. package/build/src/accounts/lucid_store/shared.js +7 -6
  13. package/build/src/accounts/lucid_store/webauthn.js +1 -1
  14. package/build/src/adapters/database_adapter.d.ts +7 -1
  15. package/build/src/adapters/database_adapter.js +24 -13
  16. package/build/src/adapters/factory.d.ts +7 -0
  17. package/build/src/adapters/factory.js +6 -2
  18. package/build/src/agents/delegation_store.d.ts +5 -1
  19. package/build/src/agents/delegation_store.js +34 -21
  20. package/build/src/agents/extension.d.ts +16 -2
  21. package/build/src/agents/registry_store.d.ts +1 -0
  22. package/build/src/agents/registry_store.js +10 -8
  23. package/build/src/agents/runtime.js +6 -2
  24. package/build/src/commands/expire_scan_command.js +2 -3
  25. package/build/src/commands/settings_commands.js +4 -4
  26. package/build/src/define_config.d.ts +8 -0
  27. package/build/src/define_config.js +23 -6
  28. package/build/src/doctor/agora_checks.js +13 -0
  29. package/build/src/doctor/db_schema_check.d.ts +6 -0
  30. package/build/src/doctor/db_schema_check.js +87 -0
  31. package/build/src/host/admin_sessions_service.js +6 -2
  32. package/build/src/host/runtime_settings.d.ts +15 -0
  33. package/build/src/host/runtime_settings.js +27 -12
  34. package/build/src/keys/keystore_manager.js +2 -1
  35. package/build/src/keys/keystore_vault.d.ts +9 -1
  36. package/build/src/keys/keystore_vault.js +27 -9
  37. package/build/src/schema/ensure.d.ts +9 -0
  38. package/build/src/schema/ensure.js +46 -19
  39. package/build/src/schema/tables_location.d.ts +31 -2
  40. package/build/src/schema/tables_location.js +64 -0
  41. package/package.json +3 -3
  42. package/skills/authkit-idp-setup/SKILL.md +13 -7
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # @adonis-agora/authkit-server
2
2
 
3
+ ## 1.0.0-rc.12
4
+
5
+ ### Minor Changes
6
+
7
+ - [#474](https://github.com/DavideCarvalho/adonis-agora-authkit/pull/474) [`df9e49d`](https://github.com/DavideCarvalho/adonis-agora-authkit/commit/df9e49d7817c03a7cce4cac1df709c7788d6307c) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - AuthKit's tables can live in their own database schema: `schema: { name: 'auth' }` in `defineConfig` puts every table AuthKit owns in `auth` (`"auth"."auth_settings"`) while the app stays in `public`. The schema is written into every statement, so it works on any connection and inside the app's own transaction, with no `searchPath` (which leaks across clients behind PgBouncer in transaction mode).
8
+
9
+ - The boot creates the schema (`CREATE SCHEMA IF NOT EXISTS`; MySQL: `CREATE DATABASE IF NOT EXISTS`) when `autoManage` is on, and fails with a message naming the schema and the grant when the role cannot create it. `ensureAuthkitSchema(db, { schema })` does the same from a migration.
10
+ - Covers the OIDC adapter table (`adapters.database({ schema })` overrides), the Lucid keystore (`jwks.store: { driver: 'lucid', schema }` overrides), the account store's tables (MFA, password history, organizations), runtime settings, session revocations, the personal-agent tables and agent extensions (`PersonalAgentHost.table(name)`, `AuthkitTable.create(table, { table })`, `ensureAuthkitTables(db, tables, { schema })`; Poppy uses them and `ensurePoppySchema(db, { schema })`).
11
+ - authkit-client: `lucidRevocationStore({ schema })`.
12
+ - SQLite with `schema.name` fails the boot instead of ignoring the option.
13
+ - `agora:doctor` gains `authkit.db-schema`: the schema exists, the tables are in it, the connection can write there. Silent without `schema.name`.
14
+ - The key-rotation lock key includes the schema, so two installs in two schemas of one database do not share it.
15
+
16
+ Without `schema.name` nothing changes.
17
+
18
+ ### Patch Changes
19
+
20
+ - Updated dependencies [[`df9e49d`](https://github.com/DavideCarvalho/adonis-agora-authkit/commit/df9e49d7817c03a7cce4cac1df709c7788d6307c)]:
21
+ - @adonis-agora/authkit-core@1.0.0-rc.2
22
+
3
23
  ## 1.0.0-rc.11
4
24
 
5
25
  ### Patch Changes
package/README.md CHANGED
@@ -119,7 +119,7 @@ router.post('/auth/interaction/:uid/consent', [AuthInteractionController, 'conse
119
119
 
120
120
  Escolha o backend no `config/authkit.ts`:
121
121
  - `adapters.redis({ connection })` — requer `@adonisjs/redis` configurado.
122
- - `adapters.database({ connection? })` — Lucid; tabela `authkit_oidc_payloads`, criada pelo `ensureAuthkitSchema` no boot (com `schema: { autoManage: false }`, chame-o numa migração). A coluna indexada `account_id` mantém as buscas e revogações por conta em SQL; linhas antigas com `account_id` NULL continuam sendo encontradas. Postgres, MySQL e SQLite.
122
+ - `adapters.database({ connection?, schema? })` — Lucid (default: `schema.connection`/`schema.name` do `defineConfig`); tabela `authkit_oidc_payloads`, criada pelo `ensureAuthkitSchema` no boot (com `schema: { autoManage: false }`, chame-o numa migração). A coluna indexada `account_id` mantém as buscas e revogações por conta em SQL; linhas antigas com `account_id` NULL continuam sendo encontradas. Postgres, MySQL e SQLite.
123
123
 
124
124
  ## Observabilidade
125
125
 
package/build/index.d.ts CHANGED
@@ -15,7 +15,7 @@ export type { PersonalAgentIdentity } from './src/agents/agent_identity.js';
15
15
  export { PersonalAgentVerifier } from './src/agents/agent_identity.js';
16
16
  export type { PersonalAgentDelegationConfigInput, PersonalAgentRegistration, PersonalAgentResolver, PersonalAgentsConfigInput, ResolvedPersonalAgentsConfig, } from './src/agents/config.js';
17
17
  export type { DelegationContext, DelegationGrantSummary, DelegationReceipt, } from './src/agents/delegation_service.js';
18
- export type { AuthkitTable, PersonalAgentAccessTokens, PersonalAgentConsoleSummary, PersonalAgentExtension, PersonalAgentExtensionContext, PersonalAgentExtensionDefinition, PersonalAgentGrant, PersonalAgentGrantLedger, PersonalAgentGrantSummary, PersonalAgentHost, PersonalAgentProtocolRuntime, PersonalAgentRefreshToken, PersonalAgentRouteKit, ResolvedPersonalAgentExtension, } from './src/agents/extension.js';
18
+ export type { AuthkitTable, AuthkitTableContext, PersonalAgentAccessTokens, PersonalAgentConsoleSummary, PersonalAgentExtension, PersonalAgentExtensionContext, PersonalAgentExtensionDefinition, PersonalAgentGrant, PersonalAgentGrantLedger, PersonalAgentGrantSummary, PersonalAgentHost, PersonalAgentProtocolRuntime, PersonalAgentRefreshToken, PersonalAgentRouteKit, ResolvedPersonalAgentExtension, } from './src/agents/extension.js';
19
19
  export type { PersonalAgentAuthOptions, PersonalAgentRequestContext, } from './src/agents/middleware.js';
20
20
  export { personalAgentAuth, personalAgentOf, personalAgentReceipt, personalAgentSecurity, personalAgentStepUp, } from './src/agents/middleware.js';
21
21
  export type { BuiltinProtocolId, PactOptions, PersonalAgentProtocol, } from './src/agents/protocol.js';
@@ -160,6 +160,7 @@ export { checkClientRegistration, classifyRedirect, type RedirectUriPolicy, type
160
160
  export { registerOidcRoutes } from './src/register_routes.js';
161
161
  export type { EnsureSchemaOptions, EnsureSchemaReport } from './src/schema/ensure.js';
162
162
  export { ensureAuthkitSchema, ensureAuthkitTables } from './src/schema/ensure.js';
163
+ export { AuthkitSchemaError } from './src/schema/tables_location.js';
163
164
  export type { ServiceTenantInfo } from './src/service_tenants/client_metadata.js';
164
165
  export type { ResolvedServiceTenantResource, ResolvedServiceTenantsConfig, ServiceTenantResourceInput, ServiceTenantsConfigInput, } from './src/service_tenants/config.js';
165
166
  export { AuthkitTenantVerifier, type AuthkitTenantVerifierFn, type AuthkitTenantVerifierOptions, authkitTenantVerifier, type TenantTokenIssuer, type TenantVerifierInput, type VerifiedTenant, } from './src/service_tenants/tenant_verifier.js';
package/build/index.js CHANGED
@@ -137,6 +137,7 @@ export { OidcService } from './src/provider/oidc_service.js';
137
137
  export { checkClientRegistration, classifyRedirect, RegistrationPolicyError, } from './src/provider/registration_policy.js';
138
138
  export { registerOidcRoutes } from './src/register_routes.js';
139
139
  export { ensureAuthkitSchema, ensureAuthkitTables } from './src/schema/ensure.js';
140
+ export { AuthkitSchemaError } from './src/schema/tables_location.js';
140
141
  export { AuthkitTenantVerifier, authkitTenantVerifier, } from './src/service_tenants/tenant_verifier.js';
141
142
  export { authkitTenantLabels, } from './src/tenancy/tenant_labels.js';
142
143
  export { stubsRoot } from './stubs/main.js';
@@ -22,7 +22,7 @@ import { authkitDoctorChecks } from '../src/doctor/agora_checks.js';
22
22
  import { registerDoctorChecks } from '../src/doctor/agora_doctor.js';
23
23
  import { resolveAppKey } from '../src/host/app_key.js';
24
24
  import { resolveEffectiveKeyRotation } from '../src/host/key_rotation.js';
25
- import { RuntimeSettings } from '../src/host/runtime_settings.js';
25
+ import { RuntimeSettings, settingsLocation } from '../src/host/runtime_settings.js';
26
26
  import { KeystoreCodec } from '../src/keys/keystore_codec.js';
27
27
  import { loadEncryptionService } from '../src/keys/keystore_crypto.js';
28
28
  import { KeystoreManager, resolveKeystoreVault } from '../src/keys/keystore_manager.js';
@@ -30,6 +30,7 @@ import { KeyRotationScheduler } from '../src/provider/key_rotation_scheduler.js'
30
30
  import { KeystoreReloadPoller } from '../src/provider/keystore_reload.js';
31
31
  import { OidcService } from '../src/provider/oidc_service.js';
32
32
  import { makeSingleFlightLock } from '../src/provider/single_flight_lock.js';
33
+ import { AuthkitSchemaError } from '../src/schema/tables_location.js';
33
34
  export default class AuthkitServerProvider {
34
35
  app;
35
36
  constructor(app) {
@@ -286,6 +287,7 @@ export default class AuthkitServerProvider {
286
287
  const { ensureAuthkitSchema } = await import('../src/schema/ensure.js');
287
288
  const report = await ensureAuthkitSchema(db, {
288
289
  connection: config.schema.connection,
290
+ schema: config.schema.name,
289
291
  accountTable: config.accountStore?.accountTable,
290
292
  accountConnection: config.accountStore?.connectionName,
291
293
  tables: config.personalAgents?.extensions?.flatMap((e) => e.tables ?? []),
@@ -306,6 +308,10 @@ export default class AuthkitServerProvider {
306
308
  }
307
309
  }
308
310
  catch (error) {
311
+ // `schema.name` que o banco não aceita (SQLite, schema que o role não pode criar):
312
+ // derruba o boot com a mensagem — nunca segue com tabelas fora do schema pedido.
313
+ if (error instanceof AuthkitSchemaError)
314
+ throw error;
309
315
  const logger = await this.app.container.make('logger').catch(() => null);
310
316
  logger?.warn({ err: error }, 'authkit: schema auto-manage failed — features degrade gracefully; ' +
311
317
  'run ensureAuthkitSchema() in a migration or fix DB connectivity');
@@ -365,10 +371,15 @@ export default class AuthkitServerProvider {
365
371
  .resolve(this.app, value)
366
372
  .catch(() => null))
367
373
  : null;
368
- const connection = config?.schema?.connection;
369
374
  // RuntimeSettings sem request (mesmo padrão de expire_scan_command / security_notice_service).
370
- const settings = new RuntimeSettings(db, connection ? { connection } : {});
371
- const withLock = makeSingleFlightLock({ key: 'authkit:keys:rotate', ttlMs: 5 * 60_000 });
375
+ const settings = new RuntimeSettings(db, settingsLocation(config));
376
+ // Com `schema.name`, a chave do lock leva o schema: duas instalações em schemas
377
+ // diferentes do mesmo banco não disputam o mesmo lock.
378
+ const schemaName = config?.schema?.name;
379
+ const withLock = makeSingleFlightLock({
380
+ key: schemaName ? `authkit:${schemaName}:keys:rotate` : 'authkit:keys:rotate',
381
+ ttlMs: 5 * 60_000,
382
+ });
372
383
  const scheduler = new KeyRotationScheduler({
373
384
  policy: () => resolveEffectiveKeyRotation(settings),
374
385
  ageDays: () => svc.keystoreAgeDays(),
@@ -1,5 +1,5 @@
1
1
  import { PasswordManager } from '../password/password_manager.js';
2
- import { authkitTablesClient, BIND_TABLES_LOCATION, } from '../schema/tables_location.js';
2
+ import { authkitTable, authkitTablesClient, BIND_TABLES_LOCATION, } from '../schema/tables_location.js';
3
3
  import { buildCore } from './lucid_store/core.js';
4
4
  import { buildLoginMethods, supportsLoginMethodsColumn } from './lucid_store/login_methods.js';
5
5
  import { buildMfa } from './lucid_store/mfa.js';
@@ -178,7 +178,7 @@ export function lucidAccountStore(Model, options = {}) {
178
178
  return authkitTablesClient(db, tablesLocation);
179
179
  };
180
180
  const orgModels = () => explicitOrgModels ??
181
- (tablesLocation ? organizationModelsOn(tablesLocation.connection) : defaultOrganizationModels);
181
+ (tablesLocation ? organizationModelsOn(tablesLocation) : defaultOrganizationModels);
182
182
  const OrgModels = orgModelsOption === false ? undefined : orgModels;
183
183
  // RP do WebAuthn: usado nas cerimônias. Default do rpName cai no mfaIssuer.
184
184
  const webauthn = options.webauthn ?? {
@@ -204,6 +204,7 @@ export function lucidAccountStore(Model, options = {}) {
204
204
  emailVerificationTtlHours: emailTokens.verificationTtlHours,
205
205
  emailChangeTtlHours: emailTokens.changeTtlHours,
206
206
  tablesClient,
207
+ tableName: (name) => authkitTable(tablesLocation ?? undefined, name),
207
208
  // Encripta o segredo antes de persistir (no-op sem encrypter).
208
209
  sealSecret: (secret) => (encrypter ? encrypter.encrypt(secret) : secret),
209
210
  // Decripta o segredo armazenado; retorna null em falha/adulteração (no-op sem encrypter).
@@ -345,7 +346,7 @@ export function lucidAccountStore(Model, options = {}) {
345
346
  // Não-enumerável: o `defineConfig` informa `schema.connection` por aqui.
346
347
  Object.defineProperty(store, BIND_TABLES_LOCATION, {
347
348
  value: (location) => {
348
- tablesLocation = { connection: location.connection };
349
+ tablesLocation = { connection: location.connection, schema: location.schema };
349
350
  },
350
351
  });
351
352
  return store;
@@ -19,7 +19,7 @@ import { buildMfaStateRepo, generateRecoveryCode, hashesEqual, sha256, } from '.
19
19
  const TOTP_PERIOD = 30;
20
20
  export function buildMfa(ctx) {
21
21
  const { Model, mfaIssuer, recoveryCodeCount, sealSecret, openSecret } = ctx;
22
- const repo = buildMfaStateRepo(Model, ctx.tablesClient);
22
+ const repo = buildMfaStateRepo(Model, ctx.tablesClient, ctx.tableName);
23
23
  return {
24
24
  async getMfaState(accountId) {
25
25
  const state = await repo.read(accountId);
@@ -11,8 +11,8 @@ import type { DateTime } from 'luxon';
11
11
  * sintoma aparece longe da causa.
12
12
  *
13
13
  * Use com `organizationModels: true` em {@link lucidAccountStore}. Eles seguem a
14
- * conexão das tabelas do authkit (`schema.connection` do `defineConfig`, como
15
- * todas as tabelas lib-owned — ver {@link organizationModelsOn}). O caminho
14
+ * conexão e o schema das tabelas do authkit (`schema.connection`/`schema.name`
15
+ * do `defineConfig`, como todas as tabelas lib-owned — ver {@link organizationModelsOn}). O caminho
16
16
  * explícito (`{ OrgModel, MemberModel, InvitationModel }`) continua valendo como
17
17
  * escape hatch; aí a conexão é a que os models do host declaram.
18
18
  *
@@ -72,7 +72,11 @@ export type OrganizationModels = {
72
72
  InvitationModel: any;
73
73
  };
74
74
  /**
75
- * Os models default numa conexão Lucid nomeada (subclasses com `static connection`),
76
- * memoizados por conexão. `undefined` → os próprios defaults (conexão primária).
75
+ * Os models default onde vivem as tabelas do authkit: subclasses com `static connection`
76
+ * (`schema.connection`) e `static table` qualificada (`schema.name`), memoizadas por
77
+ * local. Sem conexão nem schema → os próprios defaults (conexão primária).
77
78
  */
78
- export declare function organizationModelsOn(connection: string | undefined): OrganizationModels;
79
+ export declare function organizationModelsOn(location: {
80
+ connection?: string;
81
+ schema?: string;
82
+ }): OrganizationModels;
@@ -16,8 +16,8 @@ import { BaseModel, column } from '@adonisjs/lucid/orm';
16
16
  * sintoma aparece longe da causa.
17
17
  *
18
18
  * Use com `organizationModels: true` em {@link lucidAccountStore}. Eles seguem a
19
- * conexão das tabelas do authkit (`schema.connection` do `defineConfig`, como
20
- * todas as tabelas lib-owned — ver {@link organizationModelsOn}). O caminho
19
+ * conexão e o schema das tabelas do authkit (`schema.connection`/`schema.name`
20
+ * do `defineConfig`, como todas as tabelas lib-owned — ver {@link organizationModelsOn}). O caminho
21
21
  * explícito (`{ OrgModel, MemberModel, InvitationModel }`) continua valendo como
22
22
  * escape hatch; aí a conexão é a que os models do host declaram.
23
23
  *
@@ -112,30 +112,34 @@ export const defaultOrganizationModels = {
112
112
  MemberModel: AuthOrganizationMember,
113
113
  InvitationModel: AuthOrganizationInvitation,
114
114
  };
115
- const modelsByConnection = new Map();
115
+ const modelsByLocation = new Map();
116
116
  /**
117
- * Os models default numa conexão Lucid nomeada (subclasses com `static connection`),
118
- * memoizados por conexão. `undefined` → os próprios defaults (conexão primária).
117
+ * Os models default onde vivem as tabelas do authkit: subclasses com `static connection`
118
+ * (`schema.connection`) e `static table` qualificada (`schema.name`), memoizadas por
119
+ * local. Sem conexão nem schema → os próprios defaults (conexão primária).
119
120
  */
120
- export function organizationModelsOn(connection) {
121
- if (!connection)
121
+ export function organizationModelsOn(location) {
122
+ const { connection, schema } = location;
123
+ if (!connection && !schema)
122
124
  return defaultOrganizationModels;
123
- let models = modelsByConnection.get(connection);
125
+ const key = `${connection ?? ''}\x00${schema ?? ''}`;
126
+ let models = modelsByLocation.get(key);
124
127
  if (!models) {
128
+ const table = (name) => (schema ? `${schema}.${name}` : name);
125
129
  class OrgModel extends AuthOrganization {
126
- static table = 'auth_organizations';
130
+ static table = table('auth_organizations');
127
131
  static connection = connection;
128
132
  }
129
133
  class MemberModel extends AuthOrganizationMember {
130
- static table = 'auth_organization_members';
134
+ static table = table('auth_organization_members');
131
135
  static connection = connection;
132
136
  }
133
137
  class InvitationModel extends AuthOrganizationInvitation {
134
- static table = 'auth_organization_invitations';
138
+ static table = table('auth_organization_invitations');
135
139
  static connection = connection;
136
140
  }
137
141
  models = { OrgModel, MemberModel, InvitationModel };
138
- modelsByConnection.set(connection, models);
142
+ modelsByLocation.set(key, models);
139
143
  }
140
144
  return models;
141
145
  }
@@ -17,8 +17,10 @@ export function buildPasswordHistory(ctx, db) {
17
17
  * Lucid expõe `db.query().from(table)` para SELECT e `db.table(table).insert()` para INSERT.
18
18
  * Para DELETE usamos `db.query().from(table)`.
19
19
  */
20
+ // Lido a cada uso: o `defineConfig` informa o schema depois de o store existir.
21
+ const table = () => ctx.tableName ? ctx.tableName('auth_password_history') : 'auth_password_history';
20
22
  function qFrom() {
21
- return db.query().from('auth_password_history');
23
+ return db.query().from(table());
22
24
  }
23
25
  return {
24
26
  async isPasswordReused(accountId, plainPassword, count, nativeVerify) {
@@ -51,7 +53,7 @@ export function buildPasswordHistory(ctx, db) {
51
53
  },
52
54
  async recordPasswordHistory(accountId, oldHash) {
53
55
  try {
54
- await db.table('auth_password_history').insert({
56
+ await db.table(table()).insert({
55
57
  account_id: accountId,
56
58
  password_hash: oldHash,
57
59
  created_at: DateTime.now().toISO(),
@@ -84,6 +84,11 @@ export interface LucidStoreContext {
84
84
  * → the model's connection (a store used without `defineConfig`).
85
85
  */
86
86
  tablesClient?: () => any;
87
+ /**
88
+ * Nome de uma tabela do authkit como os statements a escrevem: com `schema.name`,
89
+ * `schema.tabela` (o knex cita cada parte). Ausente → o nome puro.
90
+ */
91
+ tableName?: (name: string) => string;
87
92
  }
88
93
  export declare const sha256: (value: string) => string;
89
94
  /** Recovery code legível: 10 chars hex em duas metades (ex.: a1b2c-3d4e5). */
@@ -121,7 +126,7 @@ export interface MfaStatePatch {
121
126
  * JSON (string) na escrita e desserializado na leitura (aceita tanto string —
122
127
  * sqlite/text — quanto array já parseado — pg jsonb).
123
128
  */
124
- export declare function buildMfaStateRepo(Model: any, tablesClient?: () => any): {
129
+ export declare function buildMfaStateRepo(Model: any, tablesClient?: () => any, tableName?: (name: string) => string): {
125
130
  /** Lê o estado de MFA da conta; null se não há linha em `auth_mfa`. */
126
131
  read(accountId: string): Promise<MfaState | null>;
127
132
  /**
@@ -49,8 +49,9 @@ export function hashesEqual(a, b) {
49
49
  * JSON (string) na escrita e desserializado na leitura (aceita tanto string —
50
50
  * sqlite/text — quanto array já parseado — pg jsonb).
51
51
  */
52
- export function buildMfaStateRepo(Model, tablesClient) {
53
- const TABLE = 'auth_mfa';
52
+ export function buildMfaStateRepo(Model, tablesClient, tableName = (name) => name) {
53
+ // Lido a cada uso: o `defineConfig` informa o schema depois de o store existir.
54
+ const table = () => tableName('auth_mfa');
54
55
  /** QueryClient das tabelas do authkit (equivale ao `db` do password_history). */
55
56
  function client() {
56
57
  return tablesClient ? tablesClient() : Model.query().client;
@@ -92,7 +93,7 @@ export function buildMfaStateRepo(Model, tablesClient) {
92
93
  return {
93
94
  /** Lê o estado de MFA da conta; null se não há linha em `auth_mfa`. */
94
95
  async read(accountId) {
95
- const row = await client().from(TABLE).where('account_id', accountId).first();
96
+ const row = await client().from(table()).where('account_id', accountId).first();
96
97
  if (!row)
97
98
  return null;
98
99
  return {
@@ -126,17 +127,17 @@ export function buildMfaStateRepo(Model, tablesClient) {
126
127
  }
127
128
  if ('lastTotpStep' in patch)
128
129
  update.last_totp_step = patch.lastTotpStep ?? null;
129
- const affected = await client().from(TABLE).where('account_id', accountId).update(update);
130
+ const affected = await client().from(table()).where('account_id', accountId).update(update);
130
131
  // knex update retorna o nº de linhas afetadas (0 = linha ainda não existe).
131
132
  if (!affected) {
132
133
  await client()
133
- .table(TABLE)
134
+ .table(table())
134
135
  .insert({ account_id: accountId, ...update });
135
136
  }
136
137
  },
137
138
  /** Remove o estado de MFA da conta (disable / reset total). No-op se ausente. */
138
139
  async clear(accountId) {
139
- await client().from(TABLE).where('account_id', accountId).delete();
140
+ await client().from(table()).where('account_id', accountId).delete();
140
141
  },
141
142
  };
142
143
  }
@@ -8,7 +8,7 @@ export function buildWebauthn(ctx, Credential, webauthn, ceremonies) {
8
8
  const { Model } = ctx;
9
9
  // Estado de MFA é LIB-OWNED (tabela `auth_mfa`): registrar uma passkey habilita
10
10
  // o MFA gravando `mfa_enabled_at` em `auth_mfa`, não numa coluna do model.
11
- const mfaState = buildMfaStateRepo(Model, ctx.tablesClient);
11
+ const mfaState = buildMfaStateRepo(Model, ctx.tablesClient, ctx.tableName);
12
12
  return {
13
13
  async generatePasskeyRegistrationOptions(accountId) {
14
14
  const row = await Model.find(accountId);
@@ -1,10 +1,16 @@
1
1
  import type { Database } from '@adonisjs/lucid/database';
2
2
  import type { EnumeratedArtifact, OidcAdapter, OidcPayload } from './adapter_contract.js';
3
+ /** Nome da tabela (sem schema). O adapter recebe o nome já qualificado pelo factory. */
4
+ export declare const OIDC_PAYLOADS_TABLE = "authkit_oidc_payloads";
3
5
  export declare class DatabaseAdapter implements OidcAdapter {
4
6
  #private;
5
7
  private name;
6
8
  private db;
7
- constructor(name: string, db: Database);
9
+ /** `authkit_oidc_payloads`, qualificada com `schema.name` quando há schema. */
10
+ private table;
11
+ constructor(name: string, db: Database,
12
+ /** `authkit_oidc_payloads`, qualificada com `schema.name` quando há schema. */
13
+ table?: string);
8
14
  upsert(id: string, payload: OidcPayload, expiresIn: number): Promise<void>;
9
15
  renewSession(id: string, expiresIn: number): Promise<boolean>;
10
16
  find(id: string): Promise<OidcPayload | undefined>;
@@ -1,4 +1,5 @@
1
- const TABLE = 'authkit_oidc_payloads';
1
+ /** Nome da tabela (sem schema). O adapter recebe o nome já qualificado pelo factory. */
2
+ export const OIDC_PAYLOADS_TABLE = 'authkit_oidc_payloads';
2
3
  /** Intervalo mínimo, por conexão, entre duas limpezas das linhas expiradas. */
3
4
  const PRUNE_INTERVAL_MS = 10 * 60_000;
4
5
  /** Quanto tempo a ausência da coluna `account_id` vale antes de re-checar. */
@@ -6,11 +7,17 @@ const ACCOUNT_COLUMN_RECHECK_MS = 60_000;
6
7
  /** Tamanho máximo de uma lista `IN (...)`. */
7
8
  const IN_CHUNK = 500;
8
9
  const connectionStates = new WeakMap();
9
- function connectionState(db) {
10
- let state = connectionStates.get(db);
10
+ /** Estado por conexão E tabela (duas instalações em schemas diferentes não se misturam). */
11
+ function connectionState(db, table) {
12
+ let byTable = connectionStates.get(db);
13
+ if (!byTable) {
14
+ byTable = new Map();
15
+ connectionStates.set(db, byTable);
16
+ }
17
+ let state = byTable.get(table);
11
18
  if (!state) {
12
19
  state = { accountColumn: null, accountColumnRecheckAt: 0, nextPruneAt: 0 };
13
- connectionStates.set(db, state);
20
+ byTable.set(table, state);
14
21
  }
15
22
  return state;
16
23
  }
@@ -29,12 +36,16 @@ function chunks(items) {
29
36
  export class DatabaseAdapter {
30
37
  name;
31
38
  db;
32
- constructor(name, db) {
39
+ table;
40
+ constructor(name, db,
41
+ /** `authkit_oidc_payloads`, qualificada com `schema.name` quando há schema. */
42
+ table = OIDC_PAYLOADS_TABLE) {
33
43
  this.name = name;
34
44
  this.db = db;
45
+ this.table = table;
35
46
  }
36
47
  #query() {
37
- return this.db.query().from(TABLE).where('model_name', this.name);
48
+ return this.db.query().from(this.table).where('model_name', this.name);
38
49
  }
39
50
  /**
40
51
  * Valor de `expires_at` (gravação e comparação) para o dialeto da conexão. No
@@ -43,7 +54,7 @@ export class DatabaseAdapter {
43
54
  * driver serializa no formato do banco — o MySQL recusa o ISO com `Z`.
44
55
  */
45
56
  #timestamp(ms) {
46
- const state = connectionState(this.db);
57
+ const state = connectionState(this.db, this.table);
47
58
  if (state.dialect === undefined) {
48
59
  // O factory passa o manager (`Database`) ou um `QueryClient` de conexão nomeada.
49
60
  const db = this.db;
@@ -66,13 +77,13 @@ export class DatabaseAdapter {
66
77
  * re-checado, para pegar o ALTER do boot que rodou depois do primeiro request.
67
78
  */
68
79
  async #hasAccountColumn() {
69
- const state = connectionState(this.db);
80
+ const state = connectionState(this.db, this.table);
70
81
  if (state.accountColumn === true)
71
82
  return true;
72
83
  if (state.accountColumn === false && state.accountColumnRecheckAt > Date.now())
73
84
  return false;
74
85
  try {
75
- await this.db.query().from(TABLE).select('account_id').limit(1);
86
+ await this.db.query().from(this.table).select('account_id').limit(1);
76
87
  state.accountColumn = true;
77
88
  }
78
89
  catch {
@@ -88,7 +99,7 @@ export class DatabaseAdapter {
88
99
  * do caminho do request e nunca lança.
89
100
  */
90
101
  #maybePrune() {
91
- const state = connectionState(this.db);
102
+ const state = connectionState(this.db, this.table);
92
103
  const now = Date.now();
93
104
  if (state.nextPruneAt > now)
94
105
  return;
@@ -96,7 +107,7 @@ export class DatabaseAdapter {
96
107
  Promise.resolve()
97
108
  .then(() => this.db
98
109
  .query()
99
- .from(TABLE)
110
+ .from(this.table)
100
111
  .whereNotNull('expires_at')
101
112
  .where('expires_at', '<=', this.#timestamp(now))
102
113
  .delete())
@@ -125,7 +136,7 @@ export class DatabaseAdapter {
125
136
  await this.#query().where('id', id).update(row);
126
137
  }
127
138
  else {
128
- await this.db.table(TABLE).insert(row);
139
+ await this.db.table(this.table).insert(row);
129
140
  }
130
141
  this.#maybePrune();
131
142
  }
@@ -185,7 +196,7 @@ export class DatabaseAdapter {
185
196
  await this.#query().where('id', id).delete();
186
197
  }
187
198
  async revokeByGrantId(grantId) {
188
- await this.db.query().from(TABLE).where('grant_id', grantId).delete();
199
+ await this.db.query().from(this.table).where('grant_id', grantId).delete();
189
200
  }
190
201
  /**
191
202
  * Enumeração genérica dos artefatos do model deste adapter (id + payload).
@@ -8,6 +8,8 @@ export interface AdapterResolveContext {
8
8
  * on. A database adapter without its own `connection` uses it.
9
9
  */
10
10
  connection?: string;
11
+ /** `schema.name` of `defineConfig`. A database adapter without its own `schema` uses it. */
12
+ schema?: string;
11
13
  }
12
14
  export interface AdapterFactory {
13
15
  resolver(app: ApplicationService, context?: AdapterResolveContext): Promise<OidcAdapterClass>;
@@ -48,6 +50,11 @@ export interface DatabaseAdapterConfig {
48
50
  * de todas as tabelas do authkit), e sem ela a primária.
49
51
  */
50
52
  connection?: string;
53
+ /**
54
+ * Schema do banco da tabela `authkit_oidc_payloads` (MySQL: o database). Default:
55
+ * `schema.name` do `defineConfig`. Vai escrito em cada statement — nunca via searchPath.
56
+ */
57
+ schema?: string;
51
58
  }
52
59
  export declare const adapters: {
53
60
  /**
@@ -1,4 +1,5 @@
1
- import { DatabaseAdapter } from './database_adapter.js';
1
+ import { assertSchemaSupported, authkitTable, validateSchemaName, } from '../schema/tables_location.js';
2
+ import { DatabaseAdapter, OIDC_PAYLOADS_TABLE } from './database_adapter.js';
2
3
  import { RedisAdapter } from './redis_adapter.js';
3
4
  /**
4
5
  * Modelos do oidc-provider de vida curta, amarrados ao ciclo da sessão de
@@ -70,10 +71,13 @@ export const adapters = {
70
71
  async resolver(app, context) {
71
72
  const db = await app.container.make('lucid.db');
72
73
  const connection = config.connection ?? context?.connection;
74
+ const schema = validateSchemaName(config.schema ?? context?.schema, 'adapters.database({ schema })');
73
75
  const conn = connection ? db.connection(connection) : db;
76
+ assertSchemaSupported(connection ? conn : db.connection(), schema);
77
+ const table = authkitTable({ schema }, OIDC_PAYLOADS_TABLE);
74
78
  return class extends DatabaseAdapter {
75
79
  constructor(name) {
76
- super(name, conn);
80
+ super(name, conn, table);
77
81
  }
78
82
  };
79
83
  },
@@ -46,8 +46,12 @@ export interface LiveGrantFilter {
46
46
  export declare class DelegationStore {
47
47
  #private;
48
48
  private conn;
49
+ /** Nome da tabela como os statements o escrevem (`schema.tabela` com `schema.name`). */
50
+ private t;
49
51
  /** `conn` = uma conexão Lucid (`db.connection(name?)`) — ou uma transação. */
50
- constructor(conn: () => any);
52
+ constructor(conn: () => any,
53
+ /** Nome da tabela como os statements o escrevem (`schema.tabela` com `schema.name`). */
54
+ t?: (name: string) => string);
51
55
  insertDevice(row: Omit<DeviceCodeRow, 'accountId' | 'grantId' | 'lastPolledAt'>): Promise<void>;
52
56
  findDeviceByCodeHash(hash: string): Promise<DeviceCodeRow | null>;
53
57
  findDeviceByUserCode(userCode: string): Promise<DeviceCodeRow | null>;