@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
|
@@ -238,7 +238,90 @@ export function checkSession(input) {
|
|
|
238
238
|
}
|
|
239
239
|
return findings;
|
|
240
240
|
}
|
|
241
|
-
/**
|
|
241
|
+
/**
|
|
242
|
+
* The server-to-server POST endpoints authkit mounts, which shield's CSRF check must not reject:
|
|
243
|
+
* the OIDC provider's token, introspection and revocation endpoints (under the `${mountPath}/*`
|
|
244
|
+
* wildcard route), PAT introspection, and — with `personalAgents` on — the agents' OAuth
|
|
245
|
+
* endpoints plus every extension's `csrfExemptPaths`.
|
|
246
|
+
*/
|
|
247
|
+
export function authkitCsrfExemptEndpoints(authkitConfig) {
|
|
248
|
+
const mount = String(authkitConfig?.mountPath ?? '/oidc').replace(/\/+$/, '') || '/oidc';
|
|
249
|
+
const endpoints = ['token', 'token/introspection', 'token/revocation'].map((rest) => ({ path: `${mount}/${rest}`, pattern: `${mount}/*` }));
|
|
250
|
+
endpoints.push({ path: '/authkit/pat/introspect', pattern: '/authkit/pat/introspect' });
|
|
251
|
+
const agents = authkitConfig?.personalAgents;
|
|
252
|
+
if (agents?.prefix) {
|
|
253
|
+
for (const rest of ['device_authorization', 'token']) {
|
|
254
|
+
const path = `${agents.prefix}/oauth/${rest}`;
|
|
255
|
+
endpoints.push({ path, pattern: path });
|
|
256
|
+
}
|
|
257
|
+
for (const extension of agents.extensions ?? []) {
|
|
258
|
+
for (const path of extension.csrfExemptPaths ?? [])
|
|
259
|
+
endpoints.push({ path, pattern: path });
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return endpoints;
|
|
263
|
+
}
|
|
264
|
+
/** A minimal HttpContext for a token-less `POST path`, enough for an `exceptRoutes` function. */
|
|
265
|
+
function csrfProbeContext(endpoint) {
|
|
266
|
+
const [pathname, query = ''] = endpoint.path.split('?');
|
|
267
|
+
const completeUrl = `http://localhost${endpoint.path}`;
|
|
268
|
+
const request = {
|
|
269
|
+
url: (includeQueryString) => includeQueryString && query ? `${pathname}?${query}` : pathname,
|
|
270
|
+
completeUrl: () => completeUrl,
|
|
271
|
+
method: () => 'POST',
|
|
272
|
+
intended: () => 'POST',
|
|
273
|
+
parsedUrl: { pathname, query, href: endpoint.path },
|
|
274
|
+
header: () => undefined,
|
|
275
|
+
headers: () => ({}),
|
|
276
|
+
hostname: () => 'localhost',
|
|
277
|
+
host: () => 'localhost',
|
|
278
|
+
protocol: () => 'http',
|
|
279
|
+
ip: () => '127.0.0.1',
|
|
280
|
+
input: () => undefined,
|
|
281
|
+
qs: () => ({}),
|
|
282
|
+
all: () => ({}),
|
|
283
|
+
matchesRoute: (identifier) => (Array.isArray(identifier) ? identifier : [identifier]).includes(endpoint.pattern),
|
|
284
|
+
};
|
|
285
|
+
return {
|
|
286
|
+
request,
|
|
287
|
+
route: { pattern: endpoint.pattern, methods: ['POST'] },
|
|
288
|
+
routeKey: `POST-${endpoint.pattern}`,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Whether shield's CSRF check (as configured in `config/shield.ts`) would reject a token-less POST
|
|
293
|
+
* to `endpoint`, mirroring `@adonisjs/shield`: CSRF off → no; a non-empty `methods` without POST →
|
|
294
|
+
* no (an empty or missing `methods` checks every method); `exceptRoutes` as a function → rejected
|
|
295
|
+
* unless it returns a truthy value for that request; as an array → rejected unless it holds the route
|
|
296
|
+
* pattern. `'unknown'` when the function throws on the probe context.
|
|
297
|
+
*/
|
|
298
|
+
export function csrfRejects(shieldConfig, endpoint) {
|
|
299
|
+
const csrf = shieldConfig?.csrf;
|
|
300
|
+
if (!csrf || csrf.enabled !== true)
|
|
301
|
+
return false;
|
|
302
|
+
const methods = Array.isArray(csrf.methods)
|
|
303
|
+
? csrf.methods.map((m) => String(m).toLowerCase())
|
|
304
|
+
: [];
|
|
305
|
+
if (methods.length > 0 && !methods.includes('post'))
|
|
306
|
+
return false;
|
|
307
|
+
const except = csrf.exceptRoutes ?? [];
|
|
308
|
+
if (typeof except === 'function') {
|
|
309
|
+
try {
|
|
310
|
+
// Shield skips validation on any truthy return (`!this.#routesToIgnore(ctx)`).
|
|
311
|
+
return !except(csrfProbeContext(endpoint));
|
|
312
|
+
}
|
|
313
|
+
catch {
|
|
314
|
+
return 'unknown';
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (Array.isArray(except))
|
|
318
|
+
return !except.includes(endpoint.pattern);
|
|
319
|
+
return true;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* `authkit.shield`: evaluates the app's real `config/shield.ts` against the server-to-server POST
|
|
323
|
+
* endpoints authkit mounts, and warns only about the ones shield would actually reject.
|
|
324
|
+
*/
|
|
242
325
|
export function checkShield(input) {
|
|
243
326
|
if (!input.peers.shield) {
|
|
244
327
|
return {
|
|
@@ -247,9 +330,41 @@ export function checkShield(input) {
|
|
|
247
330
|
};
|
|
248
331
|
}
|
|
249
332
|
const mountPath = input.authkitConfig?.mountPath ?? '/oidc';
|
|
333
|
+
const helper = `authkitCsrfExceptions(ctx.request.url(), { mountPath: '${mountPath}' })`;
|
|
334
|
+
if (input.shieldConfig === undefined) {
|
|
335
|
+
return {
|
|
336
|
+
level: 'warn',
|
|
337
|
+
message: `config('shield') was not read, so the CSRF exemptions could not be checked. Make sure config/shield.ts's csrf.exceptRoutes exempts the IdP POST routes under "${mountPath}" (e.g. with ${helper}).`,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
const endpoints = authkitCsrfExemptEndpoints(input.authkitConfig);
|
|
341
|
+
const rejected = [];
|
|
342
|
+
const unknown = [];
|
|
343
|
+
for (const endpoint of endpoints) {
|
|
344
|
+
const verdict = csrfRejects(input.shieldConfig, endpoint);
|
|
345
|
+
if (verdict === 'unknown')
|
|
346
|
+
unknown.push(endpoint.path);
|
|
347
|
+
else if (verdict)
|
|
348
|
+
rejected.push(endpoint.path);
|
|
349
|
+
}
|
|
350
|
+
if (rejected.length > 0) {
|
|
351
|
+
return {
|
|
352
|
+
level: 'warn',
|
|
353
|
+
message: `Shield's CSRF check rejects POST ${rejected.join(', ')} — server-to-server calls to these authkit endpoints fail. Exempt them in config/shield.ts's csrf.exceptRoutes, e.g. \`exceptRoutes: (ctx) => ${helper}\` (combine with your own exemptions using ||).`,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
if (unknown.length > 0) {
|
|
357
|
+
return {
|
|
358
|
+
level: 'warn',
|
|
359
|
+
message: `csrf.exceptRoutes in config/shield.ts threw when probed with POST ${unknown.join(', ')}, so the doctor cannot tell whether they are exempt. Make sure it exempts them (e.g. with ${helper}).`,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
const csrfOn = input.shieldConfig?.csrf?.enabled === true;
|
|
250
363
|
return {
|
|
251
|
-
level: '
|
|
252
|
-
message:
|
|
364
|
+
level: 'ok',
|
|
365
|
+
message: csrfOn
|
|
366
|
+
? `shield's CSRF check exempts authkit's server-to-server POST endpoints (${endpoints.length} checked).`
|
|
367
|
+
: 'shield CSRF is off: authkit endpoints are not affected.',
|
|
253
368
|
};
|
|
254
369
|
}
|
|
255
370
|
/** ally só é necessário quando social está configurado. */
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { AgoraDoctorResult } from './agora_doctor.js';
|
|
2
|
+
/**
|
|
3
|
+
* `agora:doctor` check for `schema.name`: the schema exists, AuthKit's tables are in it, and the
|
|
4
|
+
* connection can write there. No result at all without `schema.name`.
|
|
5
|
+
*/
|
|
6
|
+
export declare function dbSchemaResults(config: Record<string, any> | null, makeDb: () => Promise<any>, docs: string): Promise<AgoraDoctorResult[]>;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/** Thrown inside the probe transaction so the probe row is never kept. */
|
|
2
|
+
class ProbeRollback extends Error {
|
|
3
|
+
}
|
|
4
|
+
function rows(result) {
|
|
5
|
+
if (Array.isArray(result))
|
|
6
|
+
return Array.isArray(result[0]) ? result[0] : result;
|
|
7
|
+
return result?.rows ?? [];
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* `agora:doctor` check for `schema.name`: the schema exists, AuthKit's tables are in it, and the
|
|
11
|
+
* connection can write there. No result at all without `schema.name`.
|
|
12
|
+
*/
|
|
13
|
+
export async function dbSchemaResults(config, makeDb, docs) {
|
|
14
|
+
const schema = config?.schema?.name;
|
|
15
|
+
if (!schema)
|
|
16
|
+
return [];
|
|
17
|
+
const [{ AUTHKIT_TABLE_NAMES }, { authkitTable, authkitTablesClient, dialectOf }] = await Promise.all([import('../schema/ensure.js'), import('../schema/tables_location.js')]);
|
|
18
|
+
const client = authkitTablesClient(await makeDb(), { connection: config?.schema?.connection });
|
|
19
|
+
const dialect = dialectOf(client);
|
|
20
|
+
if (dialect !== 'postgres' && dialect !== 'mysql') {
|
|
21
|
+
return [
|
|
22
|
+
{
|
|
23
|
+
status: 'error',
|
|
24
|
+
message: `schema.name "${schema}" is set, but the connection uses ${dialect ?? 'an unknown dialect'}, which has no schemas`,
|
|
25
|
+
fix: 'Remove schema.name from config/authkit.ts, or use Postgres or MySQL',
|
|
26
|
+
docs,
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
}
|
|
30
|
+
const exists = rows(await client.rawQuery('select 1 as ok from information_schema.schemata where schema_name = ?', [
|
|
31
|
+
schema,
|
|
32
|
+
]));
|
|
33
|
+
if (exists.length === 0) {
|
|
34
|
+
return [
|
|
35
|
+
{
|
|
36
|
+
status: 'error',
|
|
37
|
+
message: `database schema "${schema}" does not exist on this connection`,
|
|
38
|
+
fix: dialect === 'mysql'
|
|
39
|
+
? `Boot the app once with schema.autoManage on, or run CREATE DATABASE \`${schema}\``
|
|
40
|
+
: `Boot the app once with schema.autoManage on, or run CREATE SCHEMA "${schema}"`,
|
|
41
|
+
docs,
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
}
|
|
45
|
+
const found = new Set(rows(await client.rawQuery('select table_name as name from information_schema.tables where table_schema = ?', [schema])).map((r) => String(r.name ?? r.NAME ?? r.TABLE_NAME)));
|
|
46
|
+
const missing = AUTHKIT_TABLE_NAMES.filter((name) => !found.has(name));
|
|
47
|
+
if (missing.length > 0) {
|
|
48
|
+
return [
|
|
49
|
+
{
|
|
50
|
+
status: 'error',
|
|
51
|
+
message: `AuthKit tables missing from schema "${schema}": ${missing.join(', ')}`,
|
|
52
|
+
fix: 'Boot the app once with schema.autoManage on, or run ensureAuthkitSchema(db, { schema }) in a migration',
|
|
53
|
+
docs,
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
await client.transaction(async (trx) => {
|
|
59
|
+
await trx.table(authkitTable({ schema }, 'auth_settings')).insert({
|
|
60
|
+
key: '__authkit_doctor_probe',
|
|
61
|
+
organization_id: null,
|
|
62
|
+
value: 'null',
|
|
63
|
+
});
|
|
64
|
+
throw new ProbeRollback();
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
if (!(error instanceof ProbeRollback)) {
|
|
69
|
+
return [
|
|
70
|
+
{
|
|
71
|
+
status: 'error',
|
|
72
|
+
message: `the connection cannot write to schema "${schema}": ${error?.message ?? error}`,
|
|
73
|
+
fix: dialect === 'mysql'
|
|
74
|
+
? `GRANT INSERT, UPDATE, DELETE ON \`${schema}\`.* TO the app user`
|
|
75
|
+
: `GRANT USAGE ON SCHEMA "${schema}" and INSERT, UPDATE, DELETE on its tables to the app role`,
|
|
76
|
+
docs,
|
|
77
|
+
},
|
|
78
|
+
];
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return [
|
|
82
|
+
{
|
|
83
|
+
status: 'ok',
|
|
84
|
+
message: `AuthKit tables are in schema "${schema}" and the connection can write there`,
|
|
85
|
+
},
|
|
86
|
+
];
|
|
87
|
+
}
|
|
@@ -47,9 +47,11 @@ export async function buildDoctorInput(app, logger) {
|
|
|
47
47
|
// senão todo campo aparece como "ausente" (provider cru não tem issuer etc.).
|
|
48
48
|
const authkitConfig = await resolveAuthkitConfig(app, config.get('authkit', null));
|
|
49
49
|
const sessionConfig = config.get('session', null) ?? null;
|
|
50
|
+
const shieldConfig = config.get('shield', null) ?? null;
|
|
50
51
|
return {
|
|
51
52
|
authkitConfig,
|
|
52
53
|
sessionConfig,
|
|
54
|
+
shieldConfig,
|
|
53
55
|
peers: {
|
|
54
56
|
session: await canImport('@adonisjs/session', logger),
|
|
55
57
|
shield: await canImport('@adonisjs/shield', logger),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getBootedApp } from '../../services/booted_app.js';
|
|
2
2
|
import { pickModelAdapterClass } from '../adapters/factory.js';
|
|
3
3
|
import { buildPersonalAgentsRuntime } from '../agents/runtime.js';
|
|
4
|
+
import { authkitTable } from '../schema/tables_location.js';
|
|
4
5
|
import { normalizeActiveOrg } from './active_org_cookie.js';
|
|
5
6
|
/**
|
|
6
7
|
* Serviço de inspeção/revogação das SESSÕES e GRANTS ativos de uma conta.
|
|
@@ -22,8 +23,10 @@ export class AdminSessionsService {
|
|
|
22
23
|
#AdapterClass;
|
|
23
24
|
#SessionAdapterClass;
|
|
24
25
|
#accountStore;
|
|
25
|
-
/** Conexão Lucid das tabelas authkit (schema
|
|
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,
|
package/build/src/host/csrf.d.ts
CHANGED
|
@@ -20,5 +20,8 @@ export interface AuthkitCsrfOptions {
|
|
|
20
20
|
* Retorna `true` quando `url` é uma rota AuthKit que deve ser ISENTA de CSRF
|
|
21
21
|
* (machine-to-machine). Cobre o mountPath do IdP, a introspecção de PAT e a
|
|
22
22
|
* rota de back-channel logout do client e os endpoints OAuth de personal agents.
|
|
23
|
+
*
|
|
24
|
+
* O match é ANCORADO no pathname: `/oidc` e `/oidc/...` casam, mas `/foo/oidc/x`,
|
|
25
|
+
* `/oidc-admin` ou `/x?next=/oidc` não. A query string é ignorada.
|
|
23
26
|
*/
|
|
24
27
|
export declare function authkitCsrfExceptions(url: string, options?: AuthkitCsrfOptions): boolean;
|
package/build/src/host/csrf.js
CHANGED
|
@@ -11,19 +11,39 @@
|
|
|
11
11
|
* csrf: {
|
|
12
12
|
* enabled: true,
|
|
13
13
|
* exceptRoutes: (ctx) => authkitCsrfExceptions(ctx.request.url(), { mountPath: '/oidc' })
|
|
14
|
-
* || ctx.request.url().
|
|
14
|
+
* || ctx.request.url().startsWith('/api/'),
|
|
15
15
|
* }
|
|
16
16
|
* ```
|
|
17
17
|
*/
|
|
18
18
|
import { normalizePersonalAgentsPrefix } from '../agents/config.js';
|
|
19
19
|
import { getAuthHostConfig } from './auth_host_config.js';
|
|
20
|
+
/** `path` é `prefix` ou fica debaixo dele (`prefix/...`) — nunca um substring solto. */
|
|
21
|
+
function underPrefix(path, prefix) {
|
|
22
|
+
return path === prefix || path.startsWith(`${prefix}/`);
|
|
23
|
+
}
|
|
24
|
+
/** O pathname de `url`: sem query/hash, aceitando também uma URL absoluta. */
|
|
25
|
+
function pathnameOf(url) {
|
|
26
|
+
let path = url;
|
|
27
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(path)) {
|
|
28
|
+
try {
|
|
29
|
+
path = new URL(path).pathname;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// Segue com a string crua: o match ancorado abaixo simplesmente não casa.
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return path.split(/[?#]/, 1)[0] ?? '';
|
|
36
|
+
}
|
|
20
37
|
/**
|
|
21
38
|
* Retorna `true` quando `url` é uma rota AuthKit que deve ser ISENTA de CSRF
|
|
22
39
|
* (machine-to-machine). Cobre o mountPath do IdP, a introspecção de PAT e a
|
|
23
40
|
* rota de back-channel logout do client e os endpoints OAuth de personal agents.
|
|
41
|
+
*
|
|
42
|
+
* O match é ANCORADO no pathname: `/oidc` e `/oidc/...` casam, mas `/foo/oidc/x`,
|
|
43
|
+
* `/oidc-admin` ou `/x?next=/oidc` não. A query string é ignorada.
|
|
24
44
|
*/
|
|
25
45
|
export function authkitCsrfExceptions(url, options = {}) {
|
|
26
|
-
const mountPath = options.mountPath ?? '/oidc';
|
|
46
|
+
const mountPath = (options.mountPath ?? '/oidc').replace(/\/+$/, '');
|
|
27
47
|
const backchannel = options.backchannelLogoutPath === false
|
|
28
48
|
? null
|
|
29
49
|
: (options.backchannelLogoutPath ?? '/auth/backchannel-logout');
|
|
@@ -33,12 +53,12 @@ export function authkitCsrfExceptions(url, options = {}) {
|
|
|
33
53
|
? normalizePersonalAgentsPrefix(options.personalAgentsPrefix)
|
|
34
54
|
: getAuthHostConfig()?.personalAgents?.prefix;
|
|
35
55
|
const agentsOAuth = agentsPrefix ? `${agentsPrefix}/oauth/` : null;
|
|
36
|
-
const path = url
|
|
56
|
+
const path = pathnameOf(url);
|
|
37
57
|
const extensionExempt = options.personalAgentExtensions !== false &&
|
|
38
58
|
(getAuthHostConfig()?.personalAgents?.extensions ?? []).some((e) => e.csrfExemptPaths?.includes(path));
|
|
39
|
-
return (
|
|
40
|
-
|
|
41
|
-
(backchannel !== null &&
|
|
42
|
-
(agentsOAuth !== null &&
|
|
59
|
+
return ((mountPath !== '' && underPrefix(path, mountPath)) ||
|
|
60
|
+
underPrefix(path, '/authkit/pat') ||
|
|
61
|
+
(backchannel !== null && path === backchannel) ||
|
|
62
|
+
(agentsOAuth !== null && path.startsWith(agentsOAuth)) ||
|
|
43
63
|
extensionExempt);
|
|
44
64
|
}
|
|
@@ -98,7 +98,20 @@ export interface RuntimeSettingsOptions {
|
|
|
98
98
|
* Ausente (ou undefined) → conexão default.
|
|
99
99
|
*/
|
|
100
100
|
connection?: string;
|
|
101
|
+
/**
|
|
102
|
+
* Schema do banco de `auth_settings` (MySQL: o database) — o `schema.name` do
|
|
103
|
+
* `defineConfig`. Escrito em cada statement (`"auth"."auth_settings"`).
|
|
104
|
+
*/
|
|
105
|
+
schema?: string;
|
|
101
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* `{ connection, schema }` das tabelas do authkit a partir do config resolvido
|
|
109
|
+
* (`schema.connection`/`schema.name`), no formato de {@link RuntimeSettingsOptions}.
|
|
110
|
+
*/
|
|
111
|
+
export declare function settingsLocation(config: any): {
|
|
112
|
+
connection?: string;
|
|
113
|
+
schema?: string;
|
|
114
|
+
};
|
|
102
115
|
/**
|
|
103
116
|
* Implementação default do SettingsCapability sobre um `Database` Lucid.
|
|
104
117
|
*
|
|
@@ -108,6 +121,8 @@ export declare class RuntimeSettings implements SettingsCapability {
|
|
|
108
121
|
private readonly db;
|
|
109
122
|
private readonly ttlMs;
|
|
110
123
|
private readonly connectionName;
|
|
124
|
+
/** `auth_settings`, qualificada com o schema quando há um. */
|
|
125
|
+
private readonly table;
|
|
111
126
|
private readonly shared;
|
|
112
127
|
constructor(db: any, opts?: RuntimeSettingsOptions);
|
|
113
128
|
private get cache();
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
* );
|
|
51
51
|
* ```
|
|
52
52
|
*/
|
|
53
|
+
import { authkitTable } from '../schema/tables_location.js';
|
|
53
54
|
import { isSettingLocked, SettingLockedError } from './config_locks.js';
|
|
54
55
|
/**
|
|
55
56
|
* Type guard: o objeto (store ou serviço) expõe SettingsCapability?
|
|
@@ -58,7 +59,7 @@ export function supportsSettings(obj) {
|
|
|
58
59
|
return !!obj && typeof obj.getSetting === 'function';
|
|
59
60
|
}
|
|
60
61
|
const sharedStates = new WeakMap();
|
|
61
|
-
function sharedStateFor(db, connection) {
|
|
62
|
+
function sharedStateFor(db, connection, table) {
|
|
62
63
|
const fresh = () => ({ cache: new Map(), tablePresent: null, tableAbsentUntil: 0 });
|
|
63
64
|
if (!db || (typeof db !== 'object' && typeof db !== 'function'))
|
|
64
65
|
return fresh();
|
|
@@ -67,7 +68,7 @@ function sharedStateFor(db, connection) {
|
|
|
67
68
|
byConnection = new Map();
|
|
68
69
|
sharedStates.set(db, byConnection);
|
|
69
70
|
}
|
|
70
|
-
const name = connection ?? ''
|
|
71
|
+
const name = `${connection ?? ''}\x00${table}`;
|
|
71
72
|
let state = byConnection.get(name);
|
|
72
73
|
if (!state) {
|
|
73
74
|
state = fresh();
|
|
@@ -75,6 +76,18 @@ function sharedStateFor(db, connection) {
|
|
|
75
76
|
}
|
|
76
77
|
return state;
|
|
77
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* `{ connection, schema }` das tabelas do authkit a partir do config resolvido
|
|
81
|
+
* (`schema.connection`/`schema.name`), no formato de {@link RuntimeSettingsOptions}.
|
|
82
|
+
*/
|
|
83
|
+
export function settingsLocation(config) {
|
|
84
|
+
const out = {};
|
|
85
|
+
if (config?.schema?.connection)
|
|
86
|
+
out.connection = config.schema.connection;
|
|
87
|
+
if (config?.schema?.name)
|
|
88
|
+
out.schema = config.schema.name;
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
78
91
|
/** Chave de cache: combina key + escopo de org (null = global). */
|
|
79
92
|
function cacheKey(key, orgId) {
|
|
80
93
|
return orgId ? `${key}\x00${orgId}` : key;
|
|
@@ -88,12 +101,15 @@ export class RuntimeSettings {
|
|
|
88
101
|
db;
|
|
89
102
|
ttlMs;
|
|
90
103
|
connectionName;
|
|
104
|
+
/** `auth_settings`, qualificada com o schema quando há um. */
|
|
105
|
+
table;
|
|
91
106
|
shared;
|
|
92
107
|
constructor(db, opts = {}) {
|
|
93
108
|
this.db = db;
|
|
94
109
|
this.ttlMs = opts.ttlMs ?? 15_000;
|
|
95
110
|
this.connectionName = opts.connection;
|
|
96
|
-
this.
|
|
111
|
+
this.table = authkitTable({ schema: opts.schema }, 'auth_settings');
|
|
112
|
+
this.shared = sharedStateFor(db, opts.connection, this.table);
|
|
97
113
|
}
|
|
98
114
|
get cache() {
|
|
99
115
|
return this.shared.cache;
|
|
@@ -127,7 +143,7 @@ export class RuntimeSettings {
|
|
|
127
143
|
if (shared.tablePresent === false && shared.tableAbsentUntil > Date.now())
|
|
128
144
|
return false;
|
|
129
145
|
try {
|
|
130
|
-
await this.conn().from(
|
|
146
|
+
await this.conn().from(this.table).select('key').limit(1);
|
|
131
147
|
shared.tablePresent = true;
|
|
132
148
|
return true;
|
|
133
149
|
}
|
|
@@ -152,7 +168,7 @@ export class RuntimeSettings {
|
|
|
152
168
|
return null;
|
|
153
169
|
}
|
|
154
170
|
try {
|
|
155
|
-
let q = this.conn().from(
|
|
171
|
+
let q = this.conn().from(this.table).where('key', key);
|
|
156
172
|
if (orgId) {
|
|
157
173
|
q = q.where('organization_id', orgId);
|
|
158
174
|
}
|
|
@@ -192,7 +208,7 @@ export class RuntimeSettings {
|
|
|
192
208
|
const resolvedOrgId = orgId ?? null;
|
|
193
209
|
try {
|
|
194
210
|
// Delete first then insert = upsert (compatível com sqlite + pg sem UPSERT syntax).
|
|
195
|
-
let q = this.conn().from(
|
|
211
|
+
let q = this.conn().from(this.table).where('key', key);
|
|
196
212
|
if (resolvedOrgId) {
|
|
197
213
|
q = q.where('organization_id', resolvedOrgId);
|
|
198
214
|
}
|
|
@@ -200,7 +216,7 @@ export class RuntimeSettings {
|
|
|
200
216
|
q = q.whereNull('organization_id');
|
|
201
217
|
}
|
|
202
218
|
await q.delete();
|
|
203
|
-
await this.conn().table(
|
|
219
|
+
await this.conn().table(this.table).insert({
|
|
204
220
|
key,
|
|
205
221
|
organization_id: resolvedOrgId,
|
|
206
222
|
value: json,
|
|
@@ -220,7 +236,7 @@ export class RuntimeSettings {
|
|
|
220
236
|
return;
|
|
221
237
|
const resolvedOrgId = orgId ?? null;
|
|
222
238
|
try {
|
|
223
|
-
let q = this.conn().from(
|
|
239
|
+
let q = this.conn().from(this.table).where('key', key);
|
|
224
240
|
if (resolvedOrgId) {
|
|
225
241
|
q = q.where('organization_id', resolvedOrgId);
|
|
226
242
|
}
|
|
@@ -238,7 +254,7 @@ export class RuntimeSettings {
|
|
|
238
254
|
if (!(await this.hasTable()))
|
|
239
255
|
return [];
|
|
240
256
|
try {
|
|
241
|
-
let q = this.conn().from(
|
|
257
|
+
let q = this.conn().from(this.table).select('*');
|
|
242
258
|
if (orgId !== undefined) {
|
|
243
259
|
// orgId fornecido: filtra pelo escopo
|
|
244
260
|
if (orgId) {
|
|
@@ -325,9 +341,8 @@ export async function resolveRuntimeSettings(ctx) {
|
|
|
325
341
|
try {
|
|
326
342
|
const db = await ctx.containerResolver.make('lucid.db');
|
|
327
343
|
const service = await ctx.containerResolver.make('authkit.server').catch(() => null);
|
|
328
|
-
// `auth_settings` vive
|
|
329
|
-
|
|
330
|
-
return new RuntimeSettings(db, connection ? { connection } : {});
|
|
344
|
+
// `auth_settings` vive onde estão as tabelas do authkit, não na conexão do model da conta.
|
|
345
|
+
return new RuntimeSettings(db, settingsLocation(service?.config));
|
|
331
346
|
}
|
|
332
347
|
catch {
|
|
333
348
|
return null;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { validateSchemaName } from '../schema/tables_location.js';
|
|
1
2
|
import { generateSigningJwk, planRotation, } from './keystore.js';
|
|
2
3
|
import { DriveKeystoreVault, FileKeystoreVault, HashicorpVaultKeystoreVault, LazyExternalVault, LucidKeystoreVault, RedisKeystoreVault, } from './keystore_vault.js';
|
|
3
4
|
/**
|
|
@@ -80,7 +81,7 @@ export function resolveKeystoreVault(store, ctx) {
|
|
|
80
81
|
return new LucidKeystoreVault(async () => {
|
|
81
82
|
const db = await ctx.container.make('lucid.db');
|
|
82
83
|
return cfg.connection ? db.connection(cfg.connection) : db.connection();
|
|
83
|
-
}, cfg.table, cfg.key);
|
|
84
|
+
}, cfg.table, cfg.key, validateSchemaName(cfg.schema, 'jwks.store.schema'));
|
|
84
85
|
case 'redis':
|
|
85
86
|
return new RedisKeystoreVault(async () => {
|
|
86
87
|
const rm = await ctx.container.make('redis');
|
|
@@ -30,7 +30,15 @@ export declare class LucidKeystoreVault implements KeystoreVault {
|
|
|
30
30
|
private getConn;
|
|
31
31
|
private table;
|
|
32
32
|
private key;
|
|
33
|
-
|
|
33
|
+
/** Schema do banco da tabela (MySQL: o database). Escrito em cada statement. */
|
|
34
|
+
private schema?;
|
|
35
|
+
constructor(getConn: () => Promise<any>, table?: string, key?: string,
|
|
36
|
+
/** Schema do banco da tabela (MySQL: o database). Escrito em cada statement. */
|
|
37
|
+
schema?: string | undefined);
|
|
38
|
+
/** Schema builder no schema da tabela (`withSchema`), ou o da conexão. */
|
|
39
|
+
private ddl;
|
|
40
|
+
/** `schema.tabela` (o knex cita cada parte) ou só `tabela`. */
|
|
41
|
+
private get qualified();
|
|
34
42
|
private ensureTable;
|
|
35
43
|
read(): Promise<string | null>;
|
|
36
44
|
write(blob: string): Promise<void>;
|
|
@@ -8,6 +8,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
8
8
|
};
|
|
9
9
|
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
10
10
|
import { dirname } from 'node:path';
|
|
11
|
+
import { assertSchemaSupported, createDbSchema } from '../schema/tables_location.js';
|
|
11
12
|
/** Cofre em arquivo local (default; mode 0600). */
|
|
12
13
|
export class FileKeystoreVault {
|
|
13
14
|
path;
|
|
@@ -49,16 +50,30 @@ export class LucidKeystoreVault {
|
|
|
49
50
|
getConn;
|
|
50
51
|
table;
|
|
51
52
|
key;
|
|
52
|
-
|
|
53
|
+
schema;
|
|
54
|
+
constructor(getConn, table = 'authkit_keystore', key = 'jwks',
|
|
55
|
+
/** Schema do banco da tabela (MySQL: o database). Escrito em cada statement. */
|
|
56
|
+
schema) {
|
|
53
57
|
this.getConn = getConn;
|
|
54
58
|
this.table = table;
|
|
55
59
|
this.key = key;
|
|
60
|
+
this.schema = schema;
|
|
61
|
+
}
|
|
62
|
+
/** Schema builder no schema da tabela (`withSchema`), ou o da conexão. */
|
|
63
|
+
ddl(conn) {
|
|
64
|
+
return this.schema ? conn.schema.withSchema(this.schema) : conn.schema;
|
|
65
|
+
}
|
|
66
|
+
/** `schema.tabela` (o knex cita cada parte) ou só `tabela`. */
|
|
67
|
+
get qualified() {
|
|
68
|
+
return this.schema ? `${this.schema}.${this.table}` : this.table;
|
|
56
69
|
}
|
|
57
70
|
async ensureTable(conn) {
|
|
58
|
-
if (await conn.
|
|
71
|
+
if (await this.ddl(conn).hasTable(this.table))
|
|
59
72
|
return;
|
|
73
|
+
if (this.schema)
|
|
74
|
+
await createDbSchema(conn, this.schema);
|
|
60
75
|
try {
|
|
61
|
-
await conn.
|
|
76
|
+
await this.ddl(conn).createTable(this.table, (t) => {
|
|
62
77
|
t.string('key').notNullable().primary();
|
|
63
78
|
t.text('blob').notNullable();
|
|
64
79
|
t.bigInteger('updated_at').notNullable();
|
|
@@ -67,31 +82,34 @@ export class LucidKeystoreVault {
|
|
|
67
82
|
catch (err) {
|
|
68
83
|
// Outra instância criou a tabela entre o hasTable e o createTable (race no boot
|
|
69
84
|
// multi-instância). Se já existe agora, OK; senão propaga.
|
|
70
|
-
if (!(await conn.
|
|
85
|
+
if (!(await this.ddl(conn).hasTable(this.table)))
|
|
71
86
|
throw err;
|
|
72
87
|
}
|
|
73
88
|
}
|
|
74
89
|
async read() {
|
|
75
90
|
const conn = await this.getConn();
|
|
76
|
-
|
|
91
|
+
assertSchemaSupported(conn, this.schema);
|
|
92
|
+
if (!(await this.ddl(conn).hasTable(this.table)))
|
|
77
93
|
return null;
|
|
78
|
-
const row = await conn.from(this.
|
|
94
|
+
const row = await conn.from(this.qualified).where('key', this.key).first();
|
|
79
95
|
return row ? row.blob : null;
|
|
80
96
|
}
|
|
81
97
|
async write(blob) {
|
|
82
98
|
const conn = await this.getConn();
|
|
99
|
+
assertSchemaSupported(conn, this.schema);
|
|
83
100
|
await this.ensureTable(conn);
|
|
84
101
|
await conn
|
|
85
|
-
.table(this.
|
|
102
|
+
.table(this.qualified)
|
|
86
103
|
.insert({ key: this.key, blob, updated_at: Date.now() })
|
|
87
104
|
.onConflict('key')
|
|
88
105
|
.merge();
|
|
89
106
|
}
|
|
90
107
|
async head() {
|
|
91
108
|
const conn = await this.getConn();
|
|
92
|
-
|
|
109
|
+
assertSchemaSupported(conn, this.schema);
|
|
110
|
+
if (!(await this.ddl(conn).hasTable(this.table)))
|
|
93
111
|
return null;
|
|
94
|
-
const row = await conn.from(this.
|
|
112
|
+
const row = await conn.from(this.qualified).where('key', this.key).first();
|
|
95
113
|
return row ? String(row.updated_at) : null;
|
|
96
114
|
}
|
|
97
115
|
}
|
|
@@ -24,6 +24,8 @@
|
|
|
24
24
|
* ```
|
|
25
25
|
*/
|
|
26
26
|
import type { AuthkitTable } from '../agents/extension.js';
|
|
27
|
+
/** Os nomes (sem schema) das tabelas que {@link ensureAuthkitSchema} garante. */
|
|
28
|
+
export declare const AUTHKIT_TABLE_NAMES: readonly string[];
|
|
27
29
|
export interface EnsureSchemaOptions {
|
|
28
30
|
/**
|
|
29
31
|
* Conexão Lucid das tabelas do authkit (`schema.connection`). Default: conexão
|
|
@@ -36,6 +38,12 @@ export interface EnsureSchemaOptions {
|
|
|
36
38
|
* O provider passa a do `accountStore` no boot. Ignorada quando `db` já é um client.
|
|
37
39
|
*/
|
|
38
40
|
accountConnection?: string;
|
|
41
|
+
/**
|
|
42
|
+
* Database schema of the tables (MySQL: the database) — `schema.name`. Every statement
|
|
43
|
+
* names it (`"auth"."auth_settings"`), and it is created when missing. Never a
|
|
44
|
+
* `searchPath`. Ignored for the account table, which stays where the app put it.
|
|
45
|
+
*/
|
|
46
|
+
schema?: string;
|
|
39
47
|
/**
|
|
40
48
|
* Tabelas de `personalAgents.extensions` (`tables` de cada uma), garantidas junto. O provider
|
|
41
49
|
* passa as do config no boot.
|
|
@@ -73,6 +81,7 @@ export interface EnsureSchemaReport {
|
|
|
73
81
|
*/
|
|
74
82
|
export declare function ensureAuthkitTables(db: any, tables: readonly AuthkitTable[], options?: {
|
|
75
83
|
connection?: string;
|
|
84
|
+
schema?: string;
|
|
76
85
|
}): Promise<Pick<EnsureSchemaReport, 'created' | 'altered' | 'indexed'>>;
|
|
77
86
|
/**
|
|
78
87
|
* Garante que as tabelas do authkit existem e têm todas as colunas que esta
|