@adonis-agora/authkit-server 1.0.0-rc.5 → 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 CHANGED
@@ -1,5 +1,13 @@
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
+
3
11
  ## 1.0.0-rc.5
4
12
 
5
13
  ### Minor Changes
@@ -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';
@@ -159,6 +159,18 @@ export default class AuthkitServerProvider {
159
159
  const logger = await this.app.container.make('logger').catch(() => null);
160
160
  warnAdminConsoleUnauthorized(logger);
161
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
+ }
162
174
  // `personalAgents.extensions`: o boot de cada uma (slots globais, etc.).
163
175
  for (const extension of config.personalAgents?.extensions ?? []) {
164
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[];
@@ -0,0 +1,384 @@
1
+ import { supportsAccountStatus, supportsMfa, supportsProviderIdentity, } from '../accounts/account_store.js';
2
+ import { AdminOrgsService } from '../host/admin_api/admin_orgs_service.js';
3
+ import { AdminUsersService } from '../host/admin_api/admin_users_service.js';
4
+ import { auditDto, orgDetailDto, orgDto, sessionDto, userDtoWithRoles, } from '../host/admin_api/dto.js';
5
+ import { AdminClientsService } from '../host/admin_clients_service.js';
6
+ import { AdminSessionsService } from '../host/admin_sessions_service.js';
7
+ import { computeAdminStats } from '../host/admin_stats_service.js';
8
+ import { enrichSessionsWithContext } from '../host/session_context.js';
9
+ import { LIST_FIRST_PAGE } from '../pagination.js';
10
+ /** Rows a list tool returns when `limit` is omitted. */
11
+ export const DEV_TOOLS_DEFAULT_LIMIT = 20;
12
+ /** The largest `limit` a list tool accepts. */
13
+ export const DEV_TOOLS_MAX_LIMIT = 100;
14
+ const page = {
15
+ type: 'integer',
16
+ minimum: 1,
17
+ description: 'Page number, 1-based (default 1). The result says the next one in `next`.',
18
+ };
19
+ const limit = {
20
+ type: 'integer',
21
+ minimum: 1,
22
+ maximum: DEV_TOOLS_MAX_LIMIT,
23
+ description: `Rows per page (default ${DEV_TOOLS_DEFAULT_LIMIT}, max ${DEV_TOOLS_MAX_LIMIT}).`,
24
+ };
25
+ const accountId = { type: 'string', description: 'The account id.' };
26
+ function pageOf(input) {
27
+ const p = Math.trunc(Number(input.page));
28
+ const l = Math.trunc(Number(input.limit));
29
+ return {
30
+ page: Number.isFinite(p) && p >= LIST_FIRST_PAGE ? p : LIST_FIRST_PAGE,
31
+ limit: Number.isFinite(l) && l >= 1 ? Math.min(l, DEV_TOOLS_MAX_LIMIT) : DEV_TOOLS_DEFAULT_LIMIT,
32
+ };
33
+ }
34
+ function nextPage(current, size, total) {
35
+ return current * size < total ? current + 1 : null;
36
+ }
37
+ /** One page of an array the service returns whole. */
38
+ function slice(rows, p) {
39
+ const start = (p.page - 1) * p.limit;
40
+ return {
41
+ items: rows.slice(start, start + p.limit),
42
+ total: rows.length,
43
+ next: nextPage(p.page, p.limit, rows.length),
44
+ };
45
+ }
46
+ function text(input, key) {
47
+ const value = input[key];
48
+ return typeof value === 'string' && value.trim() !== '' ? value.trim() : undefined;
49
+ }
50
+ function required(input, key) {
51
+ const value = text(input, key);
52
+ if (value === undefined)
53
+ throw new Error(`"${key}" is required`);
54
+ return value;
55
+ }
56
+ /** A session as the console shows it, without its id (an id is a handle on the session). */
57
+ function sessionView(session) {
58
+ const { id: _id, ...rest } = sessionDto(session);
59
+ return rest;
60
+ }
61
+ function grantView(grant) {
62
+ return {
63
+ accountId: grant.accountId,
64
+ clientId: grant.clientId ?? null,
65
+ accessTokens: grant.accessTokens,
66
+ refreshTokens: grant.refreshTokens,
67
+ };
68
+ }
69
+ /** The audit actor of an action taken over MCP. */
70
+ function mcpActor(call) {
71
+ return `mcp:${call.caller.id}`;
72
+ }
73
+ /**
74
+ * authkit's developer tools for the Agora dev MCP endpoint: the admin console's reads, and two
75
+ * destructive actions (revoke sessions, disable a user) that the app must opt into on the endpoint.
76
+ * Every tool goes through the same services as the console; outputs never carry session ids, client
77
+ * secrets, key material, token values or MFA secrets.
78
+ */
79
+ export function authkitDevTools(deps = {}) {
80
+ const service = (ctx) => deps.service ? deps.service(ctx) : ctx.containerResolver.make('authkit.server');
81
+ const sessions = (svc) => deps.sessions ? deps.sessions(svc) : new AdminSessionsService(svc);
82
+ const clients = (svc) => (deps.clients ? deps.clients(svc) : new AdminClientsService(svc));
83
+ async function findAccount(cfg, id) {
84
+ const account = await cfg.accountStore.findById(id);
85
+ if (!account)
86
+ throw new Error(`No account with id "${id}"`);
87
+ return account;
88
+ }
89
+ return [
90
+ {
91
+ name: 'authkit_get_overview',
92
+ kind: 'read',
93
+ description: 'Identity provider summary: total users, active sessions, monthly active users, sign-ins and sign-ups per day (last 30 days), number of OIDC clients, audit total and the 5 latest audit events.',
94
+ input: { type: 'object', properties: {} },
95
+ async run(_input, call) {
96
+ const svc = await service(call.ctx);
97
+ const cfg = svc.config;
98
+ const stats = await computeAdminStats(cfg, sessions(svc));
99
+ const clientSvc = clients(svc);
100
+ const clientsCount = clientSvc.canList ? (await clientSvc.list()).length : 0;
101
+ const recent = typeof cfg.audit?.list === 'function'
102
+ ? await cfg.audit.list({ page: LIST_FIRST_PAGE, size: 5 })
103
+ : null;
104
+ return {
105
+ usersTotal: stats.totalUsers,
106
+ activeSessions: stats.activeSessions,
107
+ mau: stats.mau,
108
+ signInsTotal: stats.signInsTotal,
109
+ signUpsTotal: stats.signUpsTotal,
110
+ signInsPerDay: stats.signInsPerDay,
111
+ signUpsPerDay: stats.signUpsPerDay,
112
+ windowDays: stats.windowDays,
113
+ auditSupported: stats.auditSupported,
114
+ clientsCount,
115
+ auditTotal: recent?.total ?? 0,
116
+ recentEvents: (recent?.data ?? []).map(auditDto),
117
+ };
118
+ },
119
+ },
120
+ {
121
+ name: 'authkit_list_users',
122
+ kind: 'read',
123
+ description: 'Accounts, newest first as the admin console lists them: id, email, name, stored global roles, effective roles and whether the account is disabled. `search` filters by email. Returns { items, total, next }.',
124
+ input: {
125
+ type: 'object',
126
+ properties: {
127
+ search: { type: 'string', description: 'Part of an email address.' },
128
+ page,
129
+ limit,
130
+ },
131
+ },
132
+ async run(input, call) {
133
+ const cfg = (await service(call.ctx)).config;
134
+ const p = pageOf(input);
135
+ const result = await cfg.accountStore.listAccounts({
136
+ search: text(input, 'search') ?? '',
137
+ page: p.page,
138
+ size: p.limit,
139
+ });
140
+ const users = new AdminUsersService(cfg);
141
+ const items = await Promise.all(result.data.map(async (u) => userDtoWithRoles(cfg, u, await users.isDisabled(u.id))));
142
+ return { items, total: result.total, next: nextPage(p.page, p.limit, result.total) };
143
+ },
144
+ },
145
+ {
146
+ name: 'authkit_get_user',
147
+ kind: 'read',
148
+ description: 'One account: profile, stored and effective roles, disabled flag, MFA on/off (and whether an authenticator app is set up), linked sign-in providers, and how many active sessions and app grants it has. No session ids.',
149
+ input: { type: 'object', properties: { id: accountId }, required: ['id'] },
150
+ async run(input, call) {
151
+ const svc = await service(call.ctx);
152
+ const cfg = svc.config;
153
+ const id = required(input, 'id');
154
+ const account = await findAccount(cfg, id);
155
+ const users = new AdminUsersService(cfg);
156
+ const store = cfg.accountStore;
157
+ const sessionSvc = sessions(svc);
158
+ const [disabled, mfa, identities, sessionList, grants] = await Promise.all([
159
+ users.isDisabled(id),
160
+ supportsMfa(store) ? store.getMfaState(id) : Promise.resolve(null),
161
+ supportsProviderIdentity(store) ? store.listProviderIdentities(id) : Promise.resolve([]),
162
+ sessionSvc.canList ? sessionSvc.listSessions(id) : Promise.resolve(null),
163
+ sessionSvc.canList ? sessionSvc.listGrants(id) : Promise.resolve(null),
164
+ ]);
165
+ return {
166
+ ...(await userDtoWithRoles(cfg, account, disabled)),
167
+ mfa: mfa === null ? null : { enabled: mfa.enabled, totp: mfa.totp ?? null },
168
+ identities: identities.map((i) => ({
169
+ provider: i.provider,
170
+ providerUserId: i.providerUserId,
171
+ email: i.email ?? null,
172
+ })),
173
+ sessionCount: sessionList === null ? null : sessionList.length,
174
+ grantCount: grants === null ? null : grants.length,
175
+ statusSupported: supportsAccountStatus(store),
176
+ };
177
+ },
178
+ },
179
+ {
180
+ name: 'authkit_list_user_sessions',
181
+ kind: 'read',
182
+ description: "An account's active sign-in sessions as the admin console shows them: sign-in time, methods (amr), browser, OS, IP and approximate location. No session ids. Returns { supported, items, total, next }.",
183
+ input: {
184
+ type: 'object',
185
+ properties: { accountId, page, limit },
186
+ required: ['accountId'],
187
+ },
188
+ async run(input, call) {
189
+ const svc = await service(call.ctx);
190
+ const id = required(input, 'accountId');
191
+ await findAccount(svc.config, id);
192
+ const sessionSvc = sessions(svc);
193
+ if (!sessionSvc.canList)
194
+ return { supported: false, items: [], total: 0, next: null };
195
+ const raw = await sessionSvc.listSessions(id);
196
+ const enriched = await enrichSessionsWithContext(svc.config, id, raw);
197
+ return { supported: true, ...slice(enriched.map(sessionView), pageOf(input)) };
198
+ },
199
+ },
200
+ {
201
+ name: 'authkit_list_user_grants',
202
+ kind: 'read',
203
+ description: 'The apps (OIDC clients) an account granted access to, with how many access and refresh tokens are live for each. Token values are never returned. Returns { supported, items, total, next }.',
204
+ input: {
205
+ type: 'object',
206
+ properties: { accountId, page, limit },
207
+ required: ['accountId'],
208
+ },
209
+ async run(input, call) {
210
+ const svc = await service(call.ctx);
211
+ const id = required(input, 'accountId');
212
+ await findAccount(svc.config, id);
213
+ const sessionSvc = sessions(svc);
214
+ if (!sessionSvc.canList)
215
+ return { supported: false, items: [], total: 0, next: null };
216
+ const grants = await sessionSvc.listGrants(id);
217
+ return { supported: true, ...slice(grants.map(grantView), pageOf(input)) };
218
+ },
219
+ },
220
+ {
221
+ name: 'authkit_list_orgs',
222
+ kind: 'read',
223
+ description: 'Organizations with their member count. Returns { supported, items, total, next }; `supported: false` when the account store has no organizations.',
224
+ input: { type: 'object', properties: { page, limit } },
225
+ async run(input, call) {
226
+ const svc = await service(call.ctx);
227
+ const orgs = new AdminOrgsService(svc.config, svc);
228
+ if (!orgs.supported)
229
+ return { supported: false, items: [], total: 0, next: null };
230
+ const result = await orgs.listOrgs();
231
+ if (!Array.isArray(result))
232
+ return { supported: false, items: [], total: 0, next: null };
233
+ return {
234
+ supported: true,
235
+ ...slice(result.map((o) => orgDto(o)), pageOf(input)),
236
+ };
237
+ },
238
+ },
239
+ {
240
+ name: 'authkit_get_org',
241
+ kind: 'read',
242
+ description: 'One organization: name, slug, members (account id, email, role, joined at) and pending invitations.',
243
+ input: {
244
+ type: 'object',
245
+ properties: { id: { type: 'string', description: 'The organization id.' } },
246
+ required: ['id'],
247
+ },
248
+ async run(input, call) {
249
+ const svc = await service(call.ctx);
250
+ const orgs = new AdminOrgsService(svc.config, svc);
251
+ if (!orgs.supported)
252
+ throw new Error('The account store has no organizations');
253
+ const result = await orgs.getOrg(required(input, 'id'));
254
+ if ('ok' in result && result.ok === false) {
255
+ throw new Error(result.reason === 'not_found'
256
+ ? `No organization with id "${input.id}"`
257
+ : 'The account store has no organizations');
258
+ }
259
+ return orgDetailDto(result);
260
+ },
261
+ },
262
+ {
263
+ name: 'authkit_list_clients',
264
+ kind: 'read',
265
+ description: 'OIDC clients (apps that sign in through this identity provider): client id, name, confidential or public, grant types, redirect URIs, auth method, disabled flag. Never the client secret. Returns { supported, items, total, next }.',
266
+ input: { type: 'object', properties: { page, limit } },
267
+ async run(input, call) {
268
+ const svc = await service(call.ctx);
269
+ const clientSvc = clients(svc);
270
+ if (!clientSvc.canList)
271
+ return { supported: false, items: [], total: 0, next: null };
272
+ const list = await clientSvc.list();
273
+ const items = list.map((c) => ({
274
+ clientId: c.clientId,
275
+ clientName: c.clientName ?? null,
276
+ confidential: c.confidential,
277
+ applicationType: c.applicationType ?? null,
278
+ grantTypes: c.grantTypes ?? [],
279
+ redirectUris: c.redirectUris ?? [],
280
+ postLogoutRedirectUris: c.postLogoutRedirectUris ?? [],
281
+ authMethod: c.tokenEndpointAuthMethod ?? null,
282
+ backchannelLogoutUri: c.backchannelLogoutUri ?? null,
283
+ logoUri: c.logoUri ?? null,
284
+ disabled: c.disabled === true,
285
+ serviceTenant: c.serviceTenant ?? null,
286
+ }));
287
+ return { supported: true, ...slice(items, pageOf(input)) };
288
+ },
289
+ },
290
+ {
291
+ name: 'authkit_list_audit',
292
+ kind: 'read',
293
+ description: 'The audit log, newest first: event type, account, actor, client, IP, metadata, time. Filter by `type` (e.g. "login.failure") and `subject` (an account id). Returns { supported, items, total, next }.',
294
+ input: {
295
+ type: 'object',
296
+ properties: {
297
+ type: { type: 'string', description: 'Event type, e.g. "login.success".' },
298
+ subject: { type: 'string', description: 'Account id the events are about.' },
299
+ page,
300
+ limit,
301
+ },
302
+ },
303
+ async run(input, call) {
304
+ const cfg = (await service(call.ctx)).config;
305
+ const sink = cfg.audit;
306
+ if (!sink || typeof sink.list !== 'function') {
307
+ return { supported: false, items: [], total: 0, next: null };
308
+ }
309
+ const p = pageOf(input);
310
+ const result = await sink.list({
311
+ page: p.page,
312
+ size: p.limit,
313
+ type: text(input, 'type'),
314
+ subject: text(input, 'subject'),
315
+ });
316
+ return {
317
+ supported: true,
318
+ items: result.data.map(auditDto),
319
+ total: result.total,
320
+ next: nextPage(p.page, p.limit, result.total),
321
+ };
322
+ },
323
+ },
324
+ {
325
+ name: 'authkit_revoke_user_sessions',
326
+ kind: 'action',
327
+ destructive: true,
328
+ description: "Signs an account out everywhere: ends every session and revokes every app grant and its tokens. Recorded in authkit's audit log (session.revoked_all) with the MCP caller as actor. Returns the counts revoked.",
329
+ input: { type: 'object', properties: { accountId }, required: ['accountId'] },
330
+ async run(input, call) {
331
+ const svc = await service(call.ctx);
332
+ const cfg = svc.config;
333
+ const id = required(input, 'accountId');
334
+ await findAccount(cfg, id);
335
+ const result = await sessions(svc).revokeAll(id);
336
+ await cfg.audit?.record({
337
+ type: 'session.revoked_all',
338
+ accountId: id,
339
+ actorId: mcpActor(call),
340
+ ip: call.ctx.request?.ip?.() ?? null,
341
+ metadata: {
342
+ sessions: result.sessions,
343
+ grants: result.grants,
344
+ accessTokens: result.accessTokens,
345
+ refreshTokens: result.refreshTokens,
346
+ source: 'mcp',
347
+ actor: 'mcp',
348
+ },
349
+ });
350
+ return { ok: true, accountId: id, ...result };
351
+ },
352
+ },
353
+ {
354
+ name: 'authkit_disable_user',
355
+ kind: 'action',
356
+ destructive: true,
357
+ description: "Disables an account: it can no longer sign in (re-enable it from the admin console). Recorded in authkit's audit log (user.disabled) with the MCP caller as actor. Only listed when the account store supports disabling accounts.",
358
+ input: { type: 'object', properties: { accountId }, required: ['accountId'] },
359
+ async available() {
360
+ if (!deps.resolve)
361
+ return true;
362
+ try {
363
+ return supportsAccountStatus((await deps.resolve()).config.accountStore);
364
+ }
365
+ catch {
366
+ return false;
367
+ }
368
+ },
369
+ async run(input, call) {
370
+ const cfg = (await service(call.ctx)).config;
371
+ const id = required(input, 'accountId');
372
+ await findAccount(cfg, id);
373
+ const applied = await new AdminUsersService(cfg).setStatus(id, true, {
374
+ actorId: mcpActor(call),
375
+ ip: call.ctx.request?.ip?.() ?? null,
376
+ source: 'mcp',
377
+ });
378
+ if (!applied)
379
+ throw new Error('The account store cannot disable accounts');
380
+ return { ok: true, accountId: id, disabled: true };
381
+ },
382
+ },
383
+ ];
384
+ }
@@ -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.5",
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",