@adonis-agora/authkit-server 1.0.0-rc.4 → 1.0.0-rc.6
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 +14 -0
- package/build/commands/agora_doctor.d.ts +22 -0
- package/build/commands/agora_doctor.js +109 -0
- package/build/commands/commands.json +64 -0
- package/build/commands/doctor.js +8 -74
- package/build/host/views/agents/consent.edge +11 -2
- package/build/host/views/agents/done.edge +1 -1
- package/build/index.d.ts +1 -1
- package/build/providers/authkit_server_provider.js +17 -0
- package/build/src/dev_tools/contract.d.ts +72 -0
- package/build/src/dev_tools/contract.js +10 -0
- package/build/src/dev_tools/gate.d.ts +17 -0
- package/build/src/dev_tools/gate.js +69 -0
- package/build/src/dev_tools/register.d.ts +11 -0
- package/build/src/dev_tools/register.js +25 -0
- package/build/src/dev_tools/tools.d.ts +24 -0
- package/build/src/dev_tools/tools.js +384 -0
- package/build/src/doctor/agora_checks.d.ts +15 -0
- package/build/src/doctor/agora_checks.js +150 -0
- package/build/src/doctor/agora_doctor.d.ts +115 -0
- package/build/src/doctor/agora_doctor.js +692 -0
- package/build/src/doctor/doctor_input.d.ts +27 -0
- package/build/src/doctor/doctor_input.js +86 -0
- package/build/src/doctor/index.d.ts +5 -0
- package/build/src/doctor/index.js +4 -0
- package/build/src/host/admin_api/admin_users_service.d.ts +5 -2
- package/build/src/host/admin_authorize.d.ts +8 -0
- package/build/src/host/admin_authorize.js +25 -0
- package/build/src/host/i18n.d.ts +2 -0
- package/build/src/host/i18n.js +2 -0
- package/build/src/host/register_auth_host.js +1 -24
- package/build/src/host/screen_props.d.ts +11 -8
- package/package.json +5 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @adonis-agora/authkit-server
|
|
2
2
|
|
|
3
|
+
## 1.0.0-rc.6
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#464](https://github.com/DavideCarvalho/adonis-agora-authkit/pull/464) [`3250c35`](https://github.com/DavideCarvalho/adonis-agora-authkit/commit/3250c35eee328e8e3780df32001323be45fe1bae) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - Developer tools over MCP. When the app runs `@adonis-agora/agent`'s dev tools endpoint (`/mcp/agora`), the admin console's data is served there with no config: `authkit_get_overview`, `authkit_list_users`, `authkit_get_user`, `authkit_list_user_sessions`, `authkit_list_user_grants`, `authkit_list_orgs`, `authkit_get_org`, `authkit_list_clients`, `authkit_list_audit`, and the destructive actions `authkit_revoke_user_sessions` and `authkit_disable_user` (hidden until the endpoint allows actions; audited with `mcp:<caller>` as actor). Access follows `admin.authorize`; when it allows the MCP request, `admin.roles` is not checked. Without `admin.authorize`, production refuses and other environments need an AuthKit sign-in holding one of `admin.roles`.
|
|
8
|
+
|
|
9
|
+
`@adonis-agora/authkit-poppy` offers the `agora:dev` scope when the app serves those dev tools (`devTools` option to force it on or off). The consent screen never ticks it for the person, and Mediated Sign-In can't grant it. The personal-agent consent screen supports opt-in scopes (`optIn`, `descriptionKey` on each scope of `agents/consent`).
|
|
10
|
+
|
|
11
|
+
## 1.0.0-rc.5
|
|
12
|
+
|
|
13
|
+
### Minor Changes
|
|
14
|
+
|
|
15
|
+
- [#463](https://github.com/DavideCarvalho/adonis-agora-authkit/pull/463) [`e9f02d8`](https://github.com/DavideCarvalho/adonis-agora-authkit/commit/e9f02d8e6858f9ec8e5cdb2b77409fd0c31cb13d) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - `node ace agora:doctor`: one health check for every installed `@adonis-agora` lib. authkit registers its checks (every `authkit:doctor` finding under a stable id such as `authkit.issuer`, plus `authkit.signing-key-age`, `authkit.console-gate`, `authkit.peers` and `authkit.agora-siblings`). Flags: `--json`, `--strict`, `--deep`, `--lib`, `--only`, `--verbose`, `--no-boot`; exit `0` / `1` (error, or warning with `--strict`) / `2` (the doctor could not run). New `@adonis-agora/authkit-server/doctor` entry exports the checks for `--no-boot`. `authkit:doctor` is unchanged.
|
|
16
|
+
|
|
3
17
|
## 1.0.0-rc.4
|
|
4
18
|
|
|
5
19
|
### Patch Changes
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { BaseCommand } from '@adonisjs/core/ace';
|
|
2
|
+
import type { CommandOptions } from '@adonisjs/core/types/ace';
|
|
3
|
+
/**
|
|
4
|
+
* `node ace agora:doctor`: one health check for every installed `@adonis-agora` lib. Exit code `0`
|
|
5
|
+
* when nothing is an error (`--strict`: nor a warning), `1` otherwise, `2` when the doctor itself
|
|
6
|
+
* cannot run.
|
|
7
|
+
*/
|
|
8
|
+
export default class AgoraDoctor extends BaseCommand {
|
|
9
|
+
static commandName: string;
|
|
10
|
+
static description: string;
|
|
11
|
+
static options: CommandOptions;
|
|
12
|
+
/** A `<lib>:doctor` alias sets this to pre-filter its lib. */
|
|
13
|
+
protected presetLib: string | undefined;
|
|
14
|
+
json: boolean;
|
|
15
|
+
lib?: string;
|
|
16
|
+
deep: boolean;
|
|
17
|
+
only?: string;
|
|
18
|
+
strict: boolean;
|
|
19
|
+
verbose: boolean;
|
|
20
|
+
boot: boolean;
|
|
21
|
+
run(): Promise<void>;
|
|
22
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// agora-doctor command v1 — keep identical across libs
|
|
2
|
+
// Revision 1.1 (see the canonical README for the changelog).
|
|
3
|
+
// Every @adonis-agora lib ships this command (commands/agora_doctor.ts); whichever copy Ace loads
|
|
4
|
+
// last wins, and they all behave the same. Each repo's CI checks its sha256 against the pinned
|
|
5
|
+
// canonical one. Never edit a lib's copy: change the canonical copy, then re-copy it into every lib.
|
|
6
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
7
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
9
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
10
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
11
|
+
};
|
|
12
|
+
import { BaseCommand, flags } from '@adonisjs/core/ace';
|
|
13
|
+
import { AgoraDoctorUsageError, discoverDoctorChecks, readDoctorChecks, renderAgoraDoctor, runAgoraDoctor, } from '../src/doctor/agora_doctor.js';
|
|
14
|
+
/**
|
|
15
|
+
* `node ace agora:doctor`: one health check for every installed `@adonis-agora` lib. Exit code `0`
|
|
16
|
+
* when nothing is an error (`--strict`: nor a warning), `1` otherwise, `2` when the doctor itself
|
|
17
|
+
* cannot run.
|
|
18
|
+
*/
|
|
19
|
+
export default class AgoraDoctor extends BaseCommand {
|
|
20
|
+
static commandName = 'agora:doctor';
|
|
21
|
+
static description = 'Check the configuration of every installed @adonis-agora lib';
|
|
22
|
+
// The app is booted by `run()` itself, so `--no-boot` can skip it.
|
|
23
|
+
static options = { startApp: false };
|
|
24
|
+
/** A `<lib>:doctor` alias sets this to pre-filter its lib. */
|
|
25
|
+
presetLib = undefined;
|
|
26
|
+
async run() {
|
|
27
|
+
const err = (line) => process.stderr.write(`${line}\n`);
|
|
28
|
+
try {
|
|
29
|
+
let checks;
|
|
30
|
+
if (this.boot === false) {
|
|
31
|
+
checks = await discoverDoctorChecks(this.app);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
if (!this.app.isReady) {
|
|
35
|
+
try {
|
|
36
|
+
await this.app.boot();
|
|
37
|
+
await this.app.start(() => { });
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
41
|
+
throw new AgoraDoctorUsageError(`the app did not boot (${reason}). Fix the boot error, or run with --no-boot.`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
checks = await readDoctorChecks(this.app.container);
|
|
45
|
+
}
|
|
46
|
+
const libs = (this.lib ?? this.presetLib)
|
|
47
|
+
?.split(',')
|
|
48
|
+
.map((name) => name.trim())
|
|
49
|
+
.filter(Boolean);
|
|
50
|
+
const { report, exitCode } = await runAgoraDoctor(this.app, checks, {
|
|
51
|
+
...(libs ? { libs } : {}),
|
|
52
|
+
...(this.only ? { only: this.only } : {}),
|
|
53
|
+
deep: this.deep === true,
|
|
54
|
+
noBoot: this.boot === false,
|
|
55
|
+
strict: this.strict === true,
|
|
56
|
+
});
|
|
57
|
+
if (this.json) {
|
|
58
|
+
process.stdout.write(`${JSON.stringify(report)}\n`);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
const fancy = process.stdout.isTTY === true && !process.env.NO_COLOR;
|
|
62
|
+
const c = this.colors;
|
|
63
|
+
process.stdout.write(renderAgoraDoctor(report, {
|
|
64
|
+
verbose: this.verbose === true,
|
|
65
|
+
fancy,
|
|
66
|
+
paint: {
|
|
67
|
+
green: (t) => c.green(t),
|
|
68
|
+
yellow: (t) => c.yellow(t),
|
|
69
|
+
red: (t) => c.red(t),
|
|
70
|
+
dim: (t) => c.dim(t),
|
|
71
|
+
bold: (t) => c.bold(t),
|
|
72
|
+
},
|
|
73
|
+
}));
|
|
74
|
+
}
|
|
75
|
+
this.exitCode = exitCode;
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
if (!(error instanceof AgoraDoctorUsageError))
|
|
79
|
+
throw error;
|
|
80
|
+
err(`agora:doctor: ${error.message}`);
|
|
81
|
+
this.exitCode = 2;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
__decorate([
|
|
86
|
+
flags.boolean({ description: 'Print one JSON object to stdout' })
|
|
87
|
+
], AgoraDoctor.prototype, "json", void 0);
|
|
88
|
+
__decorate([
|
|
89
|
+
flags.string({ description: 'Only these libs, comma separated (durable,payments)' })
|
|
90
|
+
], AgoraDoctor.prototype, "lib", void 0);
|
|
91
|
+
__decorate([
|
|
92
|
+
flags.boolean({ description: 'Also run checks that touch the database, Redis or the network' })
|
|
93
|
+
], AgoraDoctor.prototype, "deep", void 0);
|
|
94
|
+
__decorate([
|
|
95
|
+
flags.string({ description: 'Run one check by id' })
|
|
96
|
+
], AgoraDoctor.prototype, "only", void 0);
|
|
97
|
+
__decorate([
|
|
98
|
+
flags.boolean({ description: 'Fail (exit 1) on warnings too' })
|
|
99
|
+
], AgoraDoctor.prototype, "strict", void 0);
|
|
100
|
+
__decorate([
|
|
101
|
+
flags.boolean({ description: 'Show skipped checks and every message' })
|
|
102
|
+
], AgoraDoctor.prototype, "verbose", void 0);
|
|
103
|
+
__decorate([
|
|
104
|
+
flags.boolean({
|
|
105
|
+
description: 'Boot the app first; --no-boot checks the config only (when boot itself fails)',
|
|
106
|
+
default: true,
|
|
107
|
+
showNegatedVariantInHelp: true,
|
|
108
|
+
})
|
|
109
|
+
], AgoraDoctor.prototype, "boot", void 0);
|
|
@@ -333,6 +333,70 @@
|
|
|
333
333
|
"args": [],
|
|
334
334
|
"options": { "startApp": true },
|
|
335
335
|
"filePath": "expire_scan.js"
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
"commandName": "agora:doctor",
|
|
339
|
+
"description": "Check the configuration of every installed @adonis-agora lib",
|
|
340
|
+
"namespace": "agora",
|
|
341
|
+
"aliases": [],
|
|
342
|
+
"flags": [
|
|
343
|
+
{
|
|
344
|
+
"name": "json",
|
|
345
|
+
"flagName": "json",
|
|
346
|
+
"required": false,
|
|
347
|
+
"type": "boolean",
|
|
348
|
+
"description": "Print one JSON object to stdout"
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
"name": "lib",
|
|
352
|
+
"flagName": "lib",
|
|
353
|
+
"required": false,
|
|
354
|
+
"type": "string",
|
|
355
|
+
"description": "Only these libs, comma separated (durable,payments)"
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
"name": "deep",
|
|
359
|
+
"flagName": "deep",
|
|
360
|
+
"required": false,
|
|
361
|
+
"type": "boolean",
|
|
362
|
+
"description": "Also run checks that touch the database, Redis or the network"
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
"name": "only",
|
|
366
|
+
"flagName": "only",
|
|
367
|
+
"required": false,
|
|
368
|
+
"type": "string",
|
|
369
|
+
"description": "Run one check by id"
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
"name": "strict",
|
|
373
|
+
"flagName": "strict",
|
|
374
|
+
"required": false,
|
|
375
|
+
"type": "boolean",
|
|
376
|
+
"description": "Fail (exit 1) on warnings too"
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
"name": "verbose",
|
|
380
|
+
"flagName": "verbose",
|
|
381
|
+
"required": false,
|
|
382
|
+
"type": "boolean",
|
|
383
|
+
"description": "Show skipped checks and every message"
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
"name": "boot",
|
|
387
|
+
"flagName": "boot",
|
|
388
|
+
"required": false,
|
|
389
|
+
"type": "boolean",
|
|
390
|
+
"description": "Boot the app first; --no-boot checks the config only (when boot itself fails)",
|
|
391
|
+
"default": true,
|
|
392
|
+
"showNegatedVariantInHelp": true
|
|
393
|
+
}
|
|
394
|
+
],
|
|
395
|
+
"args": [],
|
|
396
|
+
"options": {
|
|
397
|
+
"startApp": false
|
|
398
|
+
},
|
|
399
|
+
"filePath": "agora_doctor.js"
|
|
336
400
|
}
|
|
337
401
|
]
|
|
338
402
|
}
|
package/build/commands/doctor.js
CHANGED
|
@@ -1,43 +1,6 @@
|
|
|
1
|
-
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
-
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
-
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
-
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
-
});
|
|
6
|
-
}
|
|
7
|
-
return path;
|
|
8
|
-
};
|
|
9
1
|
import { BaseCommand } from '@adonisjs/core/ace';
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import { defaultEncryptForStore } from '../src/define_config.js';
|
|
13
|
-
import { hasErrors, runAllChecks, signingKeyAgeFinding, } from '../src/doctor/checks.js';
|
|
14
|
-
import { signingKeyAgeDays } from '../src/keys/keystore.js';
|
|
15
|
-
import { KeystoreCodec } from '../src/keys/keystore_codec.js';
|
|
16
|
-
import { loadEncryptionService } from '../src/keys/keystore_crypto.js';
|
|
17
|
-
import { KeystoreManager, resolveKeystoreVault } from '../src/keys/keystore_manager.js';
|
|
18
|
-
/**
|
|
19
|
-
* Tenta importar um peer; `true` se importável.
|
|
20
|
-
*
|
|
21
|
-
* Distingue "não instalado" de "instalado mas quebrado": só um erro de
|
|
22
|
-
* módulo-não-encontrado (`ERR_MODULE_NOT_FOUND`/`MODULE_NOT_FOUND`) é tratado como
|
|
23
|
-
* peer ausente (silencioso — é o caso esperado). Qualquer outro erro significa que
|
|
24
|
-
* o peer ESTÁ presente mas falhou ao carregar (import circular, erro de sintaxe,
|
|
25
|
-
* config inválida) — isso o doctor precisa gritar, senão um peer quebrado aparece
|
|
26
|
-
* como "ausente" e o diagnóstico mente.
|
|
27
|
-
*/
|
|
28
|
-
async function canImport(specifier, logger) {
|
|
29
|
-
try {
|
|
30
|
-
await import(__rewriteRelativeImportExtension(specifier));
|
|
31
|
-
return true;
|
|
32
|
-
}
|
|
33
|
-
catch (err) {
|
|
34
|
-
const code = err?.code;
|
|
35
|
-
if (code !== 'ERR_MODULE_NOT_FOUND' && code !== 'MODULE_NOT_FOUND') {
|
|
36
|
-
logger?.warning(`⚠️ ${specifier} está instalado mas falhou ao carregar: ${err.message}`);
|
|
37
|
-
}
|
|
38
|
-
return false;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
2
|
+
import { hasErrors, runAllChecks } from '../src/doctor/checks.js';
|
|
3
|
+
import { buildDoctorInput, managedSigningKeyAgeFinding } from '../src/doctor/doctor_input.js';
|
|
41
4
|
export default class AuthkitDoctor extends BaseCommand {
|
|
42
5
|
static commandName = 'authkit:doctor';
|
|
43
6
|
static description = 'Valida a configuração do AuthKit no host e imprime achados (✅/⚠️/❌). Sai com código !=0 se houver erros.';
|
|
@@ -48,50 +11,21 @@ export default class AuthkitDoctor extends BaseCommand {
|
|
|
48
11
|
];
|
|
49
12
|
static options = { startApp: true };
|
|
50
13
|
async run() {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
// senão todo campo aparece como "ausente" (provider cru não tem issuer etc.).
|
|
54
|
-
let authkitConfig = null;
|
|
14
|
+
// `buildDoctorInput` resolve o config provider de `config/authkit.ts` antes de inspecionar.
|
|
15
|
+
let input;
|
|
55
16
|
try {
|
|
56
|
-
|
|
17
|
+
input = await buildDoctorInput(this.app, this.logger);
|
|
57
18
|
}
|
|
58
19
|
catch (error) {
|
|
59
20
|
this.logger.logError(`❌ config/authkit.ts falhou ao resolver: ${error.message}`);
|
|
60
21
|
this.exitCode = 1;
|
|
61
22
|
return;
|
|
62
23
|
}
|
|
63
|
-
const sessionConfig = config.get('session', null) ?? null;
|
|
64
|
-
const input = {
|
|
65
|
-
authkitConfig,
|
|
66
|
-
sessionConfig,
|
|
67
|
-
peers: {
|
|
68
|
-
session: await canImport('@adonisjs/session', this.logger),
|
|
69
|
-
shield: await canImport('@adonisjs/shield', this.logger),
|
|
70
|
-
ally: await canImport('@adonisjs/ally', this.logger),
|
|
71
|
-
limiter: await canImport('@adonisjs/limiter', this.logger),
|
|
72
|
-
},
|
|
73
|
-
__adapterClasses: { DatabaseAdapter },
|
|
74
|
-
};
|
|
75
24
|
const findings = runAllChecks(input);
|
|
76
25
|
// Adiciona o finding de idade da chave de assinatura managed (best-effort).
|
|
77
|
-
const
|
|
78
|
-
if (
|
|
79
|
-
|
|
80
|
-
const vault = resolveKeystoreVault(jwksInput.store, {
|
|
81
|
-
makePath: (p) => this.app.makePath(p),
|
|
82
|
-
container: this.app.container,
|
|
83
|
-
});
|
|
84
|
-
const encrypt = jwksInput.encrypt ?? defaultEncryptForStore(jwksInput.store);
|
|
85
|
-
const enc = encrypt ? await loadEncryptionService().catch(() => undefined) : undefined;
|
|
86
|
-
const mgr = new KeystoreManager(vault, new KeystoreCodec({ encrypt, enc }), jwksInput.algorithm ?? 'RS256');
|
|
87
|
-
const store = await mgr.read().catch(() => null);
|
|
88
|
-
const maxAge = jwksInput.rotationDays ?? 90;
|
|
89
|
-
findings.push(signingKeyAgeFinding(signingKeyAgeDays(store), maxAge));
|
|
90
|
-
}
|
|
91
|
-
catch {
|
|
92
|
-
/* idade é best-effort no doctor */
|
|
93
|
-
}
|
|
94
|
-
}
|
|
26
|
+
const keyAge = await managedSigningKeyAgeFinding(this.app, input.authkitConfig);
|
|
27
|
+
if (keyAge)
|
|
28
|
+
findings.push(keyAge);
|
|
95
29
|
this.print(findings);
|
|
96
30
|
if (hasErrors(findings)) {
|
|
97
31
|
this.exitCode = 1;
|
|
@@ -28,8 +28,17 @@
|
|
|
28
28
|
<legend class="text-sm font-medium text-gray-900">{{ t('agents.consent.scopes_label') }}</legend>
|
|
29
29
|
@each(scope in request.scopes)
|
|
30
30
|
<label class="flex items-start gap-3 rounded-lg border border-gray-200 p-3">
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
@if(scope.optIn)
|
|
32
|
+
<input type="checkbox" name="scope" value="{{ scope.id }}" class="mt-0.5">
|
|
33
|
+
@else
|
|
34
|
+
<input type="checkbox" name="scope" value="{{ scope.id }}" checked class="mt-0.5">
|
|
35
|
+
@end
|
|
36
|
+
<span class="text-sm text-gray-800">
|
|
37
|
+
{{ scope.descriptionKey ? t(scope.descriptionKey) : scope.description }}
|
|
38
|
+
@if(scope.optIn)
|
|
39
|
+
<span class="mt-1 block text-xs text-gray-500">{{ t('agents.consent.opt_in_hint') }}</span>
|
|
40
|
+
@end
|
|
41
|
+
</span>
|
|
33
42
|
</label>
|
|
34
43
|
@end
|
|
35
44
|
</fieldset>
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<h1 class="text-xl font-semibold text-gray-900">{{ t('agents.done.approved_title', { agent: agentName }) }}</h1>
|
|
9
9
|
<ul class="mt-4 space-y-1 text-sm text-gray-700">
|
|
10
10
|
@each(scope in scopes)
|
|
11
|
-
<li>• {{ scope.description }}</li>
|
|
11
|
+
<li>• {{ scope.descriptionKey ? t(scope.descriptionKey) : scope.description }}</li>
|
|
12
12
|
@end
|
|
13
13
|
</ul>
|
|
14
14
|
<p class="mt-4 text-sm text-gray-600">{{ t('agents.done.return') }}</p>
|
package/build/index.d.ts
CHANGED
|
@@ -117,7 +117,7 @@ export type { RuntimeSettingsOptions, SettingRow, SettingsCapability, } from './
|
|
|
117
117
|
export { RuntimeSettings, supportsSettings } from './src/host/runtime_settings.js';
|
|
118
118
|
export type { AuthMethodsCapabilities, AuthMethodsConfigOverride, AuthMethodsSetting, MaintenanceModeSetting, RegistrationSetting, RequireVerifiedEmailSetting, ResolvedAuthMethods, ResolvedMaintenanceMode, SettingKey, } from './src/host/runtime_toggles.js';
|
|
119
119
|
export { configLockedAuthMethods, resolveEffectiveAuthMethods, resolveEffectiveMaintenanceMode, resolveEffectiveRegistration, resolveEffectiveRequireVerifiedEmail, SETTING_KEYS, } from './src/host/runtime_toggles.js';
|
|
120
|
-
export type { AccountAppsProps, AccountOrg, AccountOrgsProps, AccountTokensProps, AgentConsentRequest, AgentsConsentProps, AgentsDoneProps, AuthkitScreen, AuthkitScreenProps, AuthkitScreenRenderer, ConsentProps, ConsentRequestParams, ConsentScope, ForgotProps, LoginAccount, LoginProps, MaintenanceProps, MfaChallengeCustomMethod, MfaChallengeProps, OtpUnlockProps, ResetProps, ScreenRenderProps, SessionExpiredProps, SignupProps, ThrottledProps, VerifyEmailProps, } from './src/host/screen_props.js';
|
|
120
|
+
export type { AccountAppsProps, AccountOrg, AccountOrgsProps, AccountTokensProps, AgentConsentRequest, AgentConsentScope, AgentsConsentProps, AgentsDoneProps, AuthkitScreen, AuthkitScreenProps, AuthkitScreenRenderer, ConsentProps, ConsentRequestParams, ConsentScope, ForgotProps, LoginAccount, LoginProps, MaintenanceProps, MfaChallengeCustomMethod, MfaChallengeProps, OtpUnlockProps, ResetProps, ScreenRenderProps, SessionExpiredProps, SignupProps, ThrottledProps, VerifyEmailProps, } from './src/host/screen_props.js';
|
|
121
121
|
export { enrichSessionsWithContext } from './src/host/session_context.js';
|
|
122
122
|
export { sudoMethods } from './src/host/sudo/index.js';
|
|
123
123
|
export { completeSudo, fail as failSudo, LAST_METHOD_SESSION_KEY, sudoContextFrom, } from './src/host/sudo/runtime.js';
|
|
@@ -17,6 +17,8 @@ import { configProvider } from '@adonisjs/core';
|
|
|
17
17
|
import { RuntimeException } from '@adonisjs/core/exceptions';
|
|
18
18
|
import { setBootedApp } from '../services/booted_app.js';
|
|
19
19
|
import { defaultEncryptForStore } from '../src/define_config.js';
|
|
20
|
+
import { authkitDoctorChecks } from '../src/doctor/agora_checks.js';
|
|
21
|
+
import { registerDoctorChecks } from '../src/doctor/agora_doctor.js';
|
|
20
22
|
import { resolveAppKey } from '../src/host/app_key.js';
|
|
21
23
|
import { resolveEffectiveKeyRotation } from '../src/host/key_rotation.js';
|
|
22
24
|
import { RuntimeSettings } from '../src/host/runtime_settings.js';
|
|
@@ -33,6 +35,9 @@ export default class AuthkitServerProvider {
|
|
|
33
35
|
this.app = app;
|
|
34
36
|
}
|
|
35
37
|
async boot() {
|
|
38
|
+
// `node ace agora:doctor` reads these after boot, from the registry every @adonis-agora lib
|
|
39
|
+
// shares. Registered here rather than in `register()`, which AdonisJS does not await.
|
|
40
|
+
await registerDoctorChecks(this.app.container, authkitDoctorChecks());
|
|
36
41
|
// EAGER + LOUD (fail-fast): se o host configurou `config/authkit.ts`, a
|
|
37
42
|
// `appKey` é indispensável — o oidc-provider a usa para assinar TODOS os
|
|
38
43
|
// cookies (keygrip). Antes, essa validação só rodava dentro do factory
|
|
@@ -154,6 +159,18 @@ export default class AuthkitServerProvider {
|
|
|
154
159
|
const logger = await this.app.container.make('logger').catch(() => null);
|
|
155
160
|
warnAdminConsoleUnauthorized(logger);
|
|
156
161
|
}
|
|
162
|
+
// Agora dev tools over MCP: the admin console's tools join the hub `@adonis-agora/agent`
|
|
163
|
+
// binds at `'agora.devTools'` (no import of the agent; nothing happens without it).
|
|
164
|
+
if (config.admin.enabled) {
|
|
165
|
+
const { registerAuthkitDevTools } = await import('../src/dev_tools/register.js');
|
|
166
|
+
try {
|
|
167
|
+
await registerAuthkitDevTools(this.app, config);
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
const logger = await this.app.container.make('logger').catch(() => null);
|
|
171
|
+
logger?.warn({ err: error }, 'authkit: could not register the Agora dev tools');
|
|
172
|
+
}
|
|
173
|
+
}
|
|
157
174
|
// `personalAgents.extensions`: o boot de cada uma (slots globais, etc.).
|
|
158
175
|
for (const extension of config.personalAgents?.extensions ?? []) {
|
|
159
176
|
await extension.boot?.();
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { HttpContext } from '@adonisjs/core/http';
|
|
2
|
+
/**
|
|
3
|
+
* The Agora dev-tools registration contract, version 1, declared here STRUCTURALLY.
|
|
4
|
+
*
|
|
5
|
+
* `@adonis-agora/agent`'s `mcp_provider` binds a hub at `'agora.devTools'` in its `register()`; every
|
|
6
|
+
* lib with a console registers its developer tools into it from its own `boot()`, and the agent
|
|
7
|
+
* serves them all on one MCP endpoint (`/mcp/agora`). authkit never imports the agent: the only
|
|
8
|
+
* coupling is the binding name and these shapes.
|
|
9
|
+
*/
|
|
10
|
+
/** The container binding the hub lives at. */
|
|
11
|
+
export declare const DEV_TOOLS_BINDING = "agora.devTools";
|
|
12
|
+
/** The hub a lib registers into. */
|
|
13
|
+
export interface DevToolsHub {
|
|
14
|
+
readonly version: number;
|
|
15
|
+
register(lib: DevToolsLib): void;
|
|
16
|
+
}
|
|
17
|
+
/** One lib's tools and the gate that decides who may use them. */
|
|
18
|
+
export interface DevToolsLib {
|
|
19
|
+
lib: string;
|
|
20
|
+
title: string;
|
|
21
|
+
authorize(ctx: HttpContext): Promise<DevToolsDecision>;
|
|
22
|
+
tools: DevTool[];
|
|
23
|
+
}
|
|
24
|
+
/** What `authorize` answers. */
|
|
25
|
+
export type DevToolsDecision = {
|
|
26
|
+
allowed: true;
|
|
27
|
+
} | {
|
|
28
|
+
allowed: false;
|
|
29
|
+
reason: 'unauthenticated' | 'forbidden';
|
|
30
|
+
redirectTo?: string;
|
|
31
|
+
error?: unknown;
|
|
32
|
+
};
|
|
33
|
+
/** One developer tool. */
|
|
34
|
+
export interface DevTool {
|
|
35
|
+
/** `authkit_<verb>_<noun>`. */
|
|
36
|
+
name: string;
|
|
37
|
+
kind: 'read' | 'action';
|
|
38
|
+
description: string;
|
|
39
|
+
input: DevToolInputSchema;
|
|
40
|
+
destructive?: boolean;
|
|
41
|
+
available?(): boolean | Promise<boolean>;
|
|
42
|
+
run(input: Record<string, unknown>, call: DevToolCall): Promise<unknown>;
|
|
43
|
+
}
|
|
44
|
+
/** What a tool's `run` receives besides its input. */
|
|
45
|
+
export interface DevToolCall {
|
|
46
|
+
ctx: HttpContext;
|
|
47
|
+
caller: {
|
|
48
|
+
id: string;
|
|
49
|
+
via: 'token' | 'session' | 'open';
|
|
50
|
+
};
|
|
51
|
+
limits: {
|
|
52
|
+
maxRows: number;
|
|
53
|
+
maxBytes: number;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/** One property of a {@link DevToolInputSchema}. */
|
|
57
|
+
export interface DevToolInputProperty {
|
|
58
|
+
type: 'string' | 'number' | 'integer' | 'boolean' | 'array';
|
|
59
|
+
description?: string;
|
|
60
|
+
enum?: readonly string[];
|
|
61
|
+
items?: {
|
|
62
|
+
type: 'string';
|
|
63
|
+
};
|
|
64
|
+
minimum?: number;
|
|
65
|
+
maximum?: number;
|
|
66
|
+
}
|
|
67
|
+
/** An object of scalar / enum / array-of-string properties. */
|
|
68
|
+
export interface DevToolInputSchema {
|
|
69
|
+
type: 'object';
|
|
70
|
+
properties: Record<string, DevToolInputProperty>;
|
|
71
|
+
required?: string[];
|
|
72
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Agora dev-tools registration contract, version 1, declared here STRUCTURALLY.
|
|
3
|
+
*
|
|
4
|
+
* `@adonis-agora/agent`'s `mcp_provider` binds a hub at `'agora.devTools'` in its `register()`; every
|
|
5
|
+
* lib with a console registers its developer tools into it from its own `boot()`, and the agent
|
|
6
|
+
* serves them all on one MCP endpoint (`/mcp/agora`). authkit never imports the agent: the only
|
|
7
|
+
* coupling is the binding name and these shapes.
|
|
8
|
+
*/
|
|
9
|
+
/** The container binding the hub lives at. */
|
|
10
|
+
export const DEV_TOOLS_BINDING = 'agora.devTools';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { HttpContext } from '@adonisjs/core/http';
|
|
2
|
+
import type { DevToolsDecision } from './contract.js';
|
|
3
|
+
/** Options for {@link authkitDevToolsAuthorize} (tests inject `inProduction`). */
|
|
4
|
+
export interface DevToolsGateOptions {
|
|
5
|
+
inProduction?: () => boolean;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The gate of authkit's dev tools on the Agora dev MCP endpoint. Never throws.
|
|
9
|
+
*
|
|
10
|
+
* - `admin.enabled: false` → forbidden.
|
|
11
|
+
* - `admin.authorize` set → its verdict alone decides: when it allows the MCP request, `admin.roles`
|
|
12
|
+
* is NOT checked (an MCP client has no console session; `authorize` is the app's explicit rule).
|
|
13
|
+
* - `admin.authorize` omitted → forbidden in production. Outside production the caller must be
|
|
14
|
+
* signed in through authkit (MCP OAuth sign-in, or a personal agent granted `agora:dev`) as an
|
|
15
|
+
* active account holding one of `admin.roles`; signed out → unauthenticated.
|
|
16
|
+
*/
|
|
17
|
+
export declare function authkitDevToolsAuthorize(options?: DevToolsGateOptions): (ctx: HttpContext) => Promise<DevToolsDecision>;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { supportsAccountStatus } from '../accounts/account_store.js';
|
|
2
|
+
import { resolveAccountRoles } from '../host/account_roles.js';
|
|
3
|
+
import { adminAuthorizeChecker, appInProduction } from '../host/admin_authorize.js';
|
|
4
|
+
function fromConsoleDecision(decision) {
|
|
5
|
+
if (decision.allowed)
|
|
6
|
+
return { allowed: true };
|
|
7
|
+
return {
|
|
8
|
+
allowed: false,
|
|
9
|
+
reason: decision.status === 401 ? 'unauthenticated' : 'forbidden',
|
|
10
|
+
...(decision.redirectTo !== undefined ? { redirectTo: decision.redirectTo } : {}),
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
/** The account signed in on this request through authkit (`ctx.authkit`), or `null`. */
|
|
14
|
+
async function signedInAccountId(ctx) {
|
|
15
|
+
const authenticator = ctx.authkit;
|
|
16
|
+
if (typeof authenticator?.getIdentity !== 'function')
|
|
17
|
+
return null;
|
|
18
|
+
try {
|
|
19
|
+
const identity = await authenticator.getIdentity();
|
|
20
|
+
return typeof identity?.userId === 'string' && identity.userId !== '' ? identity.userId : null;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The gate of authkit's dev tools on the Agora dev MCP endpoint. Never throws.
|
|
28
|
+
*
|
|
29
|
+
* - `admin.enabled: false` → forbidden.
|
|
30
|
+
* - `admin.authorize` set → its verdict alone decides: when it allows the MCP request, `admin.roles`
|
|
31
|
+
* is NOT checked (an MCP client has no console session; `authorize` is the app's explicit rule).
|
|
32
|
+
* - `admin.authorize` omitted → forbidden in production. Outside production the caller must be
|
|
33
|
+
* signed in through authkit (MCP OAuth sign-in, or a personal agent granted `agora:dev`) as an
|
|
34
|
+
* active account holding one of `admin.roles`; signed out → unauthenticated.
|
|
35
|
+
*/
|
|
36
|
+
export function authkitDevToolsAuthorize(options = {}) {
|
|
37
|
+
const inProduction = options.inProduction ?? appInProduction;
|
|
38
|
+
return async (ctx) => {
|
|
39
|
+
try {
|
|
40
|
+
const service = await ctx.containerResolver.make('authkit.server');
|
|
41
|
+
const cfg = service.config;
|
|
42
|
+
if (!cfg.admin?.enabled)
|
|
43
|
+
return { allowed: false, reason: 'forbidden' };
|
|
44
|
+
if (cfg.admin.authorize) {
|
|
45
|
+
return fromConsoleDecision(await adminAuthorizeChecker(cfg.admin.authorize)(ctx));
|
|
46
|
+
}
|
|
47
|
+
if (inProduction())
|
|
48
|
+
return { allowed: false, reason: 'forbidden' };
|
|
49
|
+
const accountId = await signedInAccountId(ctx);
|
|
50
|
+
if (accountId === null)
|
|
51
|
+
return { allowed: false, reason: 'unauthenticated' };
|
|
52
|
+
const account = await cfg.accountStore.findById(accountId);
|
|
53
|
+
if (!account)
|
|
54
|
+
return { allowed: false, reason: 'unauthenticated' };
|
|
55
|
+
const store = cfg.accountStore;
|
|
56
|
+
if (supportsAccountStatus(store) && (await store.isDisabled(accountId))) {
|
|
57
|
+
return { allowed: false, reason: 'forbidden' };
|
|
58
|
+
}
|
|
59
|
+
const allowedRoles = (cfg.admin.roles ?? []);
|
|
60
|
+
const roles = await resolveAccountRoles(cfg, account);
|
|
61
|
+
return roles.some((role) => allowedRoles.includes(role))
|
|
62
|
+
? { allowed: true }
|
|
63
|
+
: { allowed: false, reason: 'forbidden' };
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
return { allowed: false, reason: 'forbidden', error };
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ApplicationService } from '@adonisjs/core/types';
|
|
2
|
+
/**
|
|
3
|
+
* Registers authkit's developer tools into the Agora dev MCP hub (`'agora.devTools'`, bound by
|
|
4
|
+
* `@adonis-agora/agent`'s `mcp_provider`). No-op when the hub is not bound, speaks another contract
|
|
5
|
+
* version, or the admin console is off. Returns whether it registered.
|
|
6
|
+
*/
|
|
7
|
+
export declare function registerAuthkitDevTools(app: Pick<ApplicationService, 'container'>, config: {
|
|
8
|
+
admin?: {
|
|
9
|
+
enabled?: boolean;
|
|
10
|
+
} | null;
|
|
11
|
+
}): Promise<boolean>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { DEV_TOOLS_BINDING } from './contract.js';
|
|
2
|
+
import { authkitDevToolsAuthorize } from './gate.js';
|
|
3
|
+
import { authkitDevTools } from './tools.js';
|
|
4
|
+
/**
|
|
5
|
+
* Registers authkit's developer tools into the Agora dev MCP hub (`'agora.devTools'`, bound by
|
|
6
|
+
* `@adonis-agora/agent`'s `mcp_provider`). No-op when the hub is not bound, speaks another contract
|
|
7
|
+
* version, or the admin console is off. Returns whether it registered.
|
|
8
|
+
*/
|
|
9
|
+
export async function registerAuthkitDevTools(app, config) {
|
|
10
|
+
if (!config.admin?.enabled)
|
|
11
|
+
return false;
|
|
12
|
+
const container = app.container;
|
|
13
|
+
if (!container.hasBinding(DEV_TOOLS_BINDING))
|
|
14
|
+
return false;
|
|
15
|
+
const hub = (await container.make(DEV_TOOLS_BINDING));
|
|
16
|
+
if (hub?.version !== 1 || typeof hub.register !== 'function')
|
|
17
|
+
return false;
|
|
18
|
+
hub.register({
|
|
19
|
+
lib: 'authkit',
|
|
20
|
+
title: 'Identity (authkit admin)',
|
|
21
|
+
authorize: authkitDevToolsAuthorize(),
|
|
22
|
+
tools: authkitDevTools({ resolve: () => container.make('authkit.server') }),
|
|
23
|
+
});
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { HttpContext } from '@adonisjs/core/http';
|
|
2
|
+
import { AdminClientsService } from '../host/admin_clients_service.js';
|
|
3
|
+
import { AdminSessionsService } from '../host/admin_sessions_service.js';
|
|
4
|
+
import type { DevTool } from './contract.js';
|
|
5
|
+
/** Rows a list tool returns when `limit` is omitted. */
|
|
6
|
+
export declare const DEV_TOOLS_DEFAULT_LIMIT = 20;
|
|
7
|
+
/** The largest `limit` a list tool accepts. */
|
|
8
|
+
export declare const DEV_TOOLS_MAX_LIMIT = 100;
|
|
9
|
+
/** What the tools need from the app; tests replace any of it. */
|
|
10
|
+
export interface AuthkitDevToolsDeps {
|
|
11
|
+
/** The `authkit.server` service. Default: resolved from the request's container. */
|
|
12
|
+
service?(ctx: HttpContext): Promise<any>;
|
|
13
|
+
sessions?(service: any): Pick<AdminSessionsService, 'canList' | 'countActiveSessions' | 'listSessions' | 'listGrants' | 'revokeAll'>;
|
|
14
|
+
clients?(service: any): Pick<AdminClientsService, 'canList' | 'list' | 'find'>;
|
|
15
|
+
/** The `authkit.server` service outside a request, for `available()`. */
|
|
16
|
+
resolve?(): Promise<any>;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* authkit's developer tools for the Agora dev MCP endpoint: the admin console's reads, and two
|
|
20
|
+
* destructive actions (revoke sessions, disable a user) that the app must opt into on the endpoint.
|
|
21
|
+
* Every tool goes through the same services as the console; outputs never carry session ids, client
|
|
22
|
+
* secrets, key material, token values or MFA secrets.
|
|
23
|
+
*/
|
|
24
|
+
export declare function authkitDevTools(deps?: AuthkitDevToolsDeps): DevTool[];
|