@adonis-agora/authkit-server 1.0.0-rc.4 → 1.0.0-rc.5

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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # @adonis-agora/authkit-server
2
2
 
3
+ ## 1.0.0-rc.5
4
+
5
+ ### Minor Changes
6
+
7
+ - [#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.
8
+
3
9
  ## 1.0.0-rc.4
4
10
 
5
11
  ### 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;
@@ -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
@@ -0,0 +1,15 @@
1
+ import type { AgoraDoctorCheck, AgoraDoctorResult } from './agora_doctor.js';
2
+ import { type DoctorInput, type Finding } from './checks.js';
3
+ export declare const AUTHKIT_DOCTOR_DOCS = "https://davidecarvalho.github.io/agora/docs/authkit/health-check";
4
+ type CheckFn = (input: DoctorInput) => Finding | Finding[] | null;
5
+ /**
6
+ * The `authkit:doctor` checks, in `runAllChecks` order, with their frozen `agora:doctor` ids
7
+ * (`authkit.` + the function name without `check`, in kebab case). Ids are public: never rename.
8
+ */
9
+ export declare const AUTHKIT_DOCTOR_TABLE: ReadonlyArray<readonly [id: string, title: string, fn: CheckFn]>;
10
+ export declare const AUTHKIT_DOCTOR_PACKAGE = "@adonis-agora/authkit-server";
11
+ /** `admin.authorize` omitted: open outside production, denies every request in production. */
12
+ export declare function consoleGateResult(authkitConfig: Record<string, any> | null, inProduction: boolean): AgoraDoctorResult;
13
+ /** The checks authkit registers into `agora:doctor`. */
14
+ export declare function authkitDoctorChecks(): AgoraDoctorCheck[];
15
+ export {};
@@ -0,0 +1,150 @@
1
+ import { checkAccessTokens, checkAccountExpiration, checkAccountStore, checkAccountStoreColumns, checkAdapterVolatility, checkAdmin, checkAlly, checkAuthMethodsSetting, checkBotProtection, checkClients, checkConfigResolves, checkConsoleSignIn, checkEmailChange, checkFirstPartyClients, checkIssuer, checkJwks, checkOrganizations, checkOtpLockout, checkPasskeyAutofill, checkPasswordExpiration, checkPasswordHistory, checkPasswordPepper, checkPasswordPolicy, checkRateLimit, checkRequireVerifiedEmail, checkRolesCatalog, checkSecurityNotifications, checkSession, checkSessionPolicy, checkSettings, checkShield, checkSudoMode, checkWebauthn, } from './checks.js';
2
+ export const AUTHKIT_DOCTOR_DOCS = 'https://davidecarvalho.github.io/agora/docs/authkit/health-check';
3
+ /**
4
+ * The `authkit:doctor` checks, in `runAllChecks` order, with their frozen `agora:doctor` ids
5
+ * (`authkit.` + the function name without `check`, in kebab case). Ids are public: never rename.
6
+ */
7
+ export const AUTHKIT_DOCTOR_TABLE = [
8
+ ['authkit.config-resolves', 'config/authkit.ts resolves', checkConfigResolves],
9
+ ['authkit.issuer', 'Issuer and mount path', checkIssuer],
10
+ ['authkit.clients', 'OIDC clients', checkClients],
11
+ ['authkit.adapter-volatility', 'Persistent adapter', checkAdapterVolatility],
12
+ ['authkit.account-store', 'Account store capabilities', checkAccountStore],
13
+ ['authkit.account-store-columns', 'Account store columns', checkAccountStoreColumns],
14
+ ['authkit.session', 'Session', checkSession],
15
+ ['authkit.shield', 'Shield (CSRF)', checkShield],
16
+ ['authkit.ally', 'Social login (ally)', checkAlly],
17
+ ['authkit.rate-limit', 'Rate limiting', checkRateLimit],
18
+ ['authkit.admin', 'Admin console', checkAdmin],
19
+ ['authkit.require-verified-email', 'Verified e-mail gate', checkRequireVerifiedEmail],
20
+ ['authkit.bot-protection', 'Bot protection', checkBotProtection],
21
+ ['authkit.webauthn', 'Passkeys (WebAuthn)', checkWebauthn],
22
+ ['authkit.password-policy', 'Password policy', checkPasswordPolicy],
23
+ ['authkit.jwks', 'Signing keys (JWKS)', checkJwks],
24
+ ['authkit.access-tokens', 'Access tokens', checkAccessTokens],
25
+ ['authkit.organizations', 'Organizations', checkOrganizations],
26
+ ['authkit.settings', 'Runtime settings', checkSettings],
27
+ ['authkit.auth-methods-setting', 'Sign-in methods setting', checkAuthMethodsSetting],
28
+ ['authkit.email-change', 'E-mail change', checkEmailChange],
29
+ ['authkit.security-notifications', 'Security notifications', checkSecurityNotifications],
30
+ ['authkit.password-pepper', 'Password pepper', checkPasswordPepper],
31
+ ['authkit.password-history', 'Password history', checkPasswordHistory],
32
+ ['authkit.password-expiration', 'Password expiration', checkPasswordExpiration],
33
+ ['authkit.session-policy', 'Session policy', checkSessionPolicy],
34
+ ['authkit.roles-catalog', 'Roles catalog', checkRolesCatalog],
35
+ ['authkit.otp-lockout', 'OTP lockout', checkOtpLockout],
36
+ ['authkit.sudo-mode', 'Sudo mode', checkSudoMode],
37
+ ['authkit.account-expiration', 'Account expiration', checkAccountExpiration],
38
+ ['authkit.passkey-autofill', 'Passkey autofill', checkPasskeyAutofill],
39
+ ['authkit.first-party-clients', 'First-party clients', checkFirstPartyClients],
40
+ ['authkit.console-sign-in', 'Console sign-in', checkConsoleSignIn],
41
+ ];
42
+ export const AUTHKIT_DOCTOR_PACKAGE = '@adonis-agora/authkit-server';
43
+ /** One input per app per doctor run: every check reads the same resolved config. */
44
+ const prepared = new WeakMap();
45
+ function prepare(app) {
46
+ let entry = prepared.get(app);
47
+ const state = app.getState?.();
48
+ if (state === 'created' || state === 'initiated') {
49
+ return Promise.resolve({ input: null, error: null, notBooted: true });
50
+ }
51
+ if (!entry) {
52
+ // Loaded on demand: the provider imports this module, and the input builder pulls in adapters.
53
+ entry = import('./doctor_input.js')
54
+ .then(({ buildDoctorInput }) => buildDoctorInput(app))
55
+ .then((input) => ({ input, error: null }), (error) => ({
56
+ input: null,
57
+ error: error instanceof Error ? error.message : String(error),
58
+ }));
59
+ prepared.set(app, entry);
60
+ }
61
+ return entry;
62
+ }
63
+ function toResults(found) {
64
+ const list = found === null ? [] : Array.isArray(found) ? found : [found];
65
+ if (list.length === 0)
66
+ return [{ status: 'skip', message: 'not applicable to this config' }];
67
+ return list.map((finding) => ({
68
+ status: finding.level,
69
+ message: finding.message,
70
+ ...(finding.level === 'ok' ? {} : { docs: AUTHKIT_DOCTOR_DOCS }),
71
+ }));
72
+ }
73
+ /** `admin.authorize` omitted: open outside production, denies every request in production. */
74
+ export function consoleGateResult(authkitConfig, inProduction) {
75
+ const admin = authkitConfig?.admin;
76
+ if (!admin || admin.enabled !== true) {
77
+ return { status: 'skip', message: 'admin console is off' };
78
+ }
79
+ if (admin.authorize) {
80
+ return { status: 'ok', message: 'admin console has `admin.authorize`' };
81
+ }
82
+ const fix = 'Set `admin.authorize` in config/authkit.ts';
83
+ return inProduction
84
+ ? {
85
+ status: 'error',
86
+ message: 'admin console has no `admin.authorize`: it denies every request in production',
87
+ fix,
88
+ docs: AUTHKIT_DOCTOR_DOCS,
89
+ }
90
+ : {
91
+ status: 'warn',
92
+ message: 'admin console has no `admin.authorize`: open here, denies every request in production',
93
+ fix,
94
+ docs: AUTHKIT_DOCTOR_DOCS,
95
+ };
96
+ }
97
+ /** The checks authkit registers into `agora:doctor`. */
98
+ export function authkitDoctorChecks() {
99
+ const checks = AUTHKIT_DOCTOR_TABLE.map(([id, title, fn]) => ({
100
+ id,
101
+ lib: 'authkit',
102
+ package: AUTHKIT_DOCTOR_PACKAGE,
103
+ title,
104
+ async run(app) {
105
+ const { input, error, notBooted } = await prepare(app);
106
+ if (notBooted)
107
+ return [{ status: 'skip', message: 'needs a booted app (run without --no-boot)' }];
108
+ if (input === null) {
109
+ return id === 'authkit.config-resolves'
110
+ ? [
111
+ {
112
+ status: 'error',
113
+ message: `config/authkit.ts failed to resolve: ${error}`,
114
+ docs: AUTHKIT_DOCTOR_DOCS,
115
+ },
116
+ ]
117
+ : [{ status: 'skip', message: 'config/authkit.ts did not resolve' }];
118
+ }
119
+ return toResults(fn(input));
120
+ },
121
+ }));
122
+ checks.push({
123
+ id: 'authkit.signing-key-age',
124
+ lib: 'authkit',
125
+ package: AUTHKIT_DOCTOR_PACKAGE,
126
+ title: 'Signing key age',
127
+ async run(app) {
128
+ const { input, notBooted } = await prepare(app);
129
+ if (notBooted) {
130
+ return { status: 'skip', message: 'needs a booted app (run without --no-boot)' };
131
+ }
132
+ if (!input)
133
+ return toResults(null);
134
+ const { managedSigningKeyAgeFinding } = await import('./doctor_input.js');
135
+ return toResults(await managedSigningKeyAgeFinding(app, input.authkitConfig));
136
+ },
137
+ }, {
138
+ id: 'authkit.console-gate',
139
+ lib: 'authkit',
140
+ package: AUTHKIT_DOCTOR_PACKAGE,
141
+ title: 'Admin console access in production',
142
+ async run(app) {
143
+ const { input, notBooted } = await prepare(app);
144
+ if (notBooted)
145
+ return { status: 'skip', message: 'needs a booted app (run without --no-boot)' };
146
+ return consoleGateResult(input?.authkitConfig ?? null, app.inProduction);
147
+ },
148
+ });
149
+ return checks;
150
+ }
@@ -0,0 +1,115 @@
1
+ import type { ApplicationService } from '@adonisjs/core/types';
2
+ /** The container binding the registry lives at. */
3
+ export declare const AGORA_DOCTOR_BINDING = "agora.doctor";
4
+ /** Bound as a VALUE under `'agora.doctor'`. Data only. */
5
+ export interface AgoraDoctorRegistry {
6
+ readonly version: 1;
7
+ readonly checks: AgoraDoctorCheck[];
8
+ }
9
+ export type AgoraDoctorStatus = 'ok' | 'warn' | 'error' | 'skip';
10
+ export interface AgoraDoctorCheck {
11
+ /** `'<lib>.<kebab>'`, unique and frozen for 1.x: `'durable.verify-tenant'`. */
12
+ id: string;
13
+ /** `'durable'`. The package is `@adonis-agora/<lib>` unless {@link package} says otherwise. */
14
+ lib: string;
15
+ /** The npm package, when it is not `@adonis-agora/<lib>`. */
16
+ package?: string;
17
+ title: string;
18
+ /** `'cheap'` (default) reads config, env and files only; `'deep'` touches a DB, Redis or the network. */
19
+ cost?: 'cheap' | 'deep';
20
+ run(app: ApplicationService): AgoraDoctorResult | AgoraDoctorResult[] | Promise<AgoraDoctorResult | AgoraDoctorResult[]>;
21
+ }
22
+ export interface AgoraDoctorResult {
23
+ status: AgoraDoctorStatus;
24
+ /** One line, no secrets, no stack. */
25
+ message: string;
26
+ /** One line: the exact change to make. */
27
+ fix?: string;
28
+ /** Absolute URL of the lib docs page. */
29
+ docs?: string;
30
+ }
31
+ /** One reported result. */
32
+ export interface AgoraDoctorReportEntry extends AgoraDoctorResult {
33
+ id: string;
34
+ lib: string;
35
+ title: string;
36
+ ms: number;
37
+ }
38
+ /** What `agora:doctor --json` prints. */
39
+ export interface AgoraDoctorReport {
40
+ version: 1;
41
+ env: string;
42
+ summary: Record<AgoraDoctorStatus, number>;
43
+ libs: Array<{
44
+ lib: string;
45
+ package: string;
46
+ version: string | null;
47
+ }>;
48
+ results: AgoraDoctorReportEntry[];
49
+ }
50
+ export interface AgoraDoctorOptions {
51
+ libs?: string[];
52
+ only?: string;
53
+ deep?: boolean;
54
+ /** The app was not booted (`--no-boot`): deep checks are reported as skipped, never run. */
55
+ noBoot?: boolean;
56
+ strict?: boolean;
57
+ /** Per-check budget in ms (default 5000, deep checks 15000). */
58
+ timeoutMs?: number;
59
+ deepTimeoutMs?: number;
60
+ }
61
+ /** A doctor problem that stops the run itself (exit 2). */
62
+ export declare class AgoraDoctorUsageError extends Error {
63
+ }
64
+ interface ContainerLike {
65
+ hasBinding(binding: string): boolean;
66
+ bindValue(binding: string, value: unknown): void;
67
+ make(binding: string): Promise<unknown>;
68
+ }
69
+ /**
70
+ * Add a lib's checks to the app's doctor registry, creating it when absent. Call it from a provider's
71
+ * `boot()` (Adonis does not await `register()`, so an async registration there can be lost). A
72
+ * registry of another version is left alone; a duplicate id is skipped.
73
+ */
74
+ export declare function registerDoctorChecks(container: ContainerLike, checks: readonly AgoraDoctorCheck[]): Promise<void>;
75
+ /** The registered checks; throws {@link AgoraDoctorUsageError} on a registry of another version. */
76
+ export declare function readDoctorChecks(container: ContainerLike): Promise<AgoraDoctorCheck[]>;
77
+ /**
78
+ * The checks of every installed `@adonis-agora/*` package that publishes a `./doctor` entry
79
+ * (`export const agoraDoctorChecks`), imported from the app root without booting providers. A
80
+ * package whose entry fails to load is reported as an error (`<lib>.doctor-entry`), never skipped.
81
+ */
82
+ export declare function discoverDoctorChecks(app: ApplicationService): Promise<AgoraDoctorCheck[]>;
83
+ /** The directory of `name` as Node would find it from `from` (walking up `node_modules`), real path. */
84
+ export declare function packageDir(name: string, from: string): Promise<string | null>;
85
+ /** The installed version of `name` resolved from `from`, or `null` when it is not installed. */
86
+ export declare function installedVersion(name: string, from: string): Promise<string | null>;
87
+ /**
88
+ * Whether `version` satisfies `range`, with node-semver's grammar and prereleases included (like
89
+ * `semver.satisfies(version, range, { includePrerelease: true })`). An unparsable range is `false`.
90
+ */
91
+ export declare function satisfies(version: string, range: string): boolean;
92
+ /**
93
+ * The environment shown in the report, from the app itself: `production` when `app.inProduction`,
94
+ * `test` when `app.inTest`, else the app's node environment, and `development` when that is unset.
95
+ */
96
+ export declare function environmentOf(app: Pick<ApplicationService, 'inProduction' | 'inTest' | 'nodeEnvironment'>): string;
97
+ /** Run the checks and build the report. Throws {@link AgoraDoctorUsageError} for an unknown `--only`. */
98
+ export declare function runAgoraDoctor(app: ApplicationService, registered: readonly AgoraDoctorCheck[], options?: AgoraDoctorOptions): Promise<{
99
+ report: AgoraDoctorReport;
100
+ exitCode: 0 | 1;
101
+ }>;
102
+ export interface Paint {
103
+ green(text: string): string;
104
+ yellow(text: string): string;
105
+ red(text: string): string;
106
+ dim(text: string): string;
107
+ bold(text: string): string;
108
+ }
109
+ /** The human report. `fancy` = colors and symbols (a TTY without `NO_COLOR`). */
110
+ export declare function renderAgoraDoctor(report: AgoraDoctorReport, options?: {
111
+ verbose?: boolean;
112
+ fancy?: boolean;
113
+ paint?: Paint;
114
+ }): string;
115
+ export {};