@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
@@ -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
@@ -23,7 +23,7 @@
23
23
  * }
24
24
  * ```
25
25
  */
26
- import { authkitTablesClient } from './tables_location.js';
26
+ import { assertSchemaSupported, authkitTable, authkitTablesClient, createDbSchema, validateSchemaName, } from './tables_location.js';
27
27
  /** Busca por conta (`account_id`) e limpeza dos expirados (`expires_at`). */
28
28
  const OIDC_PAYLOAD_INDEXES = [
29
29
  { name: 'authkit_oidc_payloads_account_idx', columns: ['model_name', 'account_id'] },
@@ -139,12 +139,12 @@ const TABLES = [
139
139
  },
140
140
  {
141
141
  name: 'auth_organization_members',
142
- create: (t) => {
142
+ create: (t, ctx) => {
143
143
  t.string('id').primary();
144
144
  t.string('organization_id')
145
145
  .notNullable()
146
146
  .references('id')
147
- .inTable('auth_organizations')
147
+ .inTable(ctx.table('auth_organizations'))
148
148
  .onDelete('CASCADE');
149
149
  t.string('account_id').notNullable();
150
150
  t.string('role').notNullable().defaultTo('member');
@@ -186,12 +186,12 @@ const TABLES = [
186
186
  },
187
187
  {
188
188
  name: 'auth_organization_invitations',
189
- create: (t) => {
189
+ create: (t, ctx) => {
190
190
  t.string('id').primary();
191
191
  t.string('organization_id')
192
192
  .notNullable()
193
193
  .references('id')
194
- .inTable('auth_organizations')
194
+ .inTable(ctx.table('auth_organizations'))
195
195
  .onDelete('CASCADE');
196
196
  t.string('email').notNullable();
197
197
  t.string('role').notNullable().defaultTo('member');
@@ -335,6 +335,8 @@ const TABLES = [
335
335
  },
336
336
  },
337
337
  ];
338
+ /** Os nomes (sem schema) das tabelas que {@link ensureAuthkitSchema} garante. */
339
+ export const AUTHKIT_TABLE_NAMES = TABLES.map((t) => t.name);
338
340
  /** Linhas de um `rawQuery`, no formato de cada driver (pg, mysql2, sqlite). */
339
341
  function catalogRows(result) {
340
342
  if (Array.isArray(result))
@@ -420,7 +422,10 @@ async function columnExists(conn, table, column) {
420
422
  async function indexExists(conn, table, name) {
421
423
  const dialect = conn.dialect?.name;
422
424
  if (dialect === 'postgres' || dialect === 'redshift') {
423
- const found = catalogRows(await conn.rawQuery('select to_regclass(?) as oid', [name]));
425
+ // Índice mora no schema da tabela: com schema explícito, o nome vai qualificado.
426
+ const [schema] = mysqlRelation(table);
427
+ const relation = pgRelation(schema ? `${schema}.${name}` : name);
428
+ const found = catalogRows(await conn.rawQuery('select to_regclass(?) as oid', [relation]));
424
429
  return found[0]?.oid != null;
425
430
  }
426
431
  if (dialect === 'mysql') {
@@ -441,11 +446,12 @@ async function indexExists(conn, table, name) {
441
446
  * Cria o índice quando falta. Idempotente: um índice já existente (inclusive
442
447
  * criado por outra instância no mesmo boot) não é erro.
443
448
  */
444
- async function ensureIndex(conn, table, index) {
449
+ async function ensureIndex(conn, loc, name, index) {
450
+ const table = authkitTable(loc, name);
445
451
  if ((await indexExists(conn, table, index.name)) === true)
446
452
  return false;
447
453
  try {
448
- await conn.schema.alterTable(table, (t) => t.index(index.columns, index.name));
454
+ await ddl(conn, loc).alterTable(name, (t) => t.index(index.columns, index.name));
449
455
  return true;
450
456
  }
451
457
  catch (error) {
@@ -458,12 +464,23 @@ async function ensureIndex(conn, table, index) {
458
464
  throw error;
459
465
  }
460
466
  }
467
+ /**
468
+ * O schema builder das tabelas do authkit: com `schema`, `withSchema` — o knex escreve
469
+ * `"schema"."tabela"` em todo DDL e mantém os nomes de índice derivados da tabela pura.
470
+ */
471
+ function ddl(conn, loc) {
472
+ return loc.schema ? conn.schema.withSchema(loc.schema) : conn.schema;
473
+ }
461
474
  /** Cria/completa as tabelas (o miolo de {@link ensureAuthkitSchema}). */
462
- async function ensureTables(conn, tables, report) {
475
+ async function ensureTables(conn, loc, tables, report) {
476
+ const context = { table: (name) => authkitTable(loc, name) };
477
+ if (loc.schema)
478
+ await createDbSchema(conn, loc.schema);
463
479
  for (const def of tables) {
464
- if (!(await tableExists(conn, def.name))) {
480
+ const table = authkitTable(loc, def.name);
481
+ if (!(await tableExists(conn, table))) {
465
482
  try {
466
- await conn.schema.createTable(def.name, def.create);
483
+ await ddl(conn, loc).createTable(def.name, (t) => def.create(t, context));
467
484
  report.created.push(def.name);
468
485
  continue;
469
486
  }
@@ -472,20 +489,20 @@ async function ensureTables(conn, tables, report) {
472
489
  * Corrida entre instâncias subindo juntas: se outra instância criou
473
490
  * a tabela entre o probe e o CREATE, segue o jogo; senão, propaga.
474
491
  */
475
- if (!(await tableExists(conn, def.name)))
492
+ if (!(await tableExists(conn, table)))
476
493
  throw error;
477
494
  }
478
495
  }
479
496
  for (const [column, add] of Object.entries(def.columns)) {
480
- if (await columnExists(conn, def.name, column))
497
+ if (await columnExists(conn, table, column))
481
498
  continue;
482
- await conn.schema.alterTable(def.name, (t) => add(t));
499
+ await ddl(conn, loc).alterTable(def.name, (t) => add(t));
483
500
  if (!report.altered[def.name])
484
501
  report.altered[def.name] = [];
485
502
  report.altered[def.name].push(column);
486
503
  }
487
504
  for (const index of def.indexes ?? []) {
488
- if (!(await ensureIndex(conn, def.name, index)))
505
+ if (!(await ensureIndex(conn, loc, def.name, index)))
489
506
  continue;
490
507
  if (!report.indexed)
491
508
  report.indexed = {};
@@ -495,18 +512,26 @@ async function ensureTables(conn, tables, report) {
495
512
  }
496
513
  }
497
514
  }
515
+ function resolveLocation(options) {
516
+ return {
517
+ connection: options.connection,
518
+ schema: validateSchemaName(options.schema, 'schema'),
519
+ };
520
+ }
498
521
  /**
499
522
  * Garante tabelas lib-owned no formato {@link AuthkitTable} — o que `ensureAuthkitSchema` faz com as
500
523
  * do authkit: cria as que faltam, adiciona colunas e índices novos, nunca dropa nem altera. Para
501
524
  * uma extension expor o próprio `ensure*Schema()` para migrations do host. Estável no 1.x.
502
525
  */
503
526
  export async function ensureAuthkitTables(db, tables, options = {}) {
504
- const conn = authkitTablesClient(db, { connection: options.connection });
527
+ const loc = resolveLocation(options);
528
+ const conn = authkitTablesClient(db, loc);
529
+ assertSchemaSupported(conn, loc.schema);
505
530
  const report = {
506
531
  created: [],
507
532
  altered: {},
508
533
  };
509
- await ensureTables(conn, tables, report);
534
+ await ensureTables(conn, loc, tables, report);
510
535
  return report;
511
536
  }
512
537
  /**
@@ -518,13 +543,15 @@ export async function ensureAuthkitTables(db, tables, options = {}) {
518
543
  * (`this.db`, um `QueryClient`/`TransactionClient`, usado como está).
519
544
  */
520
545
  export async function ensureAuthkitSchema(db, options = {}) {
521
- const conn = authkitTablesClient(db, { connection: options.connection });
546
+ const loc = resolveLocation(options);
547
+ const conn = authkitTablesClient(db, loc);
548
+ assertSchemaSupported(conn, loc.schema);
522
549
  const report = {
523
550
  created: [],
524
551
  altered: {},
525
552
  loginMethods: { table: options.accountTable ?? 'users', ensured: false },
526
553
  };
527
- await ensureTables(conn, [...TABLES, ...(options.tables ?? [])], report);
554
+ await ensureTables(conn, loc, [...TABLES, ...(options.tables ?? [])], report);
528
555
  // Coluna `login_methods` na tabela da CONTA — a tabela é HOST-owned (não está
529
556
  // em TABLES porque o nome/shape são decisão do app), mas a LIB consome a
530
557
  // coluna (LoginMethodsPreferenceCapability). Alguns deploys de host só migram
@@ -4,19 +4,48 @@
4
4
  * `authkit_keystore` of the Lucid keystore, and the tables of agent extensions).
5
5
  *
6
6
  * The rule: every lib-owned table goes through `schema.connection` of `defineConfig`
7
- * (default: the primary Lucid connection). The app's account table is not lib-owned: it
8
- * stays on the account model's connection (`static connection` of the model).
7
+ * (default: the primary Lucid connection) and, with `schema.name`, lives in that database
8
+ * schema (MySQL: that database). The app's account table is not lib-owned: it stays on the
9
+ * account model's connection and table (`static connection`/`static table` of the model).
10
+ *
11
+ * The schema is written INTO every statement (`"auth"."auth_settings"`). Never put
12
+ * AuthKit's tables in another schema with a connection `searchPath`: knex sends
13
+ * `SET search_path` on the session, and behind PgBouncer in transaction mode that leaks to
14
+ * other clients' connections.
9
15
  */
10
16
  export interface AuthkitTablesLocation {
11
17
  /** Lucid connection name. `undefined` = the primary connection. */
12
18
  connection?: string;
19
+ /** Database schema of the tables (MySQL: the database). `undefined` = the connection's default. */
20
+ schema?: string;
21
+ }
22
+ /** Raised for a `schema.name` the database cannot use. The provider fails the boot with it. */
23
+ export declare class AuthkitSchemaError extends Error {
24
+ constructor(message: string);
13
25
  }
26
+ /** Throws {@link AuthkitSchemaError} unless `name` is a plain identifier of at most 63 chars. */
27
+ export declare function validateSchemaName(name: unknown, option?: string): string | undefined;
28
+ /** `schema.table` when a schema is set (knex quotes each part: `"schema"."table"`), else `table`. */
29
+ export declare function authkitTable(location: AuthkitTablesLocation | undefined, table: string): string;
14
30
  /**
15
31
  * The query client for AuthKit's tables. `db` is the Lucid manager (`Database`) or a
16
32
  * client the caller already holds (a migration's `this.db`, a transaction), which is
17
33
  * used as is.
18
34
  */
19
35
  export declare function authkitTablesClient(db: any, location?: AuthkitTablesLocation): any;
36
+ /** The Lucid dialect name of a client or manager (`postgres`, `mysql`, `better-sqlite3`…). */
37
+ export declare function dialectOf(client: any): string | undefined;
38
+ /**
39
+ * Throws {@link AuthkitSchemaError} when `schema` is set on a dialect AuthKit cannot use it on:
40
+ * SQLite (no schemas) and any dialect other than Postgres and MySQL. AuthKit never ignores the
41
+ * option nor falls back to unqualified tables.
42
+ */
43
+ export declare function assertSchemaSupported(client: any, schema: string | undefined): void;
44
+ /**
45
+ * `CREATE SCHEMA IF NOT EXISTS` (Postgres) / `CREATE DATABASE IF NOT EXISTS` (MySQL). A role
46
+ * that cannot create it gets an {@link AuthkitSchemaError} naming the schema and the grant.
47
+ */
48
+ export declare function createDbSchema(client: any, schema: string): Promise<void>;
20
49
  /**
21
50
  * Hidden hook a store built by `lucidAccountStore` exposes so `defineConfig` can tell it
22
51
  * where AuthKit's tables live (the store is built before `defineConfig` sees the `schema`
@@ -1,3 +1,24 @@
1
+ /** Raised for a `schema.name` the database cannot use. The provider fails the boot with it. */
2
+ export class AuthkitSchemaError extends Error {
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = 'AuthkitSchemaError';
6
+ }
7
+ }
8
+ const SCHEMA_NAME = /^[A-Za-z_][A-Za-z0-9_$]*$/;
9
+ /** Throws {@link AuthkitSchemaError} unless `name` is a plain identifier of at most 63 chars. */
10
+ export function validateSchemaName(name, option = 'schema.name') {
11
+ if (name === undefined)
12
+ return undefined;
13
+ if (typeof name !== 'string' || name.length > 63 || !SCHEMA_NAME.test(name)) {
14
+ throw new AuthkitSchemaError(`authkit: ${option} must match /^[A-Za-z_][A-Za-z0-9_$]*$/ and have at most 63 characters (got ${JSON.stringify(name)})`);
15
+ }
16
+ return name;
17
+ }
18
+ /** `schema.table` when a schema is set (knex quotes each part: `"schema"."table"`), else `table`. */
19
+ export function authkitTable(location, table) {
20
+ return location?.schema ? `${location.schema}.${table}` : table;
21
+ }
1
22
  /**
2
23
  * The query client for AuthKit's tables. `db` is the Lucid manager (`Database`) or a
3
24
  * client the caller already holds (a migration's `this.db`, a transaction), which is
@@ -8,6 +29,49 @@ export function authkitTablesClient(db, location = {}) {
8
29
  return db;
9
30
  return location.connection ? db.connection(location.connection) : db.connection();
10
31
  }
32
+ const SQLITE_DIALECTS = new Set(['sqlite3', 'better-sqlite3', 'libsql']);
33
+ /** Dialects AuthKit writes and creates schemas on. */
34
+ const SCHEMA_DIALECTS = new Set(['postgres', 'redshift', 'mysql']);
35
+ /** The Lucid dialect name of a client or manager (`postgres`, `mysql`, `better-sqlite3`…). */
36
+ export function dialectOf(client) {
37
+ return client?.dialect?.name;
38
+ }
39
+ /**
40
+ * Throws {@link AuthkitSchemaError} when `schema` is set on a dialect AuthKit cannot use it on:
41
+ * SQLite (no schemas) and any dialect other than Postgres and MySQL. AuthKit never ignores the
42
+ * option nor falls back to unqualified tables.
43
+ */
44
+ export function assertSchemaSupported(client, schema) {
45
+ if (!schema)
46
+ return;
47
+ const dialect = dialectOf(client);
48
+ if (dialect && SCHEMA_DIALECTS.has(dialect))
49
+ return;
50
+ throw new AuthkitSchemaError(dialect && SQLITE_DIALECTS.has(dialect)
51
+ ? `authkit: schema.name "${schema}" is set, but the connection uses ${dialect}, which has no schemas. Remove schema.name or use Postgres or MySQL.`
52
+ : `authkit: schema.name "${schema}" is set, but the connection uses ${dialect ?? 'an unknown dialect'}; schema.name supports Postgres and MySQL. Remove schema.name or use one of them.`);
53
+ }
54
+ /**
55
+ * `CREATE SCHEMA IF NOT EXISTS` (Postgres) / `CREATE DATABASE IF NOT EXISTS` (MySQL). A role
56
+ * that cannot create it gets an {@link AuthkitSchemaError} naming the schema and the grant.
57
+ */
58
+ export async function createDbSchema(client, schema) {
59
+ assertSchemaSupported(client, schema);
60
+ const dialect = dialectOf(client);
61
+ const mysql = dialect === 'mysql';
62
+ const sql = mysql
63
+ ? `create database if not exists \`${schema}\``
64
+ : `create schema if not exists "${schema}"`;
65
+ try {
66
+ await client.rawQuery(sql);
67
+ }
68
+ catch (error) {
69
+ const grant = mysql
70
+ ? `GRANT CREATE ON *.* (or create it: CREATE DATABASE \`${schema}\`)`
71
+ : `GRANT CREATE ON DATABASE <db> TO <role> (or create it: CREATE SCHEMA "${schema}")`;
72
+ throw new AuthkitSchemaError(`authkit: could not create schema "${schema}" (${error?.message ?? error}). The database role needs ${grant}.`);
73
+ }
74
+ }
11
75
  /**
12
76
  * Hidden hook a store built by `lucidAccountStore` exposes so `defineConfig` can tell it
13
77
  * where AuthKit's tables live (the store is built before `defineConfig` sees the `schema`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adonis-agora/authkit-server",
3
- "version": "1.0.0-rc.11",
3
+ "version": "1.0.0-rc.12",
4
4
  "description": "AdonisJS OIDC/OAuth2 provider (Identity Provider) toolkit: ejectable auth server with sessions, rate-limiting, MFA/TOTP, audit log, federated logout and OpenTelemetry metrics.",
5
5
  "license": "MIT",
6
6
  "author": "dudousxd",
@@ -108,7 +108,7 @@
108
108
  }
109
109
  },
110
110
  "dependencies": {
111
- "@adonis-agora/authkit-core": "1.0.0-rc.1",
111
+ "@adonis-agora/authkit-core": "1.0.0-rc.2",
112
112
  "@simplewebauthn/browser": "14.0.0",
113
113
  "@simplewebauthn/server": "14.1.0",
114
114
  "jose": "6.2.16",
@@ -119,7 +119,7 @@
119
119
  "qrcode": "1.5.4"
120
120
  },
121
121
  "devDependencies": {
122
- "@adonis-agora/authkit-react": "1.0.0-rc.4",
122
+ "@adonis-agora/authkit-react": "1.0.0-rc.5",
123
123
  "@adonis-agora/durable": "0.60.0",
124
124
  "@adonis-agora/telescope": "0.27.1",
125
125
  "@adonisjs/ally": "6.3.2",
@@ -4,7 +4,7 @@ description: >-
4
4
  Set up the @adonis-agora/authkit-server OIDC/OAuth2 Authorization Server (Identity
5
5
  Provider) in an AdonisJS app. Covers `node ace add @adonis-agora/authkit-server`,
6
6
  defineConfig({ issuer, adapter, jwks, ttl, globalRolesClaim, accountStore }) with
7
- adapters.redis({ connection }) vs adapters.database({ connection? }),
7
+ adapters.redis({ connection }) vs adapters.database({ connection?, schema? }),
8
8
  lucidAccountStore(AuthUser), JWKS management ({ source: 'managed' | 'jwks' } or
9
9
  'auto', keystore store file/drive/lucid/redis/hashicorp vaults plus
10
10
  @adonis-agora/authkit-vault-aws/azure/gcp createKeystoreVault add-ons),
@@ -83,12 +83,18 @@ export default authServerConfig
83
83
 
84
84
  `adapter` is where oidc-provider persists grants/tokens/codes: `adapters.redis({
85
85
  connection })` requires `@adonisjs/redis` configured; `adapters.database({
86
- connection? })` uses Lucid and stores artifacts in `authkit_oidc_payloads`, which
87
- `ensureAuthkitSchema` creates on boot (with `schema: { autoManage: false }`, call it
88
- from a migration). The table carries an indexed `account_id` column so per-account
86
+ connection?, schema? })` uses Lucid and stores artifacts in `authkit_oidc_payloads`, which
87
+ `ensureAuthkitSchema` creates on boot (with `schema: { autoManage: false }`, call
88
+ `ensureAuthkitSchema(db)` — or `ensureAuthkitSchema(db, { schema })` when `schema.name`
89
+ is set — from a migration). The `schema` defaults to `schema.name` of `defineConfig`.
90
+ The table carries an indexed `account_id` column so per-account
89
91
  session lookups and revocation stay in SQL; rows with a NULL `account_id` are still
90
- found and get classified on read, so no backfill is needed. Works on Postgres,
91
- MySQL and SQLite; expired rows are pruned every 10 minutes per connection. `accountStore` is the identity contract — `lucidAccountStore(AuthUser)`
92
+ found and get classified on read, so no backfill is needed. Without `schema.name` it
93
+ works on Postgres, MySQL and SQLite; with `schema: { name: 'auth' }` every AuthKit
94
+ table lives in that DB schema (`"auth"."auth_settings"`), the boot creates it
95
+ (`CREATE SCHEMA IF NOT EXISTS`; MySQL: `CREATE DATABASE IF NOT EXISTS`), and SQLite
96
+ fails the boot instead of ignoring the option. Never use `searchPath` for this
97
+ (it leaks across clients behind PgBouncer in transaction mode). Expired rows are pruned every 10 minutes per connection. `accountStore` is the identity contract — `lucidAccountStore(AuthUser)`
92
98
  derives `findAccount`/`verifyCredentials` from your user model.
93
99
 
94
100
  Source: `stubs/config/authkit.stub`, `src/define_config.ts` (`AuthServerConfigInput`),
@@ -112,7 +118,7 @@ blob lives); `{ source: 'jwks', keys }` supplies keys inline.
112
118
 
113
119
  The managed keystore's `store` accepts a path shortcut (`store: 'tmp/keystore.json'`)
114
120
  or `{ driver: ... }`: built-in vaults are `file`, `drive` (a `@adonisjs/drive` disk),
115
- `lucid` (table created on first write), `redis`, and `hashicorp`; for cloud secret
121
+ `lucid` (table created on first write; `schema` overrides, default `schema.name`), `redis`, and `hashicorp`; for cloud secret
116
122
  managers install the add-ons `@adonis-agora/authkit-vault-aws` / `-azure` / `-gcp`
117
123
  and pass their `createKeystoreVault(...)` result as `store` (any object with a
118
124
  `read()` method is accepted). With no `store` at all, keys are ephemeral per boot.