@adonis-agora/authkit-server 1.0.0-rc.10 → 1.0.0-rc.11
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.
- package/CHANGELOG.md +13 -0
- package/build/providers/authkit_server_provider.js +1 -0
- package/build/src/accounts/account_store.d.ts +7 -7
- package/build/src/accounts/lucid_account_store.d.ts +3 -3
- package/build/src/accounts/lucid_account_store.js +42 -17
- package/build/src/accounts/lucid_store/mfa.js +3 -3
- package/build/src/accounts/lucid_store/organization_models.d.ts +14 -3
- package/build/src/accounts/lucid_store/organization_models.js +31 -3
- package/build/src/accounts/lucid_store/organizations.d.ts +2 -2
- package/build/src/accounts/lucid_store/organizations.js +35 -32
- package/build/src/accounts/lucid_store/shared.d.ts +11 -3
- package/build/src/accounts/lucid_store/shared.js +7 -5
- package/build/src/accounts/lucid_store/webauthn.js +1 -1
- package/build/src/adapters/factory.d.ts +13 -2
- package/build/src/adapters/factory.js +2 -2
- package/build/src/commands/expire_scan_command.js +1 -1
- package/build/src/commands/settings_commands.js +2 -2
- package/build/src/define_config.d.ts +4 -1
- package/build/src/define_config.js +17 -3
- package/build/src/doctor/checks.js +2 -2
- package/build/src/host/runtime_settings.d.ts +6 -6
- package/build/src/host/runtime_settings.js +4 -3
- package/build/src/schema/ensure.d.ts +10 -1
- package/build/src/schema/ensure.js +11 -14
- package/build/src/schema/tables_location.d.ts +27 -0
- package/build/src/schema/tables_location.js +22 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# @adonis-agora/authkit-server
|
|
2
2
|
|
|
3
|
+
## 1.0.0-rc.11
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#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:
|
|
8
|
+
|
|
9
|
+
- `auth_settings` (runtime settings, the settings CLI, `authkit:expire-scan`, the SDK's embedded driver) followed the account model's connection.
|
|
10
|
+
- `auth_mfa` and `auth_password_history` used the account model's connection.
|
|
11
|
+
- The built-in organization models had no connection, so they always used the primary one.
|
|
12
|
+
- `adapters.database()` and the `lucid` keystore without their own `connection` used the primary connection.
|
|
13
|
+
|
|
14
|
+
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.
|
|
15
|
+
|
|
3
16
|
## 1.0.0-rc.10
|
|
4
17
|
|
|
5
18
|
### Minor Changes
|
|
@@ -287,6 +287,7 @@ export default class AuthkitServerProvider {
|
|
|
287
287
|
const report = await ensureAuthkitSchema(db, {
|
|
288
288
|
connection: config.schema.connection,
|
|
289
289
|
accountTable: config.accountStore?.accountTable,
|
|
290
|
+
accountConnection: config.accountStore?.connectionName,
|
|
290
291
|
tables: config.personalAgents?.extensions?.flatMap((e) => e.tables ?? []),
|
|
291
292
|
});
|
|
292
293
|
const logger = await this.app.container.make('logger').catch(() => null);
|
|
@@ -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
|
|
692
|
-
* store default — ver `lucid_account_store.ts`).
|
|
693
|
-
*
|
|
694
|
-
* `auth_settings`
|
|
695
|
-
*
|
|
696
|
-
*
|
|
697
|
-
*
|
|
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
|
|
154
|
-
*
|
|
155
|
-
*
|
|
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 { 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
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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.connection) : 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,7 @@ export function lucidAccountStore(Model, options = {}) {
|
|
|
190
203
|
audit: options.audit,
|
|
191
204
|
emailVerificationTtlHours: emailTokens.verificationTtlHours,
|
|
192
205
|
emailChangeTtlHours: emailTokens.changeTtlHours,
|
|
206
|
+
tablesClient,
|
|
193
207
|
// Encripta o segredo antes de persistir (no-op sem encrypter).
|
|
194
208
|
sealSecret: (secret) => (encrypter ? encrypter.encrypt(secret) : secret),
|
|
195
209
|
// Decripta o segredo armazenado; retorna null em falha/adulteração (no-op sem encrypter).
|
|
@@ -264,9 +278,15 @@ export function lucidAccountStore(Model, options = {}) {
|
|
|
264
278
|
// NÃO faz parte do contrato AccountStore.
|
|
265
279
|
__organizationModelsSource: usesDefaultOrgModels ? 'default' : 'explicit',
|
|
266
280
|
...buildOrganizations({
|
|
267
|
-
OrgModel
|
|
268
|
-
|
|
269
|
-
|
|
281
|
+
get OrgModel() {
|
|
282
|
+
return OrgModels().OrgModel;
|
|
283
|
+
},
|
|
284
|
+
get MemberModel() {
|
|
285
|
+
return OrgModels().MemberModel;
|
|
286
|
+
},
|
|
287
|
+
get InvitationModel() {
|
|
288
|
+
return OrgModels().InvitationModel;
|
|
289
|
+
},
|
|
270
290
|
findAccountEmail: async (accountId) => {
|
|
271
291
|
const row = await Model.find(accountId);
|
|
272
292
|
return row?.email ?? null;
|
|
@@ -292,11 +312,10 @@ export function lucidAccountStore(Model, options = {}) {
|
|
|
292
312
|
}
|
|
293
313
|
},
|
|
294
314
|
/**
|
|
295
|
-
* Nome da conexão Lucid
|
|
296
|
-
*
|
|
297
|
-
*
|
|
298
|
-
* Undefined quando o model usa a conexão default
|
|
299
|
-
* NÃO faz parte do contrato AccountStore.
|
|
315
|
+
* Nome da conexão Lucid do MODEL da conta (deriva de `Model.connection`) — onde
|
|
316
|
+
* vive a tabela da conta (e a coluna `login_methods`). As tabelas do authkit
|
|
317
|
+
* NÃO seguem esta conexão: elas usam `schema.connection`.
|
|
318
|
+
* Undefined quando o model usa a conexão default.
|
|
300
319
|
*/
|
|
301
320
|
connectionName: Model.connection ?? undefined,
|
|
302
321
|
/**
|
|
@@ -318,10 +337,16 @@ export function lucidAccountStore(Model, options = {}) {
|
|
|
318
337
|
__model: Model,
|
|
319
338
|
};
|
|
320
339
|
// Histórico de senhas: tabela lib-owned `auth_password_history`, pela conexão
|
|
321
|
-
// do
|
|
340
|
+
// das tabelas do authkit (resolvida a cada uso, como no MFA).
|
|
322
341
|
Object.assign(store, buildPasswordHistory(ctx, {
|
|
323
|
-
query: () =>
|
|
324
|
-
table: (name) =>
|
|
342
|
+
query: () => tablesClient().query(),
|
|
343
|
+
table: (name) => tablesClient().table(name),
|
|
325
344
|
}));
|
|
345
|
+
// Não-enumerável: o `defineConfig` informa `schema.connection` por aqui.
|
|
346
|
+
Object.defineProperty(store, BIND_TABLES_LOCATION, {
|
|
347
|
+
value: (location) => {
|
|
348
|
+
tablesLocation = { connection: location.connection };
|
|
349
|
+
},
|
|
350
|
+
});
|
|
326
351
|
return store;
|
|
327
352
|
}
|
|
@@ -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
|
|
12
|
-
*
|
|
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);
|
|
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}.
|
|
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
16
|
* explícito (`{ OrgModel, MemberModel, InvitationModel }`) continua valendo como
|
|
15
|
-
* escape hatch
|
|
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,13 @@ 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 numa conexão Lucid nomeada (subclasses com `static connection`),
|
|
76
|
+
* memoizados por conexão. `undefined` → os próprios defaults (conexão primária).
|
|
77
|
+
*/
|
|
78
|
+
export declare function organizationModelsOn(connection: string | undefined): 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}.
|
|
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
21
|
* explícito (`{ OrgModel, MemberModel, InvitationModel }`) continua valendo como
|
|
20
|
-
* escape hatch
|
|
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,30 @@ export const defaultOrganizationModels = {
|
|
|
111
112
|
MemberModel: AuthOrganizationMember,
|
|
112
113
|
InvitationModel: AuthOrganizationInvitation,
|
|
113
114
|
};
|
|
115
|
+
const modelsByConnection = new Map();
|
|
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).
|
|
119
|
+
*/
|
|
120
|
+
export function organizationModelsOn(connection) {
|
|
121
|
+
if (!connection)
|
|
122
|
+
return defaultOrganizationModels;
|
|
123
|
+
let models = modelsByConnection.get(connection);
|
|
124
|
+
if (!models) {
|
|
125
|
+
class OrgModel extends AuthOrganization {
|
|
126
|
+
static table = 'auth_organizations';
|
|
127
|
+
static connection = connection;
|
|
128
|
+
}
|
|
129
|
+
class MemberModel extends AuthOrganizationMember {
|
|
130
|
+
static table = 'auth_organization_members';
|
|
131
|
+
static connection = connection;
|
|
132
|
+
}
|
|
133
|
+
class InvitationModel extends AuthOrganizationInvitation {
|
|
134
|
+
static table = 'auth_organization_invitations';
|
|
135
|
+
static connection = connection;
|
|
136
|
+
}
|
|
137
|
+
models = { OrgModel, MemberModel, InvitationModel };
|
|
138
|
+
modelsByConnection.set(connection, models);
|
|
139
|
+
}
|
|
140
|
+
return models;
|
|
141
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { OrganizationsCapability } from '../account_store.js';
|
|
2
2
|
/**
|
|
3
|
-
* Contexto mínimo para o builder de organizations.
|
|
4
|
-
* (
|
|
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
|
-
|
|
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) {
|
|
@@ -78,6 +78,12 @@ export interface LucidStoreContext {
|
|
|
78
78
|
* recovery da conta. Ver nota de `emailVerificationTtlHours`.
|
|
79
79
|
*/
|
|
80
80
|
emailChangeTtlHours?: number;
|
|
81
|
+
/**
|
|
82
|
+
* Query client for AuthKit's own tables (`auth_mfa`, `auth_password_history`):
|
|
83
|
+
* `schema.connection` of `defineConfig`, NOT the account model's connection. Absent
|
|
84
|
+
* → the model's connection (a store used without `defineConfig`).
|
|
85
|
+
*/
|
|
86
|
+
tablesClient?: () => any;
|
|
81
87
|
}
|
|
82
88
|
export declare const sha256: (value: string) => string;
|
|
83
89
|
/** Recovery code legível: 10 chars hex em duas metades (ex.: a1b2c-3d4e5). */
|
|
@@ -105,15 +111,17 @@ export interface MfaStatePatch {
|
|
|
105
111
|
}
|
|
106
112
|
/**
|
|
107
113
|
* Repositório do estado de MFA na tabela LIB-OWNED `auth_mfa`, keyed por
|
|
108
|
-
* `account_id`. Acessa o banco pela
|
|
109
|
-
*
|
|
114
|
+
* `account_id`. Acessa o banco pela conexão das tabelas do authkit
|
|
115
|
+
* (`schema.connection`, via `tablesClient`), a mesma em que o `autoManage` cria a
|
|
116
|
+
* tabela — sem exigir migration no host. Sem `tablesClient` (store fora do
|
|
117
|
+
* `defineConfig`), cai na conexão do model.
|
|
110
118
|
*
|
|
111
119
|
* O `upsert` é portável (sqlite + pg sem UPSERT nativo): tenta UPDATE pela PK;
|
|
112
120
|
* se não afetou nenhuma linha, faz INSERT. `recovery_codes` é serializado como
|
|
113
121
|
* JSON (string) na escrita e desserializado na leitura (aceita tanto string —
|
|
114
122
|
* sqlite/text — quanto array já parseado — pg jsonb).
|
|
115
123
|
*/
|
|
116
|
-
export declare function buildMfaStateRepo(Model: any): {
|
|
124
|
+
export declare function buildMfaStateRepo(Model: any, tablesClient?: () => any): {
|
|
117
125
|
/** Lê o estado de MFA da conta; null se não há linha em `auth_mfa`. */
|
|
118
126
|
read(accountId: string): Promise<MfaState | null>;
|
|
119
127
|
/**
|
|
@@ -39,19 +39,21 @@ export function hashesEqual(a, b) {
|
|
|
39
39
|
}
|
|
40
40
|
/**
|
|
41
41
|
* Repositório do estado de MFA na tabela LIB-OWNED `auth_mfa`, keyed por
|
|
42
|
-
* `account_id`. Acessa o banco pela
|
|
43
|
-
*
|
|
42
|
+
* `account_id`. Acessa o banco pela conexão das tabelas do authkit
|
|
43
|
+
* (`schema.connection`, via `tablesClient`), a mesma em que o `autoManage` cria a
|
|
44
|
+
* tabela — sem exigir migration no host. Sem `tablesClient` (store fora do
|
|
45
|
+
* `defineConfig`), cai na conexão do model.
|
|
44
46
|
*
|
|
45
47
|
* O `upsert` é portável (sqlite + pg sem UPSERT nativo): tenta UPDATE pela PK;
|
|
46
48
|
* se não afetou nenhuma linha, faz INSERT. `recovery_codes` é serializado como
|
|
47
49
|
* JSON (string) na escrita e desserializado na leitura (aceita tanto string —
|
|
48
50
|
* sqlite/text — quanto array já parseado — pg jsonb).
|
|
49
51
|
*/
|
|
50
|
-
export function buildMfaStateRepo(Model) {
|
|
52
|
+
export function buildMfaStateRepo(Model, tablesClient) {
|
|
51
53
|
const TABLE = 'auth_mfa';
|
|
52
|
-
/** QueryClient
|
|
54
|
+
/** QueryClient das tabelas do authkit (equivale ao `db` do password_history). */
|
|
53
55
|
function client() {
|
|
54
|
-
return Model.query().client;
|
|
56
|
+
return tablesClient ? tablesClient() : Model.query().client;
|
|
55
57
|
}
|
|
56
58
|
/** Normaliza `recovery_codes` vindo do banco (json string OU array já parseado). */
|
|
57
59
|
function parseRecoveryCodes(raw) {
|
|
@@ -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);
|
|
11
|
+
const mfaState = buildMfaStateRepo(Model, ctx.tablesClient);
|
|
12
12
|
return {
|
|
13
13
|
async generatePasskeyRegistrationOptions(accountId) {
|
|
14
14
|
const row = await Model.find(accountId);
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import type { ApplicationService } from '@adonisjs/core/types';
|
|
2
2
|
import type { OidcAdapter } from './adapter_contract.js';
|
|
3
3
|
export type OidcAdapterClass = new (name: string) => OidcAdapter;
|
|
4
|
+
/** What `defineConfig` hands every adapter factory. */
|
|
5
|
+
export interface AdapterResolveContext {
|
|
6
|
+
/**
|
|
7
|
+
* `schema.connection` of `defineConfig`: the Lucid connection AuthKit's tables live
|
|
8
|
+
* on. A database adapter without its own `connection` uses it.
|
|
9
|
+
*/
|
|
10
|
+
connection?: string;
|
|
11
|
+
}
|
|
4
12
|
export interface AdapterFactory {
|
|
5
|
-
resolver(app: ApplicationService): Promise<OidcAdapterClass>;
|
|
13
|
+
resolver(app: ApplicationService, context?: AdapterResolveContext): Promise<OidcAdapterClass>;
|
|
6
14
|
}
|
|
7
15
|
/**
|
|
8
16
|
* Modelos do oidc-provider de vida curta, amarrados ao ciclo da sessão de
|
|
@@ -35,7 +43,10 @@ export interface RedisAdapterConfig {
|
|
|
35
43
|
prefix?: string;
|
|
36
44
|
}
|
|
37
45
|
export interface DatabaseAdapterConfig {
|
|
38
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* Nome da conexão Lucid. Default: `schema.connection` do `defineConfig` (a conexão
|
|
48
|
+
* de todas as tabelas do authkit), e sem ela a primária.
|
|
49
|
+
*/
|
|
39
50
|
connection?: string;
|
|
40
51
|
}
|
|
41
52
|
export declare const adapters: {
|
|
@@ -67,9 +67,9 @@ export const adapters = {
|
|
|
67
67
|
*/
|
|
68
68
|
database(config = {}) {
|
|
69
69
|
return {
|
|
70
|
-
async resolver(app) {
|
|
70
|
+
async resolver(app, context) {
|
|
71
71
|
const db = await app.container.make('lucid.db');
|
|
72
|
-
const connection = config.connection;
|
|
72
|
+
const connection = config.connection ?? context?.connection;
|
|
73
73
|
const conn = connection ? db.connection(connection) : db;
|
|
74
74
|
return class extends DatabaseAdapter {
|
|
75
75
|
constructor(name) {
|
|
@@ -54,7 +54,7 @@ export async function runExpireScan(app, opts) {
|
|
|
54
54
|
let runtimeSettings = null;
|
|
55
55
|
try {
|
|
56
56
|
const db = await app.container.make('lucid.db');
|
|
57
|
-
const connection =
|
|
57
|
+
const connection = cfg.schema?.connection;
|
|
58
58
|
runtimeSettings = new RuntimeSettings(db, connection ? { connection } : {});
|
|
59
59
|
}
|
|
60
60
|
catch {
|
|
@@ -131,11 +131,11 @@ function validateKnownKey(key, value) {
|
|
|
131
131
|
async function resolveSettingsService(app) {
|
|
132
132
|
try {
|
|
133
133
|
const db = await app.container.make('lucid.db');
|
|
134
|
-
//
|
|
134
|
+
// `auth_settings` vive na conexão das tabelas do authkit (`schema.connection`).
|
|
135
135
|
let connection;
|
|
136
136
|
try {
|
|
137
137
|
const cfg = await resolveCfg(app);
|
|
138
|
-
connection = cfg?.
|
|
138
|
+
connection = cfg?.schema?.connection;
|
|
139
139
|
}
|
|
140
140
|
catch {
|
|
141
141
|
// Silencioso: sem config → default connection.
|
|
@@ -1190,7 +1190,10 @@ export interface AuthServerConfigInput {
|
|
|
1190
1190
|
* adiciona colunas novas (aditivo — nunca dropa nem altera tipos).
|
|
1191
1191
|
* - `autoManage: false`: nada roda no boot; gerencie o schema você mesmo —
|
|
1192
1192
|
* de preferência chamando `ensureAuthkitSchema(this.db)` numa migration.
|
|
1193
|
-
* - `connection`: conexão Lucid
|
|
1193
|
+
* - `connection`: a conexão Lucid de TODAS as tabelas do authkit (default:
|
|
1194
|
+
* primária) — as acima, o `authkit_keystore`, as de extensions e a do
|
|
1195
|
+
* `adapters.database()`/keystore `lucid` quando eles não declaram a própria.
|
|
1196
|
+
* A tabela da conta NÃO entra: fica na conexão do model da conta.
|
|
1194
1197
|
*/
|
|
1195
1198
|
schema?: {
|
|
1196
1199
|
autoManage?: boolean;
|
|
@@ -17,6 +17,7 @@ import { loadEncryptionService } from './keys/keystore_crypto.js';
|
|
|
17
17
|
import { KeystoreManager, resolveKeystoreVault } from './keys/keystore_manager.js';
|
|
18
18
|
import { mcpClientRegistration, resolveMcpOAuth, } from './mcp/mcp_oauth.js';
|
|
19
19
|
import { OPEN_REGISTRATION_REDIRECT_POLICY, resolveRedirectUriPolicy, } from './provider/registration_policy.js';
|
|
20
|
+
import { bindTablesLocation } from './schema/tables_location.js';
|
|
20
21
|
import { resolveServiceTenants, } from './service_tenants/config.js';
|
|
21
22
|
export { adapters };
|
|
22
23
|
const RATE_LIMIT_DEFAULTS = {
|
|
@@ -284,15 +285,20 @@ export function defineConfig(config) {
|
|
|
284
285
|
(!Number.isInteger(requiredFactors) || requiredFactors < 2 || requiredFactors > 8)) {
|
|
285
286
|
throw new Error('mfa.requiredFactors must be an integer between 2 and 8');
|
|
286
287
|
}
|
|
288
|
+
// Toda tabela do AuthKit vive em `schema.connection` — inclusive as que o store da
|
|
289
|
+
// conta lê (auth_mfa, auth_password_history, organizations), que antes seguiam a
|
|
290
|
+
// conexão do model da conta.
|
|
291
|
+
bindTablesLocation(config.accountStore, { connection: config.schema?.connection });
|
|
287
292
|
return configProvider.create(async (app) => {
|
|
288
|
-
const
|
|
293
|
+
const tablesContext = { connection: config.schema?.connection };
|
|
294
|
+
const AdapterClass = await config.adapter.resolver(app, tablesContext);
|
|
289
295
|
// `session.adapter` ausente ⇒ mesma classe do default (back-compat: o
|
|
290
296
|
// dispatcher vira identidade e tudo continua no adapter único de hoje).
|
|
291
297
|
const SessionAdapterClass = config.session?.adapter
|
|
292
|
-
? await config.session.adapter.resolver(app)
|
|
298
|
+
? await config.session.adapter.resolver(app, tablesContext)
|
|
293
299
|
: AdapterClass;
|
|
294
300
|
// `jwks: 'auto'` → resolve env-aware: AUTHKIT_JWKS inline, senão managed em arquivo.
|
|
295
|
-
const
|
|
301
|
+
const jwksInput = config.jwks === 'auto'
|
|
296
302
|
? process.env.AUTHKIT_JWKS
|
|
297
303
|
? {
|
|
298
304
|
source: 'jwks',
|
|
@@ -304,6 +310,14 @@ export function defineConfig(config) {
|
|
|
304
310
|
store: 'tmp/authkit_jwks.json',
|
|
305
311
|
}
|
|
306
312
|
: config.jwks;
|
|
313
|
+
// Keystore no Lucid sem conexão própria fica na conexão das tabelas do authkit.
|
|
314
|
+
const jwksStore = jwksInput.store;
|
|
315
|
+
const jwksConfig = jwksStore?.driver === 'lucid' && !jwksStore.connection && config.schema?.connection
|
|
316
|
+
? {
|
|
317
|
+
...jwksInput,
|
|
318
|
+
store: { ...jwksStore, connection: config.schema.connection },
|
|
319
|
+
}
|
|
320
|
+
: jwksInput;
|
|
307
321
|
if (config.jwks === 'auto' && !process.env.AUTHKIT_JWKS) {
|
|
308
322
|
const warning = jwksAutoFallbackWarning(jwksConfig.store ?? null);
|
|
309
323
|
if (warning) {
|
|
@@ -553,8 +553,8 @@ export function checkSettings(input) {
|
|
|
553
553
|
// Table absent — opt-in feature, silently ok.
|
|
554
554
|
return null;
|
|
555
555
|
}
|
|
556
|
-
// Informa a conexão usada quando nomeada (
|
|
557
|
-
const connectionName = cfg.
|
|
556
|
+
// Informa a conexão usada quando nomeada (a das tabelas do authkit).
|
|
557
|
+
const connectionName = cfg.schema?.connection;
|
|
558
558
|
const connectionHint = connectionName ? ` (connection: '${connectionName}')` : '';
|
|
559
559
|
// Table present — check if botProtection.verify is configured.
|
|
560
560
|
const hasBotVerify = typeof cfg.botProtection?.verify === 'function';
|
|
@@ -92,10 +92,10 @@ export interface RuntimeSettingsOptions {
|
|
|
92
92
|
/** TTL do cache em ms. Default: 15_000 (15s). */
|
|
93
93
|
ttlMs?: number;
|
|
94
94
|
/**
|
|
95
|
-
* Nome da conexão Lucid a usar (ex.: 'auth')
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
* Ausente (ou undefined) → conexão default
|
|
95
|
+
* Nome da conexão Lucid a usar (ex.: 'auth') — a `schema.connection` do
|
|
96
|
+
* `defineConfig`, onde vivem as tabelas do authkit. Quando presente, todas as
|
|
97
|
+
* queries usam `db.connection(name)` em vez da conexão default.
|
|
98
|
+
* Ausente (ou undefined) → conexão default.
|
|
99
99
|
*/
|
|
100
100
|
connection?: string;
|
|
101
101
|
}
|
|
@@ -171,8 +171,8 @@ export declare class RuntimeSettings implements SettingsCapability {
|
|
|
171
171
|
*
|
|
172
172
|
* Esta fábrica SUBSTITUI ~16 cópias do mesmo bloco db/connection que viviam sob
|
|
173
173
|
* nomes distintos (`getRuntimeSettings`, `getSettingsService`,
|
|
174
|
-
* `getRuntimeSettingsForSudo`)
|
|
175
|
-
*
|
|
174
|
+
* `getRuntimeSettingsForSudo`). A conexão é `schema.connection`, a de todas as
|
|
175
|
+
* tabelas do authkit.
|
|
176
176
|
*
|
|
177
177
|
* @example
|
|
178
178
|
* ```ts
|
|
@@ -312,8 +312,8 @@ export class RuntimeSettings {
|
|
|
312
312
|
*
|
|
313
313
|
* Esta fábrica SUBSTITUI ~16 cópias do mesmo bloco db/connection que viviam sob
|
|
314
314
|
* nomes distintos (`getRuntimeSettings`, `getSettingsService`,
|
|
315
|
-
* `getRuntimeSettingsForSudo`)
|
|
316
|
-
*
|
|
315
|
+
* `getRuntimeSettingsForSudo`). A conexão é `schema.connection`, a de todas as
|
|
316
|
+
* tabelas do authkit.
|
|
317
317
|
*
|
|
318
318
|
* @example
|
|
319
319
|
* ```ts
|
|
@@ -325,7 +325,8 @@ export async function resolveRuntimeSettings(ctx) {
|
|
|
325
325
|
try {
|
|
326
326
|
const db = await ctx.containerResolver.make('lucid.db');
|
|
327
327
|
const service = await ctx.containerResolver.make('authkit.server').catch(() => null);
|
|
328
|
-
|
|
328
|
+
// `auth_settings` vive na conexão das tabelas do authkit, não na do model da conta.
|
|
329
|
+
const connection = service?.config?.schema?.connection;
|
|
329
330
|
return new RuntimeSettings(db, connection ? { connection } : {});
|
|
330
331
|
}
|
|
331
332
|
catch {
|
|
@@ -25,8 +25,17 @@
|
|
|
25
25
|
*/
|
|
26
26
|
import type { AuthkitTable } from '../agents/extension.js';
|
|
27
27
|
export interface EnsureSchemaOptions {
|
|
28
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* Conexão Lucid das tabelas do authkit (`schema.connection`). Default: conexão
|
|
30
|
+
* primária. Ignorada quando `db` já é um client (o `this.db` de uma migration).
|
|
31
|
+
*/
|
|
29
32
|
connection?: string;
|
|
33
|
+
/**
|
|
34
|
+
* Conexão Lucid do model da conta, onde vive a tabela da conta (a coluna
|
|
35
|
+
* `login_methods`). Default: a conexão primária — NÃO a das tabelas do authkit.
|
|
36
|
+
* O provider passa a do `accountStore` no boot. Ignorada quando `db` já é um client.
|
|
37
|
+
*/
|
|
38
|
+
accountConnection?: string;
|
|
30
39
|
/**
|
|
31
40
|
* Tabelas de `personalAgents.extensions` (`tables` de cada uma), garantidas junto. O provider
|
|
32
41
|
* passa as do config no boot.
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
* }
|
|
24
24
|
* ```
|
|
25
25
|
*/
|
|
26
|
+
import { authkitTablesClient } from './tables_location.js';
|
|
26
27
|
/** Busca por conta (`account_id`) e limpeza dos expirados (`expires_at`). */
|
|
27
28
|
const OIDC_PAYLOAD_INDEXES = [
|
|
28
29
|
{ name: 'authkit_oidc_payloads_account_idx', columns: ['model_name', 'account_id'] },
|
|
@@ -500,11 +501,7 @@ async function ensureTables(conn, tables, report) {
|
|
|
500
501
|
* uma extension expor o próprio `ensure*Schema()` para migrations do host. Estável no 1.x.
|
|
501
502
|
*/
|
|
502
503
|
export async function ensureAuthkitTables(db, tables, options = {}) {
|
|
503
|
-
const conn =
|
|
504
|
-
? db
|
|
505
|
-
: options.connection
|
|
506
|
-
? db.connection(options.connection)
|
|
507
|
-
: db.connection();
|
|
504
|
+
const conn = authkitTablesClient(db, { connection: options.connection });
|
|
508
505
|
const report = {
|
|
509
506
|
created: [],
|
|
510
507
|
altered: {},
|
|
@@ -521,11 +518,7 @@ export async function ensureAuthkitTables(db, tables, options = {}) {
|
|
|
521
518
|
* (`this.db`, um `QueryClient`/`TransactionClient`, usado como está).
|
|
522
519
|
*/
|
|
523
520
|
export async function ensureAuthkitSchema(db, options = {}) {
|
|
524
|
-
const conn =
|
|
525
|
-
? db
|
|
526
|
-
: options.connection
|
|
527
|
-
? db.connection(options.connection)
|
|
528
|
-
: db.connection();
|
|
521
|
+
const conn = authkitTablesClient(db, { connection: options.connection });
|
|
529
522
|
const report = {
|
|
530
523
|
created: [],
|
|
531
524
|
altered: {},
|
|
@@ -547,11 +540,15 @@ export async function ensureAuthkitSchema(db, options = {}) {
|
|
|
547
540
|
// `users` fixo: com a conta em `auth_users` (o nome que o próprio scaffold da
|
|
548
541
|
// lib usa), o `users` fixo acertava uma tabela qualquer de mesmo nome e a
|
|
549
542
|
// coluna ia parar no lugar errado sem erro nenhum.
|
|
543
|
+
//
|
|
544
|
+
// A tabela da conta fica na conexão do MODEL da conta, não na das tabelas do
|
|
545
|
+
// authkit (`schema.connection`).
|
|
550
546
|
const accountTable = options.accountTable ?? 'users';
|
|
551
|
-
|
|
547
|
+
const accountConn = authkitTablesClient(db, { connection: options.accountConnection });
|
|
548
|
+
if (await tableExists(accountConn, accountTable)) {
|
|
552
549
|
try {
|
|
553
|
-
if (!(await columnExists(
|
|
554
|
-
await
|
|
550
|
+
if (!(await columnExists(accountConn, accountTable, 'login_methods'))) {
|
|
551
|
+
await accountConn.schema.alterTable(accountTable, (t) => {
|
|
555
552
|
t.jsonb('login_methods').nullable();
|
|
556
553
|
});
|
|
557
554
|
report.altered[accountTable] = ['login_methods'];
|
|
@@ -568,7 +565,7 @@ export async function ensureAuthkitSchema(db, options = {}) {
|
|
|
568
565
|
* o boot não avisava nada — o silêncio que este bloco existe para acabar.
|
|
569
566
|
* (Mesma semântica do probe de `createTable` logo acima.)
|
|
570
567
|
*/
|
|
571
|
-
if (await columnExists(
|
|
568
|
+
if (await columnExists(accountConn, accountTable, 'login_methods')) {
|
|
572
569
|
report.loginMethods.ensured = true;
|
|
573
570
|
}
|
|
574
571
|
else {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where AuthKit's OWN tables live (`auth_settings`, `auth_mfa`, `auth_password_history`,
|
|
3
|
+
* the organization tables, `authkit_oidc_payloads`, the personal-agent tables, the
|
|
4
|
+
* `authkit_keystore` of the Lucid keystore, and the tables of agent extensions).
|
|
5
|
+
*
|
|
6
|
+
* The rule: every lib-owned table goes through `schema.connection` of `defineConfig`
|
|
7
|
+
* (default: the primary Lucid connection). The app's account table is not lib-owned: it
|
|
8
|
+
* stays on the account model's connection (`static connection` of the model).
|
|
9
|
+
*/
|
|
10
|
+
export interface AuthkitTablesLocation {
|
|
11
|
+
/** Lucid connection name. `undefined` = the primary connection. */
|
|
12
|
+
connection?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The query client for AuthKit's tables. `db` is the Lucid manager (`Database`) or a
|
|
16
|
+
* client the caller already holds (a migration's `this.db`, a transaction), which is
|
|
17
|
+
* used as is.
|
|
18
|
+
*/
|
|
19
|
+
export declare function authkitTablesClient(db: any, location?: AuthkitTablesLocation): any;
|
|
20
|
+
/**
|
|
21
|
+
* Hidden hook a store built by `lucidAccountStore` exposes so `defineConfig` can tell it
|
|
22
|
+
* where AuthKit's tables live (the store is built before `defineConfig` sees the `schema`
|
|
23
|
+
* option). Not part of the `AccountStore` contract.
|
|
24
|
+
*/
|
|
25
|
+
export declare const BIND_TABLES_LOCATION: unique symbol;
|
|
26
|
+
/** Hands `location` to a store that accepts it (see {@link BIND_TABLES_LOCATION}). */
|
|
27
|
+
export declare function bindTablesLocation(store: unknown, location: AuthkitTablesLocation): void;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The query client for AuthKit's tables. `db` is the Lucid manager (`Database`) or a
|
|
3
|
+
* client the caller already holds (a migration's `this.db`, a transaction), which is
|
|
4
|
+
* used as is.
|
|
5
|
+
*/
|
|
6
|
+
export function authkitTablesClient(db, location = {}) {
|
|
7
|
+
if (typeof db?.connection !== 'function')
|
|
8
|
+
return db;
|
|
9
|
+
return location.connection ? db.connection(location.connection) : db.connection();
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Hidden hook a store built by `lucidAccountStore` exposes so `defineConfig` can tell it
|
|
13
|
+
* where AuthKit's tables live (the store is built before `defineConfig` sees the `schema`
|
|
14
|
+
* option). Not part of the `AccountStore` contract.
|
|
15
|
+
*/
|
|
16
|
+
export const BIND_TABLES_LOCATION = Symbol.for('authkit.bindTablesLocation');
|
|
17
|
+
/** Hands `location` to a store that accepts it (see {@link BIND_TABLES_LOCATION}). */
|
|
18
|
+
export function bindTablesLocation(store, location) {
|
|
19
|
+
const bind = store?.[BIND_TABLES_LOCATION];
|
|
20
|
+
if (typeof bind === 'function')
|
|
21
|
+
bind(location);
|
|
22
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adonis-agora/authkit-server",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
3
|
+
"version": "1.0.0-rc.11",
|
|
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",
|