@adonis-agora/authkit-server 1.0.0-rc.10 → 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 (47) hide show
  1. package/CHANGELOG.md +33 -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 +16 -4
  6. package/build/src/accounts/account_store.d.ts +7 -7
  7. package/build/src/accounts/lucid_account_store.d.ts +3 -3
  8. package/build/src/accounts/lucid_account_store.js +43 -17
  9. package/build/src/accounts/lucid_store/mfa.js +3 -3
  10. package/build/src/accounts/lucid_store/organization_models.d.ts +18 -3
  11. package/build/src/accounts/lucid_store/organization_models.js +35 -3
  12. package/build/src/accounts/lucid_store/organizations.d.ts +2 -2
  13. package/build/src/accounts/lucid_store/organizations.js +35 -32
  14. package/build/src/accounts/lucid_store/password_hygiene.js +4 -2
  15. package/build/src/accounts/lucid_store/shared.d.ts +16 -3
  16. package/build/src/accounts/lucid_store/shared.js +13 -10
  17. package/build/src/accounts/lucid_store/webauthn.js +1 -1
  18. package/build/src/adapters/database_adapter.d.ts +7 -1
  19. package/build/src/adapters/database_adapter.js +24 -13
  20. package/build/src/adapters/factory.d.ts +20 -2
  21. package/build/src/adapters/factory.js +8 -4
  22. package/build/src/agents/delegation_store.d.ts +5 -1
  23. package/build/src/agents/delegation_store.js +34 -21
  24. package/build/src/agents/extension.d.ts +16 -2
  25. package/build/src/agents/registry_store.d.ts +1 -0
  26. package/build/src/agents/registry_store.js +10 -8
  27. package/build/src/agents/runtime.js +6 -2
  28. package/build/src/commands/expire_scan_command.js +2 -3
  29. package/build/src/commands/settings_commands.js +5 -5
  30. package/build/src/define_config.d.ts +12 -1
  31. package/build/src/define_config.js +34 -3
  32. package/build/src/doctor/agora_checks.js +13 -0
  33. package/build/src/doctor/checks.js +2 -2
  34. package/build/src/doctor/db_schema_check.d.ts +6 -0
  35. package/build/src/doctor/db_schema_check.js +87 -0
  36. package/build/src/host/admin_sessions_service.js +6 -2
  37. package/build/src/host/runtime_settings.d.ts +21 -6
  38. package/build/src/host/runtime_settings.js +29 -13
  39. package/build/src/keys/keystore_manager.js +2 -1
  40. package/build/src/keys/keystore_vault.d.ts +9 -1
  41. package/build/src/keys/keystore_vault.js +27 -9
  42. package/build/src/schema/ensure.d.ts +19 -1
  43. package/build/src/schema/ensure.js +54 -30
  44. package/build/src/schema/tables_location.d.ts +56 -0
  45. package/build/src/schema/tables_location.js +86 -0
  46. package/package.json +3 -3
  47. package/skills/authkit-idp-setup/SKILL.md +13 -7
package/CHANGELOG.md CHANGED
@@ -1,5 +1,38 @@
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
+
23
+ ## 1.0.0-rc.11
24
+
25
+ ### Patch Changes
26
+
27
+ - [#473](https://github.com/DavideCarvalho/adonis-agora-authkit/pull/473) [`427c4a3`](https://github.com/DavideCarvalho/adonis-agora-authkit/commit/427c4a301907dc78d8e7c5405d4e73e29b8a2be4) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - Every table AuthKit owns now uses one Lucid connection: `schema.connection` (default: the primary connection). Before, some of them followed other connections, so with the account model on a named connection the boot created a table on one connection and the code read it on another:
28
+
29
+ - `auth_settings` (runtime settings, the settings CLI, `authkit:expire-scan`, the SDK's embedded driver) followed the account model's connection.
30
+ - `auth_mfa` and `auth_password_history` used the account model's connection.
31
+ - The built-in organization models had no connection, so they always used the primary one.
32
+ - `adapters.database()` and the `lucid` keystore without their own `connection` used the primary connection.
33
+
34
+ They all use `schema.connection` now; `adapters.database({ connection })` and `jwks.store: { driver: 'lucid', connection }` still win when set. The app's account table stays on the account model's connection, and the boot adds the `login_methods` column there. If your app had `schema.connection` unset and the account model on a named connection, those tables move to the primary connection: set `schema.connection` to the connection where they already are.
35
+
3
36
  ## 1.0.0-rc.10
4
37
 
5
38
  ### Minor 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,7 +287,9 @@ 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,
292
+ accountConnection: config.accountStore?.connectionName,
290
293
  tables: config.personalAgents?.extensions?.flatMap((e) => e.tables ?? []),
291
294
  });
