@adonis-agora/authkit-server 1.0.0-rc.10 → 1.0.0-rc.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -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 +16 -4
- package/build/src/accounts/account_store.d.ts +7 -7
- package/build/src/accounts/lucid_account_store.d.ts +3 -3
- package/build/src/accounts/lucid_account_store.js +43 -17
- package/build/src/accounts/lucid_store/mfa.js +3 -3
- package/build/src/accounts/lucid_store/organization_models.d.ts +18 -3
- package/build/src/accounts/lucid_store/organization_models.js +35 -3
- package/build/src/accounts/lucid_store/organizations.d.ts +2 -2
- package/build/src/accounts/lucid_store/organizations.js +35 -32
- package/build/src/accounts/lucid_store/password_hygiene.js +4 -2
- package/build/src/accounts/lucid_store/shared.d.ts +16 -3
- package/build/src/accounts/lucid_store/shared.js +13 -10
- 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 +20 -2
- package/build/src/adapters/factory.js +8 -4
- 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 +5 -5
- package/build/src/define_config.d.ts +12 -1
- package/build/src/define_config.js +34 -3
- package/build/src/doctor/agora_checks.js +13 -0
- package/build/src/doctor/checks.js +2 -2
- 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 +21 -6
- package/build/src/host/runtime_settings.js +29 -13
- 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 +19 -1
- package/build/src/schema/ensure.js +54 -30
- package/build/src/schema/tables_location.d.ts +56 -0
- package/build/src/schema/tables_location.js +86 -0
- package/package.json +3 -3
- package/skills/authkit-idp-setup/SKILL.md +13 -7
|
@@ -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
|
// ---------------------------------------------------------------------------
|
|
@@ -131,16 +131,16 @@ function validateKnownKey(key, value) {
|
|
|
131
131
|
async function resolveSettingsService(app) {
|
|
132
132
|
try {
|
|
133
133
|
const db = await app.container.make('lucid.db');
|
|
134
|
-
//
|
|
135
|
-
let
|
|
134
|
+
// `auth_settings` vive na conexão das tabelas do authkit (`schema.connection`).
|
|
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;
|
|
@@ -1190,11 +1190,21 @@ export interface AuthServerConfigInput {
|
|
|
1190
1190
|
* adiciona colunas novas (aditivo — nunca dropa nem altera tipos).
|
|
1191
1191
|
* - `autoManage: false`: nada roda no boot; gerencie o schema você mesmo —
|
|
1192
1192
|
* de preferência chamando `ensureAuthkitSchema(this.db)` numa migration.
|
|
1193
|
-
* - `connection`: conexão Lucid
|
|
1193
|
+
* - `connection`: a conexão Lucid de TODAS as tabelas do authkit (default:
|
|
1194
|
+
* primária) — as acima, o `authkit_keystore`, as de extensions e a do
|
|
1195
|
+
* `adapters.database()`/keystore `lucid` quando eles não declaram a própria.
|
|
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.
|
|
1194
1203
|
*/
|
|
1195
1204
|
schema?: {
|
|
1196
1205
|
autoManage?: boolean;
|
|
1197
1206
|
connection?: string;
|
|
1207
|
+
name?: string;
|
|
1198
1208
|
};
|
|
1199
1209
|
/**
|
|
1200
1210
|
* Ciclo de vida da conta (LGPD/GDPR). OPT-IN para workflows DURÁVEIS:
|
|
@@ -1404,6 +1414,7 @@ export interface ResolvedServerConfig {
|
|
|
1404
1414
|
schema: {
|
|
1405
1415
|
autoManage: boolean;
|
|
1406
1416
|
connection?: string;
|
|
1417
|
+
name?: string;
|
|
1407
1418
|
};
|
|
1408
1419
|
/**
|
|
1409
1420
|
* Ciclo de vida da conta resolvido. `durable` default `false` → caminho
|
|
@@ -17,6 +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 { assertSchemaSupported, authkitTablesClient, bindTablesLocation, validateSchemaName, } from './schema/tables_location.js';
|
|
20
21
|
import { resolveServiceTenants, } from './service_tenants/config.js';
|
|
21
22
|
export { adapters };
|
|
22
23
|
const RATE_LIMIT_DEFAULTS = {
|
|
@@ -284,15 +285,31 @@ export function defineConfig(config) {
|
|
|
284
285
|
(!Number.isInteger(requiredFactors) || requiredFactors < 2 || requiredFactors > 8)) {
|
|
285
286
|
throw new Error('mfa.requiredFactors must be an integer between 2 and 8');
|
|
286
287
|
}
|
|
288
|
+
// Toda tabela do AuthKit vive em `schema.connection` — inclusive as que o store da
|
|
289
|
+
// conta lê (auth_mfa, auth_password_history, organizations), que antes seguiam a
|
|
290
|
+
// conexão do model da conta.
|
|
291
|
+
const tablesSchema = validateSchemaName(config.schema?.name);
|
|
292
|
+
bindTablesLocation(config.accountStore, {
|
|
293
|
+
connection: config.schema?.connection,
|
|
294
|
+
schema: tablesSchema,
|
|
295
|
+
});
|
|
287
296
|
return configProvider.create(async (app) => {
|
|
288
|
-
const
|
|
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
|
+
}
|
|
305
|
+
const AdapterClass = await config.adapter.resolver(app, tablesContext);
|
|
289
306
|
// `session.adapter` ausente ⇒ mesma classe do default (back-compat: o
|
|
290
307
|
// dispatcher vira identidade e tudo continua no adapter único de hoje).
|
|
291
308
|
const SessionAdapterClass = config.session?.adapter
|
|
292
|
-
? await config.session.adapter.resolver(app)
|
|
309
|
+
? await config.session.adapter.resolver(app, tablesContext)
|
|
293
310
|
: AdapterClass;
|
|
294
311
|
// `jwks: 'auto'` → resolve env-aware: AUTHKIT_JWKS inline, senão managed em arquivo.
|
|
295
|
-
const
|
|
312
|
+
const jwksInput = config.jwks === 'auto'
|
|
296
313
|
? process.env.AUTHKIT_JWKS
|
|
297
314
|
? {
|
|
298
315
|
source: 'jwks',
|
|
@@ -304,6 +321,19 @@ export function defineConfig(config) {
|
|
|
304
321
|
store: 'tmp/authkit_jwks.json',
|
|
305
322
|
}
|
|
306
323
|
: config.jwks;
|
|
324
|
+
// Keystore no Lucid sem conexão/schema próprios fica onde estão as tabelas do authkit.
|
|
325
|
+
const jwksStore = jwksInput.store;
|
|
326
|
+
const jwksConfig = jwksStore?.driver === 'lucid' &&
|
|
327
|
+
((!jwksStore.connection && config.schema?.connection) || (!jwksStore.schema && tablesSchema))
|
|
328
|
+
? {
|
|
329
|
+
...jwksInput,
|
|
330
|
+
store: {
|
|
331
|
+
...jwksStore,
|
|
332
|
+
connection: jwksStore.connection ?? config.schema?.connection,
|
|
333
|
+
schema: jwksStore.schema ?? tablesSchema,
|
|
334
|
+
},
|
|
335
|
+
}
|
|
336
|
+
: jwksInput;
|
|
307
337
|
if (config.jwks === 'auto' && !process.env.AUTHKIT_JWKS) {
|
|
308
338
|
const warning = jwksAutoFallbackWarning(jwksConfig.store ?? null);
|
|
309
339
|
if (warning) {
|
|
@@ -490,6 +520,7 @@ export function defineConfig(config) {
|
|
|
490
520
|
schema: {
|
|
491
521
|
autoManage: config.schema?.autoManage !== false,
|
|
492
522
|
connection: config.schema?.connection,
|
|
523
|
+
name: tablesSchema,
|
|
493
524
|
},
|
|
494
525
|
// OPT-IN: default false → caminho síncrono de sempre (byte-idêntico).
|
|
495
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',
|
|
@@ -553,8 +553,8 @@ export function checkSettings(input) {
|
|
|
553
553
|
// Table absent — opt-in feature, silently ok.
|
|
554
554
|
return null;
|
|
555
555
|
}
|
|
556
|
-
// Informa a conexão usada quando nomeada (
|
|
557
|
-
const connectionName = cfg.
|
|
556
|
+
// Informa a conexão usada quando nomeada (a das tabelas do authkit).
|
|
557
|
+
const connectionName = cfg.schema?.connection;
|
|
558
558
|
const connectionHint = connectionName ? ` (connection: '${connectionName}')` : '';
|
|
559
559
|
// Table present — check if botProtection.verify is configured.
|
|
560
560
|
const hasBotVerify = typeof cfg.botProtection?.verify === 'function';
|
|
@@ -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,
|
|
@@ -92,13 +92,26 @@ export interface RuntimeSettingsOptions {
|
|
|
92
92
|
/** TTL do cache em ms. Default: 15_000 (15s). */
|
|
93
93
|
ttlMs?: number;
|
|
94
94
|
/**
|
|
95
|
-
* Nome da conexão Lucid a usar (ex.: 'auth')
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
* Ausente (ou undefined) → conexão default
|
|
95
|
+
* Nome da conexão Lucid a usar (ex.: 'auth') — a `schema.connection` do
|
|
96
|
+
* `defineConfig`, onde vivem as tabelas do authkit. Quando presente, todas as
|
|
97
|
+
* queries usam `db.connection(name)` em vez da conexão default.
|
|
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();
|
|
@@ -171,8 +186,8 @@ export declare class RuntimeSettings implements SettingsCapability {
|
|
|
171
186
|
*
|
|
172
187
|
* Esta fábrica SUBSTITUI ~16 cópias do mesmo bloco db/connection que viviam sob
|
|
173
188
|
* nomes distintos (`getRuntimeSettings`, `getSettingsService`,
|
|
174
|
-
* `getRuntimeSettingsForSudo`)
|
|
175
|
-
*
|
|
189
|
+
* `getRuntimeSettingsForSudo`). A conexão é `schema.connection`, a de todas as
|
|
190
|
+
* tabelas do authkit.
|
|
176
191
|
*
|
|
177
192
|
* @example
|
|
178
193
|
* ```ts
|
|
@@ -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) {
|
|
@@ -312,8 +328,8 @@ export class RuntimeSettings {
|
|
|
312
328
|
*
|
|
313
329
|
* Esta fábrica SUBSTITUI ~16 cópias do mesmo bloco db/connection que viviam sob
|
|
314
330
|
* nomes distintos (`getRuntimeSettings`, `getSettingsService`,
|
|
315
|
-
* `getRuntimeSettingsForSudo`)
|
|
316
|
-
*
|
|
331
|
+
* `getRuntimeSettingsForSudo`). A conexão é `schema.connection`, a de todas as
|
|
332
|
+
* tabelas do authkit.
|
|
317
333
|
*
|
|
318
334
|
* @example
|
|
319
335
|
* ```ts
|
|
@@ -325,8 +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
|
-
|
|
329
|
-
return new RuntimeSettings(db,
|
|
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));
|
|
330
346
|
}
|
|
331
347
|
catch {
|
|
332
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
|
}
|