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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +1 -1
  3. package/build/index.d.ts +2 -1
  4. package/build/index.js +1 -0
  5. package/build/providers/authkit_server_provider.js +16 -4
  6. package/build/src/accounts/account_store.d.ts +7 -7
  7. package/build/src/accounts/lucid_account_store.d.ts +3 -3
  8. package/build/src/accounts/lucid_account_store.js +43 -17
  9. package/build/src/accounts/lucid_store/mfa.js +3 -3
  10. package/build/src/accounts/lucid_store/organization_models.d.ts +18 -3
  11. package/build/src/accounts/lucid_store/organization_models.js +35 -3
  12. package/build/src/accounts/lucid_store/organizations.d.ts +2 -2
  13. package/build/src/accounts/lucid_store/organizations.js +35 -32
  14. package/build/src/accounts/lucid_store/password_hygiene.js +4 -2
  15. package/build/src/accounts/lucid_store/shared.d.ts +16 -3
  16. package/build/src/accounts/lucid_store/shared.js +13 -10
  17. package/build/src/accounts/lucid_store/webauthn.js +1 -1
  18. package/build/src/adapters/database_adapter.d.ts +7 -1
  19. package/build/src/adapters/database_adapter.js +24 -13
  20. package/build/src/adapters/factory.d.ts +20 -2
  21. package/build/src/adapters/factory.js +8 -4
  22. package/build/src/agents/delegation_store.d.ts +5 -1
  23. package/build/src/agents/delegation_store.js +34 -21
  24. package/build/src/agents/extension.d.ts +16 -2
  25. package/build/src/agents/registry_store.d.ts +1 -0
  26. package/build/src/agents/registry_store.js +10 -8
  27. package/build/src/agents/runtime.js +6 -2
  28. package/build/src/commands/expire_scan_command.js +2 -3
  29. package/build/src/commands/settings_commands.js +5 -5
  30. package/build/src/define_config.d.ts +12 -1
  31. package/build/src/define_config.js +34 -3
  32. package/build/src/doctor/agora_checks.js +13 -0
  33. package/build/src/doctor/checks.js +2 -2
  34. package/build/src/doctor/db_schema_check.d.ts +6 -0
  35. package/build/src/doctor/db_schema_check.js +87 -0
  36. package/build/src/host/admin_sessions_service.js +6 -2
  37. package/build/src/host/runtime_settings.d.ts +21 -6
  38. package/build/src/host/runtime_settings.js +29 -13
  39. package/build/src/keys/keystore_manager.js +2 -1
  40. package/build/src/keys/keystore_vault.d.ts +9 -1
  41. package/build/src/keys/keystore_vault.js +27 -9
  42. package/build/src/schema/ensure.d.ts +19 -1
  43. package/build/src/schema/ensure.js +54 -30
  44. package/build/src/schema/tables_location.d.ts +56 -0
  45. package/build/src/schema/tables_location.js +86 -0
  46. package/package.json +3 -3
  47. package/skills/authkit-idp-setup/SKILL.md +13 -7
@@ -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 = accountStore?.connectionName;
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
  // ---------------------------------------------------------------------------
