@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
@@ -0,0 +1,27 @@
1
+ import type { ApplicationService } from '@adonisjs/core/types';
2
+ import { type DoctorInput, type Finding } from './checks.js';
3
+ /** Logger mínimo para reportar peers instalados-mas-quebrados. */
4
+ export interface CanImportLogger {
5
+ warning(msg: string): void;
6
+ }
7
+ /**
8
+ * Tenta importar um peer; `true` se importável.
9
+ *
10
+ * Distingue "não instalado" de "instalado mas quebrado": só um erro de
11
+ * módulo-não-encontrado (`ERR_MODULE_NOT_FOUND`/`MODULE_NOT_FOUND`) é tratado como
12
+ * peer ausente (silencioso — é o caso esperado). Qualquer outro erro significa que
13
+ * o peer ESTÁ presente mas falhou ao carregar (import circular, erro de sintaxe,
14
+ * config inválida) — isso o doctor precisa gritar, senão um peer quebrado aparece
15
+ * como "ausente" e o diagnóstico mente.
16
+ */
17
+ export declare function canImport(specifier: string, logger?: CanImportLogger): Promise<boolean>;
18
+ /**
19
+ * Monta o `DoctorInput` a partir do app (config `authkit` resolvida, `session`, peers). Usado por
20
+ * `authkit:doctor` e pelos checks do `agora:doctor`. Lança se `config/authkit.ts` não resolver.
21
+ */
22
+ export declare function buildDoctorInput(app: ApplicationService, logger?: CanImportLogger): Promise<DoctorInput>;
23
+ /**
24
+ * O finding de idade da chave de assinatura managed (best-effort): `null` quando o jwks não é
25
+ * `managed` com store, ou quando o keystore não pôde ser lido.
26
+ */
27
+ export declare function managedSigningKeyAgeFinding(app: ApplicationService, authkitConfig: Record<string, any> | null): Promise<Finding | null>;
@@ -0,0 +1,86 @@
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
+ import { DatabaseAdapter } from '../adapters/database_adapter.js';
10
+ import { resolveAuthkitConfig } from '../commands/resolve_config.js';
11
+ import { defaultEncryptForStore } from '../define_config.js';
12
+ import { signingKeyAgeDays } from '../keys/keystore.js';
13
+ import { KeystoreCodec } from '../keys/keystore_codec.js';
14
+ import { loadEncryptionService } from '../keys/keystore_crypto.js';
15
+ import { KeystoreManager, resolveKeystoreVault } from '../keys/keystore_manager.js';
16
+ import { signingKeyAgeFinding } from './checks.js';
17
+ /**
18
+ * Tenta importar um peer; `true` se importável.
19
+ *
20
+ * Distingue "não instalado" de "instalado mas quebrado": só um erro de
21
+ * módulo-não-encontrado (`ERR_MODULE_NOT_FOUND`/`MODULE_NOT_FOUND`) é tratado como
22
+ * peer ausente (silencioso — é o caso esperado). Qualquer outro erro significa que
23
+ * o peer ESTÁ presente mas falhou ao carregar (import circular, erro de sintaxe,
24
+ * config inválida) — isso o doctor precisa gritar, senão um peer quebrado aparece
25
+ * como "ausente" e o diagnóstico mente.
26
+ */
27
+ export async function canImport(specifier, logger) {
28
+ try {
29
+ await import(__rewriteRelativeImportExtension(specifier));
30
+ return true;
31
+ }
32
+ catch (err) {
33
+ const code = err?.code;
34
+ if (code !== 'ERR_MODULE_NOT_FOUND' && code !== 'MODULE_NOT_FOUND') {
35
+ logger?.warning(`⚠️ ${specifier} está instalado mas falhou ao carregar: ${err.message}`);
36
+ }
37
+ return false;
38
+ }
39
+ }
40
+ /**
41
+ * Monta o `DoctorInput` a partir do app (config `authkit` resolvida, `session`, peers). Usado por
42
+ * `authkit:doctor` e pelos checks do `agora:doctor`. Lança se `config/authkit.ts` não resolver.
43
+ */
44
+ export async function buildDoctorInput(app, logger) {
45
+ const config = await app.container.make('config');
46
+ // `defineConfig` exporta um config provider — resolve antes de inspecionar,
47
+ // senão todo campo aparece como "ausente" (provider cru não tem issuer etc.).
48
+ const authkitConfig = await resolveAuthkitConfig(app, config.get('authkit', null));
49
+ const sessionConfig = config.get('session', null) ?? null;
50
+ return {
51
+ authkitConfig,
52
+ sessionConfig,
53
+ peers: {
54
+ session: await canImport('@adonisjs/session', logger),
55
+ shield: await canImport('@adonisjs/shield', logger),
56
+ ally: await canImport('@adonisjs/ally', logger),
57
+ limiter: await canImport('@adonisjs/limiter', logger),
58
+ },
59
+ __adapterClasses: { DatabaseAdapter },
60
+ };
61
+ }
62
+ /**
63
+ * O finding de idade da chave de assinatura managed (best-effort): `null` quando o jwks não é
64
+ * `managed` com store, ou quando o keystore não pôde ser lido.
65
+ */
66
+ export async function managedSigningKeyAgeFinding(app, authkitConfig) {
67
+ const jwksInput = (authkitConfig?.jwksConfig ?? authkitConfig?.jwks);
68
+ if (jwksInput?.source !== 'managed' || !jwksInput?.store)
69
+ return null;
70
+ try {
71
+ const vault = resolveKeystoreVault(jwksInput.store, {
72
+ makePath: (p) => app.makePath(p),
73
+ container: app.container,
74
+ });
75
+ const encrypt = jwksInput.encrypt ?? defaultEncryptForStore(jwksInput.store);
76
+ const enc = encrypt ? await loadEncryptionService().catch(() => undefined) : undefined;
77
+ const mgr = new KeystoreManager(vault, new KeystoreCodec({ encrypt, enc }), jwksInput.algorithm ?? 'RS256');
78
+ const store = await mgr.read().catch(() => null);
79
+ const maxAge = jwksInput.rotationDays ?? 90;
80
+ return signingKeyAgeFinding(signingKeyAgeDays(store), maxAge);
81
+ }
82
+ catch {
83
+ /* idade é best-effort no doctor */
84
+ return null;
85
+ }
86
+ }
@@ -0,0 +1,5 @@
1
+ import type { AgoraDoctorCheck } from './agora_doctor.js';
2
+ export type { AgoraDoctorCheck, AgoraDoctorRegistry, AgoraDoctorReport, AgoraDoctorResult, AgoraDoctorStatus, } from './agora_doctor.js';
3
+ export { AGORA_DOCTOR_BINDING, registerDoctorChecks } from './agora_doctor.js';
4
+ /** authkit's checks, for `node ace agora:doctor --no-boot` (which imports this entry instead of booting). */
5
+ export declare const agoraDoctorChecks: AgoraDoctorCheck[];
@@ -0,0 +1,4 @@
1
+ import { authkitDoctorChecks } from './agora_checks.js';
2
+ export { AGORA_DOCTOR_BINDING, registerDoctorChecks } from './agora_doctor.js';
3
+ /** authkit's checks, for `node ace agora:doctor --no-boot` (which imports this entry instead of booting). */
4
+ export const agoraDoctorChecks = authkitDoctorChecks();
@@ -8,8 +8,11 @@ import type { SettingsCapability } from '../runtime_settings.js';
8
8
  export interface AdminActor {
9
9
  actorId: string | null;
10
10
  ip: string | null;
11
- /** Marca metadata da auditoria — 'admin-api' nas escritas via REST, 'admin' no console HTML. */
12
- source?: 'admin-api' | 'admin';
11
+ /**
12
+ * Marca metadata da auditoria — 'admin-api' nas escritas via REST, 'admin' no console HTML,
13
+ * 'mcp' nas ações dos dev tools (endpoint MCP do Agora).
14
+ */
15
+ source?: 'admin-api' | 'admin' | 'mcp';
13
16
  }
