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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +1 -1
  3. package/build/index.d.ts +2 -1
  4. package/build/index.js +1 -0
  5. package/build/providers/authkit_server_provider.js +15 -4
  6. package/build/src/accounts/lucid_account_store.js +4 -3
  7. package/build/src/accounts/lucid_store/mfa.js +1 -1
  8. package/build/src/accounts/lucid_store/organization_models.d.ts +9 -5
  9. package/build/src/accounts/lucid_store/organization_models.js +16 -12
  10. package/build/src/accounts/lucid_store/password_hygiene.js +4 -2
  11. package/build/src/accounts/lucid_store/shared.d.ts +6 -1
  12. package/build/src/accounts/lucid_store/shared.js +7 -6
  13. package/build/src/accounts/lucid_store/webauthn.js +1 -1
  14. package/build/src/adapters/database_adapter.d.ts +7 -1
  15. package/build/src/adapters/database_adapter.js +24 -13
  16. package/build/src/adapters/factory.d.ts +7 -0
  17. package/build/src/adapters/factory.js +6 -2
  18. package/build/src/agents/delegation_store.d.ts +5 -1
  19. package/build/src/agents/delegation_store.js +34 -21
  20. package/build/src/agents/extension.d.ts +16 -2
  21. package/build/src/agents/registry_store.d.ts +1 -0
  22. package/build/src/agents/registry_store.js +10 -8
  23. package/build/src/agents/runtime.js +6 -2
  24. package/build/src/commands/expire_scan_command.js +2 -3
  25. package/build/src/commands/settings_commands.js +4 -4
  26. package/build/src/define_config.d.ts +8 -0
  27. package/build/src/define_config.js +23 -6
  28. package/build/src/doctor/agora_checks.js +13 -0
  29. package/build/src/doctor/db_schema_check.d.ts +6 -0
  30. package/build/src/doctor/db_schema_check.js +87 -0
  31. package/build/src/host/admin_sessions_service.js +6 -2
  32. package/build/src/host/runtime_settings.d.ts +15 -0
  33. package/build/src/host/runtime_settings.js +27 -12
  34. package/build/src/keys/keystore_manager.js +2 -1
  35. package/build/src/keys/keystore_vault.d.ts +9 -1
  36. package/build/src/keys/keystore_vault.js +27 -9
  37. package/build/src/schema/ensure.d.ts +9 -0
  38. package/build/src/schema/ensure.js +46 -19
  39. package/build/src/schema/tables_location.d.ts +31 -2
  40. package/build/src/schema/tables_location.js +64 -0
  41. package/package.json +3 -3
  42. package/skills/authkit-idp-setup/SKILL.md +13 -7
@@ -63,13 +63,17 @@ function affected(result) {
63
63
  }
