@adonis-agora/authkit-server 0.66.2 → 0.67.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.d.ts CHANGED
@@ -7,6 +7,7 @@ export type { AccountDeletionCapability, AccountImportCapability, AccountSecurit
7
7
  export { supportsAccountDeletion, supportsAccountImport, supportsAccountSecurity, supportsAccountStatus, supportsEmailVerificationStatus, supportsLoginMethodsPreference, supportsMagicLink, supportsMfa, supportsOrganizations, supportsOtpLogin, supportsPasskeys, supportsProfile, supportsProviderIdentity, } from './src/accounts/account_store.js';
8
8
  export type { AccountSecretEncrypter, LucidAccountStoreOptions, } from './src/accounts/lucid_account_store.js';
9
9
  export { appKeyEncrypter, lucidAccountStore } from './src/accounts/lucid_account_store.js';
10
+ export { AuthOrganization, AuthOrganizationInvitation, AuthOrganizationMember, defaultOrganizationModels, } from './src/accounts/lucid_store/organization_models.js';
10
11
  export type { LucidStoresModels, LucidStoresOptions, LucidStoresResult, } from './src/accounts/lucid_stores.js';
11
12
  export { lucidStores } from './src/accounts/lucid_stores.js';
12
13
  export type { AuditEvent, AuditEventType, AuditPage, AuditSink, ListAuditParams, StoredAuditEvent, } from './src/audit/audit_sink.js';
package/build/index.js CHANGED
@@ -5,6 +5,7 @@
5
5
  export { configure } from './commands/configure.js';
6
6
  export { supportsAccountDeletion, supportsAccountImport, supportsAccountSecurity, supportsAccountStatus, supportsEmailVerificationStatus, supportsLoginMethodsPreference, supportsMagicLink, supportsMfa, supportsOrganizations, supportsOtpLogin, supportsPasskeys, supportsProfile, supportsProviderIdentity, } from './src/accounts/account_store.js';
7
7
  export { appKeyEncrypter, lucidAccountStore } from './src/accounts/lucid_account_store.js';
8
+ export { AuthOrganization, AuthOrganizationInvitation, AuthOrganizationMember, defaultOrganizationModels, } from './src/accounts/lucid_store/organization_models.js';
8
9
  export { lucidStores } from './src/accounts/lucid_stores.js';
9
10
  export { lucidAuditSink } from './src/audit/lucid_audit_sink.js';
10
11
  export { adapters, defineConfig, resolveAdmin, resolveAdminApi, resolveAuthMethodsConfig, resolveDynamicRegistration, resolveLogin, resolveNotifications, resolveOrganizations, resolvePasswordless, resolveRateLimit, resolveRegistration, resolveWebauthn, toSeconds, } from './src/define_config.js';
@@ -218,7 +218,10 @@ export default class AuthkitServerProvider {
218
218
  if (!db)
219
219
  return; // host sem @adonisjs/lucid — nada a gerenciar
220
220
  const { ensureAuthkitSchema } = await import('../src/schema/ensure.js');
221
- const report = await ensureAuthkitSchema(db, { connection: config.schema.connection });
221
+ const report = await ensureAuthkitSchema(db, {
222
+ connection: config.schema.connection,
223
+ accountTable: config.accountStore?.accountTable,
224
+ });
222
225
  const logger = await this.app.container.make('logger').catch(() => null);
223
226
  if (report.created.length > 0) {
224
227
  logger?.info('authkit: created tables %s', report.created.join(', '));
@@ -226,6 +229,13 @@ export default class AuthkitServerProvider {
226
229
  for (const [table, columns] of Object.entries(report.altered)) {
227
230
  logger?.info('authkit: added columns to %s: %s', table, columns.join(', '));
228
231
  }
232
+ if (!report.loginMethods.ensured) {
233
+ // Sem este aviso o host não tinha como saber: a coluna não foi criada e a
234
+ // preferência de método de login por conta fica no-op em silêncio.
235
+ logger?.warn('authkit: tabela da conta "%s" não existe — a coluna `login_methods` não foi ' +
236
+ 'garantida e a preferência de método de login por conta não vai funcionar. ' +
237
+ 'Defina `accountStore.accountTable` (ou crie a tabela) se o nome estiver errado.', report.loginMethods.table);
238
+ }
229
239
  }
230
240
  catch (error) {
231
241
  const logger = await this.app.container.make('logger').catch(() => null);
@@ -627,6 +627,20 @@ export type AccountStore = CoreAccountStore & {
627
627
  * blocos `Partial<...>` de capacidades probáveis.
628
628
  */
629
629
  readonly connectionName?: string;
630
+ /**
631
+ * Nome da tabela da conta (deriva de `Model.table` — e, se o model ainda não
632
+ * bootou, da naming strategy; ver `lucid_account_store.ts`). Mesmo espírito do
633
+ * {@link AccountStore.connectionName}: metadado opcional, não método de
634
+ * capacidade.
635
+ *
636
+ * Existe porque a coluna opcional `login_methods` é garantida no boot pelo
637
+ * `ensureAuthkitSchema`, e ele precisa saber em QUAL tabela ela pertence. Sem
638
+ * este metadado só resta assumir `users`, e aí uma conta cuja tabela tem outro
639
+ * nome (por exemplo `auth_users`, que é o que o scaffold da lib usa) fica com
640
+ * a coluna numa tabela que ninguém consulta — sem erro, sem aviso.
641
+ * Undefined → `users` (back-compat com stores próprios).
642
+ */
643
+ readonly accountTable?: string;
630
644
  } & Partial<MfaCapability & WebauthnCapability & ProviderIdentityCapability & AccountSecurityCapability & AccountStatusCapability & ProfileCapability & MagicLinkCapability & OtpLoginCapability & EmailVerificationStatusCapability & AccountDeletionCapability & AccountImportCapability & OrganizationsCapability & PasswordHistoryCapability & PasswordExpirationCapability & LoginMethodsPreferenceCapability>;
631
645
  /** Type guard: o store implementa a capacidade de MFA / TOTP. */
632
646
  export declare function supportsMfa(store: AccountStore): store is AccountStore & MfaCapability;
@@ -136,12 +136,22 @@ export interface LucidAccountStoreOptions {
136
136
  */
137
137
  pwnedFetch?: FetchLike;
138
138
  /**
139
- * Models Lucid para organizations (multi-tenancy). Quando os três forem fornecidos,
140
- * a capacidade `OrganizationsCapability` fica disponível no store. Os models devem
141
- * ser tabelas `auth_organizations`, `auth_organization_members` e
142
- * `auth_organization_invitations`. Ausente → capability AUSENTE (sem tabelas = desligado).
139
+ * Models Lucid para organizations (multi-tenancy).
140
+ *
141
+ * - `true` → usa os models DEFAULT da lib ({@link defaultOrganizationModels}),
142
+ * que já mapeiam as três tabelas lib-owned (`auth_organizations`,
143
+ * `auth_organization_members`, `auth_organization_invitations`). É o caminho
144
+ * recomendado: as tabelas são criadas/evoluídas pelo `ensureAuthkitSchema`,
145
+ * então o mapeamento não é decisão do host.
146
+ * - `{ OrgModel, MemberModel, InvitationModel }` → escape hatch, para quem
147
+ * guarda as tabelas de auth numa conexão/schema próprios (os defaults não
148
+ * declaram `static connection`).
149
+ * - Ausente → `OrganizationsCapability` AUSENTE no store. Como as rotas
150
+ * `/account/orgs*` são montadas por capability-probing, isso deixa a
151
+ * feature desligada — silenciosamente, se o host não olhar o
152
+ * `authkit:doctor`.
143
153
  */
144
- organizationModels?: {
154
+ organizationModels?: true | {
145
155
  OrgModel: any;
146
156
  MemberModel: any;
147
157
  InvitationModel: any;
@@ -153,23 +163,6 @@ export interface LucidAccountStoreOptions {
153
163
  */
154
164
  emailTokens?: EmailTokensConfigInput;
155
165
  }
156
- /**
157
- * Implementação default do {@link AccountStore} sobre um model Lucid composto
158
- * de `withAuthUser()` + `withCredentials()` (+ opcionalmente `withMfa()`). O
159
- * model carrega `connection`/`table` (app-específico) e, por convenção, uma
160
- * coluna `fullName` (mapeada de `name`).
161
- *
162
- * Composição por CAPACIDADE: o núcleo + MFA são sempre montados; passkeys
163
- * (WebAuthn) e account linking por provider só são montados quando o model
164
- * correspondente é fornecido — caso contrário a capacidade fica ABSENTE (os
165
- * métodos não existem no objeto retornado, em vez de presentes-mas-lançando).
166
- *
167
- * @remarks Para capabilities que dependem de tabelas opcionais (ex.:
168
- * `auth_password_history`), use {@link lucidAccountStoreAsync} que probe o DB
169
- * e monta o store já com as capabilities detectadas, sem a necessidade de
170
- * fornecer um model separado. A versão síncrona (`lucidAccountStore`) é mantida
171
- * por back-compat — capabilities de tabela ficam AUSENTES nela.
172
- */
173
166
  export declare function lucidAccountStore(Model: any, options?: LucidAccountStoreOptions): AccountStore;
174
167
  /**
175
168
  * Options estendidas para uso em testes ou quando o DB é injetado diretamente
@@ -3,6 +3,7 @@ import { PasswordManager } from '../password/password_manager.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';
6
+ import { defaultOrganizationModels } 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';
@@ -103,23 +104,24 @@ export function appKeyEncrypter() {
103
104
  },
104
105
  };
105
106
  }
106
- /**
107
- * Implementação default do {@link AccountStore} sobre um model Lucid composto
108
- * de `withAuthUser()` + `withCredentials()` (+ opcionalmente `withMfa()`). O
109
- * model carrega `connection`/`table` (app-específico) e, por convenção, uma
110
- * coluna `fullName` (mapeada de `name`).
111
- *
112
- * Composição por CAPACIDADE: o núcleo + MFA são sempre montados; passkeys
113
- * (WebAuthn) e account linking por provider só são montados quando o model
114
- * correspondente é fornecido — caso contrário a capacidade fica ABSENTE (os
115
- * métodos não existem no objeto retornado, em vez de presentes-mas-lançando).
116
- *
117
- * @remarks Para capabilities que dependem de tabelas opcionais (ex.:
118
- * `auth_password_history`), use {@link lucidAccountStoreAsync} que probe o DB
119
- * e monta o store já com as capabilities detectadas, sem a necessidade de
120
- * fornecer um model separado. A versão síncrona (`lucidAccountStore`) é mantida
121
- * por back-compat — capabilities de tabela ficam AUSENTES nela.
122
- */
107
+ function resolveAccountTable(Model) {
108
+ try {
109
+ if (typeof Model?.table === 'string' && Model.table.length > 0) {
110
+ return Model.table;
111
+ }
112
+ const naming = Model?.namingStrategy;
113
+ if (naming && typeof naming.tableName === 'function') {
114
+ const resolved = naming.tableName(Model);
115
+ if (typeof resolved === 'string' && resolved.length > 0) {
116
+ return resolved;
117
+ }
118
+ }
119
+ }
120
+ catch {
121
+ // Model ainda não pronto (ou não-Lucid) — metadado é opcional.
122
+ }
123
+ return undefined;
124
+ }
123
125
  export function lucidAccountStore(Model, options = {}) {
124
126
  const mfaIssuer = options.mfaIssuer ?? 'AuthKit';
125
127
  const recoveryCodeCount = options.recoveryCodeCount ?? 8;
@@ -128,7 +130,7 @@ export function lucidAccountStore(Model, options = {}) {
128
130
  const encrypter = options.encrypter === false ? undefined : (options.encrypter ?? appKeyEncrypter());
129
131
  const ProviderIdentityModel = options.providerIdentityModel;
130
132
  const WebauthnCredentialModel = options.webauthnCredentialModel;
131
- const OrgModels = options.organizationModels;
133
+ const OrgModels = options.organizationModels === true ? defaultOrganizationModels : options.organizationModels;
132
134
  // RP do WebAuthn: usado nas cerimônias. Default do rpName cai no mfaIssuer.
133
135
  const webauthn = options.webauthn ?? {
134
136
  rpName: mfaIssuer,
@@ -199,6 +201,10 @@ export function lucidAccountStore(Model, options = {}) {
199
201
  const store = {
200
202
  ...buildCore(ctx),
201
203
  ...buildMfa(ctx),
204
+ // Metadado (não capacidade): a tabela da conta, para o ensure da coluna
205
+ // `login_methods` e o doctor saberem onde ela pertence em vez de assumir
206
+ // `users`. Ver `resolveAccountTable`.
207
+ accountTable: resolveAccountTable(Model),
202
208
  ...(ProviderIdentityModel ? buildProviderIdentity(ctx, ProviderIdentityModel) : {}),
203
209
  ...(WebauthnCredentialModel
204
210
  ? buildWebauthn(ctx, WebauthnCredentialModel, webauthn, ceremonies)
@@ -0,0 +1,67 @@
1
+ import { BaseModel } from '@adonisjs/lucid/orm';
2
+ import type { DateTime } from 'luxon';
3
+ /**
4
+ * Models default das três tabelas de organizations.
5
+ *
6
+ * Estas tabelas são LIB-OWNED: quem as cria e evolui é o `ensureAuthkitSchema`
7
+ * (ver `TABLES` em `src/schema/ensure.ts`). Exigir que cada host reescrevesse
8
+ * este mapeamento à mão rendia duas coisas ruins — boilerplate sem nenhuma
9
+ * decisão do host, e drift silencioso: uma coluna nova nestas tabelas chega
10
+ * pelo `autoManage` sem que o model escrito à mão no host saiba dela, e o
11
+ * sintoma aparece longe da causa.
12
+ *
13
+ * Use com `organizationModels: true` em {@link lucidAccountStore}. O caminho
14
+ * 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
+ *
18
+ * Não crie migration para estas tabelas: elas são criadas/atualizadas pelo
19
+ * `schema.autoManage` do authkit.
20
+ */
21
+ export declare class AuthOrganization extends BaseModel {
22
+ static table: string;
23
+ /** Os ids são sempre fornecidos pelo builder (randomUUID) — nunca pelo banco. */
24
+ static selfAssignPrimaryKey: boolean;
25
+ id: string;
26
+ name: string;
27
+ slug: string;
28
+ logoUrl: string | null;
29
+ /**
30
+ * Coluna `json`. O contrato público (`OrgSummary.metadata`) já é
31
+ * `Record<string, unknown> | null`, e é o que o builder grava/lê — declarar o
32
+ * mesmo aqui mantém o model alinhado ao que a lib promete.
33
+ */
34
+ metadata: Record<string, unknown> | null;
35
+ createdAt: DateTime | null;
36
+ updatedAt: DateTime | null;
37
+ }
38
+ export declare class AuthOrganizationMember extends BaseModel {
39
+ static table: string;
40
+ static selfAssignPrimaryKey: boolean;
41
+ id: string;
42
+ organizationId: string;
43
+ accountId: string;
44
+ role: string;
45
+ createdAt: DateTime | null;
46
+ updatedAt: DateTime | null;
47
+ }
48
+ export declare class AuthOrganizationInvitation extends BaseModel {
49
+ static table: string;
50
+ static selfAssignPrimaryKey: boolean;
51
+ id: string;
52
+ organizationId: string;
53
+ email: string;
54
+ role: string;
55
+ tokenHash: string;
56
+ invitedBy: string;
57
+ expiresAt: DateTime;
58
+ acceptedAt: DateTime | null;
59
+ createdAt: DateTime | null;
60
+ updatedAt: DateTime | null;
61
+ }
62
+ /** O trio que {@link lucidAccountStore} aceita em `organizationModels: true`. */
63
+ export declare const defaultOrganizationModels: {
64
+ readonly OrgModel: typeof AuthOrganization;
65
+ readonly MemberModel: typeof AuthOrganizationMember;
66
+ readonly InvitationModel: typeof AuthOrganizationInvitation;
67
+ };
@@ -0,0 +1,113 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { BaseModel, column } from '@adonisjs/lucid/orm';
8
+ /**
9
+ * Models default das três tabelas de organizations.
10
+ *
11
+ * Estas tabelas são LIB-OWNED: quem as cria e evolui é o `ensureAuthkitSchema`
12
+ * (ver `TABLES` em `src/schema/ensure.ts`). Exigir que cada host reescrevesse
13
+ * este mapeamento à mão rendia duas coisas ruins — boilerplate sem nenhuma
14
+ * decisão do host, e drift silencioso: uma coluna nova nestas tabelas chega
15
+ * pelo `autoManage` sem que o model escrito à mão no host saiba dela, e o
16
+ * sintoma aparece longe da causa.
17
+ *
18
+ * Use com `organizationModels: true` em {@link lucidAccountStore}. O caminho
19
+ * 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
+ *
23
+ * Não crie migration para estas tabelas: elas são criadas/atualizadas pelo
24
+ * `schema.autoManage` do authkit.
25
+ */
26
+ export class AuthOrganization extends BaseModel {
27
+ static table = 'auth_organizations';
28
+ /** Os ids são sempre fornecidos pelo builder (randomUUID) — nunca pelo banco. */
29
+ static selfAssignPrimaryKey = true;
30
+ }
31
+ __decorate([
32
+ column({ isPrimary: true })
33
+ ], AuthOrganization.prototype, "id", void 0);
34
+ __decorate([
35
+ column()
36
+ ], AuthOrganization.prototype, "name", void 0);
37
+ __decorate([
38
+ column()
39
+ ], AuthOrganization.prototype, "slug", void 0);
40
+ __decorate([
41
+ column()
42
+ ], AuthOrganization.prototype, "logoUrl", void 0);
43
+ __decorate([
44
+ column()
45
+ ], AuthOrganization.prototype, "metadata", void 0);
46
+ __decorate([
47
+ column.dateTime({ autoCreate: true })
48
+ ], AuthOrganization.prototype, "createdAt", void 0);
49
+ __decorate([
50
+ column.dateTime({ autoCreate: true, autoUpdate: true })
51
+ ], AuthOrganization.prototype, "updatedAt", void 0);
52
+ export class AuthOrganizationMember extends BaseModel {
53
+ static table = 'auth_organization_members';
54
+ static selfAssignPrimaryKey = true;
55
+ }
56
+ __decorate([
57
+ column({ isPrimary: true })
58
+ ], AuthOrganizationMember.prototype, "id", void 0);
59
+ __decorate([
60
+ column()
61
+ ], AuthOrganizationMember.prototype, "organizationId", void 0);
62
+ __decorate([
63
+ column()
64
+ ], AuthOrganizationMember.prototype, "accountId", void 0);
65
+ __decorate([
66
+ column()
67
+ ], AuthOrganizationMember.prototype, "role", void 0);
68
+ __decorate([
69
+ column.dateTime({ autoCreate: true })
70
+ ], AuthOrganizationMember.prototype, "createdAt", void 0);
71
+ __decorate([
72
+ column.dateTime({ autoCreate: true, autoUpdate: true })
73
+ ], AuthOrganizationMember.prototype, "updatedAt", void 0);
74
+ export class AuthOrganizationInvitation extends BaseModel {
75
+ static table = 'auth_organization_invitations';
76
+ static selfAssignPrimaryKey = true;
77
+ }
78
+ __decorate([
79
+ column({ isPrimary: true })
80
+ ], AuthOrganizationInvitation.prototype, "id", void 0);
81
+ __decorate([
82
+ column()
83
+ ], AuthOrganizationInvitation.prototype, "organizationId", void 0);
84
+ __decorate([
85
+ column()
86
+ ], AuthOrganizationInvitation.prototype, "email", void 0);
87
+ __decorate([
88
+ column()
89
+ ], AuthOrganizationInvitation.prototype, "role", void 0);
90
+ __decorate([
91
+ column()
92
+ ], AuthOrganizationInvitation.prototype, "tokenHash", void 0);
93
+ __decorate([
94
+ column()
95
+ ], AuthOrganizationInvitation.prototype, "invitedBy", void 0);
96
+ __decorate([
97
+ column.dateTime()
98
+ ], AuthOrganizationInvitation.prototype, "expiresAt", void 0);
99
+ __decorate([
100
+ column.dateTime()
101
+ ], AuthOrganizationInvitation.prototype, "acceptedAt", void 0);
102
+ __decorate([
103
+ column.dateTime({ autoCreate: true })
104
+ ], AuthOrganizationInvitation.prototype, "createdAt", void 0);
105
+ __decorate([
106
+ column.dateTime({ autoCreate: true, autoUpdate: true })
107
+ ], AuthOrganizationInvitation.prototype, "updatedAt", void 0);
108
+ /** O trio que {@link lucidAccountStore} aceita em `organizationModels: true`. */
109
+ export const defaultOrganizationModels = {
110
+ OrgModel: AuthOrganization,
111
+ MemberModel: AuthOrganizationMember,
112
+ InvitationModel: AuthOrganizationInvitation,
113
+ };
@@ -14,8 +14,15 @@ export interface LucidStoresModels {
14
14
  providerIdentity?: any;
15
15
  /** Model de `withWebauthnCredential()` — habilita passkeys. */
16
16
  webauthnCredential?: any;
17
- /** Trio de models de organizations — habilita multi-tenancy. */
18
- organizations?: {
17
+ /**
18
+ * Organizations (multi-tenancy) — habilita a `OrganizationsCapability`.
19
+ *
20
+ * `true` usa os models default da lib (as três tabelas são lib-owned, criadas
21
+ * pelo `ensureAuthkitSchema`), que é o caminho recomendado. O trio explícito
22
+ * fica como escape hatch para quem guarda as tabelas de auth numa
23
+ * conexão/schema próprios.
24
+ */
25
+ organizations?: true | {
19
26
  OrgModel: any;
20
27
  MemberModel: any;
21
28
  InvitationModel: any;
@@ -37,9 +44,14 @@ export type LucidStoresOptions = Omit<LucidAccountStoreOptions, 'providerIdentit
37
44
  * const { accountStore, patStore, audit } = lucidStores(
38
45
  * { account: AuthUser, pat: PersonalAccessToken, audit: AuditLog,
39
46
  * providerIdentity: ProviderIdentity, webauthnCredential: WebauthnCredential,
40
- * organizations: { OrgModel: Organization, MemberModel: OrganizationMember, InvitationModel: OrganizationInvitation } },
47
+ * organizations: true },
41
48
  * { mfaIssuer: 'educ(a)ção', webauthn }
42
49
  * )
43
50
  * ```
51
+ *
52
+ * `organizations: true` usa os models default da lib. Se as tabelas de auth
53
+ * vivem numa conexão/schema próprios, passe o trio explícito
54
+ * (`{ OrgModel, MemberModel, InvitationModel }`) — os defaults não declaram
55
+ * `static connection`.
44
56
  */
45
57
  export declare function lucidStores(models: LucidStoresModels, options?: LucidStoresOptions): LucidStoresResult;
@@ -10,10 +10,15 @@ import { lucidAccountStore } from './lucid_account_store.js';
10
10
  * const { accountStore, patStore, audit } = lucidStores(
11
11
  * { account: AuthUser, pat: PersonalAccessToken, audit: AuditLog,
12
12
  * providerIdentity: ProviderIdentity, webauthnCredential: WebauthnCredential,
13
- * organizations: { OrgModel: Organization, MemberModel: OrganizationMember, InvitationModel: OrganizationInvitation } },
13
+ * organizations: true },
14
14
  * { mfaIssuer: 'educ(a)ção', webauthn }
15
15
  * )
16
16
  * ```
17
+ *
18
+ * `organizations: true` usa os models default da lib. Se as tabelas de auth
19
+ * vivem numa conexão/schema próprios, passe o trio explícito
20
+ * (`{ OrgModel, MemberModel, InvitationModel }`) — os defaults não declaram
21
+ * `static connection`.
17
22
  */
18
23
  export function lucidStores(models, options = {}) {
19
24
  const accountStore = lucidAccountStore(models.account, {
@@ -501,8 +501,10 @@ export function checkOrganizations(input) {
501
501
  return {
502
502
  level: 'warn',
503
503
  message: 'organizations.enabled: true, but the accountStore has no OrganizationsCapability — ' +
504
- 'pass `organizationModels: { OrgModel, MemberModel, InvitationModel }` to `lucidAccountStore()`. ' +
505
- 'Expected tables: auth_organizations, auth_organization_members, auth_organization_invitations.',
504
+ 'pass `organizationModels: true` to `lucidAccountStore()` (usa os models default da lib), ' +
505
+ 'ou `{ OrgModel, MemberModel, InvitationModel }` se as tabelas de auth vivem numa ' +
506
+ 'conexão/schema próprios. Expected tables: auth_organizations, ' +
507
+ 'auth_organization_members, auth_organization_invitations.',
506
508
  };
507
509
  }
508
510
  if (storeSupports) {
@@ -26,12 +26,28 @@
26
26
  export interface EnsureSchemaOptions {
27
27
  /** Conexão Lucid a usar. Default: conexão primária. */
28
28
  connection?: string;
29
+ /**
30
+ * Tabela da conta, onde a coluna opcional `login_methods` pertence. Vem do
31
+ * metadado `accountStore.accountTable` (o provider repassa no boot). Undefined
32
+ * → `users`, que era o único valor possível antes deste metadado existir.
33
+ */
34
+ accountTable?: string;
29
35
  }
30
36
  export interface EnsureSchemaReport {
31
37
  /** Tabelas criadas do zero nesta execução. */
32
38
  created: string[];
33
39
  /** Colunas adicionadas em tabelas já existentes: tabela → colunas. */
34
40
  altered: Record<string, string[]>;
41
+ /**
42
+ * Desfecho da coluna `login_methods`. `ensured: false` significa que a tabela
43
+ * da conta não existe no banco, então NADA foi feito — e é o caso que
44
+ * silenciava o problema: o host precisa saber que a preferência de método de
45
+ * login não vai funcionar até a tabela existir.
46
+ */
47
+ loginMethods: {
48
+ table: string;
49
+ ensured: boolean;
50
+ };
35
51
  }
36
52
  /**
37
53
  * Garante que as tabelas do authkit existem e têm todas as colunas que esta
@@ -234,7 +234,11 @@ async function columnExists(conn, table, column) {
234
234
  */
235
235
  export async function ensureAuthkitSchema(db, options = {}) {
236
236
  const conn = options.connection ? db.connection(options.connection) : db.connection();
237
- const report = { created: [], altered: {} };
237
+ const report = {
238
+ created: [],
239
+ altered: {},
240
+ loginMethods: { table: options.accountTable ?? 'users', ensured: false },
241
+ };
238
242
  for (const def of TABLES) {
239
243
  if (!(await tableExists(conn, def.name))) {
240
244
  try {
@@ -260,29 +264,48 @@ export async function ensureAuthkitSchema(db, options = {}) {
260
264
  report.altered[def.name].push(column);
261
265
  }
262
266
  }
263
- // Coluna `login_methods` em `auth.users` — a tabela é HOST-owned (não está em
264
- // TABLES porque o nome/shape são decisão do app), mas a LI B consome a coluna
265
- // (LoginMethodsPreferenceCapability). Alguns deploys de host só migram o schema
266
- // do domínio, não o schema `auth` — então a migration manual da coluna pode
267
- // nunca rodar, e isso derrubava TODO login/callback OIDC com "column users.login_methods
268
- // does not exist" no instante em que o model passou a declará-la.
269
- // Aqui garantimos: SE `auth.users` existe E a coluna falta, adicionamos. Nunca
270
- // criamos a tabela (host-owned). Aditivo + idempotente — roda em todo boot.
271
- try {
272
- if (await tableExists(conn, 'users')) {
273
- if (!(await columnExists(conn, 'users', 'login_methods'))) {
274
- await conn.schema.alterTable('users', (t) => {
267
+ // Coluna `login_methods` na tabela da CONTA — a tabela é HOST-owned (não está
268
+ // em TABLES porque o nome/shape são decisão do app), mas a LIB consome a
269
+ // coluna (LoginMethodsPreferenceCapability). Alguns deploys de host só migram
270
+ // o schema do domínio, não o schema de auth — então a migration manual da
271
+ // coluna pode nunca rodar, e isso derrubava TODO login/callback OIDC com
272
+ // "column ...login_methods does not exist" no instante em que o model passou a
273
+ // declará-la.
274
+ //
275
+ // Aqui garantimos: SE a tabela da conta existe E a coluna falta, adicionamos.
276
+ // Nunca criamos a tabela (host-owned). Aditivo + idempotente — roda em todo boot.
277
+ //
278
+ // O nome da tabela vem do store (`accountStore.accountTable`), NÃO de um
279
+ // `users` fixo: com a conta em `auth_users` (o nome que o próprio scaffold da
280
+ // lib usa), o `users` fixo acertava uma tabela qualquer de mesmo nome e a
281
+ // coluna ia parar no lugar errado sem erro nenhum.
282
+ const accountTable = options.accountTable ?? 'users';
283
+ if (await tableExists(conn, accountTable)) {
284
+ try {
285
+ if (!(await columnExists(conn, accountTable, 'login_methods'))) {
286
+ await conn.schema.alterTable(accountTable, (t) => {
275
287
  t.jsonb('login_methods').nullable();
276
288
  });
277
- report.altered.users = ['login_methods'];
289
+ report.altered[accountTable] = ['login_methods'];
278
290
  }
291
+ report.loginMethods.ensured = true;
279
292
  }
280
- }
281
- catch (error) {
282
- // Tabela host-owned pode não existir num banco que nunca criou users — não
283
- // é erro: fail-soft (a migração do host cuida). Nunca criar a tabela aqui.
284
- if (!(await tableExists(conn, 'users'))) {
285
- throw error; // alguém criou entre o probe e o ALTER — repropaga se sumiu
293
+ catch (error) {
294
+ /**
295
+ * Corrida entre instâncias subindo juntas: se a coluna já existe agora,
296
+ * outra ganhou o ALTER — segue o jogo. Qualquer outra falha PROPAGA.
297
+ *
298
+ * O re-probe tem de ser da COLUNA, não da tabela: checar a tabela fazia um
299
+ * ALTER que falhou virar `ensured: true`, ou seja, o report dizia sucesso e
300
+ * o boot não avisava nada — o silêncio que este bloco existe para acabar.
301
+ * (Mesma semântica do probe de `createTable` logo acima.)
302
+ */
303
+ if (await columnExists(conn, accountTable, 'login_methods')) {
304
+ report.loginMethods.ensured = true;
305
+ }
306
+ else {
307
+ throw error;
308
+ }
286
309
  }
287
310
  }
288
311
  return report;
@@ -3,7 +3,7 @@
3
3
  }}}
4
4
  import env from '#start/env'
5
5
  import AuthUser from '#models/auth_user'
6
- import { defineConfig, adapters, inertiaRenderer } from '@adonis-agora/authkit-server'
6
+ import { defineConfig, adapters, lucidAccountStore, inertiaRenderer } from '@adonis-agora/authkit-server'
7
7
 
8
8
  const authServerConfig = defineConfig({
9
9
  issuer: env.get('AUTHKIT_ISSUER'),
@@ -14,18 +14,9 @@ const authServerConfig = defineConfig({
14
14
  ],
15
15
  ttl: { accessToken: '15m', refreshToken: '30d' },
16
16
  globalRolesClaim: 'roles',
17
- findAccount: async (sub) => {
18
- const user = await AuthUser.find(sub)
19
- if (!user) return null
20
- return { id: user.id, email: user.email, globalRoles: user.globalRoles }
21
- },
22
- verifyCredentials: async (email, password) => {
23
- const user = await AuthUser.query().where('email', email).first()
24
- if (!user || !(await user.verifyPassword(password))) return null
25
- return { id: user.id }
26
- },
17
+ accountStore: lucidAccountStore(AuthUser),
27
18
  /**
28
- * Renderer Inertia/React: apenas as views listadas em `views` vão ao Inertia;
19
+ * Renderer Inertia/React: apenas as views listadas em "views" vão ao Inertia;
29
20
  * as demais (incluindo todas as telas admin/*) recaem silenciosamente no
30
21
  * renderer Edge built-in da lib — evitando SSR crash por página inexistente.
31
22
  *
@@ -11,10 +11,10 @@ export default class extends BaseSchema {
11
11
  async up() {
12
12
  this.schema.createTable(this.tableName, (table) => {
13
13
  /**
14
- * String, NÃO increments(): o hook `@beforeCreate` do model scaffoldado
15
- * (`models/auth_user.stub`) atribui um `randomUUID()` antes do insert.
14
+ * String, NÃO increments(): o hook @beforeCreate do model scaffoldado
15
+ * (models/auth_user.stub) atribui um randomUUID() antes do insert.
16
16
  * Uma coluna auto-increment aceitaria o INSERT mesmo assim, mas o valor
17
- * do UUID seria descartado e o `id` do model voltaria como o rowid
17
+ * do UUID seria descartado e o id do model voltaria como o rowid
18
18
  * interno do banco — a conta ficaria inalcançável pelo id real na
19
19
  * próxima request. Ver packages/authkit-server/stubs/models/auth_user.stub.
20
20
  */
@@ -34,7 +34,7 @@ export default class extends BaseSchema {
34
34
 
35
35
  // Coluna própria do model scaffoldado (não vem de nenhum mixin): a tela
36
36
  // de signup embutida sempre coleta "Nome" e o Lucid store passa esse
37
- // valor direto para `AuthUser.create()` — sem esta coluna o primeiro
37
+ // valor direto para AuthUser.create() — sem esta coluna o primeiro
38
38
  // signup quebra com "Cannot define 'fullName' on 'AuthUser' model,
39
39
  // since it is not defined as a model property".
40
40
  table.string('full_name').nullable()
@@ -20,7 +20,7 @@ import { withAuthUser, withCredentials } from '@adonis-agora/authkit-server'
20
20
  export default class AuthUser extends compose(BaseModel, withAuthUser(), withCredentials()) {
21
21
  /**
22
22
  * Sem isto, o Lucid ignora o id atribuído pelo hook abaixo e sobrescreve
23
- * `id` com o retorno bruto do INSERT (o rowid interno do SQLite/Postgres)
23
+ * id com o retorno bruto do INSERT (o rowid interno do SQLite/Postgres)
24
24
  * assim que a linha é salva — mesmo resultado prático de não ter o hook: a
25
25
  * conta some do próprio id que acabou de criar.
26
26
  */
@@ -31,7 +31,7 @@ export default class AuthUser extends compose(BaseModel, withAuthUser(), withCre
31
31
 
32
32
  /**
33
33
  * Nem withAuthUser() nem withCredentials() geram o id — sem este hook o
34
- * Lucid insere NULL nesta coluna string e o `id` do model volta como o
34
+ * Lucid insere NULL nesta coluna string e o id do model volta como o
35
35
  * rowid interno do banco, tornando a conta inalcançável pelo id real na
36
36
  * próxima request.
37
37
  */
@@ -42,7 +42,7 @@ export default class AuthUser extends compose(BaseModel, withAuthUser(), withCre
42
42
 
43
43
  /**
44
44
  * A tela de signup embutida sempre coleta um campo "Nome", e o Lucid store
45
- * passa esse valor direto para `AuthUser.create()` — sem esta coluna o
45
+ * passa esse valor direto para AuthUser.create() — sem esta coluna o
46
46
  * primeiro signup quebra com "Cannot define 'fullName' on 'AuthUser'
47
47
  * model, since it is not defined as a model property".
48
48
  */
package/package.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "@adonis-agora/authkit-server",
3
- "version": "0.66.2",
3
+ "version": "0.67.0",
4
4
  "description": "AdonisJS OIDC/OAuth2 provider (Identity Provider) toolkit: ejectable auth server with sessions, rate-limiting, MFA/TOTP, audit log, federated logout and OpenTelemetry metrics.",
5
5
  "license": "MIT",
6
6
  "author": "dudousxd",
7
- "homepage": "https://github.com/DavideCarvalho/adonis-agora-authkit/tree/main/packages/authkit-server#readme",
8
7
  "repository": {
9
8
  "type": "git",
10
9
  "url": "https://github.com/DavideCarvalho/adonis-agora-authkit.git",
11
10
  "directory": "packages/authkit-server"
12
11
  },
12
+ "homepage": "https://davidecarvalho.github.io/agora/docs/authkit",
13
13
  "bugs": {
14
14
  "url": "https://github.com/DavideCarvalho/adonis-agora-authkit/issues"
15
15
  },
@@ -104,7 +104,7 @@
104
104
  "dependencies": {
105
105
  "@adonis-agora/authkit-core": "0.8.0",
106
106
  "@simplewebauthn/browser": "14.0.0",
107
- "@simplewebauthn/server": "14.0.1",
107
+ "@simplewebauthn/server": "14.0.2",
108
108
  "jose": "6.2.12",
109
109
  "koa": "3.2.1",
110
110
  "koa-mount": "4.2.0",
@@ -114,8 +114,8 @@
114
114
  },
115
115
  "devDependencies": {
116
116
  "@adonis-agora/authkit-react": "0.21.0",
117
- "@adonis-agora/durable": "0.39.1",
118
- "@adonis-agora/telescope": "0.21.0",
117
+ "@adonis-agora/durable": "0.40.0",
118
+ "@adonis-agora/telescope": "0.21.1",
119
119
  "@adonisjs/ally": "6.3.0",
120
120
  "@adonisjs/auth": "10.1.0",
121
121
  "@adonisjs/core": "7.5.0",
@@ -153,6 +153,7 @@
153
153
  "react-dom": "19.2.8",
154
154
  "react-error-boundary": "6.1.5",
155
155
  "recharts": "3.10.1",
156
+ "tempura": "^0.4.1",
156
157
  "typescript": "7.0.2",
157
158
  "vite": "8.2.2"
158
159
  },
@@ -6,7 +6,7 @@ description: >-
6
6
  routes every IdP app must register (show/login/consent on AuthInteractionController),
7
7
  `node ace configure --ui=edge|react|headless` presets, the shell-controller +
8
8
  service.interactions split (details(ctx), login(ctx,{email,password}), consent(ctx)),
9
- overriding verifyCredentials in config/authkit.ts, renderers edgeRenderer/inertiaRenderer,
9
+ overriding accountStore.verifyCredentials in config/authkit.ts, renderers edgeRenderer/inertiaRenderer,
10
10
  and end-to-end testing with @adonis-agora/authkit-testing (createTestIdentity,
11
11
  mintTestIdToken, serveJwks, fakeAuthenticator). Use when the authorization flow 404s at
12
12
  the login screen, wiring custom login UI, plugging an external user base, or testing
@@ -81,24 +81,30 @@ the shell only translates between HTTP and those calls.
81
81
  Source: `packages/authkit-server/README.md` § UI de login/consent ("o controller
82
82
  ejetado é casca: a lógica vive em `service.interactions`").
83
83
 
84
- ### Pattern 2 — plug your user base via `verifyCredentials`
84
+ ### Pattern 2 — plug your user base via `accountStore.verifyCredentials`
85
85
 
86
- `verifyCredentials` in `config/authkit.ts` decides whether credentials are valid;
87
- `service.interactions.login` calls it. The default queries the `AuthUser` model by
88
- email and uses `verifyPassword` — override to authenticate against anything else:
86
+ `verifyCredentials` on the configured `accountStore` decides whether credentials
87
+ are valid; `service.interactions.login` calls it. The default
88
+ `lucidAccountStore(AuthUser)` queries the `AuthUser` model by email and uses the
89
+ `withCredentials` mixin — override the store method to authenticate against
90
+ anything else (`findAccount`/`verifyCredentials` are NOT top-level `defineConfig`
91
+ keys; `AuthServerConfigInput` accepts only `accountStore`, from which the lib
92
+ derives both):
89
93
 
90
94
  ```ts
91
95
  // config/authkit.ts
92
96
  defineConfig({
93
97
  issuer: env.get('AUTHKIT_ISSUER'),
94
98
  adapter: adapters.redis({ connection: 'main' }),
95
- accountStore: lucidAccountStore(AuthUser),
96
- verifyCredentials: async (email, password) => {
97
- // Return the account on success; throw/falsy paths fail the login.
98
- const account = await AuthUser.query().where('email', email).first()
99
- if (!account) throw new Error('Invalid credentials')
100
- await verifyPassword(account.passwordHash, password)
101
- return account
99
+ accountStore: {
100
+ ...lucidAccountStore(AuthUser),
101
+ verifyCredentials: async (email, password) => {
102
+ // Return the account on success; throw/falsy paths fail the login.
103
+ const account = await AuthUser.query().where('email', email).first()
104
+ if (!account) throw new Error('Invalid credentials')
105
+ await verifyPassword(account.passwordHash, password)
106
+ return account
107
+ },
102
108
  },
103
109
  })
104
110
  ```
@@ -3,7 +3,7 @@
3
3
  }}}
4
4
  import env from '#start/env'
5
5
  import AuthUser from '#models/auth_user'
6
- import { defineConfig, adapters, inertiaRenderer } from '@adonis-agora/authkit-server'
6
+ import { defineConfig, adapters, lucidAccountStore, inertiaRenderer } from '@adonis-agora/authkit-server'
7
7
 
8
8
  const authServerConfig = defineConfig({
9
9
  issuer: env.get('AUTHKIT_ISSUER'),
@@ -14,18 +14,9 @@ const authServerConfig = defineConfig({
14
14
  ],
15
15
  ttl: { accessToken: '15m', refreshToken: '30d' },
16
16
  globalRolesClaim: 'roles',
17
- findAccount: async (sub) => {
18
- const user = await AuthUser.find(sub)
19
- if (!user) return null
20
- return { id: user.id, email: user.email, globalRoles: user.globalRoles }
21
- },
22
- verifyCredentials: async (email, password) => {
23
- const user = await AuthUser.query().where('email', email).first()
24
- if (!user || !(await user.verifyPassword(password))) return null
25
- return { id: user.id }
26
- },
17
+ accountStore: lucidAccountStore(AuthUser),
27
18
  /**
28
- * Renderer Inertia/React: apenas as views listadas em `views` vão ao Inertia;
19
+ * Renderer Inertia/React: apenas as views listadas em "views" vão ao Inertia;
29
20
  * as demais (incluindo todas as telas admin/*) recaem silenciosamente no
30
21
  * renderer Edge built-in da lib — evitando SSR crash por página inexistente.
31
22
  *
@@ -11,10 +11,10 @@ export default class extends BaseSchema {
11
11
  async up() {
12
12
  this.schema.createTable(this.tableName, (table) => {
13
13
  /**
14
- * String, NÃO increments(): o hook `@beforeCreate` do model scaffoldado
15
- * (`models/auth_user.stub`) atribui um `randomUUID()` antes do insert.
14
+ * String, NÃO increments(): o hook @beforeCreate do model scaffoldado
15
+ * (models/auth_user.stub) atribui um randomUUID() antes do insert.
16
16
  * Uma coluna auto-increment aceitaria o INSERT mesmo assim, mas o valor
17
- * do UUID seria descartado e o `id` do model voltaria como o rowid
17
+ * do UUID seria descartado e o id do model voltaria como o rowid
18
18
  * interno do banco — a conta ficaria inalcançável pelo id real na
19
19
  * próxima request. Ver packages/authkit-server/stubs/models/auth_user.stub.
20
20
  */
@@ -34,7 +34,7 @@ export default class extends BaseSchema {
34
34
 
35
35
  // Coluna própria do model scaffoldado (não vem de nenhum mixin): a tela
36
36
  // de signup embutida sempre coleta "Nome" e o Lucid store passa esse
37
- // valor direto para `AuthUser.create()` — sem esta coluna o primeiro
37
+ // valor direto para AuthUser.create() — sem esta coluna o primeiro
38
38
  // signup quebra com "Cannot define 'fullName' on 'AuthUser' model,
39
39
  // since it is not defined as a model property".
40
40
  table.string('full_name').nullable()
@@ -20,7 +20,7 @@ import { withAuthUser, withCredentials } from '@adonis-agora/authkit-server'
20
20
  export default class AuthUser extends compose(BaseModel, withAuthUser(), withCredentials()) {
21
21
  /**
22
22
  * Sem isto, o Lucid ignora o id atribuído pelo hook abaixo e sobrescreve
23
- * `id` com o retorno bruto do INSERT (o rowid interno do SQLite/Postgres)
23
+ * id com o retorno bruto do INSERT (o rowid interno do SQLite/Postgres)
24
24
  * assim que a linha é salva — mesmo resultado prático de não ter o hook: a
25
25
  * conta some do próprio id que acabou de criar.
26
26
  */
@@ -31,7 +31,7 @@ export default class AuthUser extends compose(BaseModel, withAuthUser(), withCre
31
31
 
32
32
  /**
33
33
  * Nem withAuthUser() nem withCredentials() geram o id — sem este hook o
34
- * Lucid insere NULL nesta coluna string e o `id` do model volta como o
34
+ * Lucid insere NULL nesta coluna string e o id do model volta como o
35
35
  * rowid interno do banco, tornando a conta inalcançável pelo id real na
36
36
  * próxima request.
37
37
  */
@@ -42,7 +42,7 @@ export default class AuthUser extends compose(BaseModel, withAuthUser(), withCre
42
42
 
43
43
  /**
44
44
  * A tela de signup embutida sempre coleta um campo "Nome", e o Lucid store
45
- * passa esse valor direto para `AuthUser.create()` — sem esta coluna o
45
+ * passa esse valor direto para AuthUser.create() — sem esta coluna o
46
46
  * primeiro signup quebra com "Cannot define 'fullName' on 'AuthUser'
47
47
  * model, since it is not defined as a model property".
48
48
  */