@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
@@ -17,8 +17,10 @@ export function buildPasswordHistory(ctx, db) {
17
17
  * Lucid expõe `db.query().from(table)` para SELECT e `db.table(table).insert()` para INSERT.
18
18
  * Para DELETE usamos `db.query().from(table)`.
19
19
  */
20
+ // Lido a cada uso: o `defineConfig` informa o schema depois de o store existir.
21
+ const table = () => ctx.tableName ? ctx.tableName('auth_password_history') : 'auth_password_history';
20
22
  function qFrom() {
21
- return db.query().from('auth_password_history');
23
+ return db.query().from(table());
22
24
  }
23
25
  return {
24
26
  async isPasswordReused(accountId, plainPassword, count, nativeVerify) {
@@ -51,7 +53,7 @@ export function buildPasswordHistory(ctx, db) {
51
53
  },
52
54
  async recordPasswordHistory(accountId, oldHash) {
53
55
  try {
54
- await db.table('auth_password_history').insert({
56
+ await db.table(table()).insert({
55
57
  account_id: accountId,
56
58
  password_hash: oldHash,
57
59
  created_at: DateTime.now().toISO(),
@@ -78,6 +78,17 @@ export interface LucidStoreContext {
78
78
  * recovery da conta. Ver nota de `emailVerificationTtlHours`.
79
79
  */
80
80
  emailChangeTtlHours?: number;
81
+ /**
82
+ * Query client for AuthKit's own tables (`auth_mfa`, `auth_password_history`):
83
+ * `schema.connection` of `defineConfig`, NOT the account model's connection. Absent
84
+ * → the model's connection (a store used without `defineConfig`).
85
+ */
86
+ tablesClient?: () => any;
87
+ /**
88
+ * Nome de uma tabela do authkit como os statements a escrevem: com `schema.name`,
89
+ * `schema.tabela` (o knex cita cada parte). Ausente → o nome puro.
90
+ */
91
+ tableName?: (name: string) => string;
81
92
  }
82
93
  export declare const sha256: (value: string) => string;
83
94
  /** Recovery code legível: 10 chars hex em duas metades (ex.: a1b2c-3d4e5). */
@@ -105,15 +116,17 @@ export interface MfaStatePatch {
105
116
  }
106
117
  /**
107
118
  * Repositório do estado de MFA na tabela LIB-OWNED `auth_mfa`, keyed por
108
- * `account_id`. Acessa o banco pela CONEXÃO do próprio model (`Model.query().client`),
109
- * a mesma usada pelo restante do store — sem exigir migration no host.
119
+ * `account_id`. Acessa o banco pela conexão das tabelas do authkit
120
+ * (`schema.connection`, via `tablesClient`), a mesma em que o `autoManage` cria a
121
+ * tabela — sem exigir migration no host. Sem `tablesClient` (store fora do
122
+ * `defineConfig`), cai na conexão do model.
110
123
  *
111
124
  * O `upsert` é portável (sqlite + pg sem UPSERT nativo): tenta UPDATE pela PK;
112
125
  * se não afetou nenhuma linha, faz INSERT. `recovery_codes` é serializado como
113
126
  * JSON (string) na escrita e desserializado na leitura (aceita tanto string —
114
127
  * sqlite/text — quanto array já parseado — pg jsonb).
115
128
  */
116
- export declare function buildMfaStateRepo(Model: any): {
129
+ export declare function buildMfaStateRepo(Model: any, tablesClient?: () => any, tableName?: (name: string) => string): {
117
130
  /** Lê o estado de MFA da conta; null se não há linha em `auth_mfa`. */
118
131
  read(accountId: string): Promise<MfaState | null>;
119
132
  /**
@@ -39,19 +39,22 @@ export function hashesEqual(a, b) {
39
39
  }
40
40
  /**
41
41
  * Repositório do estado de MFA na tabela LIB-OWNED `auth_mfa`, keyed por
42
- * `account_id`. Acessa o banco pela CONEXÃO do próprio model (`Model.query().client`),
43
- * a mesma usada pelo restante do store — sem exigir migration no host.
42
+ * `account_id`. Acessa o banco pela conexão das tabelas do authkit
43
+ * (`schema.connection`, via `tablesClient`), a mesma em que o `autoManage` cria a
44
+ * tabela — sem exigir migration no host. Sem `tablesClient` (store fora do
45
+ * `defineConfig`), cai na conexão do model.
44
46
  *
45
47
  * O `upsert` é portável (sqlite + pg sem UPSERT nativo): tenta UPDATE pela PK;
46
48
  * se não afetou nenhuma linha, faz INSERT. `recovery_codes` é serializado como
47
49
  * JSON (string) na escrita e desserializado na leitura (aceita tanto string —
48
50
  * sqlite/text — quanto array já parseado — pg jsonb).
49
51
  */
50
- export function buildMfaStateRepo(Model) {
51
- const TABLE = 'auth_mfa';
52
- /** QueryClient ligado à conexão do model (equivale ao `db` do password_history). */
52
+ export function buildMfaStateRepo(Model, tablesClient, tableName = (name) => name) {
53
+ // Lido a cada uso: o `defineConfig` informa o schema depois de o store existir.
54
+ const table = () => tableName('auth_mfa');
55
+ /** QueryClient das tabelas do authkit (equivale ao `db` do password_history). */
53
56
  function client() {
54
- return Model.query().client;
57
+ return tablesClient ? tablesClient() : Model.query().client;
55
58
  }
56
59
  /** Normaliza `recovery_codes` vindo do banco (json string OU array já parseado). */
57
60
  function parseRecoveryCodes(raw) {
@@ -90,7 +93,7 @@ export function buildMfaStateRepo(Model) {
90
93
  return {
91
94
  /** Lê o estado de MFA da conta; null se não há linha em `auth_mfa`. */
92
95
  async read(accountId) {
93
- const row = await client().from(TABLE).where('account_id', accountId).first();
96
+ const row = await client().from(table()).where('account_id', accountId).first();
94
97
  if (!row)
95
98
  return null;
96
99
  return {
@@ -124,17 +127,17 @@ export function buildMfaStateRepo(Model) {
124
127
  }
125
128
  if ('lastTotpStep' in patch)
126
129
  update.last_totp_step = patch.lastTotpStep ?? null;
127
- const affected = await client().from(TABLE).where('account_id', accountId).update(update);
130
+ const affected = await client().from(table()).where('account_id', accountId).update(update);
128
131
  // knex update retorna o nº de linhas afetadas (0 = linha ainda não existe).
129
132
  if (!affected) {
130
133
  await client()
131
- .table(TABLE)
134
+ .table(table())
132
135
  .insert({ account_id: accountId, ...update });
133
136
  }
134
137
  },
135
138
  /** Remove o estado de MFA da conta (disable / reset total). No-op se ausente. */
136
139
  async clear(accountId) {
137
- await client().from(TABLE).where('account_id', accountId).delete();
140
+ await client().from(table()).where('account_id', accountId).delete();
138
141
  },
139
142
  };
140
143
  }
@@ -8,7 +8,7 @@ export function buildWebauthn(ctx, Credential, webauthn, ceremonies) {
8
8
  const { Model } = ctx;
9
9
  // Estado de MFA é LIB-OWNED (tabela `auth_mfa`): registrar uma passkey habilita
10
10
  // o MFA gravando `mfa_enabled_at` em `auth_mfa`, não numa coluna do model.
11
- const mfaState = buildMfaStateRepo(Model);
11
+ const mfaState = buildMfaStateRepo(Model, ctx.tablesClient, ctx.tableName);
12
12
  return {
13
13
  async generatePasskeyRegistrationOptions(accountId) {
14
14
  const row = await Model.find(accountId);
@@ -1,10 +1,16 @@
1
1
  import type { Database } from '@adonisjs/lucid/database';
2
2
  import type { EnumeratedArtifact, OidcAdapter, OidcPayload } from './adapter_contract.js';
3
+ /** Nome da tabela (sem schema). O adapter recebe o nome já qualificado pelo factory. */
4
+ export declare const OIDC_PAYLOADS_TABLE = "authkit_oidc_payloads";
3
5
  export declare class DatabaseAdapter implements OidcAdapter {
4
6
  #private;
5
7
  private name;
6
8
  private db;
7
- constructor(name: string, db: Database);
9
+ /** `authkit_oidc_payloads`, qualificada com `schema.name` quando há schema. */
10
+ private table;
11
+ constructor(name: string, db: Database,
12
+ /** `authkit_oidc_payloads`, qualificada com `schema.name` quando há schema. */
13
+ table?: string);
8
14
  upsert(id: string, payload: OidcPayload, expiresIn: number): Promise<void>;
9
15
  renewSession(id: string, expiresIn: number): Promise<boolean>;
10
16
  find(id: string): Promise<OidcPayload | undefined>;
@@ -1,4 +1,5 @@
1
- const TABLE = 'authkit_oidc_payloads';
1
+ /** Nome da tabela (sem schema). O adapter recebe o nome já qualificado pelo factory. */
2
+ export const OIDC_PAYLOADS_TABLE = 'authkit_oidc_payloads';
2
3
  /** Intervalo mínimo, por conexão, entre duas limpezas das linhas expiradas. */
3
4
  const PRUNE_INTERVAL_MS = 10 * 60_000;
4
5
  /** Quanto tempo a ausência da coluna `account_id` vale antes de re-checar. */
@@ -6,11 +7,17 @@ const ACCOUNT_COLUMN_RECHECK_MS = 60_000;
6
7
  /** Tamanho máximo de uma lista `IN (...)`. */
7
8
  const IN_CHUNK = 500;
8
9
  const connectionStates = new WeakMap();
9
- function connectionState(db) {
10
- let state = connectionStates.get(db);
10
+ /** Estado por conexão E tabela (duas instalações em schemas diferentes não se misturam). */
11
+ function connectionState(db, table) {
12
+ let byTable = connectionStates.get(db);
13
+ if (!byTable) {
14
+ byTable = new Map();
15
+ connectionStates.set(db, byTable);
16
+ }
17
+ let state = byTable.get(table);
11
18
  if (!state) {
12
19
  state = { accountColumn: null, accountColumnRecheckAt: 0, nextPruneAt: 0 };
13
- connectionStates.set(db, state);
20
+ byTable.set(table, state);
14
21
  }
15
22
  return state;
16
23
  }
@@ -29,12 +36,16 @@ function chunks(items) {
29
36
  export class DatabaseAdapter {
30
37
  name;
31
38
  db;
32
- constructor(name, db) {
39
+ table;
40
+ constructor(name, db,
41
+ /** `authkit_oidc_payloads`, qualificada com `schema.name` quando há schema. */
42
+ table = OIDC_PAYLOADS_TABLE) {
33
43
  this.name = name;
34
44
  this.db = db;
45
+ this.table = table;
35
46
  }
36
47
  #query() {
37
- return this.db.query().from(TABLE).where('model_name', this.name);
48
+ return this.db.query().from(this.table).where('model_name', this.name);
38
49
  }
39
50
  /**
40
51
  * Valor de `expires_at` (gravação e comparação) para o dialeto da conexão. No
@@ -43,7 +54,7 @@ export class DatabaseAdapter {
43
54
  * driver serializa no formato do banco — o MySQL recusa o ISO com `Z`.
44
55
  */
45
56
  #timestamp(ms) {
46
- const state = connectionState(this.db);
57
+ const state = connectionState(this.db, this.table);
47
58
  if (state.dialect === undefined) {
48
59
  // O factory passa o manager (`Database`) ou um `QueryClient` de conexão nomeada.
49
60
  const db = this.db;
@@ -66,13 +77,13 @@ export class DatabaseAdapter {
66
77
  * re-checado, para pegar o ALTER do boot que rodou depois do primeiro request.
67
78
  */
68
79
  async #hasAccountColumn() {
69
- const state = connectionState(this.db);
80
+ const state = connectionState(this.db, this.table);
70
81
  if (state.accountColumn === true)
71
82
  return true;
72
83
  if (state.accountColumn === false && state.accountColumnRecheckAt > Date.now())
73
84
  return false;
74
85
  try {
75
- await this.db.query().from(TABLE).select('account_id').limit(1);
86
+ await this.db.query().from(this.table).select('account_id').limit(1);
76
87
  state.accountColumn = true;
77
88
  }
78
89
  catch {
@@ -88,7 +99,7 @@ export class DatabaseAdapter {
88
99
  * do caminho do request e nunca lança.
89
100
  */
90
101
  #maybePrune() {
91
- const state = connectionState(this.db);
102
+ const state = connectionState(this.db, this.table);
92
103
  const now = Date.now();
93
104
  if (state.nextPruneAt > now)
94
105
  return;
@@ -96,7 +107,7 @@ export class DatabaseAdapter {
96
107
  Promise.resolve()
97
108
  .then(() => this.db
98
109
  .query()
99
- .from(TABLE)
110
+ .from(this.table)
100
111
  .whereNotNull('expires_at')
101
112
  .where('expires_at', '<=', this.#timestamp(now))
102
113
  .delete())
@@ -125,7 +136,7 @@ export class DatabaseAdapter {
125
136
  await this.#query().where('id', id).update(row);
126
137
  }
127
138
  else {
128
- await this.db.table(TABLE).insert(row);
139
+ await this.db.table(this.table).insert(row);
129
140
  }
130
141
  this.#maybePrune();
131
142
  }
@@ -185,7 +196,7 @@ export class DatabaseAdapter {
185
196
  await this.#query().where('id', id).delete();
186
197
  }
187
198
  async revokeByGrantId(grantId) {
188
- await this.db.query().from(TABLE).where('grant_id', grantId).delete();
199
+ await this.db.query().from(this.table).where('grant_id', grantId).delete();
189
200
  }
190
201
  /**
191
202
  * Enumeração genérica dos artefatos do model deste adapter (id + payload).
@@ -1,8 +1,18 @@
1
1
  import type { ApplicationService } from '@adonisjs/core/types';
2
2
  import type { OidcAdapter } from './adapter_contract.js';
3
3
  export type OidcAdapterClass = new (name: string) => OidcAdapter;
4
+ /** What `defineConfig` hands every adapter factory. */
5
+ export interface AdapterResolveContext {
6
+ /**
7
+ * `schema.connection` of `defineConfig`: the Lucid connection AuthKit's tables live
8
+ * on. A database adapter without its own `connection` uses it.
9
+ */
10
+ connection?: string;
11
+ /** `schema.name` of `defineConfig`. A database adapter without its own `schema` uses it. */
12
+ schema?: string;
13
+ }
4
14
  export interface AdapterFactory {
5
- resolver(app: ApplicationService): Promise<OidcAdapterClass>;
15
+ resolver(app: ApplicationService, context?: AdapterResolveContext): Promise<OidcAdapterClass>;
6
16
  }
7
17
  /**
8
18
  * Modelos do oidc-provider de vida curta, amarrados ao ciclo da sessão de
@@ -35,8 +45,16 @@ export interface RedisAdapterConfig {
35
45
  prefix?: string;
36
46
  }
37
47
  export interface DatabaseAdapterConfig {
38
- /** nome da conexão Lucid (default: a primária) */
48
+ /**
49
+ * Nome da conexão Lucid. Default: `schema.connection` do `defineConfig` (a conexão
50
+ * de todas as tabelas do authkit), e sem ela a primária.
51
+ */
39
52
  connection?: string;
53
+ /**
54
+ * Schema do banco da tabela `authkit_oidc_payloads` (MySQL: o database). Default:
55
+ * `schema.name` do `defineConfig`. Vai escrito em cada statement — nunca via searchPath.
56
+ */
57
+ schema?: string;
40
58
  }
41
59
  export declare const adapters: {
42
60
  /**
@@ -1,4 +1,5 @@
1
- import { DatabaseAdapter } from './database_adapter.js';
1
+ import { assertSchemaSupported, authkitTable, validateSchemaName, } from '../schema/tables_location.js';
2
+ import { DatabaseAdapter, OIDC_PAYLOADS_TABLE } from './database_adapter.js';
2
3
  import { RedisAdapter } from './redis_adapter.js';
3
4
  /**
4
5
  * Modelos do oidc-provider de vida curta, amarrados ao ciclo da sessão de
@@ -67,13 +68,16 @@ export const adapters = {
67
68
  */
68
69
  database(config = {}) {
69
70
  return {
70
- async resolver(app) {
71
+ async resolver(app, context) {
71
72
  const db = await app.container.make('lucid.db');
72
- const connection = config.connection;
73
+ const connection = config.connection ?? context?.connection;
74
+ const schema = validateSchemaName(config.schema ?? context?.schema, 'adapters.database({ schema })');
73
75
  const conn = connection ? db.connection(connection) : db;
76
+ assertSchemaSupported(connection ? conn : db.connection(), schema);
77
+ const table = authkitTable({ schema }, OIDC_PAYLOADS_TABLE);
74
78
  return class extends DatabaseAdapter {
75
79
  constructor(name) {
76
- super(name, conn);
80
+ super(name, conn, table);
77
81
  }
78
82
  };
79
83
  },
@@ -46,8 +46,12 @@ export interface LiveGrantFilter {
46
46
  export declare class DelegationStore {
47
47
  #private;
48
48
  private conn;
49
+ /** Nome da tabela como os statements o escrevem (`schema.tabela` com `schema.name`). */
50
+ private t;
49
51
  /** `conn` = uma conexão Lucid (`db.connection(name?)`) — ou uma transação. */
50
- constructor(conn: () => any);
52
+ constructor(conn: () => any,
53
+ /** Nome da tabela como os statements o escrevem (`schema.tabela` com `schema.name`). */
54
+ t?: (name: string) => string);
51
55
  insertDevice(row: Omit<DeviceCodeRow, 'accountId' | 'grantId' | 'lastPolledAt'>): Promise<void>;
52
56
  findDeviceByCodeHash(hash: string): Promise<DeviceCodeRow | null>;
53
57
  findDeviceByUserCode(userCode: string): Promise<DeviceCodeRow | null>;
@@ -63,13 +63,17 @@ function affected(result) {
63
63
  }
64
64
  export class DelegationStore {
65
65
  conn;
66
+ t;
66
67
  /** `conn` = uma conexão Lucid (`db.connection(name?)`) — ou uma transação. */
67
- constructor(conn) {
68
+ constructor(conn,
69
+ /** Nome da tabela como os statements o escrevem (`schema.tabela` com `schema.name`). */
70
+ t = (name) => name) {
68
71
  this.conn = conn;
72
+ this.t = t;
69
73
  }
70
74
  // ─── device codes ─────────────────────────────────────────────────────────
71
75
  async insertDevice(row) {
72
- await this.conn().table(DEVICE_TABLE).insert({
76
+ await this.conn().table(this.t(DEVICE_TABLE)).insert({
73
77
  id: row.id,
74
78
  device_code_hash: row.deviceCodeHash,
75
79
  user_code: row.userCode,
@@ -83,19 +87,22 @@ export class DelegationStore {
83
87
  });
84
88
  }
85
89
  async findDeviceByCodeHash(hash) {
86
- const row = await this.conn().from(DEVICE_TABLE).where('device_code_hash', hash).first();
90
+ const row = await this.conn()
91
+ .from(this.t(DEVICE_TABLE))
92
+ .where('device_code_hash', hash)
93
+ .first();
87
94
  return row ? toDevice(row) : null;
88
95
  }
89
96
  async findDeviceByUserCode(userCode) {
90
- const row = await this.conn().from(DEVICE_TABLE).where('user_code', userCode).first();
97
+ const row = await this.conn().from(this.t(DEVICE_TABLE)).where('user_code', userCode).first();
91
98
  return row ? toDevice(row) : null;
92
99
  }
93
100
  /** Liga o pedido aprovado ao grant — a partir daqui o polling do agente recebe o token. */
94
101
  async setDeviceGrant(id, grantId) {
95
- await this.conn().from(DEVICE_TABLE).where('id', id).update({ grant_id: grantId });
102
+ await this.conn().from(this.t(DEVICE_TABLE)).where('id', id).update({ grant_id: grantId });
96
103
  }
97
104
  async markPolled(id, at) {
98
- await this.conn().from(DEVICE_TABLE).where('id', id).update({ last_polled_at: at });
105
+ await this.conn().from(this.t(DEVICE_TABLE)).where('id', id).update({ last_polled_at: at });
99
106
  }
100
107
  /**
101
108
  * Transição de estado ATÔMICA: só aplica se o pedido ainda está em `from`.
@@ -108,7 +115,7 @@ export class DelegationStore {
108
115
  if (patch.grantId !== undefined)
109
116
  update.grant_id = patch.grantId;
110
117
  const result = await this.conn()
111
- .from(DEVICE_TABLE)
118
+ .from(this.t(DEVICE_TABLE))
112
119
  .where('id', id)
113
120
  .where('status', from)
114
121
  .update(update);
@@ -119,11 +126,11 @@ export class DelegationStore {
119
126
  * vai pelo mesmo binding do INSERT, então a comparação é coerente no dialeto.
120
127
  */
121
128
  async deleteDevicesExpiredBefore(before) {
122
- await this.conn().from(DEVICE_TABLE).where('expires_at', '<', before).delete();
129
+ await this.conn().from(this.t(DEVICE_TABLE)).where('expires_at', '<', before).delete();
123
130
  }
124
131
  // ─── grants ───────────────────────────────────────────────────────────────
125
132
  async insertGrant(row) {
126
- await this.conn().table(GRANT_TABLE).insert({
133
+ await this.conn().table(this.t(GRANT_TABLE)).insert({
127
134
  id: row.id,
128
135
  account_id: row.accountId,
129
136
  client_id: row.clientId,
@@ -135,13 +142,13 @@ export class DelegationStore {
135
142
  });
136
143
  }
137
144
  async findGrant(id) {
138
- const row = await this.conn().from(GRANT_TABLE).where('id', id).first();
145
+ const row = await this.conn().from(this.t(GRANT_TABLE)).where('id', id).first();
139
146
  return row ? toGrant(row) : null;
140
147
  }
141
148
  /** Grants NÃO revogados da tripla (conta, agente, usuário do agente). */
142
149
  async findGrantsFor(accountId, clientId, agentSub) {
143
150
  const rows = await this.conn()
144
- .from(GRANT_TABLE)
151
+ .from(this.t(GRANT_TABLE))
145
152
  .where('account_id', accountId)
146
153
  .where('client_id', clientId)
147
154
  .where('agent_sub', agentSub)
@@ -151,7 +158,7 @@ export class DelegationStore {
151
158
  /** Atualiza um grant NÃO revogado. `false` = revogado (ou sumiu) nesse meio-tempo. */
152
159
  async updateGrant(id, patch) {
153
160
  const result = await this.conn()
154
- .from(GRANT_TABLE)
161
+ .from(this.t(GRANT_TABLE))
155
162
  .where('id', id)
156
163
  .whereNull('revoked_at')
157
164
  .update({ scope: patch.scope, expires_at: patch.expiresAt, updated_at: patch.updatedAt });
@@ -159,7 +166,7 @@ export class DelegationStore {
159
166
  }
160
167
  async listGrants(accountId) {
161
168
  const rows = await this.conn()
162
- .from(GRANT_TABLE)
169
+ .from(this.t(GRANT_TABLE))
163
170
  .where('account_id', accountId)
164
171
  .whereNull('revoked_at')
165
172
  .orderBy('created_at', 'desc');
@@ -168,7 +175,7 @@ export class DelegationStore {
168
175
  /** Revoga um grant DA CONTA. `false` = não existe, é de outra conta ou já revogado. */
169
176
  async revokeGrant(accountId, id, at) {
170
177
  const result = await this.conn()
171
- .from(GRANT_TABLE)
178
+ .from(this.t(GRANT_TABLE))
172
179
  .where('id', id)
173
180
  .where('account_id', accountId)
174
181
  .whereNull('revoked_at')
@@ -177,7 +184,7 @@ export class DelegationStore {
177
184
  }
178
185
  async revokeAllGrants(accountId, at) {
179
186
  const result = await this.conn()
180
- .from(GRANT_TABLE)
187
+ .from(this.t(GRANT_TABLE))
181
188
  .where('account_id', accountId)
182
189
  .whereNull('revoked_at')
183
190
  .update({ revoked_at: at, updated_at: at });
@@ -189,7 +196,10 @@ export class DelegationStore {
189
196
  * protocolo sai do prefixo do id: `agrant_` (PACT) ou o `grantIdPrefix` de uma extension.
190
197
  */
191
198
  #live(now, filter = {}) {
192
- let q = this.conn().from(GRANT_TABLE).whereNull('revoked_at').where('expires_at', '>', now);
199
+ let q = this.conn()
200
+ .from(this.t(GRANT_TABLE))
201
+ .whereNull('revoked_at')
202
+ .where('expires_at', '>', now);
193
203
  // O prefixo é `[a-z0-9]+_`: o `_` do LIKE (um caractere qualquer) casa com ele mesmo.
194
204
  if (filter.idPrefix)
195
205
  q = q.where('id', 'like', `${filter.idPrefix}%`);
@@ -222,7 +232,7 @@ export class DelegationStore {
222
232
  /** Ids dos grants não revogados de um agente (vivos ou não) — para revogar todos. */
223
233
  async unrevokedGrantIdsOfClient(clientId) {
224
234
  const rows = await this.conn()
225
- .from(GRANT_TABLE)
235
+ .from(this.t(GRANT_TABLE))
226
236
  .where('client_id', clientId)
227
237
  .whereNull('revoked_at')
228
238
  .select('id');
@@ -231,14 +241,14 @@ export class DelegationStore {
231
241
  /** Revoga um grant sem checar a conta — reação a reuso de refresh token. */
232
242
  async revokeGrantById(id, at) {
233
243
  await this.conn()
234
- .from(GRANT_TABLE)
244
+ .from(this.t(GRANT_TABLE))
235
245
  .where('id', id)
236
246
  .whereNull('revoked_at')
237
247
  .update({ revoked_at: at, updated_at: at });
238
248
  }
239
249
  // ─── refresh tokens ───────────────────────────────────────────────────────
240
250
  async insertRefreshToken(row) {
241
- await this.conn().table(REFRESH_TABLE).insert({
251
+ await this.conn().table(this.t(REFRESH_TABLE)).insert({
242
252
  token_hash: row.tokenHash,
243
253
  grant_id: row.grantId,
244
254
  expires_at: row.expiresAt,
@@ -246,7 +256,10 @@ export class DelegationStore {
246
256
  });
247
257
  }
248
258
  async findRefreshToken(tokenHash) {
249
- const row = await this.conn().from(REFRESH_TABLE).where('token_hash', tokenHash).first();
259
+ const row = await this.conn()
260
+ .from(this.t(REFRESH_TABLE))
261
+ .where('token_hash', tokenHash)
262
+ .first();
250
263
  return row
251
264
  ? {
252
265
  grantId: row.grant_id,
@@ -258,7 +271,7 @@ export class DelegationStore {
258
271
  /** Marca o refresh como usado (uso único, atômico). `false` = já tinha sido usado. */
259
272
  async useRefreshToken(tokenHash, at) {
260
273
  const result = await this.conn()
261
- .from(REFRESH_TABLE)
274
+ .from(this.t(REFRESH_TABLE))
262
275
  .where('token_hash', tokenHash)
263
276
  .whereNull('used_at')
264
277
  .update({ used_at: at });
@@ -27,10 +27,18 @@ import type { HttpContext } from '@adonisjs/core/http';
27
27
  * A lib-owned table: created when missing, new columns added with `ALTER ADD`, never dropped or
28
28
  * altered. The builders receive a Knex `TableBuilder`.
29
29
  */
30
+ /** What {@link AuthkitTable.create} receives next to the table builder. */
31
+ export interface AuthkitTableContext {
32
+ /** `name` as AuthKit's tables are addressed: `schema.name` + `.` + `name` when a schema is set. */
33
+ table(name: string): string;
34
+ }
30
35
  export interface AuthkitTable {
31
36
  name: string;
32
- /** Full builder — runs when the table does not exist. */
33
- create: (table: any) => void;
37
+ /**
38
+ * Full builder — runs when the table does not exist. A foreign key into another lib-owned
39
+ * table names it through `context.table(name)` (qualified with `schema.name` when set).
40
+ */
41
+ create: (table: any, context: AuthkitTableContext) => void;
34
42
  /**
35
43
  * One builder per column — runs (`ALTER ADD`) when the table exists but the column does not.
36
44
  * No constraints that break on populated tables (unique/FK belong in `create`).
@@ -125,6 +133,12 @@ export interface PersonalAgentHost {
125
133
  readonly issuer: string;
126
134
  /** The Lucid connection AuthKit's tables live on (`schema.connection`). */
127
135
  db(): any;
136
+ /**
137
+ * `name` as every statement must write it: `schema.name` + `.` + `name` when the app puts
138
+ * AuthKit's tables in a database schema (knex quotes each part), else `name`. Name every
139
+ * lib-owned table through it — the shared ledger tables and the extension's own.
140
+ */
141
+ table(name: string): string;
128
142
  readonly grants: PersonalAgentGrantLedger;
129
143
  /** The admin console's verdict on an agent of this protocol. `null` = not listed. */
130
144
  registryStatus(identifier: string): Promise<'allowed' | 'blocked' | null>;
@@ -22,6 +22,7 @@ export declare class AgentRegistryStore {
22
22
  #private;
23
23
  constructor(conn: () => any, options?: {
24
24
  now?: () => number;
25
+ table?: (name: string) => string;
25
26
  });
26
27
  /** Todas as linhas, mais recentes primeiro. Lê do banco (console). */
27
28
  list(): Promise<AgentRegistryRow[]>;
@@ -47,13 +47,15 @@ export class AgentRegistryStore {
47
47
  #now;
48
48
  #snapshot = null;
49
49
  #loading = null;
50
+ #table;
50
51
  constructor(conn, options = {}) {
51
52
  this.#conn = conn;
53
+ this.#table = (options.table ?? ((name) => name))(REGISTRY_TABLE);
52
54
  this.#now = options.now ?? Date.now;
53
55
  }
54
56
  /** Todas as linhas, mais recentes primeiro. Lê do banco (console). */
55
57
  async list() {
56
- const rows = await this.#conn().from(REGISTRY_TABLE).orderBy('updated_at', 'desc');
58
+ const rows = await this.#conn().from(this.#table).orderBy('updated_at', 'desc');
57
59
  return rows.map(toRow);
58
60
  }
59
61
  /** A linha do agente, pelo snapshot. `null` = o console não diz nada sobre ele. */
@@ -61,14 +63,14 @@ export class AgentRegistryStore {
61
63
  return (await this.#rows()).get(key(protocol, identifier)) ?? null;
62
64
  }
63
65
  async findById(id) {
64
- const row = await this.#conn().from(REGISTRY_TABLE).where('id', id).first();
66
+ const row = await this.#conn().from(this.#table).where('id', id).first();
65
67
  return row ? toRow(row) : null;
66
68
  }
67
69
  /** Cria ou troca a linha de (protocolo, identificador). */
68
70
  async upsert(input) {
69
71
  const now = new Date(this.#now());
70
72
  const existing = await this.#conn()
71
- .from(REGISTRY_TABLE)
73
+ .from(this.#table)
72
74
  .where('protocol', input.protocol)
73
75
  .where('identifier', input.identifier)
74
76
  .first();
@@ -80,11 +82,11 @@ export class AgentRegistryStore {
80
82
  updated_at: now,
81
83
  };
82
84
  if (existing) {
83
- await this.#conn().from(REGISTRY_TABLE).where('id', existing.id).update(values);
85
+ await this.#conn().from(this.#table).where('id', existing.id).update(values);
84
86
  }
85
87
  else {
86
88
  await this.#conn()
87
- .table(REGISTRY_TABLE)
89
+ .table(this.#table)
88
90
  .insert({
89
91
  id: `areg_${randomBytes(12).toString('base64url')}`,
90
92
  protocol: input.protocol,
@@ -95,14 +97,14 @@ export class AgentRegistryStore {
95
97
  }
96
98
  this.invalidate();
97
99
  const row = await this.#conn()
98
- .from(REGISTRY_TABLE)
100
+ .from(this.#table)
99
101
  .where('protocol', input.protocol)
100
102
  .where('identifier', input.identifier)
101
103
  .first();
102
104
  return toRow(row);
103
105
  }
104
106
  async delete(id) {
105
- const result = await this.#conn().from(REGISTRY_TABLE).where('id', id).delete();
107
+ const result = await this.#conn().from(this.#table).where('id', id).delete();
106
108
  this.invalidate();
107
109
  return (typeof result === 'number' ? result : Array.isArray(result) ? result.length : 0) > 0;
108
110
  }
@@ -116,7 +118,7 @@ export class AgentRegistryStore {
116
118
  if (!this.#loading) {
117
119
  this.#loading = (async () => {
118
120
  try {
119
- const rows = (await this.#conn().from(REGISTRY_TABLE)).map(toRow);
121
+ const rows = (await this.#conn().from(this.#table)).map(toRow);
120
122
  const map = new Map(rows.map((r) => [key(r.protocol, r.identifier), r]));
121
123
  this.#snapshot = { rows: map, expiresAt: this.#now() + SNAPSHOT_TTL_MS };
122
124
  return map;