@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
|
@@ -17,8 +17,10 @@ export function buildPasswordHistory(ctx, db) {
|
|
|
17
17
|
* Lucid expõe `db.query().from(table)` para SELECT e `db.table(table).insert()` para INSERT.
|
|
18
18
|
* Para DELETE usamos `db.query().from(table)`.
|
|
19
19
|
*/
|
|
20
|
+
// Lido a cada uso: o `defineConfig` informa o schema depois de o store existir.
|
|
21
|
+
const table = () => ctx.tableName ? ctx.tableName('auth_password_history') : 'auth_password_history';
|
|
20
22
|
function qFrom() {
|
|
21
|
-
return db.query().from(
|
|
23
|
+
return db.query().from(table());
|
|
22
24
|
}
|
|
23
25
|
return {
|
|
24
26
|
async isPasswordReused(accountId, plainPassword, count, nativeVerify) {
|
|
@@ -51,7 +53,7 @@ export function buildPasswordHistory(ctx, db) {
|
|
|
51
53
|
},
|
|
52
54
|
async recordPasswordHistory(accountId, oldHash) {
|
|
53
55
|
try {
|
|
54
|
-
await db.table(
|
|
56
|
+
await db.table(table()).insert({
|
|
55
57
|
account_id: accountId,
|
|
56
58
|
password_hash: oldHash,
|
|
57
59
|
created_at: DateTime.now().toISO(),
|
|
@@ -78,6 +78,17 @@ export interface LucidStoreContext {
|
|
|
78
78
|
* recovery da conta. Ver nota de `emailVerificationTtlHours`.
|
|
79
79
|
*/
|
|
80
80
|
emailChangeTtlHours?: number;
|
|
81
|
+
/**
|
|
82
|
+
* Query client for AuthKit's own tables (`auth_mfa`, `auth_password_history`):
|
|
83
|
+
* `schema.connection` of `defineConfig`, NOT the account model's connection. Absent
|
|
84
|
+
* → the model's connection (a store used without `defineConfig`).
|
|
85
|
+
*/
|
|
86
|
+
tablesClient?: () => any;
|
|
87
|
+
/**
|
|
88
|
+
* Nome de uma tabela do authkit como os statements a escrevem: com `schema.name`,
|
|
89
|
+
* `schema.tabela` (o knex cita cada parte). Ausente → o nome puro.
|
|
90
|
+
*/
|
|
91
|
+
tableName?: (name: string) => string;
|
|
81
92
|
}
|
|
82
93
|
export declare const sha256: (value: string) => string;
|
|
83
94
|
/** Recovery code legível: 10 chars hex em duas metades (ex.: a1b2c-3d4e5). */
|
|
@@ -105,15 +116,17 @@ export interface MfaStatePatch {
|
|
|
105
116
|
}
|
|
106
117
|
/**
|
|
107
118
|
* Repositório do estado de MFA na tabela LIB-OWNED `auth_mfa`, keyed por
|
|
108
|
-
* `account_id`. Acessa o banco pela
|
|
109
|
-
*
|
|
119
|
+
* `account_id`. Acessa o banco pela conexão das tabelas do authkit
|
|
120
|
+
* (`schema.connection`, via `tablesClient`), a mesma em que o `autoManage` cria a
|
|
121
|
+
* tabela — sem exigir migration no host. Sem `tablesClient` (store fora do
|
|
122
|
+
* `defineConfig`), cai na conexão do model.
|
|
110
123
|
*
|
|
111
124
|
* O `upsert` é portável (sqlite + pg sem UPSERT nativo): tenta UPDATE pela PK;
|
|
112
125
|
* se não afetou nenhuma linha, faz INSERT. `recovery_codes` é serializado como
|
|
113
126
|
* JSON (string) na escrita e desserializado na leitura (aceita tanto string —
|
|
114
127
|
* sqlite/text — quanto array já parseado — pg jsonb).
|
|
115
128
|
*/
|
|
116
|
-
export declare function buildMfaStateRepo(Model: any): {
|
|
129
|
+
export declare function buildMfaStateRepo(Model: any, tablesClient?: () => any, tableName?: (name: string) => string): {
|
|
117
130
|
/** Lê o estado de MFA da conta; null se não há linha em `auth_mfa`. */
|
|
118
131
|
read(accountId: string): Promise<MfaState | null>;
|
|
119
132
|
/**
|
|
@@ -39,19 +39,22 @@ export function hashesEqual(a, b) {
|
|
|
39
39
|
}
|
|
40
40
|
/**
|
|
41
41
|
* Repositório do estado de MFA na tabela LIB-OWNED `auth_mfa`, keyed por
|
|
42
|
-
* `account_id`. Acessa o banco pela
|
|
43
|
-
*
|
|
42
|
+
* `account_id`. Acessa o banco pela conexão das tabelas do authkit
|
|
43
|
+
* (`schema.connection`, via `tablesClient`), a mesma em que o `autoManage` cria a
|
|
44
|
+
* tabela — sem exigir migration no host. Sem `tablesClient` (store fora do
|
|
45
|
+
* `defineConfig`), cai na conexão do model.
|
|
44
46
|
*
|
|
45
47
|
* O `upsert` é portável (sqlite + pg sem UPSERT nativo): tenta UPDATE pela PK;
|
|
46
48
|
* se não afetou nenhuma linha, faz INSERT. `recovery_codes` é serializado como
|
|
47
49
|
* JSON (string) na escrita e desserializado na leitura (aceita tanto string —
|
|
48
50
|
* sqlite/text — quanto array já parseado — pg jsonb).
|
|
49
51
|
*/
|
|
50
|
-
export function buildMfaStateRepo(Model) {
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
export function buildMfaStateRepo(Model, tablesClient, tableName = (name) => name) {
|
|
53
|
+
// Lido a cada uso: o `defineConfig` informa o schema depois de o store existir.
|
|
54
|
+
const table = () => tableName('auth_mfa');
|
|
55
|
+
/** QueryClient das tabelas do authkit (equivale ao `db` do password_history). */
|
|
53
56
|
function client() {
|
|
54
|
-
return Model.query().client;
|
|
57
|
+
return tablesClient ? tablesClient() : Model.query().client;
|
|
55
58
|
}
|
|
56
59
|
/** Normaliza `recovery_codes` vindo do banco (json string OU array já parseado). */
|
|
57
60
|
function parseRecoveryCodes(raw) {
|
|
@@ -90,7 +93,7 @@ export function buildMfaStateRepo(Model) {
|
|
|
90
93
|
return {
|
|
91
94
|
/** Lê o estado de MFA da conta; null se não há linha em `auth_mfa`. */
|
|
92
95
|
async read(accountId) {
|
|
93
|
-
const row = await client().from(
|
|
96
|
+
const row = await client().from(table()).where('account_id', accountId).first();
|
|
94
97
|
if (!row)
|
|
95
98
|
return null;
|
|
96
99
|
return {
|
|
@@ -124,17 +127,17 @@ export function buildMfaStateRepo(Model) {
|
|
|
124
127
|
}
|
|
125
128
|
if ('lastTotpStep' in patch)
|
|
126
129
|
update.last_totp_step = patch.lastTotpStep ?? null;
|
|
127
|
-
const affected = await client().from(
|
|
130
|
+
const affected = await client().from(table()).where('account_id', accountId).update(update);
|
|
128
131
|
// knex update retorna o nº de linhas afetadas (0 = linha ainda não existe).
|
|
129
132
|
if (!affected) {
|
|
130
133
|
await client()
|
|
131
|
-
.table(
|
|
134
|
+
.table(table())
|
|
132
135
|
.insert({ account_id: accountId, ...update });
|
|
133
136
|
}
|
|
134
137
|
},
|
|
135
138
|
/** Remove o estado de MFA da conta (disable / reset total). No-op se ausente. */
|
|
136
139
|
async clear(accountId) {
|
|
137
|
-
await client().from(
|
|
140
|
+
await client().from(table()).where('account_id', accountId).delete();
|
|
138
141
|
},
|
|
139
142
|
};
|
|
140
143
|
}
|
|
@@ -8,7 +8,7 @@ export function buildWebauthn(ctx, Credential, webauthn, ceremonies) {
|
|
|
8
8
|
const { Model } = ctx;
|
|
9
9
|
// Estado de MFA é LIB-OWNED (tabela `auth_mfa`): registrar uma passkey habilita
|
|
10
10
|
// o MFA gravando `mfa_enabled_at` em `auth_mfa`, não numa coluna do model.
|
|
11
|
-
const mfaState = buildMfaStateRepo(Model);
|
|
11
|
+
const mfaState = buildMfaStateRepo(Model, ctx.tablesClient, ctx.tableName);
|
|
12
12
|
return {
|
|
13
13
|
async generatePasskeyRegistrationOptions(accountId) {
|
|
14
14
|
const row = await Model.find(accountId);
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import type { Database } from '@adonisjs/lucid/database';
|
|
2
2
|
import type { EnumeratedArtifact, OidcAdapter, OidcPayload } from './adapter_contract.js';
|
|
3
|
+
/** Nome da tabela (sem schema). O adapter recebe o nome já qualificado pelo factory. */
|
|
4
|
+
export declare const OIDC_PAYLOADS_TABLE = "authkit_oidc_payloads";
|
|
3
5
|
export declare class DatabaseAdapter implements OidcAdapter {
|
|
4
6
|
#private;
|
|
5
7
|
private name;
|
|
6
8
|
private db;
|
|
7
|
-
|
|
9
|
+
/** `authkit_oidc_payloads`, qualificada com `schema.name` quando há schema. */
|
|
10
|
+
private table;
|
|
11
|
+
constructor(name: string, db: Database,
|
|
12
|
+
/** `authkit_oidc_payloads`, qualificada com `schema.name` quando há schema. */
|
|
13
|
+
table?: string);
|
|
8
14
|
upsert(id: string, payload: OidcPayload, expiresIn: number): Promise<void>;
|
|
9
15
|
renewSession(id: string, expiresIn: number): Promise<boolean>;
|
|
10
16
|
find(id: string): Promise<OidcPayload | undefined>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
/** Nome da tabela (sem schema). O adapter recebe o nome já qualificado pelo factory. */
|
|
2
|
+
export const OIDC_PAYLOADS_TABLE = 'authkit_oidc_payloads';
|
|
2
3
|
/** Intervalo mínimo, por conexão, entre duas limpezas das linhas expiradas. */
|
|
3
4
|
const PRUNE_INTERVAL_MS = 10 * 60_000;
|
|
4
5
|
/** Quanto tempo a ausência da coluna `account_id` vale antes de re-checar. */
|
|
@@ -6,11 +7,17 @@ const ACCOUNT_COLUMN_RECHECK_MS = 60_000;
|
|
|
6
7
|
/** Tamanho máximo de uma lista `IN (...)`. */
|
|
7
8
|
const IN_CHUNK = 500;
|
|
8
9
|
const connectionStates = new WeakMap();
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
/** Estado por conexão E tabela (duas instalações em schemas diferentes não se misturam). */
|
|
11
|
+
function connectionState(db, table) {
|
|
12
|
+
let byTable = connectionStates.get(db);
|
|
13
|
+
if (!byTable) {
|
|
14
|
+
byTable = new Map();
|
|
15
|
+
connectionStates.set(db, byTable);
|
|
16
|
+
}
|
|
17
|
+
let state = byTable.get(table);
|
|
11
18
|
if (!state) {
|
|
12
19
|
state = { accountColumn: null, accountColumnRecheckAt: 0, nextPruneAt: 0 };
|
|
13
|
-
|
|
20
|
+
byTable.set(table, state);
|
|
14
21
|
}
|
|
15
22
|
return state;
|
|
16
23
|
}
|
|
@@ -29,12 +36,16 @@ function chunks(items) {
|
|
|
29
36
|
export class DatabaseAdapter {
|
|
30
37
|
name;
|
|
31
38
|
db;
|
|
32
|
-
|
|
39
|
+
table;
|
|
40
|
+
constructor(name, db,
|
|
41
|
+
/** `authkit_oidc_payloads`, qualificada com `schema.name` quando há schema. */
|
|
42
|
+
table = OIDC_PAYLOADS_TABLE) {
|
|
33
43
|
this.name = name;
|
|
34
44
|
this.db = db;
|
|
45
|
+
this.table = table;
|
|
35
46
|
}
|
|
36
47
|
#query() {
|
|
37
|
-
return this.db.query().from(
|
|
48
|
+
return this.db.query().from(this.table).where('model_name', this.name);
|
|
38
49
|
}
|
|
39
50
|
/**
|
|
40
51
|
* Valor de `expires_at` (gravação e comparação) para o dialeto da conexão. No
|
|
@@ -43,7 +54,7 @@ export class DatabaseAdapter {
|
|
|
43
54
|
* driver serializa no formato do banco — o MySQL recusa o ISO com `Z`.
|
|
44
55
|
*/
|
|
45
56
|
#timestamp(ms) {
|
|
46
|
-
const state = connectionState(this.db);
|
|
57
|
+
const state = connectionState(this.db, this.table);
|
|
47
58
|
if (state.dialect === undefined) {
|
|
48
59
|
// O factory passa o manager (`Database`) ou um `QueryClient` de conexão nomeada.
|
|
49
60
|
const db = this.db;
|
|
@@ -66,13 +77,13 @@ export class DatabaseAdapter {
|
|
|
66
77
|
* re-checado, para pegar o ALTER do boot que rodou depois do primeiro request.
|
|
67
78
|
*/
|
|
68
79
|
async #hasAccountColumn() {
|
|
69
|
-
const state = connectionState(this.db);
|
|
80
|
+
const state = connectionState(this.db, this.table);
|
|
70
81
|
if (state.accountColumn === true)
|
|
71
82
|
return true;
|
|
72
83
|
if (state.accountColumn === false && state.accountColumnRecheckAt > Date.now())
|
|
73
84
|
return false;
|
|
74
85
|
try {
|
|
75
|
-
await this.db.query().from(
|
|
86
|
+
await this.db.query().from(this.table).select('account_id').limit(1);
|
|
76
87
|
state.accountColumn = true;
|
|
77
88
|
}
|
|
78
89
|
catch {
|
|
@@ -88,7 +99,7 @@ export class DatabaseAdapter {
|
|
|
88
99
|
* do caminho do request e nunca lança.
|
|
89
100
|
*/
|
|
90
101
|
#maybePrune() {
|
|
91
|
-
const state = connectionState(this.db);
|
|
102
|
+
const state = connectionState(this.db, this.table);
|
|
92
103
|
const now = Date.now();
|
|
93
104
|
if (state.nextPruneAt > now)
|
|
94
105
|
return;
|
|
@@ -96,7 +107,7 @@ export class DatabaseAdapter {
|
|
|
96
107
|
Promise.resolve()
|
|
97
108
|
.then(() => this.db
|
|
98
109
|
.query()
|
|
99
|
-
.from(
|
|
110
|
+
.from(this.table)
|
|
100
111
|
.whereNotNull('expires_at')
|
|
101
112
|
.where('expires_at', '<=', this.#timestamp(now))
|
|
102
113
|
.delete())
|
|
@@ -125,7 +136,7 @@ export class DatabaseAdapter {
|
|
|
125
136
|
await this.#query().where('id', id).update(row);
|
|
126
137
|
}
|
|
127
138
|
else {
|
|
128
|
-
await this.db.table(
|
|
139
|
+
await this.db.table(this.table).insert(row);
|
|
129
140
|
}
|
|
130
141
|
this.#maybePrune();
|
|
131
142
|
}
|
|
@@ -185,7 +196,7 @@ export class DatabaseAdapter {
|
|
|
185
196
|
await this.#query().where('id', id).delete();
|
|
186
197
|
}
|
|
187
198
|
async revokeByGrantId(grantId) {
|
|
188
|
-
await this.db.query().from(
|
|
199
|
+
await this.db.query().from(this.table).where('grant_id', grantId).delete();
|
|
189
200
|
}
|
|
190
201
|
/**
|
|
191
202
|
* Enumeração genérica dos artefatos do model deste adapter (id + payload).
|
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
import type { ApplicationService } from '@adonisjs/core/types';
|
|
2
2
|
import type { OidcAdapter } from './adapter_contract.js';
|
|
3
3
|
export type OidcAdapterClass = new (name: string) => OidcAdapter;
|
|
4
|
+
/** What `defineConfig` hands every adapter factory. */
|
|
5
|
+
export interface AdapterResolveContext {
|
|
6
|
+
/**
|
|
7
|
+
* `schema.connection` of `defineConfig`: the Lucid connection AuthKit's tables live
|
|
8
|
+
* on. A database adapter without its own `connection` uses it.
|
|
9
|
+
*/
|
|
10
|
+
connection?: string;
|
|
11
|
+
/** `schema.name` of `defineConfig`. A database adapter without its own `schema` uses it. */
|
|
12
|
+
schema?: string;
|
|
13
|
+
}
|
|
4
14
|
export interface AdapterFactory {
|
|
5
|
-
resolver(app: ApplicationService): Promise<OidcAdapterClass>;
|
|
15
|
+
resolver(app: ApplicationService, context?: AdapterResolveContext): Promise<OidcAdapterClass>;
|
|
6
16
|
}
|
|
7
17
|
/**
|
|
8
18
|
* Modelos do oidc-provider de vida curta, amarrados ao ciclo da sessão de
|
|
@@ -35,8 +45,16 @@ export interface RedisAdapterConfig {
|
|
|
35
45
|
prefix?: string;
|
|
36
46
|
}
|
|
37
47
|
export interface DatabaseAdapterConfig {
|
|
38
|
-
/**
|
|
48
|
+
/**
|
|
49
|
+
* Nome da conexão Lucid. Default: `schema.connection` do `defineConfig` (a conexão
|
|
50
|
+
* de todas as tabelas do authkit), e sem ela a primária.
|
|
51
|
+
*/
|
|
39
52
|
connection?: string;
|
|
53
|
+
/**
|
|
54
|
+
* Schema do banco da tabela `authkit_oidc_payloads` (MySQL: o database). Default:
|
|
55
|
+
* `schema.name` do `defineConfig`. Vai escrito em cada statement — nunca via searchPath.
|
|
56
|
+
*/
|
|
57
|
+
schema?: string;
|
|
40
58
|
}
|
|
41
59
|
export declare const adapters: {
|
|
42
60
|
/**
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { assertSchemaSupported, authkitTable, validateSchemaName, } from '../schema/tables_location.js';
|
|
2
|
+
import { DatabaseAdapter, OIDC_PAYLOADS_TABLE } from './database_adapter.js';
|
|
2
3
|
import { RedisAdapter } from './redis_adapter.js';
|
|
3
4
|
/**
|
|
4
5
|
* Modelos do oidc-provider de vida curta, amarrados ao ciclo da sessão de
|
|
@@ -67,13 +68,16 @@ export const adapters = {
|
|
|
67
68
|
*/
|
|
68
69
|
database(config = {}) {
|
|
69
70
|
return {
|
|
70
|
-
async resolver(app) {
|
|
71
|
+
async resolver(app, context) {
|
|
71
72
|
const db = await app.container.make('lucid.db');
|
|
72
|
-
const connection = config.connection;
|
|
73
|
+
const connection = config.connection ?? context?.connection;
|
|
74
|
+
const schema = validateSchemaName(config.schema ?? context?.schema, 'adapters.database({ schema })');
|
|
73
75
|
const conn = connection ? db.connection(connection) : db;
|
|
76
|
+
assertSchemaSupported(connection ? conn : db.connection(), schema);
|
|
77
|
+
const table = authkitTable({ schema }, OIDC_PAYLOADS_TABLE);
|
|
74
78
|
return class extends DatabaseAdapter {
|
|
75
79
|
constructor(name) {
|
|
76
|
-
super(name, conn);
|
|
80
|
+
super(name, conn, table);
|
|
77
81
|
}
|
|
78
82
|
};
|
|
79
83
|
},
|
|
@@ -46,8 +46,12 @@ export interface LiveGrantFilter {
|
|
|
46
46
|
export declare class DelegationStore {
|
|
47
47
|
#private;
|
|
48
48
|
private conn;
|
|
49
|
+
/** Nome da tabela como os statements o escrevem (`schema.tabela` com `schema.name`). */
|
|
50
|
+
private t;
|
|
49
51
|
/** `conn` = uma conexão Lucid (`db.connection(name?)`) — ou uma transação. */
|
|
50
|
-
constructor(conn: () => any
|
|
52
|
+
constructor(conn: () => any,
|
|
53
|
+
/** Nome da tabela como os statements o escrevem (`schema.tabela` com `schema.name`). */
|
|
54
|
+
t?: (name: string) => string);
|
|
51
55
|
insertDevice(row: Omit<DeviceCodeRow, 'accountId' | 'grantId' | 'lastPolledAt'>): Promise<void>;
|
|
52
56
|
findDeviceByCodeHash(hash: string): Promise<DeviceCodeRow | null>;
|
|
53
57
|
findDeviceByUserCode(userCode: string): Promise<DeviceCodeRow | null>;
|
|
@@ -63,13 +63,17 @@ function affected(result) {
|
|
|
63
63
|
}
|
|
64
64
|
export class DelegationStore {
|
|
65
65
|
conn;
|
|
66
|
+
t;
|
|
66
67
|
/** `conn` = uma conexão Lucid (`db.connection(name?)`) — ou uma transação. */
|
|
67
|
-
constructor(conn
|
|
68
|
+
constructor(conn,
|
|
69
|
+
/** Nome da tabela como os statements o escrevem (`schema.tabela` com `schema.name`). */
|
|
70
|
+
t = (name) => name) {
|
|
68
71
|
this.conn = conn;
|
|
72
|
+
this.t = t;
|
|
69
73
|
}
|
|
70
74
|
// ─── device codes ─────────────────────────────────────────────────────────
|
|
71
75
|
async insertDevice(row) {
|
|
72
|
-
await this.conn().table(DEVICE_TABLE).insert({
|
|
76
|
+
await this.conn().table(this.t(DEVICE_TABLE)).insert({
|
|
73
77
|
id: row.id,
|
|
74
78
|
device_code_hash: row.deviceCodeHash,
|
|
75
79
|
user_code: row.userCode,
|
|
@@ -83,19 +87,22 @@ export class DelegationStore {
|
|
|
83
87
|
});
|
|
84
88
|
}
|
|
85
89
|
async findDeviceByCodeHash(hash) {
|
|
86
|
-
const row = await this.conn()
|
|
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;
|