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

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 +26 -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/checks.d.ts +31 -1
  30. package/build/src/doctor/checks.js +118 -3
  31. package/build/src/doctor/db_schema_check.d.ts +6 -0
  32. package/build/src/doctor/db_schema_check.js +87 -0
  33. package/build/src/doctor/doctor_input.js +2 -0
  34. package/build/src/host/admin_sessions_service.js +6 -2
  35. package/build/src/host/csrf.d.ts +3 -0
  36. package/build/src/host/csrf.js +27 -7
  37. package/build/src/host/runtime_settings.d.ts +15 -0
  38. package/build/src/host/runtime_settings.js +27 -12
  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 +9 -0
  43. package/build/src/schema/ensure.js +46 -19
  44. package/build/src/schema/tables_location.d.ts +31 -2
  45. package/build/src/schema/tables_location.js +64 -0
  46. package/package.json +3 -3
  47. package/skills/authkit-idp-setup/SKILL.md +13 -7
@@ -238,7 +238,90 @@ export function checkSession(input) {
238
238
  }
239
239
  return findings;
240
240
  }
241
- /** Hint de exceções de CSRF do shield para o mountPath. */
241
+ /**
242
+ * The server-to-server POST endpoints authkit mounts, which shield's CSRF check must not reject:
243
+ * the OIDC provider's token, introspection and revocation endpoints (under the `${mountPath}/*`
244
+ * wildcard route), PAT introspection, and — with `personalAgents` on — the agents' OAuth
245
+ * endpoints plus every extension's `csrfExemptPaths`.
246
+ */
247
+ export function authkitCsrfExemptEndpoints(authkitConfig) {
248
+ const mount = String(authkitConfig?.mountPath ?? '/oidc').replace(/\/+$/, '') || '/oidc';
249
+ const endpoints = ['token', 'token/introspection', 'token/revocation'].map((rest) => ({ path: `${mount}/${rest}`, pattern: `${mount}/*` }));
250
+ endpoints.push({ path: '/authkit/pat/introspect', pattern: '/authkit/pat/introspect' });
251
+ const agents = authkitConfig?.personalAgents;
252
+ if (agents?.prefix) {
253
+ for (const rest of ['device_authorization', 'token']) {
254
+ const path = `${agents.prefix}/oauth/${rest}`;
255
+ endpoints.push({ path, pattern: path });
256
+ }
257
+ for (const extension of agents.extensions ?? []) {
258
+ for (const path of extension.csrfExemptPaths ?? [])
259
+ endpoints.push({ path, pattern: path });
260
+ }
261
+ }
262
+ return endpoints;
263
+ }
264
+ /** A minimal HttpContext for a token-less `POST path`, enough for an `exceptRoutes` function. */
265
+ function csrfProbeContext(endpoint) {
266
+ const [pathname, query = ''] = endpoint.path.split('?');
267
+ const completeUrl = `http://localhost${endpoint.path}`;
268
+ const request = {
269
+ url: (includeQueryString) => includeQueryString && query ? `${pathname}?${query}` : pathname,
270
+ completeUrl: () => completeUrl,
271
+ method: () => 'POST',
272
+ intended: () => 'POST',
273
+ parsedUrl: { pathname, query, href: endpoint.path },
274
+ header: () => undefined,
275
+ headers: () => ({}),
276
+ hostname: () => 'localhost',
277
+ host: () => 'localhost',
278
+ protocol: () => 'http',
279
+ ip: () => '127.0.0.1',
280
+ input: () => undefined,
281
+ qs: () => ({}),
282
+ all: () => ({}),
283
+ matchesRoute: (identifier) => (Array.isArray(identifier) ? identifier : [identifier]).includes(endpoint.pattern),
284
+ };
285
+ return {
286
+ request,
287
+ route: { pattern: endpoint.pattern, methods: ['POST'] },
288
+ routeKey: `POST-${endpoint.pattern}`,
289
+ };
290
+ }
291
+ /**
292
+ * Whether shield's CSRF check (as configured in `config/shield.ts`) would reject a token-less POST
293
+ * to `endpoint`, mirroring `@adonisjs/shield`: CSRF off → no; a non-empty `methods` without POST →
294
+ * no (an empty or missing `methods` checks every method); `exceptRoutes` as a function → rejected
295
+ * unless it returns a truthy value for that request; as an array → rejected unless it holds the route
296
+ * pattern. `'unknown'` when the function throws on the probe context.
297
+ */
298
+ export function csrfRejects(shieldConfig, endpoint) {
299
+ const csrf = shieldConfig?.csrf;
300
+ if (!csrf || csrf.enabled !== true)
301
+ return false;
302
+ const methods = Array.isArray(csrf.methods)
303
+ ? csrf.methods.map((m) => String(m).toLowerCase())
304
+ : [];
305
+ if (methods.length > 0 && !methods.includes('post'))
306
+ return false;
307
+ const except = csrf.exceptRoutes ?? [];
308
+ if (typeof except === 'function') {
309
+ try {
310
+ // Shield skips validation on any truthy return (`!this.#routesToIgnore(ctx)`).
311
+ return !except(csrfProbeContext(endpoint));
312
+ }
313
+ catch {
314
+ return 'unknown';
315
+ }
316
+ }
317
+ if (Array.isArray(except))
318
+ return !except.includes(endpoint.pattern);
319
+ return true;
320
+ }
321
+ /**
322
+ * `authkit.shield`: evaluates the app's real `config/shield.ts` against the server-to-server POST
323
+ * endpoints authkit mounts, and warns only about the ones shield would actually reject.
324
+ */
242
325
  export function checkShield(input) {
243
326
  if (!input.peers.shield) {
244
327
  return {
@@ -247,9 +330,41 @@ export function checkShield(input) {
247
330
  };
248
331
  }
249
332
  const mountPath = input.authkitConfig?.mountPath ?? '/oidc';
333
+ const helper = `authkitCsrfExceptions(ctx.request.url(), { mountPath: '${mountPath}' })`;
334
+ if (input.shieldConfig === undefined) {
335
+ return {
336
+ level: 'warn',
337
+ message: `config('shield') was not read, so the CSRF exemptions could not be checked. Make sure config/shield.ts's csrf.exceptRoutes exempts the IdP POST routes under "${mountPath}" (e.g. with ${helper}).`,
338
+ };
339
+ }
340
+ const endpoints = authkitCsrfExemptEndpoints(input.authkitConfig);
341
+ const rejected = [];
342
+ const unknown = [];
343
+ for (const endpoint of endpoints) {
344
+ const verdict = csrfRejects(input.shieldConfig, endpoint);
345
+ if (verdict === 'unknown')
346
+ unknown.push(endpoint.path);
347
+ else if (verdict)
348
+ rejected.push(endpoint.path);
349
+ }
350
+ if (rejected.length > 0) {
351
+ return {
352
+ level: 'warn',
353
+ message: `Shield's CSRF check rejects POST ${rejected.join(', ')} — server-to-server calls to these authkit endpoints fail. Exempt them in config/shield.ts's csrf.exceptRoutes, e.g. \`exceptRoutes: (ctx) => ${helper}\` (combine with your own exemptions using ||).`,
354
+ };
355
+ }
356
+ if (unknown.length > 0) {
357
+ return {
358
+ level: 'warn',
359
+ message: `csrf.exceptRoutes in config/shield.ts threw when probed with POST ${unknown.join(', ')}, so the doctor cannot tell whether they are exempt. Make sure it exempts them (e.g. with ${helper}).`,
360
+ };
361
+ }
362
+ const csrfOn = input.shieldConfig?.csrf?.enabled === true;
250
363
  return {
251
- level: 'warn',
252
- message: `Make sure the IdP POST routes under "${mountPath}" are in the shield CSRF exceptions (e.g. the /token endpoint), otherwise server-to-server calls fail. Use the exported authkitCsrfExceptions(url, { mountPath: '${mountPath}' }) helper in config/shield.ts's exceptRoutes instead of hand-writing the route-pattern check.`,
364
+ level: 'ok',
365
+ message: csrfOn
366
+ ? `shield's CSRF check exempts authkit's server-to-server POST endpoints (${endpoints.length} checked).`
367
+ : 'shield CSRF is off: authkit endpoints are not affected.',
253
368
  };
254
369
  }
255
370
  /** ally só é necessário quando social está configurado. */
@@ -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
+ }
@@ -47,9 +47,11 @@ export async function buildDoctorInput(app, logger) {
47
47
  // senão todo campo aparece como "ausente" (provider cru não tem issuer etc.).
48
48
  const authkitConfig = await resolveAuthkitConfig(app, config.get('authkit', null));
49
49
  const sessionConfig = config.get('session', null) ?? null;
50
+ const shieldConfig = config.get('shield', null) ?? null;
50
51
  return {
51
52
  authkitConfig,
52
53
  sessionConfig,
54
+ shieldConfig,
53
55
  peers: {
54
56
  session: await canImport('@adonisjs/session', logger),
55
57
  shield: await canImport('@adonisjs/shield', logger),
@@ -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,
@@ -20,5 +20,8 @@ export interface AuthkitCsrfOptions {
20
20
  * Retorna `true` quando `url` é uma rota AuthKit que deve ser ISENTA de CSRF
21
21
  * (machine-to-machine). Cobre o mountPath do IdP, a introspecção de PAT e a
22
22
  * rota de back-channel logout do client e os endpoints OAuth de personal agents.
23
+ *
24
+ * O match é ANCORADO no pathname: `/oidc` e `/oidc/...` casam, mas `/foo/oidc/x`,
25
+ * `/oidc-admin` ou `/x?next=/oidc` não. A query string é ignorada.
23
26
  */
24
27
  export declare function authkitCsrfExceptions(url: string, options?: AuthkitCsrfOptions): boolean;
@@ -11,19 +11,39 @@
11
11
  * csrf: {
12
12
  * enabled: true,
13
13
  * exceptRoutes: (ctx) => authkitCsrfExceptions(ctx.request.url(), { mountPath: '/oidc' })
14
- * || ctx.request.url().includes('/api'),
14
+ * || ctx.request.url().startsWith('/api/'),
15
15
  * }
16
16
  * ```
17
17
  */
18
18
  import { normalizePersonalAgentsPrefix } from '../agents/config.js';
19
19
  import { getAuthHostConfig } from './auth_host_config.js';
20
+ /** `path` é `prefix` ou fica debaixo dele (`prefix/...`) — nunca um substring solto. */
21
+ function underPrefix(path, prefix) {
22
+ return path === prefix || path.startsWith(`${prefix}/`);
23
+ }
24
+ /** O pathname de `url`: sem query/hash, aceitando também uma URL absoluta. */
25
+ function pathnameOf(url) {
26
+ let path = url;
27
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(path)) {
28
+ try {
29
+ path = new URL(path).pathname;
30
+ }
31
+ catch {
32
+ // Segue com a string crua: o match ancorado abaixo simplesmente não casa.
33
+ }
34
+ }
35
+ return path.split(/[?#]/, 1)[0] ?? '';
36
+ }
20
37
  /**
21
38
  * Retorna `true` quando `url` é uma rota AuthKit que deve ser ISENTA de CSRF
22
39
  * (machine-to-machine). Cobre o mountPath do IdP, a introspecção de PAT e a
23
40
  * rota de back-channel logout do client e os endpoints OAuth de personal agents.
41
+ *
42
+ * O match é ANCORADO no pathname: `/oidc` e `/oidc/...` casam, mas `/foo/oidc/x`,
43
+ * `/oidc-admin` ou `/x?next=/oidc` não. A query string é ignorada.
24
44
  */
25
45
  export function authkitCsrfExceptions(url, options = {}) {
26
- const mountPath = options.mountPath ?? '/oidc';
46
+ const mountPath = (options.mountPath ?? '/oidc').replace(/\/+$/, '');
27
47
  const backchannel = options.backchannelLogoutPath === false
28
48
  ? null
29
49
  : (options.backchannelLogoutPath ?? '/auth/backchannel-logout');
@@ -33,12 +53,12 @@ export function authkitCsrfExceptions(url, options = {}) {
33
53
  ? normalizePersonalAgentsPrefix(options.personalAgentsPrefix)
34
54
  : getAuthHostConfig()?.personalAgents?.prefix;
35
55
  const agentsOAuth = agentsPrefix ? `${agentsPrefix}/oauth/` : null;
36
- const path = url.split('?')[0];
56
+ const path = pathnameOf(url);
37
57
  const extensionExempt = options.personalAgentExtensions !== false &&
38
58
  (getAuthHostConfig()?.personalAgents?.extensions ?? []).some((e) => e.csrfExemptPaths?.includes(path));
39
- return (url.includes(mountPath) ||
40
- url.includes('/authkit/pat') ||
41
- (backchannel !== null && url === backchannel) ||
42
- (agentsOAuth !== null && url.startsWith(agentsOAuth)) ||
59
+ return ((mountPath !== '' && underPrefix(path, mountPath)) ||
60
+ underPrefix(path, '/authkit/pat') ||
61
+ (backchannel !== null && path === backchannel) ||
62
+ (agentsOAuth !== null && path.startsWith(agentsOAuth)) ||
43
63
  extensionExempt);
44
64
  }
@@ -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();
@@ -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) {
@@ -325,9 +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
- // `auth_settings` vive na conexão das tabelas do authkit, não na do model da conta.
329
- const connection = service?.config?.schema?.connection;
330
- 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));
331
346
  }
332
347
  catch {
333
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
  }
@@ -24,6 +24,8 @@
24
24
  * ```
25
25
  */
26
26
  import type { AuthkitTable } from '../agents/extension.js';
27
+ /** Os nomes (sem schema) das tabelas que {@link ensureAuthkitSchema} garante. */
28
+ export declare const AUTHKIT_TABLE_NAMES: readonly string[];
27
29
  export interface EnsureSchemaOptions {
28
30
  /**
29
31
  * Conexão Lucid das tabelas do authkit (`schema.connection`). Default: conexão
@@ -36,6 +38,12 @@ export interface EnsureSchemaOptions {
36
38
  * O provider passa a do `accountStore` no boot. Ignorada quando `db` já é um client.
37
39
  */
38
40
  accountConnection?: string;
41
+ /**
42
+ * Database schema of the tables (MySQL: the database) — `schema.name`. Every statement
43
+ * names it (`"auth"."auth_settings"`), and it is created when missing. Never a
44
+ * `searchPath`. Ignored for the account table, which stays where the app put it.
45
+ */
46
+ schema?: string;
39
47
  /**
40
48
  * Tabelas de `personalAgents.extensions` (`tables` de cada uma), garantidas junto. O provider
41
49
  * passa as do config no boot.
@@ -73,6 +81,7 @@ export interface EnsureSchemaReport {
73
81
  */
74
82
  export declare function ensureAuthkitTables(db: any, tables: readonly AuthkitTable[], options?: {
75
83
  connection?: string;
84
+ schema?: string;
76
85
  }): Promise<Pick<EnsureSchemaReport, 'created' | 'altered' | 'indexed'>>;
77
86
  /**
78
87
  * Garante que as tabelas do authkit existem e têm todas as colunas que esta