@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.
Files changed (33) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/build/commands/agora_doctor.d.ts +22 -0
  3. package/build/commands/agora_doctor.js +109 -0
  4. package/build/commands/commands.json +64 -0
  5. package/build/commands/doctor.js +8 -74
  6. package/build/host/views/agents/consent.edge +11 -2
  7. package/build/host/views/agents/done.edge +1 -1
  8. package/build/index.d.ts +1 -1
  9. package/build/providers/authkit_server_provider.js +17 -0
  10. package/build/src/dev_tools/contract.d.ts +72 -0
  11. package/build/src/dev_tools/contract.js +10 -0
  12. package/build/src/dev_tools/gate.d.ts +17 -0
  13. package/build/src/dev_tools/gate.js +69 -0
  14. package/build/src/dev_tools/register.d.ts +11 -0
  15. package/build/src/dev_tools/register.js +25 -0
  16. package/build/src/dev_tools/tools.d.ts +24 -0
  17. package/build/src/dev_tools/tools.js +384 -0
  18. package/build/src/doctor/agora_checks.d.ts +15 -0
  19. package/build/src/doctor/agora_checks.js +150 -0
  20. package/build/src/doctor/agora_doctor.d.ts +115 -0
  21. package/build/src/doctor/agora_doctor.js +692 -0
  22. package/build/src/doctor/doctor_input.d.ts +27 -0
  23. package/build/src/doctor/doctor_input.js +86 -0
  24. package/build/src/doctor/index.d.ts +5 -0
  25. package/build/src/doctor/index.js +4 -0
  26. package/build/src/host/admin_api/admin_users_service.d.ts +5 -2
  27. package/build/src/host/admin_authorize.d.ts +8 -0
  28. package/build/src/host/admin_authorize.js +25 -0
  29. package/build/src/host/i18n.d.ts +2 -0
  30. package/build/src/host/i18n.js +2 -0
  31. package/build/src/host/register_auth_host.js +1 -24
  32. package/build/src/host/screen_props.d.ts +11 -8
  33. 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
  }
@@ -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 { DatabaseAdapter } from '../src/adapters/database_adapter.js';
11
- import { resolveAuthkitConfig } from '../src/commands/resolve_config.js';
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
- const config = await this.app.container.make('config');
52
- // `defineConfig` exporta um config provider — resolve antes de inspecionar,
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
- authkitConfig = await resolveAuthkitConfig(this.app, config.get('authkit', null));
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 jwksInput = (authkitConfig?.jwksConfig ?? authkitConfig?.jwks);
78
- if (jwksInput?.source === 'managed' && jwksInput?.store) {
79
- try {
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
- <input type="checkbox" name="scope" value="{{ scope.id }}" checked class="mt-0.5">
32
- <span class="text-sm text-gray-800">{{ scope.description }}</span>
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[];