292
295
  const logger = await this.app.container.make('logger').catch(() => null);
@@ -305,6 +308,10 @@ export default class AuthkitServerProvider {
305
308
  }
306
309
  }
307
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;
308
315
  const logger = await this.app.container.make('logger').catch(() => null);
309
316
  logger?.warn({ err: error }, 'authkit: schema auto-manage failed — features degrade gracefully; ' +
310
317
  'run ensureAuthkitSchema() in a migration or fix DB connectivity');
@@ -364,10 +371,15 @@ export default class AuthkitServerProvider {
364
371
  .resolve(this.app, value)
365
372
  .catch(() => null))
366
373
  : null;
367
- const connection = config?.schema?.connection;
368
374
  // RuntimeSettings sem request (mesmo padrão de expire_scan_command / security_notice_service).
369
- const settings = new RuntimeSettings(db, connection ? { connection } : {});
370
- 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
+ });
371
383
  const scheduler = new KeyRotationScheduler({
372
384
  policy: () => resolveEffectiveKeyRotation(settings),
373
385
  ageDays: () => svc.keystoreAgeDays(),
@@ -688,13 +688,13 @@ export declare function supportsOrganizations(store: AccountStore): store is Acc
688
688
  */
689
689
  export type AccountStore = CoreAccountStore & {
690
690
  /**
691
- * Nome da conexão Lucid usada por este store (deriva de `Model.connection` no
692
- * store default — ver `lucid_account_store.ts`). Quando presente, os call-sites
693
- * de RuntimeSettings o repassam como `{ connection }` para que o probe da tabela
694
- * `auth_settings` seja searchPath-aware (auth vivendo numa conexão/schema
695
- * próprios). Undefined → conexão default (back-compat total). É metadado
696
- * opcional, não um método de capacidade — por isso `readonly` e fora dos
697
- * blocos `Partial<...>` de capacidades probáveis.
691
+ * Nome da conexão Lucid da tabela da CONTA (deriva de `Model.connection` no
692
+ * store default — ver `lucid_account_store.ts`). O boot a usa para garantir a
693
+ * coluna `login_methods` na tabela da conta. As tabelas do authkit
694
+ * (`auth_settings` etc.) NÃO seguem esta conexão: usam `schema.connection`.
695
+ * Undefined → conexão default. É metadado opcional, não um método de
696
+ * capacidade — por isso `readonly` e fora dos blocos `Partial<...>` de
697
+ * capacidades probáveis.
698
698
  */
699
699
  readonly connectionName?: string;
700
700
  /**
@@ -150,9 +150,9 @@ export interface LucidAccountStoreOptions {
150
150
  * `ensureAuthkitSchema`, então o mapeamento não é decisão do host.
151
151
  * - `true` → idêntico ao default (atalho explícito; é o que apps existentes
152
152
  * já usam e continua valendo).
153
- * - `{ OrgModel, MemberModel, InvitationModel }` → escape hatch, para quem
154
- * guarda as tabelas de auth numa conexão/schema próprios (os defaults não
155
- * declaram `static connection`).
153
+ * - `{ OrgModel, MemberModel, InvitationModel }` → escape hatch, para usar
154
+ * models próprios do host; eles rodam na conexão que declaram. Os defaults
155
+ * seguem `schema.connection`, como toda tabela lib-owned.
156
156
  * - `false` → opt-out explícito: `OrganizationsCapability` AUSENTE no store.
157
157
  * Use quando a dependência atual é `supportsOrganizations === false` (ex.:
158
158
  * store/rotas que assumem a ausência). O `authkit:doctor` avisa se
@@ -1,8 +1,9 @@
1
1
  import { PasswordManager } from '../password/password_manager.js';
2
+ import { authkitTable, authkitTablesClient, BIND_TABLES_LOCATION, } from '../schema/tables_location.js';
2
3
  import { buildCore } from './lucid_store/core.js';
3
4
  import { buildLoginMethods, supportsLoginMethodsColumn } from './lucid_store/login_methods.js';
4
5
  import { buildMfa } from './lucid_store/mfa.js';
5
- import { defaultOrganizationModels } from './lucid_store/organization_models.js';
6
+ import { defaultOrganizationModels, organizationModelsOn, } from './lucid_store/organization_models.js';
6
7
  import { buildOrganizations } from './lucid_store/organizations.js';
7
8
  import { buildPasswordExpiration, buildPasswordHistory } from './lucid_store/password_hygiene.js';
8
9
  import { buildProviderIdentity } from './lucid_store/provider_identity.js';
@@ -162,11 +163,23 @@ export function lucidAccountStore(Model, options = {}) {
162
163
  // opt-out explícito é `false`. O trio explícito segue como escape hatch.
163
164
  const orgModelsOption = options.organizationModels;
164
165
  const usesDefaultOrgModels = orgModelsOption === undefined || orgModelsOption === true;
165
- const OrgModels = orgModelsOption === false
166
- ? undefined
167
- : usesDefaultOrgModels
168
- ? defaultOrganizationModels
169
- : orgModelsOption;
166
+ const explicitOrgModels = orgModelsOption === false || usesDefaultOrgModels ? undefined : orgModelsOption;
167
+ /**
168
+ * Onde vivem as tabelas do AuthKit (`schema.connection`). O `defineConfig` informa
169
+ * via {@link BIND_TABLES_LOCATION} — o store é construído antes de o config existir.
170
+ * `null` = ainda não informado (store usado fora do `defineConfig`): as tabelas
171
+ * lib-owned seguem a conexão do model e os models de org default, a primária.
172
+ */
173
+ let tablesLocation = null;
174
+ const tablesClient = () => {
175
+ const db = Model.$adapter?.db;
176
+ if (!tablesLocation || !db)
177
+ return Model.query().client;
178
+ return authkitTablesClient(db, tablesLocation);
179
+ };
180
+ const orgModels = () => explicitOrgModels ??
181
+ (tablesLocation ? organizationModelsOn(tablesLocation) : defaultOrganizationModels);
182
+ const OrgModels = orgModelsOption === false ? undefined : orgModels;
170
183
  // RP do WebAuthn: usado nas cerimônias. Default do rpName cai no mfaIssuer.
171
184
  const webauthn = options.webauthn ?? {
172
185
  rpName: mfaIssuer,
@@ -190,6 +203,8 @@ export function lucidAccountStore(Model, options = {}) {
190
203
  audit: options.audit,
191
204
  emailVerificationTtlHours: emailTokens.verificationTtlHours,
192
205
  emailChangeTtlHours: emailTokens.changeTtlHours,
206
+ tablesClient,
207
+ tableName: (name) => authkitTable(tablesLocation ?? undefined, name),
193
208
  // Encripta o segredo antes de persistir (no-op sem encrypter).
194
209
  sealSecret: (secret) => (encrypter ? encrypter.encrypt(secret) : secret),
195
210
  // Decripta o segredo armazenado; retorna null em falha/adulteração (no-op sem encrypter).
@@ -264,9 +279,15 @@ export function lucidAccountStore(Model, options = {}) {
264
279
  // NÃO faz parte do contrato AccountStore.
265
280
  __organizationModelsSource: usesDefaultOrgModels ? 'default' : 'explicit',
266
281
  ...buildOrganizations({
267
- OrgModel: OrgModels.OrgModel,
268
- MemberModel: OrgModels.MemberModel,
269
- InvitationModel: OrgModels.InvitationModel,
282
+ get OrgModel() {
283
+ return OrgModels().OrgModel;
284
+ },
285
+ get MemberModel() {
286
+ return OrgModels().MemberModel;
287
+ },
288
+ get InvitationModel() {
289
+ return OrgModels().InvitationModel;
290
+ },
270
291
  findAccountEmail: async (accountId) => {
271
292
  const row = await Model.find(accountId);
272
293
  return row?.email ?? null;
@@ -292,11 +313,10 @@ export function lucidAccountStore(Model, options = {}) {
292
313
  }
293
314
  },
294
315
  /**
295
- * Nome da conexão Lucid usada por este store (deriva de `Model.connection`).
296
- * Exposto para que os call-sites de RuntimeSettings possam passar
297
- * `{ connection: store.connectionName }` e usar a conexão correta.
298
- * Undefined quando o model usa a conexão default (back-compat total).
299
- * NÃO faz parte do contrato AccountStore.
316
+ * Nome da conexão Lucid do MODEL da conta (deriva de `Model.connection`) — onde
317
+ * vive a tabela da conta (e a coluna `login_methods`). As tabelas do authkit
318
+ * NÃO seguem esta conexão: elas usam `schema.connection`.
319
+ * Undefined quando o model usa a conexão default.
300
320
  */
301
321
  connectionName: Model.connection ?? undefined,
302
322
  /**
@@ -318,10 +338,16 @@ export function lucidAccountStore(Model, options = {}) {
318
338
  __model: Model,
319
339
  };
320
340
  // Histórico de senhas: tabela lib-owned `auth_password_history`, pela conexão
321
- // do model (resolvida a cada uso, como no MFA).
341
+ // das tabelas do authkit (resolvida a cada uso, como no MFA).
322
342
  Object.assign(store, buildPasswordHistory(ctx, {
323
- query: () => Model.query().client.query(),
324
- table: (name) => Model.query().client.table(name),
343
+ query: () => tablesClient().query(),
344
+ table: (name) => tablesClient().table(name),
325
345
  }));
346
+ // Não-enumerável: o `defineConfig` informa `schema.connection` por aqui.
347
+ Object.defineProperty(store, BIND_TABLES_LOCATION, {
348
+ value: (location) => {
349
+ tablesLocation = { connection: location.connection, schema: location.schema };
350
+ },
351
+ });
326
352
  return store;
327
353
  }
@@ -8,8 +8,8 @@ import { buildMfaStateRepo, generateRecoveryCode, hashesEqual, sha256, } from '.
8
8
  * {@link ensureAuthkitSchema}), keyed por `account_id`. O host NÃO precisa migrar
9
9
  * nada e o model não declara colunas de MFA.
10
10
  *
11
- * Acesso ao banco via a CONEXÃO do próprio model (`Model.query().client`), a mesma
12
- * usada pelo restante do store. O `email` (para o keyuri/QR) continua vindo do
11
+ * Acesso ao banco pela conexão das tabelas do authkit (`schema.connection`), a
12
+ * mesma em que o `autoManage` cria `auth_mfa`. O `email` (para o keyuri/QR) continua vindo do
13
13
  * model principal — só o ESTADO de MFA migrou para `auth_mfa`.
14
14
  */
15
15
  /**
@@ -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);
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);
@@ -10,10 +10,11 @@ import type { DateTime } from 'luxon';
10
10
  * pelo `autoManage` sem que o model escrito à mão no host saiba dela, e o
11
11
  * sintoma aparece longe da causa.
12
12
  *
13
- * Use com `organizationModels: true` em {@link lucidAccountStore}. O caminho
13
+ * Use com `organizationModels: true` em {@link lucidAccountStore}. Eles seguem a
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
14
16
  * explícito (`{ OrgModel, MemberModel, InvitationModel }`) continua valendo como
15
- * escape hatch — é para quem guarda as tabelas de auth numa conexão/schema
16
- * próprios (`static connection = 'auth'`), que estes defaults não declaram.
17
+ * escape hatch; aí a conexão é a que os models do host declaram.
17
18
  *
18
19
  * Não crie migration para estas tabelas: elas são criadas/atualizadas pelo
19
20
  * `schema.autoManage` do authkit.
@@ -65,3 +66,17 @@ export declare const defaultOrganizationModels: {
65
66
  readonly MemberModel: typeof AuthOrganizationMember;
66
67
  readonly InvitationModel: typeof AuthOrganizationInvitation;
67
68
  };
69
+ export type OrganizationModels = {
70
+ OrgModel: any;
71
+ MemberModel: any;
72
+ InvitationModel: any;
73
+ };
74
+ /**
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).
78
+ */
79
+ export declare function organizationModelsOn(location: {
80
+ connection?: string;
81
+ schema?: string;
82
+ }): OrganizationModels;
@@ -15,10 +15,11 @@ import { BaseModel, column } from '@adonisjs/lucid/orm';
15
15
  * pelo `autoManage` sem que o model escrito à mão no host saiba dela, e o
16
16
  * sintoma aparece longe da causa.
17
17
  *
18
- * Use com `organizationModels: true` em {@link lucidAccountStore}. O caminho
18
+ * Use com `organizationModels: true` em {@link lucidAccountStore}. Eles seguem a
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
19
21
  * explícito (`{ OrgModel, MemberModel, InvitationModel }`) continua valendo como
20
- * escape hatch — é para quem guarda as tabelas de auth numa conexão/schema
21
- * próprios (`static connection = 'auth'`), que estes defaults não declaram.
22
+ * escape hatch; aí a conexão é a que os models do host declaram.
22
23
  *
23
24
  * Não crie migration para estas tabelas: elas são criadas/atualizadas pelo
24
25
  * `schema.autoManage` do authkit.
@@ -111,3 +112,34 @@ export const defaultOrganizationModels = {
111
112
  MemberModel: AuthOrganizationMember,
112
113
  InvitationModel: AuthOrganizationInvitation,
113
114
  };
115
+ const modelsByLocation = new Map();
116
+ /**
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).
120
+ */
121
+ export function organizationModelsOn(location) {
122
+ const { connection, schema } = location;
123
+ if (!connection && !schema)
124
+ return defaultOrganizationModels;
125
+ const key = `${connection ?? ''}\x00${schema ?? ''}`;
126
+ let models = modelsByLocation.get(key);
127
+ if (!models) {
128
+ const table = (name) => (schema ? `${schema}.${name}` : name);
129
+ class OrgModel extends AuthOrganization {
130
+ static table = table('auth_organizations');
131
+ static connection = connection;
132
+ }
133
+ class MemberModel extends AuthOrganizationMember {
134
+ static table = table('auth_organization_members');
135
+ static connection = connection;
136
+ }
137
+ class InvitationModel extends AuthOrganizationInvitation {
138
+ static table = table('auth_organization_invitations');
139
+ static connection = connection;
140
+ }
141
+ models = { OrgModel, MemberModel, InvitationModel };
142
+ modelsByLocation.set(key, models);
143
+ }
144
+ return models;
145
+ }
@@ -1,7 +1,7 @@
1
1
  import type { OrganizationsCapability } from '../account_store.js';
2
2
  /**
3
- * Contexto mínimo para o builder de organizations. Recebe os três models direto
4
- * (já construídos pelo lucidAccountStore).
3
+ * Contexto mínimo para o builder de organizations. Os três models são lidos a cada
4
+ * uso (podem ser getters — ver `lucidAccountStore`).
5
5
  */
6
6
  export interface OrgStoreContext {
7
7
  OrgModel: any;
@@ -59,7 +59,10 @@ async function countOwners(MemberModel, orgId) {
59
59
  return Number(result[0]?.$extras?.total ?? 0);
60
60
  }
61
61
  export function buildOrganizations(ctx) {
62
- const { OrgModel, MemberModel, InvitationModel, findAccountEmail, listAccountIdsInAdminOrder } = ctx;
62
+ // Os models são lidos do ctx a CADA uso (não desestruturados): o `lucidAccountStore`
63
+ // troca os models default pelos da conexão das tabelas do authkit quando o
64
+ // `defineConfig` informa o `schema.connection`, depois de construir o store.
65
+ const { findAccountEmail, listAccountIdsInAdminOrder } = ctx;
63
66
  return {
64
67
  ...(listAccountIdsInAdminOrder
65
68
  ? {
@@ -73,7 +76,7 @@ export function buildOrganizations(ctx) {
73
76
  * (like `listOrgMembers(orgId).length`).
74
77
  */
75
78
  async listAllOrgsWithMemberCount() {
76
- const memberships = await MemberModel.query();
79
+ const memberships = await ctx.MemberModel.query();
77
80
  if (memberships.length === 0)
78
81
  return [];
79
82
  const byAccount = new Map();
@@ -90,7 +93,7 @@ export function buildOrganizations(ctx) {
90
93
  }
91
94
  const accountIds = await listAccountIdsInAdminOrder([...byAccount.keys()]);
92
95
  const orgs = new Map();
93
- for (const org of await findManyChunked(OrgModel, [...memberCount.keys()])) {
96
+ for (const org of await findManyChunked(ctx.OrgModel, [...memberCount.keys()])) {
94
97
  orgs.set(String(org.id), org);
95
98
  }
96
99
  const result = [];
@@ -117,7 +120,7 @@ export function buildOrganizations(ctx) {
117
120
  : {}),
118
121
  async createOrg(input) {
119
122
  const { randomUUID } = await import('node:crypto');
120
- const org = await OrgModel.create({
123
+ const org = await ctx.OrgModel.create({
121
124
  id: randomUUID(),
122
125
  name: input.name,
123
126
  slug: input.slug,
@@ -125,7 +128,7 @@ export function buildOrganizations(ctx) {
125
128
  metadata: input.metadata ?? null,
126
129
  });
127
130
  // Cria a membership do owner automaticamente
128
- await MemberModel.create({
131
+ await ctx.MemberModel.create({
129
132
  id: randomUUID(),
130
133
  organizationId: org.id,
131
134
  accountId: input.ownerAccountId,
@@ -134,28 +137,28 @@ export function buildOrganizations(ctx) {
134
137
  return toOrgSummary(org);
135
138
  },
136
139
  async findOrgById(orgId) {
137
- const row = await OrgModel.find(orgId);
140
+ const row = await ctx.OrgModel.find(orgId);
138
141
  return row ? toOrgSummary(row) : null;
139
142
  },
140
143
  async findOrgsByIds(orgIds) {
141
144
  const ids = [...new Set(orgIds)];
142
145
  if (ids.length === 0)
143
146
  return [];
144
- return (await findManyChunked(OrgModel, ids)).map(toOrgSummary);
147
+ return (await findManyChunked(ctx.OrgModel, ids)).map(toOrgSummary);
145
148
  },
146
149
  async findOrgBySlug(slug) {
147
- const row = await OrgModel.query().where('slug', slug).first();
150
+ const row = await ctx.OrgModel.query().where('slug', slug).first();
148
151
  return row ? toOrgSummary(row) : null;
149
152
  },
150
153
  async listOrgsForAccount(accountId) {
151
- const memberships = await MemberModel.query().where('account_id', accountId);
154
+ const memberships = await ctx.MemberModel.query().where('account_id', accountId);
152
155
  if (memberships.length === 0)
153
156
  return [];
154
157
  // One query for every org (by primary key, like `find`), instead of one per
155
158
  // membership. The result keeps the memberships' order.
156
159
  const ids = [...new Set(memberships.map((m) => m.organizationId))];
157
160
  const orgs = new Map();
158
- for (const org of await OrgModel.findMany(ids))
161
+ for (const org of await ctx.OrgModel.findMany(ids))
159
162
  orgs.set(String(org.id), org);
160
163
  const result = [];
161
164
  for (const m of memberships) {
@@ -166,7 +169,7 @@ export function buildOrganizations(ctx) {
166
169
  return result;
167
170
  },
168
171
  async updateOrg(orgId, patch) {
169
- const row = await OrgModel.find(orgId);
172
+ const row = await ctx.OrgModel.find(orgId);
170
173
  if (!row)
171
174
  return null;
172
175
  if (patch.name !== undefined)
@@ -179,16 +182,16 @@ export function buildOrganizations(ctx) {
179
182
  return toOrgSummary(row);
180
183
  },
181
184
  async deleteOrg(orgId) {
182
- const row = await OrgModel.find(orgId);
185
+ const row = await ctx.OrgModel.find(orgId);
183
186
  if (!row)
184
187
  return false;
185
- await MemberModel.query().where('organization_id', orgId).delete();
186
- await InvitationModel.query().where('organization_id', orgId).delete();
188
+ await ctx.MemberModel.query().where('organization_id', orgId).delete();
189
+ await ctx.InvitationModel.query().where('organization_id', orgId).delete();
187
190
  await row.delete();
188
191
  return true;
189
192
  },
190
193
  async listOrgMembers(orgId) {
191
- const rows = await MemberModel.query().where('organization_id', orgId);
194
+ const rows = await ctx.MemberModel.query().where('organization_id', orgId);
192
195
  return rows.map((r) => ({
193
196
  accountId: r.accountId,
194
197
  email: null, // o caller enriquece com findById se necessário
@@ -199,7 +202,7 @@ export function buildOrganizations(ctx) {
199
202
  async addOrgMember(orgId, accountId, role) {
200
203
  const { randomUUID } = await import('node:crypto');
201
204
  // Upsert: se já existe, atualiza a role
202
- const existing = await MemberModel.query()
205
+ const existing = await ctx.MemberModel.query()
203
206
  .where('organization_id', orgId)
204
207
  .where('account_id', accountId)
205
208
  .first();
@@ -208,11 +211,11 @@ export function buildOrganizations(ctx) {
208
211
  await existing.save();
209
212
  }
210
213
  else {
211
- await MemberModel.create({ id: randomUUID(), organizationId: orgId, accountId, role });
214
+ await ctx.MemberModel.create({ id: randomUUID(), organizationId: orgId, accountId, role });
212
215
  }
213
216
  },
214
217
  async removeOrgMember(orgId, accountId) {
215
- const row = await MemberModel.query()
218
+ const row = await ctx.MemberModel.query()
216
219
  .where('organization_id', orgId)
217
220
  .where('account_id', accountId)
218
221
  .first();
@@ -220,7 +223,7 @@ export function buildOrganizations(ctx) {
220
223
  return { ok: false, reason: 'not_found' };
221
224
  // Invariante: org deve ter sempre >= 1 owner
222
225
  if (row.role === 'owner') {
223
- const ownerCount = await countOwners(MemberModel, orgId);
226
+ const ownerCount = await countOwners(ctx.MemberModel, orgId);
224
227
  if (ownerCount <= 1)
225
228
  return { ok: false, reason: 'last_owner' };
226
229
  }
@@ -228,7 +231,7 @@ export function buildOrganizations(ctx) {
228
231
  return { ok: true };
229
232
  },
230
233
  async updateOrgMemberRole(orgId, accountId, newRole) {
231
- const row = await MemberModel.query()
234
+ const row = await ctx.MemberModel.query()
232
235
  .where('organization_id', orgId)
233
236
  .where('account_id', accountId)
234
237
  .first();
@@ -236,7 +239,7 @@ export function buildOrganizations(ctx) {
236
239
  return { ok: false, reason: 'not_found' };
237
240
  // Invariante: não rebaixa o último owner
238
241
  if (row.role === 'owner' && newRole !== 'owner') {
239
- const ownerCount = await countOwners(MemberModel, orgId);
242
+ const ownerCount = await countOwners(ctx.MemberModel, orgId);
240
243
  if (ownerCount <= 1)
241
244
  return { ok: false, reason: 'last_owner' };
242
245
  }
@@ -245,7 +248,7 @@ export function buildOrganizations(ctx) {
245
248
  return { ok: true };
246
249
  },
247
250
  async getOrgMembership(orgId, accountId) {
248
- const row = await MemberModel.query()
251
+ const row = await ctx.MemberModel.query()
249
252
  .where('organization_id', orgId)
250
253
  .where('account_id', accountId)
251
254
  .first();
@@ -256,7 +259,7 @@ export function buildOrganizations(ctx) {
256
259
  const token = randomBytes(32).toString('hex');
257
260
  const tokenHash = createHash('sha256').update(token).digest('hex');
258
261
  const expiresAt = DateTime.now().plus({ hours: input.ttlHours });
259
- const inv = await InvitationModel.create({
262
+ const inv = await ctx.InvitationModel.create({
260
263
  id: randomUUID(),
261
264
  organizationId: input.organizationId,
262
265
  email: input.email,
@@ -269,21 +272,21 @@ export function buildOrganizations(ctx) {
269
272
  return { invitation: toOrgInvitation(inv), token };
270
273
  },
271
274
  async findInvitationByTokenHash(tokenHash) {
272
- const row = await InvitationModel.query().where('token_hash', tokenHash).first();
275
+ const row = await ctx.InvitationModel.query().where('token_hash', tokenHash).first();
273
276
  return row ? toOrgInvitation(row) : null;
274
277
  },
275
278
  async listPendingInvitationsForOrg(orgId) {
276
- const rows = await InvitationModel.query()
279
+ const rows = await ctx.InvitationModel.query()
277
280
  .where('organization_id', orgId)
278
281
  .whereNull('accepted_at');
279
282
  return rows.map(toOrgInvitation);
280
283
  },
281
284
  async listPendingInvitationsForEmail(email) {
282
- const rows = await InvitationModel.query().where('email', email).whereNull('accepted_at');
285
+ const rows = await ctx.InvitationModel.query().where('email', email).whereNull('accepted_at');
283
286
  return rows.map(toOrgInvitation);
284
287
  },
285
288
  async acceptInvitation(invitationId, accountId) {
286
- const inv = await InvitationModel.find(invitationId);
289
+ const inv = await ctx.InvitationModel.find(invitationId);
287
290
  if (!inv)
288
291
  return { ok: false, reason: 'not_found' };
289
292
  // Verifica expiração (expiresAt pode ser DateTime Luxon, Date, ou string do SQLite)
@@ -311,7 +314,7 @@ export function buildOrganizations(ctx) {
311
314
  return { ok: false, reason: 'email_mismatch' };
312
315
  }
313
316
  // Verifica se já é membro
314
- const existing = await MemberModel.query()
317
+ const existing = await ctx.MemberModel.query()
315
318
  .where('organization_id', inv.organizationId)
316
319
  .where('account_id', accountId)
317
320
  .first();
@@ -323,7 +326,7 @@ export function buildOrganizations(ctx) {
323
326
  }
324
327
  // Cria membership + marca accepted_at
325
328
  const { randomUUID } = await import('node:crypto');
326
- await MemberModel.create({
329
+ await ctx.MemberModel.create({
327
330
  id: randomUUID(),
328
331
  organizationId: inv.organizationId,
329
332
  accountId,
@@ -337,7 +340,7 @@ export function buildOrganizations(ctx) {
337
340
  // Escopado por org: exige que o convite pertença à org informada antes de
338
341
  // deletar. Sem esse filtro, um owner/admin de qualquer org revogaria
339
342
  // convites de outra org sabendo apenas o invitationId (IDOR cross-org).
340
- const inv = await InvitationModel.query()
343
+ const inv = await ctx.InvitationModel.query()
341
344
  .where('id', invitationId)
342
345
  .where('organization_id', organizationId)
343
346
  .first();
@@ -348,7 +351,7 @@ export function buildOrganizations(ctx) {
348
351
  },
349
352
  async removeAccountFromAllOrgs(accountId) {
350
353
  // Memberships: remove todas, mesmo se for único owner (LGPD não pode bloquear)
351
- const memberships = await MemberModel.query().where('account_id', accountId);
354
+ const memberships = await ctx.MemberModel.query().where('account_id', accountId);
352
355
  for (const m of memberships) {
353
356
  await m.delete();
354
357
  }
@@ -356,7 +359,7 @@ export function buildOrganizations(ctx) {
356
359
  const email = await findAccountEmail(accountId);
357
360
  let invCount = 0;
358
361
  if (email) {
359
- const invitations = await InvitationModel.query()
362
+ const invitations = await ctx.InvitationModel.query()
360
363
  .where('email', email)
361
364
  .whereNull('accepted_at');
362
365
  for (const inv of invitations) {