64
64
  export class DelegationStore {
65
65
  conn;
66
+ t;
66
67
  /** `conn` = uma conexão Lucid (`db.connection(name?)`) — ou uma transação. */
67
- constructor(conn) {
68
+ constructor(conn,
69
+ /** Nome da tabela como os statements o escrevem (`schema.tabela` com `schema.name`). */
70
+ t = (name) => name) {
68
71
  this.conn = conn;
72
+ this.t = t;
69
73
  }
70
74
  // ─── device codes ─────────────────────────────────────────────────────────
71
75
  async insertDevice(row) {
72
- await this.conn().table(DEVICE_TABLE).insert({
76
+ await this.conn().table(this.t(DEVICE_TABLE)).insert({
73
77
  id: row.id,
74
78
  device_code_hash: row.deviceCodeHash,
75
79
  user_code: row.userCode,
@@ -83,19 +87,22 @@ export class DelegationStore {
83
87
  });
84
88
  }
85
89
  async findDeviceByCodeHash(hash) {
86
- const row = await this.conn().from(DEVICE_TABLE).where('device_code_hash', hash).first();
90
+ const row = await this.conn()
91
+ .from(this.t(DEVICE_TABLE))
92
+ .where('device_code_hash', hash)
93
+ .first();
87
94
  return row ? toDevice(row) : null;
88
95
  }
89
96
  async findDeviceByUserCode(userCode) {
90
- const row = await this.conn().from(DEVICE_TABLE).where('user_code', userCode).first();
97
+ const row = await this.conn().from(this.t(DEVICE_TABLE)).where('user_code', userCode).first();
91
98
  return row ? toDevice(row) : null;
92
99
  }
93
100
  /** Liga o pedido aprovado ao grant — a partir daqui o polling do agente recebe o token. */
94
101
  async setDeviceGrant(id, grantId) {
95
- await this.conn().from(DEVICE_TABLE).where('id', id).update({ grant_id: grantId });
102
+ await this.conn().from(this.t(DEVICE_TABLE)).where('id', id).update({ grant_id: grantId });
96
103
  }
97
104
  async markPolled(id, at) {
98
- await this.conn().from(DEVICE_TABLE).where('id', id).update({ last_polled_at: at });
105
+ await this.conn().from(this.t(DEVICE_TABLE)).where('id', id).update({ last_polled_at: at });
99
106
  }
100
107
  /**
101
108
  * Transição de estado ATÔMICA: só aplica se o pedido ainda está em `from`.
@@ -108,7 +115,7 @@ export class DelegationStore {
108
115
  if (patch.grantId !== undefined)
109
116
  update.grant_id = patch.grantId;
110
117
  const result = await this.conn()
111
- .from(DEVICE_TABLE)
118
+ .from(this.t(DEVICE_TABLE))
112
119
  .where('id', id)
113
120
  .where('status', from)
114
121
  .update(update);
@@ -119,11 +126,11 @@ export class DelegationStore {
119
126
  * vai pelo mesmo binding do INSERT, então a comparação é coerente no dialeto.
120
127
  */
121
128
  async deleteDevicesExpiredBefore(before) {
122
- await this.conn().from(DEVICE_TABLE).where('expires_at', '<', before).delete();
129
+ await this.conn().from(this.t(DEVICE_TABLE)).where('expires_at', '<', before).delete();
123
130
  }
124
131
  // ─── grants ───────────────────────────────────────────────────────────────
125
132
  async insertGrant(row) {
126
- await this.conn().table(GRANT_TABLE).insert({
133
+ await this.conn().table(this.t(GRANT_TABLE)).insert({
127
134
  id: row.id,
128
135
  account_id: row.accountId,
129
136
  client_id: row.clientId,
@@ -135,13 +142,13 @@ export class DelegationStore {
135
142
  });
136
143
  }
137
144
  async findGrant(id) {
138
- const row = await this.conn().from(GRANT_TABLE).where('id', id).first();
145
+ const row = await this.conn().from(this.t(GRANT_TABLE)).where('id', id).first();
139
146
  return row ? toGrant(row) : null;
140
147
  }
141
148
  /** Grants NÃO revogados da tripla (conta, agente, usuário do agente). */
142
149
  async findGrantsFor(accountId, clientId, agentSub) {
143
150
  const rows = await this.conn()
144
- .from(GRANT_TABLE)
151
+ .from(this.t(GRANT_TABLE))
145
152
  .where('account_id', accountId)
146
153
  .where('client_id', clientId)
147
154
  .where('agent_sub', agentSub)
@@ -151,7 +158,7 @@ export class DelegationStore {
151
158
  /** Atualiza um grant NÃO revogado. `false` = revogado (ou sumiu) nesse meio-tempo. */
152
159
  async updateGrant(id, patch) {
153
160
  const result = await this.conn()
154
- .from(GRANT_TABLE)
161
+ .from(this.t(GRANT_TABLE))
155
162
  .where('id', id)
156
163
  .whereNull('revoked_at')
157
164
  .update({ scope: patch.scope, expires_at: patch.expiresAt, updated_at: patch.updatedAt });
@@ -159,7 +166,7 @@ export class DelegationStore {
159
166
  }
160
167
  async listGrants(accountId) {
161
168
  const rows = await this.conn()
162
- .from(GRANT_TABLE)
169
+ .from(this.t(GRANT_TABLE))
163
170
  .where('account_id', accountId)
164
171
  .whereNull('revoked_at')
165
172
  .orderBy('created_at', 'desc');
@@ -168,7 +175,7 @@ export class DelegationStore {
168
175
  /** Revoga um grant DA CONTA. `false` = não existe, é de outra conta ou já revogado. */
169
176
  async revokeGrant(accountId, id, at) {
170
177
  const result = await this.conn()
171
- .from(GRANT_TABLE)
178
+ .from(this.t(GRANT_TABLE))
172
179
  .where('id', id)
173
180
  .where('account_id', accountId)
174
181
  .whereNull('revoked_at')
@@ -177,7 +184,7 @@ export class DelegationStore {
177
184
  }
178
185
  async revokeAllGrants(accountId, at) {
179
186
  const result = await this.conn()
180
- .from(GRANT_TABLE)
187
+ .from(this.t(GRANT_TABLE))
181
188
  .where('account_id', accountId)
182
189
  .whereNull('revoked_at')
183
190
  .update({ revoked_at: at, updated_at: at });
@@ -189,7 +196,10 @@ export class DelegationStore {
189
196
  * protocolo sai do prefixo do id: `agrant_` (PACT) ou o `grantIdPrefix` de uma extension.
190
197
  */
191
198
  #live(now, filter = {}) {
192
- let q = this.conn().from(GRANT_TABLE).whereNull('revoked_at').where('expires_at', '>', now);
199
+ let q = this.conn()
200
+ .from(this.t(GRANT_TABLE))
201
+ .whereNull('revoked_at')
202
+ .where('expires_at', '>', now);
193
203
  // O prefixo é `[a-z0-9]+_`: o `_` do LIKE (um caractere qualquer) casa com ele mesmo.
194
204
  if (filter.idPrefix)
195
205
  q = q.where('id', 'like', `${filter.idPrefix}%`);
@@ -222,7 +232,7 @@ export class DelegationStore {
222
232
  /** Ids dos grants não revogados de um agente (vivos ou não) — para revogar todos. */
223
233
  async unrevokedGrantIdsOfClient(clientId) {
224
234
  const rows = await this.conn()
225
- .from(GRANT_TABLE)
235
+ .from(this.t(GRANT_TABLE))
226
236
  .where('client_id', clientId)
227
237
  .whereNull('revoked_at')
228
238
  .select('id');
@@ -231,14 +241,14 @@ export class DelegationStore {
231
241
  /** Revoga um grant sem checar a conta — reação a reuso de refresh token. */
232
242
  async revokeGrantById(id, at) {
233
243
  await this.conn()
234
- .from(GRANT_TABLE)
244
+ .from(this.t(GRANT_TABLE))
235
245
  .where('id', id)
236
246
  .whereNull('revoked_at')
237
247
  .update({ revoked_at: at, updated_at: at });
238
248
  }
239
249
  // ─── refresh tokens ───────────────────────────────────────────────────────
240
250
  async insertRefreshToken(row) {
241
- await this.conn().table(REFRESH_TABLE).insert({
251
+ await this.conn().table(this.t(REFRESH_TABLE)).insert({
242
252
  token_hash: row.tokenHash,
243
253
  grant_id: row.grantId,
244
254
  expires_at: row.expiresAt,
@@ -246,7 +256,10 @@ export class DelegationStore {
246
256
  });
247
257
  }
248
258
  async findRefreshToken(tokenHash) {
249
- const row = await this.conn().from(REFRESH_TABLE).where('token_hash', tokenHash).first();
259
+ const row = await this.conn()
260
+ .from(this.t(REFRESH_TABLE))
261
+ .where('token_hash', tokenHash)
262
+ .first();
250
263
  return row
251
264
  ? {
252
265
  grantId: row.grant_id,
@@ -258,7 +271,7 @@ export class DelegationStore {
258
271
  /** Marca o refresh como usado (uso único, atômico). `false` = já tinha sido usado. */
259
272
  async useRefreshToken(tokenHash, at) {
260
273
  const result = await this.conn()
261
- .from(REFRESH_TABLE)
274
+ .from(this.t(REFRESH_TABLE))
262
275
  .where('token_hash', tokenHash)
263
276
  .whereNull('used_at')
264
277
  .update({ used_at: at });
@@ -27,10 +27,18 @@ import type { HttpContext } from '@adonisjs/core/http';
27
27
  * A lib-owned table: created when missing, new columns added with `ALTER ADD`, never dropped or
28
28
  * altered. The builders receive a Knex `TableBuilder`.
29
29
  */
30
+ /** What {@link AuthkitTable.create} receives next to the table builder. */
31
+ export interface AuthkitTableContext {
32
+ /** `name` as AuthKit's tables are addressed: `schema.name` + `.` + `name` when a schema is set. */
33
+ table(name: string): string;
34
+ }
30
35
  export interface AuthkitTable {
31
36
  name: string;
32
- /** Full builder — runs when the table does not exist. */
33
- create: (table: any) => void;
37
+ /**
38
+ * Full builder — runs when the table does not exist. A foreign key into another lib-owned
39
+ * table names it through `context.table(name)` (qualified with `schema.name` when set).
40
+ */
41
+ create: (table: any, context: AuthkitTableContext) => void;
34
42
  /**
35
43
  * One builder per column — runs (`ALTER ADD`) when the table exists but the column does not.
36
44
  * No constraints that break on populated tables (unique/FK belong in `create`).
@@ -125,6 +133,12 @@ export interface PersonalAgentHost {
125
133
  readonly issuer: string;
126
134
  /** The Lucid connection AuthKit's tables live on (`schema.connection`). */
127
135
  db(): any;
136
+ /**
137
+ * `name` as every statement must write it: `schema.name` + `.` + `name` when the app puts
138
+ * AuthKit's tables in a database schema (knex quotes each part), else `name`. Name every
139
+ * lib-owned table through it — the shared ledger tables and the extension's own.
140
+ */
141
+ table(name: string): string;
128
142
  readonly grants: PersonalAgentGrantLedger;
129
143
  /** The admin console's verdict on an agent of this protocol. `null` = not listed. */
130
144
  registryStatus(identifier: string): Promise<'allowed' | 'blocked' | null>;
@@ -22,6 +22,7 @@ export declare class AgentRegistryStore {
22
22
  #private;
23
23
  constructor(conn: () => any, options?: {
24
24
  now?: () => number;
25
+ table?: (name: string) => string;
25
26
  });
26
27
  /** Todas as linhas, mais recentes primeiro. Lê do banco (console). */
27
28
  list(): Promise<AgentRegistryRow[]>;
@@ -47,13 +47,15 @@ export class AgentRegistryStore {
47
47
  #now;
48
48
  #snapshot = null;
49
49
  #loading = null;
50
+ #table;
50
51
  constructor(conn, options = {}) {
51
52
  this.#conn = conn;
53
+ this.#table = (options.table ?? ((name) => name))(REGISTRY_TABLE);
52
54
  this.#now = options.now ?? Date.now;
53
55
  }
54
56
  /** Todas as linhas, mais recentes primeiro. Lê do banco (console). */
55
57
  async list() {
56
- const rows = await this.#conn().from(REGISTRY_TABLE).orderBy('updated_at', 'desc');
58
+ const rows = await this.#conn().from(this.#table).orderBy('updated_at', 'desc');
57
59
  return rows.map(toRow);
58
60
  }
59
61
  /** A linha do agente, pelo snapshot. `null` = o console não diz nada sobre ele. */
@@ -61,14 +63,14 @@ export class AgentRegistryStore {
61
63
  return (await this.#rows()).get(key(protocol, identifier)) ?? null;
62
64
  }
63
65
  async findById(id) {
64
- const row = await this.#conn().from(REGISTRY_TABLE).where('id', id).first();
66
+ const row = await this.#conn().from(this.#table).where('id', id).first();
65
67
  return row ? toRow(row) : null;
66
68
  }
67
69
  /** Cria ou troca a linha de (protocolo, identificador). */
68
70
  async upsert(input) {
69
71
  const now = new Date(this.#now());
70
72
  const existing = await this.#conn()
71
- .from(REGISTRY_TABLE)
73
+ .from(this.#table)
72
74
  .where('protocol', input.protocol)
73
75
  .where('identifier', input.identifier)
74
76
  .first();
@@ -80,11 +82,11 @@ export class AgentRegistryStore {
80
82
  updated_at: now,
81
83
  };
82
84
  if (existing) {
83
- await this.#conn().from(REGISTRY_TABLE).where('id', existing.id).update(values);
85
+ await this.#conn().from(this.#table).where('id', existing.id).update(values);
84
86
  }
85
87
  else {
86
88
  await this.#conn()
87
- .table(REGISTRY_TABLE)
89
+ .table(this.#table)
88
90
  .insert({
89
91
  id: `areg_${randomBytes(12).toString('base64url')}`,
90
92
  protocol: input.protocol,
@@ -95,14 +97,14 @@ export class AgentRegistryStore {
95
97
  }
96
98
  this.invalidate();
97
99
  const row = await this.#conn()
98
- .from(REGISTRY_TABLE)
100
+ .from(this.#table)
99
101
  .where('protocol', input.protocol)
100
102
  .where('identifier', input.identifier)
101
103
  .first();
102
104
  return toRow(row);
103
105
  }
104
106
  async delete(id) {
105
- const result = await this.#conn().from(REGISTRY_TABLE).where('id', id).delete();
107
+ const result = await this.#conn().from(this.#table).where('id', id).delete();
106
108
  this.invalidate();
107
109
  return (typeof result === 'number' ? result : Array.isArray(result) ? result.length : 0) > 0;
108
110
  }
@@ -116,7 +118,7 @@ export class AgentRegistryStore {
116
118
  if (!this.#loading) {
117
119
  this.#loading = (async () => {
118
120
  try {
119
- const rows = (await this.#conn().from(REGISTRY_TABLE)).map(toRow);
121
+ const rows = (await this.#conn().from(this.#table)).map(toRow);
120
122
  const map = new Map(rows.map((r) => [key(r.protocol, r.identifier), r]));
121
123
  this.#snapshot = { rows: map, expiresAt: this.#now() + SNAPSHOT_TTL_MS };
122
124
  return map;
@@ -1,6 +1,7 @@
1
1
  import { getBootedApp } from '../../services/booted_app.js';
2
2
  import { supportsAccountStatus } from '../accounts/account_store.js';
3
3
  import { findMcpResource } from '../mcp/mcp_oauth.js';
4
+ import { authkitTable } from '../schema/tables_location.js';
4
5
  import { PersonalAgentVerifier } from './agent_identity.js';
5
6
  import { PersonalAgentDelegation } from './delegation_service.js';
6
7
  import { DelegationStore } from './delegation_store.js';
@@ -156,8 +157,10 @@ export function buildPersonalAgentsRuntime(service, makeDb, options = {}) {
156
157
  conn = null;
157
158
  }
158
159
  }
159
- const store = conn ? new DelegationStore(conn) : null;
160
- const registry = conn ? new AgentRegistryStore(conn) : null;
160
+ // Com `schema.name`, cada statement escreve `schema.tabela`.
161
+ const tableName = (name) => authkitTable({ schema: service.config.schema.name }, name);
162
+ const store = conn ? new DelegationStore(conn, tableName) : null;
163
+ const registry = conn ? new AgentRegistryStore(conn, { table: tableName }) : null;
161
164
  const effective = registry ? withRegistry(config, registry) : config;
162
165
  let delegation = null;
163
166
  if (config.delegation && store) {
@@ -179,6 +182,7 @@ export function buildPersonalAgentsRuntime(service, makeDb, options = {}) {
179
182
  protocol: extension.id,
180
183
  issuer: service.config.issuer,
181
184
  db: () => db(),
185
+ table: tableName,
182
186
  grants: scopedLedger(store, extension.grantIdPrefix),
183
187
  registryStatus: async (identifier) => (await registry.find(extension.id, identifier))?.status ?? null,
184
188
  deriveSecret: (label) => service.deriveSecret(label),
@@ -27,7 +27,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
27
27
  }
28
28
  return path;
29
29
  };
30
- import { RuntimeSettings } from '../host/runtime_settings.js';
30
+ import { RuntimeSettings, settingsLocation } from '../host/runtime_settings.js';
31
31
  import { resolveEffectiveAccountExpiration } from '../host/runtime_toggles.js';
32
32
  import { ADMIN_LIST_DEFAULT_SIZE, LIST_FIRST_PAGE } from '../pagination.js';
33
33
  import { resolveAuthkitConfig } from './resolve_config.js';
@@ -54,8 +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 = cfg.schema?.connection;
58
- runtimeSettings = new RuntimeSettings(db, connection ? { connection } : {});
57
+ runtimeSettings = new RuntimeSettings(db, settingsLocation(cfg));
59
58
  }
60
59
  catch {
61
60
  // Sem DB → usa defaults.
@@ -14,7 +14,7 @@
14
14
  *
15
15
  * Todas suportam --json para output machine-readable.
16
16
  */
17
- import { RuntimeSettings } from '../host/runtime_settings.js';
17
+ import { RuntimeSettings, settingsLocation } from '../host/runtime_settings.js';
18
18
  import { SETTING_KEYS } from '../host/runtime_toggles.js';
19
19
  import { resolveAuthkitConfig } from './resolve_config.js';
20
20
  // ---------------------------------------------------------------------------
@@ -132,15 +132,15 @@ 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
- let connection;
135
+ let location = {};
136
136
  try {
137
137
  const cfg = await resolveCfg(app);
138
- connection = cfg?.schema?.connection;
138
+ location = settingsLocation(cfg);
139
139
  }
140
140
  catch {
141
141
  // Silencioso: sem config → default connection.
142
142
  }
143
- return new RuntimeSettings(db, connection ? { connection } : {});
143
+ return new RuntimeSettings(db, location);
144
144
  }
145
145
  catch {
146
146
  return null;
@@ -1194,10 +1194,17 @@ export interface AuthServerConfigInput {
1194
1194
  * primária) — as acima, o `authkit_keystore`, as de extensions e a do
1195
1195
  * `adapters.database()`/keystore `lucid` quando eles não declaram a própria.
1196
1196
  * A tabela da conta NÃO entra: fica na conexão do model da conta.
1197
+ * - `name`: o schema do banco dessas tabelas (MySQL: o database), ex.: `'auth'`
1198
+ * → `"auth"."auth_settings"`. Vai escrito em cada statement; o boot cria o
1199
+ * schema quando falta (com `autoManage`). Default: o schema corrente da
1200
+ * conexão. SQLite não tem schemas: com `name`, o boot falha. NÃO use
1201
+ * `searchPath` na conexão para isso — atrás de um PgBouncer em modo
1202
+ * transaction, o `SET search_path` vaza para as conexões de outros clientes.
1197
1203
  */
1198
1204
  schema?: {
1199
1205
  autoManage?: boolean;
1200
1206
  connection?: string;
1207
+ name?: string;
1201
1208
  };
1202
1209
  /**
1203
1210
  * Ciclo de vida da conta (LGPD/GDPR). OPT-IN para workflows DURÁVEIS:
@@ -1407,6 +1414,7 @@ export interface ResolvedServerConfig {
1407
1414
  schema: {
1408
1415
  autoManage: boolean;
1409
1416
  connection?: string;
1417
+ name?: string;
1410
1418
  };
1411
1419
  /**
1412
1420
  * Ciclo de vida da conta resolvido. `durable` default `false` → caminho
@@ -17,7 +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
+ import { assertSchemaSupported, authkitTablesClient, bindTablesLocation, validateSchemaName, } from './schema/tables_location.js';
21
21
  import { resolveServiceTenants, } from './service_tenants/config.js';
22
22
  export { adapters };
23
23
  const RATE_LIMIT_DEFAULTS = {
@@ -288,9 +288,20 @@ export function defineConfig(config) {
288
288
  // Toda tabela do AuthKit vive em `schema.connection` — inclusive as que o store da
289
289
  // conta lê (auth_mfa, auth_password_history, organizations), que antes seguiam a
290
290
  // conexão do model da conta.
291
- bindTablesLocation(config.accountStore, { connection: config.schema?.connection });
291
+ const tablesSchema = validateSchemaName(config.schema?.name);
292
+ bindTablesLocation(config.accountStore, {
293
+ connection: config.schema?.connection,
294
+ schema: tablesSchema,
295
+ });
292
296
  return configProvider.create(async (app) => {
293
- const tablesContext = { connection: config.schema?.connection };
297
+ const tablesContext = { connection: config.schema?.connection, schema: tablesSchema };
298
+ if (tablesSchema) {
299
+ // Schema num dialeto sem schemas (SQLite) falha aqui, na resolução do config — nunca
300
+ // cai em silêncio para tabelas sem schema.
301
+ const db = await app.container.make('lucid.db').catch(() => null);
302
+ if (db)
303
+ assertSchemaSupported(authkitTablesClient(db, tablesContext), tablesSchema);
304
+ }
294
305
  const AdapterClass = await config.adapter.resolver(app, tablesContext);
295
306
  // `session.adapter` ausente ⇒ mesma classe do default (back-compat: o
296
307
  // dispatcher vira identidade e tudo continua no adapter único de hoje).
@@ -310,12 +321,17 @@ export function defineConfig(config) {
310
321
  store: 'tmp/authkit_jwks.json',
311
322
  }
312
323
  : config.jwks;
313
- // Keystore no Lucid sem conexão própria fica na conexão das tabelas do authkit.
324
+ // Keystore no Lucid sem conexão/schema próprios fica onde estão as tabelas do authkit.
314
325
  const jwksStore = jwksInput.store;
315
- const jwksConfig = jwksStore?.driver === 'lucid' && !jwksStore.connection && config.schema?.connection
326
+ const jwksConfig = jwksStore?.driver === 'lucid' &&
327
+ ((!jwksStore.connection && config.schema?.connection) || (!jwksStore.schema && tablesSchema))
316
328
  ? {
317
329
  ...jwksInput,
318
- store: { ...jwksStore, connection: config.schema.connection },
330
+ store: {
331
+ ...jwksStore,
332
+ connection: jwksStore.connection ?? config.schema?.connection,
333
+ schema: jwksStore.schema ?? tablesSchema,
334
+ },
319
335
  }
320
336
  : jwksInput;
321
337
  if (config.jwks === 'auto' && !process.env.AUTHKIT_JWKS) {
@@ -504,6 +520,7 @@ export function defineConfig(config) {
504
520
  schema: {
505
521
  autoManage: config.schema?.autoManage !== false,
506
522
  connection: config.schema?.connection,
523
+ name: tablesSchema,
507
524
  },
508
525
  // OPT-IN: default false → caminho síncrono de sempre (byte-idêntico).
509
526
  accountLifecycle: {
@@ -134,6 +134,19 @@ export function authkitDoctorChecks() {
134
134
  const { managedSigningKeyAgeFinding } = await import('./doctor_input.js');
135
135
  return toResults(await managedSigningKeyAgeFinding(app, input.authkitConfig));
136
136
  },
137
+ }, {
138
+ id: 'authkit.db-schema',
139
+ lib: 'authkit',
140
+ package: AUTHKIT_DOCTOR_PACKAGE,
141
+ title: 'Database schema',
142
+ cost: 'deep',
143
+ async run(app) {
144
+ const { input, notBooted } = await prepare(app);
145
+ if (notBooted || !input?.authkitConfig?.schema?.name)
146
+ return [];
147
+ const { dbSchemaResults } = await import('./db_schema_check.js');
148
+ return dbSchemaResults(input.authkitConfig, () => app.container.make('lucid.db'), AUTHKIT_DOCTOR_DOCS);
149
+ },
137
150
  }, {
138
151
  id: 'authkit.console-gate',
139
152
  lib: 'authkit',
@@ -0,0 +1,6 @@
1
+ import type { AgoraDoctorResult } from './agora_doctor.js';
2
+ /**
3
+ * `agora:doctor` check for `schema.name`: the schema exists, AuthKit's tables are in it, and the
4
+ * connection can write there. No result at all without `schema.name`.
5
+ */
6
+ export declare function dbSchemaResults(config: Record<string, any> | null, makeDb: () => Promise<any>, docs: string): Promise<AgoraDoctorResult[]>;
@@ -0,0 +1,87 @@
1
+ /** Thrown inside the probe transaction so the probe row is never kept. */
2
+ class ProbeRollback extends Error {
3
+ }
4
+ function rows(result) {
5
+ if (Array.isArray(result))
6
+ return Array.isArray(result[0]) ? result[0] : result;
7
+ return result?.rows ?? [];
8
+ }
9
+ /**
10
+ * `agora:doctor` check for `schema.name`: the schema exists, AuthKit's tables are in it, and the
11
+ * connection can write there. No result at all without `schema.name`.
12
+ */
13
+ export async function dbSchemaResults(config, makeDb, docs) {
14
+ const schema = config?.schema?.name;
15
+ if (!schema)
16
+ return [];
17
+ const [{ AUTHKIT_TABLE_NAMES }, { authkitTable, authkitTablesClient, dialectOf }] = await Promise.all([import('../schema/ensure.js'), import('../schema/tables_location.js')]);
18
+ const client = authkitTablesClient(await makeDb(), { connection: config?.schema?.connection });
19
+ const dialect = dialectOf(client);
20
+ if (dialect !== 'postgres' && dialect !== 'mysql') {
21
+ return [
22
+ {
23
+ status: 'error',
24
+ message: `schema.name "${schema}" is set, but the connection uses ${dialect ?? 'an unknown dialect'}, which has no schemas`,
25
+ fix: 'Remove schema.name from config/authkit.ts, or use Postgres or MySQL',
26
+ docs,
27
+ },
28
+ ];
29
+ }
30
+ const exists = rows(await client.rawQuery('select 1 as ok from information_schema.schemata where schema_name = ?', [
31
+ schema,
32
+ ]));
33
+ if (exists.length === 0) {
34
+ return [
35
+ {
36
+ status: 'error',
37
+ message: `database schema "${schema}" does not exist on this connection`,
38
+ fix: dialect === 'mysql'
39
+ ? `Boot the app once with schema.autoManage on, or run CREATE DATABASE \`${schema}\``
40
+ : `Boot the app once with schema.autoManage on, or run CREATE SCHEMA "${schema}"`,
41
+ docs,
42
+ },
43
+ ];
44
+ }
45
+ const found = new Set(rows(await client.rawQuery('select table_name as name from information_schema.tables where table_schema = ?', [schema])).map((r) => String(r.name ?? r.NAME ?? r.TABLE_NAME)));
46
+ const missing = AUTHKIT_TABLE_NAMES.filter((name) => !found.has(name));
47
+ if (missing.length > 0) {
48
+ return [
49
+ {
50
+ status: 'error',
51
+ message: `AuthKit tables missing from schema "${schema}": ${missing.join(', ')}`,
52
+ fix: 'Boot the app once with schema.autoManage on, or run ensureAuthkitSchema(db, { schema }) in a migration',
53
+ docs,
54
+ },
55
+ ];
56
+ }
57
+ try {
58
+ await client.transaction(async (trx) => {
59
+ await trx.table(authkitTable({ schema }, 'auth_settings')).insert({
60
+ key: '__authkit_doctor_probe',
61
+ organization_id: null,
62
+ value: 'null',
63
+ });
64
+ throw new ProbeRollback();
65
+ });
66
+ }
67
+ catch (error) {
68
+ if (!(error instanceof ProbeRollback)) {
69
+ return [
70
+ {
71
+ status: 'error',
72
+ message: `the connection cannot write to schema "${schema}": ${error?.message ?? error}`,
73
+ fix: dialect === 'mysql'
74
+ ? `GRANT INSERT, UPDATE, DELETE ON \`${schema}\`.* TO the app user`
75
+ : `GRANT USAGE ON SCHEMA "${schema}" and INSERT, UPDATE, DELETE on its tables to the app role`,
76
+ docs,
77
+ },
78
+ ];
79
+ }
80
+ }
81
+ return [
82
+ {
83
+ status: 'ok',
84
+ message: `AuthKit tables are in schema "${schema}" and the connection can write there`,
85
+ },
86
+ ];
87
+ }
@@ -1,6 +1,7 @@
1
1
  import { getBootedApp } from '../../services/booted_app.js';
2
2
  import { pickModelAdapterClass } from '../adapters/factory.js';
3
3
  import { buildPersonalAgentsRuntime } from '../agents/runtime.js';
4
+ import { authkitTable } from '../schema/tables_location.js';
4
5
  import { normalizeActiveOrg } from './active_org_cookie.js';
5
6
  /**
6
7
  * Serviço de inspeção/revogação das SESSÕES e GRANTS ativos de uma conta.
@@ -22,8 +23,10 @@ export class AdminSessionsService {
22
23
  #AdapterClass;
23
24
  #SessionAdapterClass;
24
25
  #accountStore;
25
- /** Conexão Lucid das tabelas authkit (schema `auth`) — onde vive auth_session_revocations. */
26
+ /** Conexão Lucid das tabelas authkit (`schema.connection`) — onde vive auth_session_revocations. */
26
27
  #schemaConnection;
28
+ /** `auth_session_revocations`, qualificada com `schema.name` quando há um. */
29
+ #revocationsTable;
27
30
  #oidc;
28
31
  constructor(oidc) {
29
32
  this.#oidc = oidc;
@@ -34,6 +37,7 @@ export class AdminSessionsService {
34
37
  this.#SessionAdapterClass = oidc.config.SessionAdapterClass ?? oidc.config.AdapterClass;
35
38
  this.#accountStore = oidc.config.accountStore;
36
39
  this.#schemaConnection = oidc.config.schema?.connection;
40
+ this.#revocationsTable = authkitTable({ schema: oidc.config.schema?.name }, 'auth_session_revocations');
37
41
  }
38
42
  #adapter(model) {
39
43
  return new (pickModelAdapterClass(model, this.#AdapterClass, this.#SessionAdapterClass))(model);
@@ -70,7 +74,7 @@ export class AdminSessionsService {
70
74
  const conn = this.#schemaConnection
71
75
  ? db.connection(this.#schemaConnection)
72
76
  : db.connection();
73
- await conn.insertQuery().table('auth_session_revocations').insert({
77
+ await conn.insertQuery().table(this.#revocationsTable).insert({
74
78
  sid: null,
75
79
  sub: accountId,
76
80
  revoked_at: revokedAt,
@@ -98,7 +98,20 @@ export interface RuntimeSettingsOptions {
98
98
  * Ausente (ou undefined) → conexão default.
99
99
  */
100
100
  connection?: string;
101
+ /**
102
+ * Schema do banco de `auth_settings` (MySQL: o database) — o `schema.name` do
103
+ * `defineConfig`. Escrito em cada statement (`"auth"."auth_settings"`).
104
+ */
105
+ schema?: string;
101
106
  }
107
+ /**
108
+ * `{ connection, schema }` das tabelas do authkit a partir do config resolvido
109
+ * (`schema.connection`/`schema.name`), no formato de {@link RuntimeSettingsOptions}.
110
+ */
111
+ export declare function settingsLocation(config: any): {
112
+ connection?: string;
113
+ schema?: string;
114
+ };
102
115
  /**
103
116
  * Implementação default do SettingsCapability sobre um `Database` Lucid.
104
117
  *
@@ -108,6 +121,8 @@ export declare class RuntimeSettings implements SettingsCapability {
108
121
  private readonly db;
109
122
  private readonly ttlMs;
110
123
  private readonly connectionName;
124
+ /** `auth_settings`, qualificada com o schema quando há um. */
125
+ private readonly table;
111
126
  private readonly shared;
112
127
  constructor(db: any, opts?: RuntimeSettingsOptions);
113
128
  private get cache();