14
17
  /** Resultado da deleção via admin: false quando o store não suporta hard delete. */
15
18
  export type DeleteUserResult = {
@@ -0,0 +1,8 @@
1
+ import { type ConsoleAuthorize, type ConsoleAuthorizeDecision } from '../console_authorize.js';
2
+ /**
3
+ * Whether the app runs in production. Unknown (the provider did not register the
4
+ * app) counts as production, so an omitted `admin.authorize` fails closed.
5
+ */
6
+ export declare function appInProduction(): boolean;
7
+ /** The resolved `admin.authorize` checker (memoized per configured value). Never throws. */
8
+ export declare function adminAuthorizeChecker(authorize: ConsoleAuthorize): (ctx: any) => Promise<ConsoleAuthorizeDecision>;
@@ -0,0 +1,25 @@
1
+ import { getBootedApp } from '../../services/booted_app.js';
2
+ import { resolveConsoleAuthorize, } from '../console_authorize.js';
3
+ /**
4
+ * Whether the app runs in production. Unknown (the provider did not register the
5
+ * app) counts as production, so an omitted `admin.authorize` fails closed.
6
+ */
7
+ export function appInProduction() {
8
+ try {
9
+ return getBootedApp().inProduction !== false;
10
+ }
11
+ catch {
12
+ return true;
13
+ }
14
+ }
15
+ /** One checker per configured `admin.authorize`, so a lazy import runs once. */
16
+ const adminAuthorizeCheckers = new WeakMap();
17
+ /** The resolved `admin.authorize` checker (memoized per configured value). Never throws. */
18
+ export function adminAuthorizeChecker(authorize) {
19
+ let checker = adminAuthorizeCheckers.get(authorize);
20
+ if (!checker) {
21
+ checker = resolveConsoleAuthorize(authorize);
22
+ adminAuthorizeCheckers.set(authorize, checker);
23
+ }
24
+ return checker;
25
+ }
@@ -191,6 +191,7 @@ export declare const DEFAULT_MESSAGES: {
191
191
  'agents.consent.body': string;
192
192
  'agents.consent.signed_in_as': string;
193
193
  'agents.consent.scopes_label': string;
194
+ 'agents.consent.opt_in_hint': string;
194
195
  'agents.consent.code': string;
195
196
  'agents.consent.allow': string;
196
197
  'agents.consent.deny': string;
@@ -944,6 +945,7 @@ export declare const PT_BR_MESSAGES: {
944
945
  'agents.consent.body': string;
945
946
  'agents.consent.signed_in_as': string;
946
947
  'agents.consent.scopes_label': string;
948
+ 'agents.consent.opt_in_hint': string;
947
949
  'agents.consent.code': string;
948
950
  'agents.consent.allow': string;
949
951
  'agents.consent.deny': string;
@@ -200,6 +200,7 @@ export const DEFAULT_MESSAGES = {
200
200
  'agents.consent.body': 'The request came from {origin}. Choose what it can do — you can revoke access at any time.',
201
201
  'agents.consent.signed_in_as': 'Signed in as {email}',
202
202
  'agents.consent.scopes_label': 'Allow it to:',
203
+ 'agents.consent.opt_in_hint': 'Not selected for you: tick it only if you want to allow it.',
203
204
  'agents.consent.code': 'Request code: {code}. Make sure it matches the code your assistant shows.',
204
205
  'agents.consent.allow': 'Allow',
205
206
  'agents.consent.deny': 'Deny',
@@ -1040,6 +1041,7 @@ export const PT_BR_MESSAGES = {
1040
1041
  'agents.consent.body': 'O pedido veio de {origin}. Escolha o que ele pode fazer — você pode revogar o acesso quando quiser.',
1041
1042
  'agents.consent.signed_in_as': 'Conectado como {email}',
1042
1043
  'agents.consent.scopes_label': 'Permitir que ele:',
1044
+ 'agents.consent.opt_in_hint': 'Não vem marcado: marque só se quiser permitir.',
1043
1045
  'agents.consent.code': 'Código do pedido: {code}. Confira se é o mesmo que o seu assistente mostra.',
1044
1046
  'agents.consent.allow': 'Permitir',
1045
1047
  'agents.consent.deny': 'Negar',
@@ -1,5 +1,3 @@
1
- import { getBootedApp } from '../../services/booted_app.js';
2
- import { resolveConsoleAuthorize, } from '../console_authorize.js';
3
1
  import { resolveRateLimit } from '../define_config.js';
4
2
  import { accountHome } from './account_home.js';
5
3
  import { getAccountLoginUrl, setAccountLoginUrl } from './account_login_url.js';
@@ -7,6 +5,7 @@ import { accountPath, accountPathsMap, accountPrefix, joinAccountPath, setAccoun
7
5
  import { resolveAccountRoles } from './account_roles.js';
8
6
  import { ACCOUNT_SESSION_KEY } from './account_session_key.js';
9
7
  import { adminApiGuard } from './admin_api/admin_api_guard.js';
8
+ import { adminAuthorizeChecker, appInProduction } from './admin_authorize.js';
10
9
  import { normalizeAdminApiPrefix, normalizeAdminPrefix, setAdminApiPrefix, setAdminPrefix, } from './admin_prefix.js';
11
10
  import { getAuthHostConfig, markAuthHostAutoMounted, wasAuthHostAutoMounted, } from './auth_host_config.js';
12
11
  import { consoleDeniedPage } from './console_denied_page.js';
@@ -238,18 +237,6 @@ export const adminGuard = async (ctx, next) => {
238
237
  return consoleDenied(ctx, decision);
239
238
  return next();
240
239
  };
241
- /**
242
- * Whether the app runs in production. Unknown (the provider did not register the
243
- * app) counts as production, so an omitted `admin.authorize` fails closed.
244
- */
245
- function appInProduction() {
246
- try {
247
- return getBootedApp().inProduction !== false;
248
- }
249
- catch {
250
- return true;
251
- }
252
- }
253
240
  /** The response for an `admin.authorize` denial: same rules in every `@adonis-agora` console. */
254
241
  function consoleDenied(ctx, decision) {
255
242
  const json = wantsJson(ctx);
@@ -272,16 +259,6 @@ function consoleDenied(ctx, decision) {
272
259
  ctx.response.header('Content-Type', 'text/html; charset=utf-8');
273
260
  return ctx.response.status(403).send(consoleDeniedPage(403));
274
261
  }
275
- /** One checker per configured `admin.authorize`, so a lazy import runs once. */
276
- const adminAuthorizeCheckers = new WeakMap();
277
- function adminAuthorizeChecker(authorize) {
278
- let checker = adminAuthorizeCheckers.get(authorize);
279
- if (!checker) {
280
- checker = resolveConsoleAuthorize(authorize);
281
- adminAuthorizeCheckers.set(authorize, checker);
282
- }
283
- return checker;
284
- }
285
262
  const C = {
286
263
  oidc: () => import('../controllers/oidc_callback_controller.js'),
287
264
  authorizationServerMetadata: () => import('../controllers/authorization_server_metadata_controller.js'),
@@ -183,16 +183,22 @@ export interface ThrottledProps extends WithMessages {
183
183
  retryUrl: string;
184
184
  }
185
185
  /** The agent asking for access, on `agents/consent`. */
186
+ /** One scope on the personal-agent consent screen. */
187
+ export interface AgentConsentScope {
188
+ id: string;
189
+ description: string;
190
+ /** i18n key shown instead of `description` (an extension's message). */
191
+ descriptionKey?: string;
192
+ /** Not ticked for the person: granted only if they tick it themselves. */
193
+ optIn?: boolean;
194
+ }
186
195
  export interface AgentConsentRequest {
187
196
  /** Device-flow user code, when the agent uses it. */
188
197
  userCode?: string;
189
198
  agentName: string | null;
190
199
  agentOrigin: string | null;
191
200
  logoUri?: string | null;
192
- scopes: Array<{
193
- id: string;
194
- description: string;
195
- }>;
201
+ scopes: Array<AgentConsentScope>;
196
202
  /** Extra hidden form fields to send back. */
197
203
  hidden?: Array<{
198
204
  name: string;
@@ -216,10 +222,7 @@ export interface AgentsConsentProps extends WithMessages {
216
222
  export interface AgentsDoneProps extends WithMessages {
217
223
  status: 'approved' | 'denied' | 'expired' | 'invalid';
218
224
  agentName: string | null;
219
- scopes: Array<{
220
- id: string;
221
- description: string;
222
- }>;
225
+ scopes: Array<AgentConsentScope>;
223
226
  }
224
227
  /** `account/apps` — apps and personal agents with access to the account. */
225
228
  export interface AccountAppsProps extends WithMessages {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adonis-agora/authkit-server",
3
- "version": "1.0.0-rc.4",
3
+ "version": "1.0.0-rc.6",
4
4
  "description": "AdonisJS OIDC/OAuth2 provider (Identity Provider) toolkit: ejectable auth server with sessions, rate-limiting, MFA/TOTP, audit log, federated logout and OpenTelemetry metrics.",
5
5
  "license": "MIT",
6
6
  "author": "dudousxd",
@@ -46,7 +46,8 @@
46
46
  "./telescope": "./build/src/observability/telescope/index.js",
47
47
  "./authkit_server_provider": "./build/providers/authkit_server_provider.js",
48
48
  "./commands": "./build/commands/main.js",
49
- "./commands/configure": "./build/commands/configure.js"
49
+ "./commands/configure": "./build/commands/configure.js",
50
+ "./doctor": "./build/src/doctor/index.js"
50
51
  },
51
52
  "adonisjs": {
52
53
  "types": "@adonis-agora/authkit-server",
@@ -118,7 +119,7 @@
118
119
  },
119
120
  "devDependencies": {
120
121
  "@adonis-agora/authkit-react": "1.0.0-rc.2",
121
- "@adonis-agora/durable": "0.59.0",
122
+ "@adonis-agora/durable": "0.60.0",
122
123
  "@adonis-agora/telescope": "0.27.1",
123
124
  "@adonisjs/ally": "6.3.2",
124
125
  "@adonisjs/auth": "10.1.0",
@@ -169,6 +170,7 @@
169
170
  "build": "node scripts/build_host_css.mjs && node scripts/build_webauthn.mjs && node scripts/build_ui.mjs && node -e \"const fs=require('node:fs');for(const d of ['build/stubs','build/host/views'])fs.rmSync(d,{recursive:true,force:true})\" && tsc && node -e \"require('node:fs').cpSync('src/host/assets','build/src/host/assets',{recursive:true})\" && node -e \"require('node:fs').cpSync('stubs','build/stubs',{recursive:true,filter:(s)=>!s.endsWith('.ts')})\" && node -e \"const fs=require('node:fs');if(fs.existsSync('assets'))fs.cpSync('assets','build/assets',{recursive:true})\" && node -e \"require('node:fs').cpSync('src/host/views','build/host/views',{recursive:true})\" && node -e \"const fs=require('node:fs');fs.mkdirSync('build/host/ui',{recursive:true});fs.readdirSync('src/host/ui').filter(f=>f.endsWith('.html')).forEach(f=>fs.copyFileSync('src/host/ui/'+f,'build/host/ui/'+f))\" && node -e \"require('node:fs').copyFileSync('commands/commands.json','build/commands/commands.json')\" && node -e \"const fs=require('node:fs');fs.mkdirSync('build/src/password',{recursive:true});fs.copyFileSync('src/password/common_passwords.txt','build/src/password/common_passwords.txt')\"",
170
171
  "build:ui": "node scripts/build_ui.mjs",
171
172
  "build:webauthn": "node scripts/build_webauthn.mjs",
173
+ "check:agora-doctor": "node ../../scripts/check-agora-doctor.mjs .",
172
174
  "check:host-css": "node scripts/check_host_css.mjs",
173
175
  "check:webauthn-bundle": "node scripts/check_webauthn_bundle.mjs",
174
176
  "typecheck": "tsc --noEmit && node scripts/typecheck_ui.mjs && node scripts/typecheck_tests.mjs",