@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.
- package/CHANGELOG.md +20 -0
- package/README.md +1 -1
- package/build/index.d.ts +2 -1
- package/build/index.js +1 -0
- package/build/providers/authkit_server_provider.js +15 -4
- package/build/src/accounts/lucid_account_store.js +4 -3
- package/build/src/accounts/lucid_store/mfa.js +1 -1
- package/build/src/accounts/lucid_store/organization_models.d.ts +9 -5
- package/build/src/accounts/lucid_store/organization_models.js +16 -12
- package/build/src/accounts/lucid_store/password_hygiene.js +4 -2
- package/build/src/accounts/lucid_store/shared.d.ts +6 -1
- package/build/src/accounts/lucid_store/shared.js +7 -6
- package/build/src/accounts/lucid_store/webauthn.js +1 -1
- package/build/src/adapters/database_adapter.d.ts +7 -1
- package/build/src/adapters/database_adapter.js +24 -13
- package/build/src/adapters/factory.d.ts +7 -0
- package/build/src/adapters/factory.js +6 -2
- package/build/src/agents/delegation_store.d.ts +5 -1
- package/build/src/agents/delegation_store.js +34 -21
- package/build/src/agents/extension.d.ts +16 -2
- package/build/src/agents/registry_store.d.ts +1 -0
- package/build/src/agents/registry_store.js +10 -8
- package/build/src/agents/runtime.js +6 -2
- package/build/src/commands/expire_scan_command.js +2 -3
- package/build/src/commands/settings_commands.js +4 -4
- package/build/src/define_config.d.ts +8 -0
- package/build/src/define_config.js +23 -6
- package/build/src/doctor/agora_checks.js +13 -0
- package/build/src/doctor/db_schema_check.d.ts +6 -0
- package/build/src/doctor/db_schema_check.js +87 -0
- package/build/src/host/admin_sessions_service.js +6 -2
- package/build/src/host/runtime_settings.d.ts +15 -0
- package/build/src/host/runtime_settings.js +27 -12
- package/build/src/keys/keystore_manager.js +2 -1
- package/build/src/keys/keystore_vault.d.ts +9 -1
- package/build/src/keys/keystore_vault.js +27 -9
- package/build/src/schema/ensure.d.ts +9 -0
- package/build/src/schema/ensure.js +46 -19
- package/build/src/schema/tables_location.d.ts +31 -2
- package/build/src/schema/tables_location.js +64 -0
- package/package.json +3 -3
- package/skills/authkit-idp-setup/SKILL.md +13 -7
|
@@ -63,13 +63,17 @@ function affected(result) {
|
|
|
63
63
|
}
|
|
64
64
|
export class DelegationStore {
|
|
65
65
|
conn;
|
|
66
|
+
t;
|
|
66
67
|
/** `conn` = uma conexão Lucid (`db.connection(name?)`) — ou uma transação. */
|
|
67
|
-
constructor(conn
|
|
68
|
+
constructor(conn,
|
|
69
|
+
/** Nome da tabela como os statements o escrevem (`schema.tabela` com `schema.name`). */
|
|
70
|
+
t = (name) => name) {
|
|
68
71
|
this.conn = conn;
|
|
72
|
+
this.t = t;
|
|
69
73
|
}
|
|
70
74
|
// ─── device codes ─────────────────────────────────────────────────────────
|
|
71
75
|
async insertDevice(row) {
|
|
72
|
-
await this.conn().table(DEVICE_TABLE).insert({
|
|
76
|
+
await this.conn().table(this.t(DEVICE_TABLE)).insert({
|
|
73
77
|
id: row.id,
|
|
74
78
|
device_code_hash: row.deviceCodeHash,
|
|
75
79
|
user_code: row.userCode,
|
|
@@ -83,19 +87,22 @@ export class DelegationStore {
|
|
|
83
87
|
});
|
|
84
88
|
}
|
|
85
89
|
async findDeviceByCodeHash(hash) {
|
|
86
|
-
const row = await this.conn()
|
|
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()
|
|
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()
|
|
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
|
-
/**
|
|
33
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
121
|
+
const rows = (await this.#conn().from(this.#table)).map(toRow);
|
|
120
122
|
const map = new Map(rows.map((r) => [key(r.protocol, r.identifier), r]));
|
|
121
123
|
this.#snapshot = { rows: map, expiresAt: this.#now() + SNAPSHOT_TTL_MS };
|
|
122
124
|
return map;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getBootedApp } from '../../services/booted_app.js';
|
|
2
2
|
import { supportsAccountStatus } from '../accounts/account_store.js';
|
|
3
3
|
import { findMcpResource } from '../mcp/mcp_oauth.js';
|
|
4
|
+
import { authkitTable } from '../schema/tables_location.js';
|
|
4
5
|
import { PersonalAgentVerifier } from './agent_identity.js';
|
|
5
6
|
import { PersonalAgentDelegation } from './delegation_service.js';
|
|
6
7
|
import { DelegationStore } from './delegation_store.js';
|
|
@@ -156,8 +157,10 @@ export function buildPersonalAgentsRuntime(service, makeDb, options = {}) {
|
|
|
156
157
|
conn = null;
|
|
157
158
|
}
|
|
158
159
|
}
|
|
159
|
-
|
|
160
|
-
const
|
|
160
|
+
// Com `schema.name`, cada statement escreve `schema.tabela`.
|
|
161
|
+
const tableName = (name) => authkitTable({ schema: service.config.schema.name }, name);
|
|
162
|
+
const store = conn ? new DelegationStore(conn, tableName) : null;
|
|
163
|
+
const registry = conn ? new AgentRegistryStore(conn, { table: tableName }) : null;
|
|
161
164
|
const effective = registry ? withRegistry(config, registry) : config;
|
|
162
165
|
let delegation = null;
|
|
163
166
|
if (config.delegation && store) {
|
|
@@ -179,6 +182,7 @@ export function buildPersonalAgentsRuntime(service, makeDb, options = {}) {
|
|
|
179
182
|
protocol: extension.id,
|
|
180
183
|
issuer: service.config.issuer,
|
|
181
184
|
db: () => db(),
|
|
185
|
+
table: tableName,
|
|
182
186
|
grants: scopedLedger(store, extension.grantIdPrefix),
|
|
183
187
|
registryStatus: async (identifier) => (await registry.find(extension.id, identifier))?.status ?? null,
|
|
184
188
|
deriveSecret: (label) => service.deriveSecret(label),
|
|
@@ -27,7 +27,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
27
27
|
}
|
|
28
28
|
return path;
|
|
29
29
|
};
|
|
30
|
-
import { RuntimeSettings } from '../host/runtime_settings.js';
|
|
30
|
+
import { RuntimeSettings, settingsLocation } from '../host/runtime_settings.js';
|
|
31
31
|
import { resolveEffectiveAccountExpiration } from '../host/runtime_toggles.js';
|
|
32
32
|
import { ADMIN_LIST_DEFAULT_SIZE, LIST_FIRST_PAGE } from '../pagination.js';
|
|
33
33
|
import { resolveAuthkitConfig } from './resolve_config.js';
|
|
@@ -54,8 +54,7 @@ export async function runExpireScan(app, opts) {
|
|
|
54
54
|
let runtimeSettings = null;
|
|
55
55
|
try {
|
|
56
56
|
const db = await app.container.make('lucid.db');
|
|
57
|
-
|
|
58
|
-
runtimeSettings = new RuntimeSettings(db, connection ? { connection } : {});
|
|
57
|
+
runtimeSettings = new RuntimeSettings(db, settingsLocation(cfg));
|
|
59
58
|
}
|
|
60
59
|
catch {
|
|
61
60
|
// Sem DB → usa defaults.
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*
|
|
15
15
|
* Todas suportam --json para output machine-readable.
|
|
16
16
|
*/
|
|
17
|
-
import { RuntimeSettings } from '../host/runtime_settings.js';
|
|
17
|
+
import { RuntimeSettings, settingsLocation } from '../host/runtime_settings.js';
|
|
18
18
|
import { SETTING_KEYS } from '../host/runtime_toggles.js';
|
|
19
19
|
import { resolveAuthkitConfig } from './resolve_config.js';
|
|
20
20
|
// ---------------------------------------------------------------------------
|
|
@@ -132,15 +132,15 @@ async function resolveSettingsService(app) {
|
|
|
132
132
|
try {
|
|
133
133
|
const db = await app.container.make('lucid.db');
|
|
134
134
|
// `auth_settings` vive na conexão das tabelas do authkit (`schema.connection`).
|
|
135
|
-
let
|
|
135
|
+
let location = {};
|
|
136
136
|
try {
|
|
137
137
|
const cfg = await resolveCfg(app);
|
|
138
|
-
|
|
138
|
+
location = settingsLocation(cfg);
|
|
139
139
|
}
|
|
140
140
|
catch {
|
|
141
141
|
// Silencioso: sem config → default connection.
|
|
142
142
|
}
|
|
143
|
-
return new RuntimeSettings(db,
|
|
143
|
+
return new RuntimeSettings(db, location);
|
|
144
144
|
}
|
|
145
145
|
catch {
|
|
146
146
|
return null;
|
|
@@ -1194,10 +1194,17 @@ export interface AuthServerConfigInput {
|
|
|
1194
1194
|
* primária) — as acima, o `authkit_keystore`, as de extensions e a do
|
|
1195
1195
|
* `adapters.database()`/keystore `lucid` quando eles não declaram a própria.
|
|
1196
1196
|
* A tabela da conta NÃO entra: fica na conexão do model da conta.
|
|
1197
|
+
* - `name`: o schema do banco dessas tabelas (MySQL: o database), ex.: `'auth'`
|
|
1198
|
+
* → `"auth"."auth_settings"`. Vai escrito em cada statement; o boot cria o
|
|
1199
|
+
* schema quando falta (com `autoManage`). Default: o schema corrente da
|
|
1200
|
+
* conexão. SQLite não tem schemas: com `name`, o boot falha. NÃO use
|
|
1201
|
+
* `searchPath` na conexão para isso — atrás de um PgBouncer em modo
|
|
1202
|
+
* transaction, o `SET search_path` vaza para as conexões de outros clientes.
|
|
1197
1203
|
*/
|
|
1198
1204
|
schema?: {
|
|
1199
1205
|
autoManage?: boolean;
|
|
1200
1206
|
connection?: string;
|
|
1207
|
+
name?: string;
|
|
1201
1208
|
};
|
|
1202
1209
|
/**
|
|
1203
1210
|
* Ciclo de vida da conta (LGPD/GDPR). OPT-IN para workflows DURÁVEIS:
|
|
@@ -1407,6 +1414,7 @@ export interface ResolvedServerConfig {
|
|
|
1407
1414
|
schema: {
|
|
1408
1415
|
autoManage: boolean;
|
|
1409
1416
|
connection?: string;
|
|
1417
|
+
name?: string;
|
|
1410
1418
|
};
|
|
1411
1419
|
/**
|
|
1412
1420
|
* Ciclo de vida da conta resolvido. `durable` default `false` → caminho
|
|
@@ -17,7 +17,7 @@ import { loadEncryptionService } from './keys/keystore_crypto.js';
|
|
|
17
17
|
import { KeystoreManager, resolveKeystoreVault } from './keys/keystore_manager.js';
|
|
18
18
|
import { mcpClientRegistration, resolveMcpOAuth, } from './mcp/mcp_oauth.js';
|
|
19
19
|
import { OPEN_REGISTRATION_REDIRECT_POLICY, resolveRedirectUriPolicy, } from './provider/registration_policy.js';
|
|
20
|
-
import { bindTablesLocation } from './schema/tables_location.js';
|
|
20
|
+
import { assertSchemaSupported, authkitTablesClient, bindTablesLocation, validateSchemaName, } from './schema/tables_location.js';
|
|
21
21
|
import { resolveServiceTenants, } from './service_tenants/config.js';
|
|
22
22
|
export { adapters };
|
|
23
23
|
const RATE_LIMIT_DEFAULTS = {
|
|
@@ -288,9 +288,20 @@ export function defineConfig(config) {
|
|
|
288
288
|
// Toda tabela do AuthKit vive em `schema.connection` — inclusive as que o store da
|
|
289
289
|
// conta lê (auth_mfa, auth_password_history, organizations), que antes seguiam a
|
|
290
290
|
// conexão do model da conta.
|
|
291
|
-
|
|
291
|
+
const tablesSchema = validateSchemaName(config.schema?.name);
|
|
292
|
+
bindTablesLocation(config.accountStore, {
|
|
293
|
+
connection: config.schema?.connection,
|
|
294
|
+
schema: tablesSchema,
|
|
295
|
+
});
|
|
292
296
|
return configProvider.create(async (app) => {
|
|
293
|
-
const tablesContext = { connection: config.schema?.connection };
|
|
297
|
+
const tablesContext = { connection: config.schema?.connection, schema: tablesSchema };
|
|
298
|
+
if (tablesSchema) {
|
|
299
|
+
// Schema num dialeto sem schemas (SQLite) falha aqui, na resolução do config — nunca
|
|
300
|
+
// cai em silêncio para tabelas sem schema.
|
|
301
|
+
const db = await app.container.make('lucid.db').catch(() => null);
|
|
302
|
+
if (db)
|
|
303
|
+
assertSchemaSupported(authkitTablesClient(db, tablesContext), tablesSchema);
|
|
304
|
+
}
|
|
294
305
|
const AdapterClass = await config.adapter.resolver(app, tablesContext);
|
|
295
306
|
// `session.adapter` ausente ⇒ mesma classe do default (back-compat: o
|
|
296
307
|
// dispatcher vira identidade e tudo continua no adapter único de hoje).
|
|
@@ -310,12 +321,17 @@ export function defineConfig(config) {
|
|
|
310
321
|
store: 'tmp/authkit_jwks.json',
|
|
311
322
|
}
|
|
312
323
|
: config.jwks;
|
|
313
|
-
// Keystore no Lucid sem conexão
|
|
324
|
+
// Keystore no Lucid sem conexão/schema próprios fica onde estão as tabelas do authkit.
|
|
314
325
|
const jwksStore = jwksInput.store;
|
|
315
|
-
const jwksConfig = jwksStore?.driver === 'lucid' &&
|
|
326
|
+
const jwksConfig = jwksStore?.driver === 'lucid' &&
|
|
327
|
+
((!jwksStore.connection && config.schema?.connection) || (!jwksStore.schema && tablesSchema))
|
|
316
328
|
? {
|
|
317
329
|
...jwksInput,
|
|
318
|
-
store: {
|
|
330
|
+
store: {
|
|
331
|
+
...jwksStore,
|
|
332
|
+
connection: jwksStore.connection ?? config.schema?.connection,
|
|
333
|
+
schema: jwksStore.schema ?? tablesSchema,
|
|
334
|
+
},
|
|
319
335
|
}
|
|
320
336
|
: jwksInput;
|
|
321
337
|
if (config.jwks === 'auto' && !process.env.AUTHKIT_JWKS) {
|
|
@@ -504,6 +520,7 @@ export function defineConfig(config) {
|
|
|
504
520
|
schema: {
|
|
505
521
|
autoManage: config.schema?.autoManage !== false,
|
|
506
522
|
connection: config.schema?.connection,
|
|
523
|
+
name: tablesSchema,
|
|
507
524
|
},
|
|
508
525
|
// OPT-IN: default false → caminho síncrono de sempre (byte-idêntico).
|
|
509
526
|
accountLifecycle: {
|
|
@@ -134,6 +134,19 @@ export function authkitDoctorChecks() {
|
|
|
134
134
|
const { managedSigningKeyAgeFinding } = await import('./doctor_input.js');
|
|
135
135
|
return toResults(await managedSigningKeyAgeFinding(app, input.authkitConfig));
|
|
136
136
|
},
|
|
137
|
+
}, {
|
|
138
|
+
id: 'authkit.db-schema',
|
|
139
|
+
lib: 'authkit',
|
|
140
|
+
package: AUTHKIT_DOCTOR_PACKAGE,
|
|
141
|
+
title: 'Database schema',
|
|
142
|
+
cost: 'deep',
|
|
143
|
+
async run(app) {
|
|
144
|
+
const { input, notBooted } = await prepare(app);
|
|
145
|
+
if (notBooted || !input?.authkitConfig?.schema?.name)
|
|
146
|
+
return [];
|
|
147
|
+
const { dbSchemaResults } = await import('./db_schema_check.js');
|
|
148
|
+
return dbSchemaResults(input.authkitConfig, () => app.container.make('lucid.db'), AUTHKIT_DOCTOR_DOCS);
|
|
149
|
+
},
|
|
137
150
|
}, {
|
|
138
151
|
id: 'authkit.console-gate',
|
|
139
152
|
lib: 'authkit',
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { AgoraDoctorResult } from './agora_doctor.js';
|
|
2
|
+
/**
|
|
3
|
+
* `agora:doctor` check for `schema.name`: the schema exists, AuthKit's tables are in it, and the
|
|
4
|
+
* connection can write there. No result at all without `schema.name`.
|
|
5
|
+
*/
|
|
6
|
+
export declare function dbSchemaResults(config: Record<string, any> | null, makeDb: () => Promise<any>, docs: string): Promise<AgoraDoctorResult[]>;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/** Thrown inside the probe transaction so the probe row is never kept. */
|
|
2
|
+
class ProbeRollback extends Error {
|
|
3
|
+
}
|
|
4
|
+
function rows(result) {
|
|
5
|
+
if (Array.isArray(result))
|
|
6
|
+
return Array.isArray(result[0]) ? result[0] : result;
|
|
7
|
+
return result?.rows ?? [];
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* `agora:doctor` check for `schema.name`: the schema exists, AuthKit's tables are in it, and the
|
|
11
|
+
* connection can write there. No result at all without `schema.name`.
|
|
12
|
+
*/
|
|
13
|
+
export async function dbSchemaResults(config, makeDb, docs) {
|
|
14
|
+
const schema = config?.schema?.name;
|
|
15
|
+
if (!schema)
|
|
16
|
+
return [];
|
|
17
|
+
const [{ AUTHKIT_TABLE_NAMES }, { authkitTable, authkitTablesClient, dialectOf }] = await Promise.all([import('../schema/ensure.js'), import('../schema/tables_location.js')]);
|
|
18
|
+
const client = authkitTablesClient(await makeDb(), { connection: config?.schema?.connection });
|
|
19
|
+
const dialect = dialectOf(client);
|
|
20
|
+
if (dialect !== 'postgres' && dialect !== 'mysql') {
|
|
21
|
+
return [
|
|
22
|
+
{
|
|
23
|
+
status: 'error',
|
|
24
|
+
message: `schema.name "${schema}" is set, but the connection uses ${dialect ?? 'an unknown dialect'}, which has no schemas`,
|
|
25
|
+
fix: 'Remove schema.name from config/authkit.ts, or use Postgres or MySQL',
|
|
26
|
+
docs,
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
}
|
|
30
|
+
const exists = rows(await client.rawQuery('select 1 as ok from information_schema.schemata where schema_name = ?', [
|
|
31
|
+
schema,
|
|
32
|
+
]));
|
|
33
|
+
if (exists.length === 0) {
|
|
34
|
+
return [
|
|
35
|
+
{
|
|
36
|
+
status: 'error',
|
|
37
|
+
message: `database schema "${schema}" does not exist on this connection`,
|
|
38
|
+
fix: dialect === 'mysql'
|
|
39
|
+
? `Boot the app once with schema.autoManage on, or run CREATE DATABASE \`${schema}\``
|
|
40
|
+
: `Boot the app once with schema.autoManage on, or run CREATE SCHEMA "${schema}"`,
|
|
41
|
+
docs,
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
}
|
|
45
|
+
const found = new Set(rows(await client.rawQuery('select table_name as name from information_schema.tables where table_schema = ?', [schema])).map((r) => String(r.name ?? r.NAME ?? r.TABLE_NAME)));
|
|
46
|
+
const missing = AUTHKIT_TABLE_NAMES.filter((name) => !found.has(name));
|
|
47
|
+
if (missing.length > 0) {
|
|
48
|
+
return [
|
|
49
|
+
{
|
|
50
|
+
status: 'error',
|
|
51
|
+
message: `AuthKit tables missing from schema "${schema}": ${missing.join(', ')}`,
|
|
52
|
+
fix: 'Boot the app once with schema.autoManage on, or run ensureAuthkitSchema(db, { schema }) in a migration',
|
|
53
|
+
docs,
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
await client.transaction(async (trx) => {
|
|
59
|
+
await trx.table(authkitTable({ schema }, 'auth_settings')).insert({
|
|
60
|
+
key: '__authkit_doctor_probe',
|
|
61
|
+
organization_id: null,
|
|
62
|
+
value: 'null',
|
|
63
|
+
});
|
|
64
|
+
throw new ProbeRollback();
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
if (!(error instanceof ProbeRollback)) {
|
|
69
|
+
return [
|
|
70
|
+
{
|
|
71
|
+
status: 'error',
|
|
72
|
+
message: `the connection cannot write to schema "${schema}": ${error?.message ?? error}`,
|
|
73
|
+
fix: dialect === 'mysql'
|
|
74
|
+
? `GRANT INSERT, UPDATE, DELETE ON \`${schema}\`.* TO the app user`
|
|
75
|
+
: `GRANT USAGE ON SCHEMA "${schema}" and INSERT, UPDATE, DELETE on its tables to the app role`,
|
|
76
|
+
docs,
|
|
77
|
+
},
|
|
78
|
+
];
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return [
|
|
82
|
+
{
|
|
83
|
+
status: 'ok',
|
|
84
|
+
message: `AuthKit tables are in schema "${schema}" and the connection can write there`,
|
|
85
|
+
},
|
|
86
|
+
];
|
|
87
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getBootedApp } from '../../services/booted_app.js';
|
|
2
2
|
import { pickModelAdapterClass } from '../adapters/factory.js';
|
|
3
3
|
import { buildPersonalAgentsRuntime } from '../agents/runtime.js';
|
|
4
|
+
import { authkitTable } from '../schema/tables_location.js';
|
|
4
5
|
import { normalizeActiveOrg } from './active_org_cookie.js';
|
|
5
6
|
/**
|
|
6
7
|
* Serviço de inspeção/revogação das SESSÕES e GRANTS ativos de uma conta.
|
|
@@ -22,8 +23,10 @@ export class AdminSessionsService {
|
|
|
22
23
|
#AdapterClass;
|
|
23
24
|
#SessionAdapterClass;
|
|
24
25
|
#accountStore;
|
|
25
|
-
/** Conexão Lucid das tabelas authkit (schema
|
|
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(
|
|
77
|
+
await conn.insertQuery().table(this.#revocationsTable).insert({
|
|
74
78
|
sid: null,
|
|
75
79
|
sub: accountId,
|
|
76
80
|
revoked_at: revokedAt,
|
|
@@ -98,7 +98,20 @@ export interface RuntimeSettingsOptions {
|
|
|
98
98
|
* Ausente (ou undefined) → conexão default.
|
|
99
99
|
*/
|
|
100
100
|
connection?: string;
|
|
101
|
+
/**
|
|
102
|
+
* Schema do banco de `auth_settings` (MySQL: o database) — o `schema.name` do
|
|
103
|
+
* `defineConfig`. Escrito em cada statement (`"auth"."auth_settings"`).
|
|
104
|
+
*/
|
|
105
|
+
schema?: string;
|
|
101
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* `{ connection, schema }` das tabelas do authkit a partir do config resolvido
|
|
109
|
+
* (`schema.connection`/`schema.name`), no formato de {@link RuntimeSettingsOptions}.
|
|
110
|
+
*/
|
|
111
|
+
export declare function settingsLocation(config: any): {
|
|
112
|
+
connection?: string;
|
|
113
|
+
schema?: string;
|
|
114
|
+
};
|
|
102
115
|
/**
|
|
103
116
|
* Implementação default do SettingsCapability sobre um `Database` Lucid.
|
|
104
117
|
*
|
|
@@ -108,6 +121,8 @@ export declare class RuntimeSettings implements SettingsCapability {
|
|
|
108
121
|
private readonly db;
|
|
109
122
|
private readonly ttlMs;
|
|
110
123
|
private readonly connectionName;
|
|
124
|
+
/** `auth_settings`, qualificada com o schema quando há um. */
|
|
125
|
+
private readonly table;
|
|
111
126
|
private readonly shared;
|
|
112
127
|
constructor(db: any, opts?: RuntimeSettingsOptions);
|
|
113
128
|
private get cache();
|