@adonis-agora/authkit-server 1.0.0-rc.11 → 1.0.0-rc.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -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/checks.d.ts +31 -1
- package/build/src/doctor/checks.js +118 -3
- package/build/src/doctor/db_schema_check.d.ts +6 -0
- package/build/src/doctor/db_schema_check.js +87 -0
- package/build/src/doctor/doctor_input.js +2 -0
- package/build/src/host/admin_sessions_service.js +6 -2
- package/build/src/host/csrf.d.ts +3 -0
- package/build/src/host/csrf.js +27 -7
- 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',
|
|
@@ -27,6 +27,11 @@ export interface DoctorInput {
|
|
|
27
27
|
* Provided by the doctor command; undefined = not checked (doctor runs old version).
|
|
28
28
|
*/
|
|
29
29
|
settingsTablePresent?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* The app's `config('shield')` (only its `csrf` block is read). `null` = no shield config;
|
|
32
|
+
* `undefined` = not collected (the CSRF check then cannot tell, and says so).
|
|
33
|
+
*/
|
|
34
|
+
shieldConfig?: Record<string, any> | null;
|
|
30
35
|
/**
|
|
31
36
|
* Classes de adapter para detecção de volatilidade (injetadas pelo doctor command
|
|
32
37
|
* em runtime). Quando ausente, o check de volatilidade usa heurística conservadora.
|
|
@@ -71,7 +76,32 @@ export declare function checkAccountStore(input: DoctorInput): Finding[];
|
|
|
71
76
|
export declare function checkAccountStoreColumns(input: DoctorInput): Finding[];
|
|
72
77
|
/** session provider configurado + warn se cookie store com tokenSets grandes. */
|
|
73
78
|
export declare function checkSession(input: DoctorInput): Finding[];
|
|
74
|
-
/**
|
|
79
|
+
/** A POST endpoint authkit mounts that is called without a browser CSRF token. */
|
|
80
|
+
export interface CsrfExemptEndpoint {
|
|
81
|
+
/** A concrete request path (what `ctx.request.url()` returns). */
|
|
82
|
+
path: string;
|
|
83
|
+
/** The route pattern shield compares against an `exceptRoutes` array (`ctx.route.pattern`). */
|
|
84
|
+
pattern: string;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The server-to-server POST endpoints authkit mounts, which shield's CSRF check must not reject:
|
|
88
|
+
* the OIDC provider's token, introspection and revocation endpoints (under the `${mountPath}/*`
|
|
89
|
+
* wildcard route), PAT introspection, and — with `personalAgents` on — the agents' OAuth
|
|
90
|
+
* endpoints plus every extension's `csrfExemptPaths`.
|
|
91
|
+
*/
|
|
92
|
+
export declare function authkitCsrfExemptEndpoints(authkitConfig: Record<string, any> | null): CsrfExemptEndpoint[];
|
|
93
|
+
/**
|
|
94
|
+
* Whether shield's CSRF check (as configured in `config/shield.ts`) would reject a token-less POST
|
|
95
|
+
* to `endpoint`, mirroring `@adonisjs/shield`: CSRF off → no; a non-empty `methods` without POST →
|
|
96
|
+
* no (an empty or missing `methods` checks every method); `exceptRoutes` as a function → rejected
|
|
97
|
+
* unless it returns a truthy value for that request; as an array → rejected unless it holds the route
|
|
98
|
+
* pattern. `'unknown'` when the function throws on the probe context.
|
|
99
|
+
*/
|
|
100
|
+
export declare function csrfRejects(shieldConfig: Record<string, any> | null | undefined, endpoint: CsrfExemptEndpoint): boolean | 'unknown';
|
|
101
|
+
/**
|
|
102
|
+
* `authkit.shield`: evaluates the app's real `config/shield.ts` against the server-to-server POST
|
|
103
|
+
* endpoints authkit mounts, and warns only about the ones shield would actually reject.
|
|
104
|
+
*/
|
|
75
105
|
export declare function checkShield(input: DoctorInput): Finding;
|
|
76
106
|
/** ally só é necessário quando social está configurado. */
|
|
77
107
|
export declare function checkAlly(input: DoctorInput): Finding;
|