@@ -131,16 +131,16 @@ function validateKnownKey(key, value) {
131
131
  async function resolveSettingsService(app) {
132
132
  try {
133
133
  const db = await app.container.make('lucid.db');
134
- // Tenta derivar a conexão do accountStore para probe searchPath-aware.
135
- let connection;
134
+ // `auth_settings` vive na conexão das tabelas do authkit (`schema.connection`).
135
+ let location = {};
136
136
  try {
137
137
  const cfg = await resolveCfg(app);
138
- connection = cfg?.accountStore?.connectionName;
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;
@@ -1190,11 +1190,21 @@ 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 a usar (default: primária).
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.
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.
1194
1203
  */
1195
1204
  schema?: {
1196
1205
  autoManage?: boolean;
1197
1206
  connection?: string;
1207
+ name?: string;
1198
1208
  };
1199
1209
  /**
1200
1210
  * Ciclo de vida da conta (LGPD/GDPR). OPT-IN para workflows DURÁVEIS:
@@ -1404,6 +1414,7 @@ export interface ResolvedServerConfig {
1404
1414
  schema: {
1405
1415
  autoManage: boolean;
1406
1416
  connection?: string;
1417
+ name?: string;
1407
1418
  };
1408
1419
  /**
1409
1420
  * Ciclo de vida da conta resolvido. `durable` default `false` → caminho
@@ -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 { assertSchemaSupported, authkitTablesClient, bindTablesLocation, validateSchemaName, } 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,31 @@ 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
+ const tablesSchema = validateSchemaName(config.schema?.name);
292
+ bindTablesLocation(config.accountStore, {
293
+ connection: config.schema?.connection,
294
+ schema: tablesSchema,
295
+ });
287
296
  return configProvider.create(async (app) => {
288
- const AdapterClass = await config.adapter.resolver(app);
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
+ }
305
+ const AdapterClass = await config.adapter.resolver(app, tablesContext);
289
306
  // `session.adapter` ausente ⇒ mesma classe do default (back-compat: o
290
307
  // dispatcher vira identidade e tudo continua no adapter único de hoje).
291
308
  const SessionAdapterClass = config.session?.adapter
292
- ? await config.session.adapter.resolver(app)
309
+ ? await config.session.adapter.resolver(app, tablesContext)
293
310
  : AdapterClass;
294
311
  // `jwks: 'auto'` → resolve env-aware: AUTHKIT_JWKS inline, senão managed em arquivo.
295
- const jwksConfig = config.jwks === 'auto'
312
+ const jwksInput = config.jwks === 'auto'
296
313
  ? process.env.AUTHKIT_JWKS
297
314
  ? {
298
315
  source: 'jwks',
@@ -304,6 +321,19 @@ export function defineConfig(config) {
304
321
  store: 'tmp/authkit_jwks.json',
305
322
  }
306
323
  : config.jwks;
324
+ // Keystore no Lucid sem conexão/schema próprios fica onde estão as tabelas do authkit.
325
+ const jwksStore = jwksInput.store;
326
+ const jwksConfig = jwksStore?.driver === 'lucid' &&
327
+ ((!jwksStore.connection && config.schema?.connection) || (!jwksStore.schema && tablesSchema))
328
+ ? {
329
+ ...jwksInput,
330
+ store: {
331
+ ...jwksStore,
332
+ connection: jwksStore.connection ?? config.schema?.connection,
333
+ schema: jwksStore.schema ?? tablesSchema,
334
+ },
335
+ }
336
+ : jwksInput;
307
337
  if (config.jwks === 'auto' && !process.env.AUTHKIT_JWKS) {
308
338
  const warning = jwksAutoFallbackWarning(jwksConfig.store ?? null);
309
339
  if (warning) {
@@ -490,6 +520,7 @@ export function defineConfig(config) {
490
520
  schema: {
491
521
  autoManage: config.schema?.autoManage !== false,
492
522
  connection: config.schema?.connection,
523
+ name: tablesSchema,
493
524
  },
494
525
  // OPT-IN: default false → caminho síncrono de sempre (byte-idêntico).
495
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',
@@ -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 (ajuda a diagnosticar bugs de searchPath).
557
- const connectionName = cfg.accountStore?.connectionName;
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';
@@ -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,
@@ -92,13 +92,26 @@ 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'). Quando presente, todas as
96
- * queries usam `db.connection(name)` em vez da conexão default. Necessário
97
- * quando o schema de auth vive numa conexão nomeada com searchPath próprio.
98
- * Ausente (ou undefined) → conexão default (back-compat total).
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
+ /**
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();
@@ -171,8 +186,8 @@ export declare class RuntimeSettings implements SettingsCapability {
171
186
  *
172
187
  * Esta fábrica SUBSTITUI ~16 cópias do mesmo bloco db/connection que viviam sob
173
188
  * nomes distintos (`getRuntimeSettings`, `getSettingsService`,
174
- * `getRuntimeSettingsForSudo`) e elimina o cast `as any` (via `connectionName`
175
- * tipado em {@link AccountStore}).
189
+ * `getRuntimeSettingsForSudo`). A conexão é `schema.connection`, a de todas as
190
+ * tabelas do authkit.
176
191
  *
177
192
  * @example
178
193
  * ```ts
@@ -50,6 +50,7 @@
50
50
  * );
51
51
  * ```
52
52
  */
53
+ import { authkitTable } from '../schema/tables_location.js';
53
54
  import { isSettingLocked, SettingLockedError } from './config_locks.js';
54
55
  /**
55
56
  * Type guard: o objeto (store ou serviço) expõe SettingsCapability?
@@ -58,7 +59,7 @@ export function supportsSettings(obj) {
58
59
  return !!obj && typeof obj.getSetting === 'function';
59
60
  }
60
61
  const sharedStates = new WeakMap();
61
- function sharedStateFor(db, connection) {
62
+ function sharedStateFor(db, connection, table) {
62
63
  const fresh = () => ({ cache: new Map(), tablePresent: null, tableAbsentUntil: 0 });
63
64
  if (!db || (typeof db !== 'object' && typeof db !== 'function'))
64
65
  return fresh();
@@ -67,7 +68,7 @@ function sharedStateFor(db, connection) {
67
68
  byConnection = new Map();
68
69
  sharedStates.set(db, byConnection);
69
70
  }
70
- const name = connection ?? '';
71
+ const name = `${connection ?? ''}\x00${table}`;
71
72
  let state = byConnection.get(name);
72
73
  if (!state) {
73
74
  state = fresh();
@@ -75,6 +76,18 @@ function sharedStateFor(db, connection) {
75
76
  }
76
77
  return state;
77
78
  }
79
+ /**
80
+ * `{ connection, schema }` das tabelas do authkit a partir do config resolvido
81
+ * (`schema.connection`/`schema.name`), no formato de {@link RuntimeSettingsOptions}.
82
+ */
83
+ export function settingsLocation(config) {
84
+ const out = {};
85
+ if (config?.schema?.connection)
86
+ out.connection = config.schema.connection;
87
+ if (config?.schema?.name)
88
+ out.schema = config.schema.name;
89
+ return out;
90
+ }
78
91
  /** Chave de cache: combina key + escopo de org (null = global). */
79
92
  function cacheKey(key, orgId) {
80
93
  return orgId ? `${key}\x00${orgId}` : key;
@@ -88,12 +101,15 @@ export class RuntimeSettings {
88
101
  db;
89
102
  ttlMs;
90
103
  connectionName;
104
+ /** `auth_settings`, qualificada com o schema quando há um. */
105
+ table;
91
106
  shared;
92
107
  constructor(db, opts = {}) {
93
108
  this.db = db;
94
109
  this.ttlMs = opts.ttlMs ?? 15_000;
95
110
  this.connectionName = opts.connection;
96
- this.shared = sharedStateFor(db, opts.connection);
111
+ this.table = authkitTable({ schema: opts.schema }, 'auth_settings');
112
+ this.shared = sharedStateFor(db, opts.connection, this.table);
97
113
  }
98
114
  get cache() {
99
115
  return this.shared.cache;
@@ -127,7 +143,7 @@ export class RuntimeSettings {
127
143
  if (shared.tablePresent === false && shared.tableAbsentUntil > Date.now())
128
144
  return false;
129
145
  try {
130
- await this.conn().from('auth_settings').select('key').limit(1);
146
+ await this.conn().from(this.table).select('key').limit(1);
131
147
  shared.tablePresent = true;
132
148
  return true;
133
149
  }
@@ -152,7 +168,7 @@ export class RuntimeSettings {
152
168
  return null;
153
169
  }
154
170
  try {
155
- let q = this.conn().from('auth_settings').where('key', key);
171
+ let q = this.conn().from(this.table).where('key', key);
156
172
  if (orgId) {
157
173
  q = q.where('organization_id', orgId);
158
174
  }
@@ -192,7 +208,7 @@ export class RuntimeSettings {
192
208
  const resolvedOrgId = orgId ?? null;
193
209
  try {
194
210
  // Delete first then insert = upsert (compatível com sqlite + pg sem UPSERT syntax).
195
- let q = this.conn().from('auth_settings').where('key', key);
211
+ let q = this.conn().from(this.table).where('key', key);
196
212
  if (resolvedOrgId) {
197
213
  q = q.where('organization_id', resolvedOrgId);
198
214
  }
@@ -200,7 +216,7 @@ export class RuntimeSettings {
200
216
  q = q.whereNull('organization_id');
201
217
  }
202
218
  await q.delete();
203
- await this.conn().table('auth_settings').insert({
219
+ await this.conn().table(this.table).insert({
204
220
  key,
205
221
  organization_id: resolvedOrgId,
206
222
  value: json,
@@ -220,7 +236,7 @@ export class RuntimeSettings {
220
236
  return;
221
237
  const resolvedOrgId = orgId ?? null;
222
238
  try {
223
- let q = this.conn().from('auth_settings').where('key', key);
239
+ let q = this.conn().from(this.table).where('key', key);
224
240
  if (resolvedOrgId) {
225
241
  q = q.where('organization_id', resolvedOrgId);
226
242
  }
@@ -238,7 +254,7 @@ export class RuntimeSettings {
238
254
  if (!(await this.hasTable()))
239
255
  return [];
240
256
  try {
241
- let q = this.conn().from('auth_settings').select('*');
257
+ let q = this.conn().from(this.table).select('*');
242
258
  if (orgId !== undefined) {
243
259
  // orgId fornecido: filtra pelo escopo
244
260
  if (orgId) {
@@ -312,8 +328,8 @@ export class RuntimeSettings {
312
328
  *
313
329
  * Esta fábrica SUBSTITUI ~16 cópias do mesmo bloco db/connection que viviam sob
314
330
  * nomes distintos (`getRuntimeSettings`, `getSettingsService`,
315
- * `getRuntimeSettingsForSudo`) e elimina o cast `as any` (via `connectionName`
316
- * tipado em {@link AccountStore}).
331
+ * `getRuntimeSettingsForSudo`). A conexão é `schema.connection`, a de todas as
332
+ * tabelas do authkit.
317
333
  *
318
334
  * @example
319
335
  * ```ts
@@ -325,8 +341,8 @@ export async function resolveRuntimeSettings(ctx) {
325
341
  try {
326
342
  const db = await ctx.containerResolver.make('lucid.db');
327
343
  const service = await ctx.containerResolver.make('authkit.server').catch(() => null);
328
- const connection = service?.config?.accountStore?.connectionName;
329
- return new RuntimeSettings(db, connection ? { connection } : {});
344
+ // `auth_settings` vive onde estão as tabelas do authkit, não na conexão do model da conta.
345
+ return new RuntimeSettings(db, settingsLocation(service?.config));
330
346
  }
331
347
  catch {
332
348
  return null;
@@ -1,3 +1,4 @@
1
+ import { validateSchemaName } from '../schema/tables_location.js';
1
2
  import { generateSigningJwk, planRotation, } from './keystore.js';
2
3
  import { DriveKeystoreVault, FileKeystoreVault, HashicorpVaultKeystoreVault, LazyExternalVault, LucidKeystoreVault, RedisKeystoreVault, } from './keystore_vault.js';
3
4
  /**
@@ -80,7 +81,7 @@ export function resolveKeystoreVault(store, ctx) {
80
81
  return new LucidKeystoreVault(async () => {
81
82
  const db = await ctx.container.make('lucid.db');
82
83
  return cfg.connection ? db.connection(cfg.connection) : db.connection();
83
- }, cfg.table, cfg.key);
84
+ }, cfg.table, cfg.key, validateSchemaName(cfg.schema, 'jwks.store.schema'));
84
85
  case 'redis':
85
86
  return new RedisKeystoreVault(async () => {
86
87
  const rm = await ctx.container.make('redis');
@@ -30,7 +30,15 @@ export declare class LucidKeystoreVault implements KeystoreVault {
30
30
  private getConn;
31
31
  private table;
32
32
  private key;
33
- constructor(getConn: () => Promise<any>, table?: string, key?: string);
33
+ /** Schema do banco da tabela (MySQL: o database). Escrito em cada statement. */
34
+ private schema?;
35
+ constructor(getConn: () => Promise<any>, table?: string, key?: string,
36
+ /** Schema do banco da tabela (MySQL: o database). Escrito em cada statement. */
37
+ schema?: string | undefined);
38
+ /** Schema builder no schema da tabela (`withSchema`), ou o da conexão. */
39
+ private ddl;
40
+ /** `schema.tabela` (o knex cita cada parte) ou só `tabela`. */
41
+ private get qualified();
34
42
  private ensureTable;
35
43
  read(): Promise<string | null>;
36
44
  write(blob: string): Promise<void>;
@@ -8,6 +8,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
8
8
  };
9
9
  import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
10
10
  import { dirname } from 'node:path';
11
+ import { assertSchemaSupported, createDbSchema } from '../schema/tables_location.js';
11
12
  /** Cofre em arquivo local (default; mode 0600). */
12
13
  export class FileKeystoreVault {
13
14
  path;
@@ -49,16 +50,30 @@ export class LucidKeystoreVault {
49
50
  getConn;
50
51
  table;
51
52
  key;
52
- constructor(getConn, table = 'authkit_keystore', key = 'jwks') {
53
+ schema;
54
+ constructor(getConn, table = 'authkit_keystore', key = 'jwks',
55
+ /** Schema do banco da tabela (MySQL: o database). Escrito em cada statement. */
56
+ schema) {
53
57
  this.getConn = getConn;
54
58
  this.table = table;
55
59
  this.key = key;
60
+ this.schema = schema;
61
+ }
62
+ /** Schema builder no schema da tabela (`withSchema`), ou o da conexão. */
63
+ ddl(conn) {
64
+ return this.schema ? conn.schema.withSchema(this.schema) : conn.schema;
65
+ }
66
+ /** `schema.tabela` (o knex cita cada parte) ou só `tabela`. */
67
+ get qualified() {
68
+ return this.schema ? `${this.schema}.${this.table}` : this.table;
56
69
  }
57
70
  async ensureTable(conn) {
58
- if (await conn.schema.hasTable(this.table))
71
+ if (await this.ddl(conn).hasTable(this.table))
59
72
  return;
73
+ if (this.schema)
74
+ await createDbSchema(conn, this.schema);
60
75
  try {
61
- await conn.schema.createTable(this.table, (t) => {
76
+ await this.ddl(conn).createTable(this.table, (t) => {
62
77
  t.string('key').notNullable().primary();
63
78
  t.text('blob').notNullable();
64
79
  t.bigInteger('updated_at').notNullable();
@@ -67,31 +82,34 @@ export class LucidKeystoreVault {
67
82
  catch (err) {
68
83
  // Outra instância criou a tabela entre o hasTable e o createTable (race no boot
69
84
  // multi-instância). Se já existe agora, OK; senão propaga.
70
- if (!(await conn.schema.hasTable(this.table)))
85
+ if (!(await this.ddl(conn).hasTable(this.table)))
71
86
  throw err;
72
87
  }
73
88
  }
74
89
  async read() {
75
90
  const conn = await this.getConn();
76
- if (!(await conn.schema.hasTable(this.table)))
91
+ assertSchemaSupported(conn, this.schema);
92
+ if (!(await this.ddl(conn).hasTable(this.table)))
77
93
  return null;
78
- const row = await conn.from(this.table).where('key', this.key).first();
94
+ const row = await conn.from(this.qualified).where('key', this.key).first();
79
95
  return row ? row.blob : null;
80
96
  }
81
97
  async write(blob) {
82
98
  const conn = await this.getConn();
99
+ assertSchemaSupported(conn, this.schema);
83
100
  await this.ensureTable(conn);
84
101
  await conn
85
- .table(this.table)
102
+ .table(this.qualified)
86
103
  .insert({ key: this.key, blob, updated_at: Date.now() })
87
104
  .onConflict('key')
88
105
  .merge();
89
106
  }
90
107
  async head() {
91
108
  const conn = await this.getConn();
92
- if (!(await conn.schema.hasTable(this.table)))
109
+ assertSchemaSupported(conn, this.schema);
110
+ if (!(await this.ddl(conn).hasTable(this.table)))
93
111
  return null;
94
- const row = await conn.from(this.table).where('key', this.key).first();
112
+ const row = await conn.from(this.qualified).where('key', this.key).first();
95
113
  return row ? String(row.updated_at) : null;
96
114
  }
97
115
  }