@adonis-agora/authkit-server 1.0.0-rc.0 → 1.0.0-rc.10
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 +111 -0
- package/README.md +4 -5
- package/build/commands/agora_doctor.d.ts +22 -0
- package/build/commands/agora_doctor.js +109 -0
- package/build/commands/clients_create.d.ts +4 -0
- package/build/commands/clients_create.js +97 -0
- package/build/commands/commands.json +78 -0
- package/build/commands/doctor.js +8 -74
- package/build/host/views/account/apps.edge +1 -5
- package/build/host/views/account/extension.edge +77 -0
- package/build/host/views/account/mfa.edge +2 -0
- package/build/host/views/account/partials/extension_actions.edge +14 -0
- package/build/host/views/account/partials/extension_block.edge +197 -0
- package/build/host/views/account/partials/extension_value.edge +8 -0
- package/build/host/views/account/partials/nav.edge +14 -0
- package/build/host/views/account/security.edge +1 -5
- package/build/host/views/account/tokens.edge +2 -2
- package/build/host/views/agents/consent.edge +11 -2
- package/build/host/views/agents/done.edge +1 -1
- package/build/host/views/partials/styles.edge +1 -1
- package/build/index.d.ts +14 -12
- package/build/index.js +8 -9
- package/build/providers/authkit_server_provider.js +36 -8
- package/build/src/account/extension.d.ts +10 -0
- package/build/src/account/extension.js +9 -0
- package/build/src/account/http.d.ts +77 -0
- package/build/src/account/http.js +381 -0
- package/build/src/account/mount.d.ts +40 -0
- package/build/src/account/mount.js +171 -0
- package/build/src/account/registry.d.ts +41 -0
- package/build/src/account/registry.js +81 -0
- package/build/src/account/remote.d.ts +72 -0
- package/build/src/account/remote.js +406 -0
- package/build/src/account/runtime.d.ts +289 -0
- package/build/src/account/runtime.js +641 -0
- package/build/src/accounts/account_store.d.ts +6 -0
- package/build/src/accounts/lucid_store/organizations.js +6 -0
- package/build/src/agents/config.d.ts +9 -13
- package/build/src/agents/config.js +17 -11
- package/build/src/agents/delegation_service.js +3 -3
- package/build/src/agents/delegation_store.d.ts +3 -2
- package/build/src/agents/delegation_store.js +6 -5
- package/build/src/agents/extension.d.ts +261 -0
- package/build/src/agents/extension.js +40 -0
- package/build/src/agents/middleware.js +1 -1
- package/build/src/agents/protocol.d.ts +4 -4
- package/build/src/agents/registry_store.d.ts +3 -2
- package/build/src/agents/registry_store.js +1 -1
- package/build/src/agents/runtime.d.ts +26 -7
- package/build/src/agents/runtime.js +110 -75
- package/build/src/audit/audit_sink.d.ts +1 -1
- package/build/src/audit/audit_sink.js +10 -1
- package/build/src/console_authorize.d.ts +56 -0
- package/build/src/console_authorize.js +85 -0
- package/build/src/console_switcher.d.ts +132 -0
- package/build/src/console_switcher.js +206 -0
- package/build/src/define_config.d.ts +54 -14
- package/build/src/define_config.js +17 -8
- package/build/src/dev_tools/contract.d.ts +72 -0
- package/build/src/dev_tools/contract.js +10 -0
- package/build/src/dev_tools/gate.d.ts +17 -0
- package/build/src/dev_tools/gate.js +69 -0
- package/build/src/dev_tools/register.d.ts +11 -0
- package/build/src/dev_tools/register.js +25 -0
- package/build/src/dev_tools/tools.d.ts +24 -0
- package/build/src/dev_tools/tools.js +384 -0
- package/build/src/doctor/agora_checks.d.ts +15 -0
- package/build/src/doctor/agora_checks.js +150 -0
- package/build/src/doctor/agora_doctor.d.ts +115 -0
- package/build/src/doctor/agora_doctor.js +692 -0
- package/build/src/doctor/checks.d.ts +7 -1
- package/build/src/doctor/checks.js +47 -1
- package/build/src/doctor/doctor_input.d.ts +27 -0
- package/build/src/doctor/doctor_input.js +86 -0
- package/build/src/doctor/index.d.ts +5 -0
- package/build/src/doctor/index.js +4 -0
- package/build/src/host/access_token_verifier.js +7 -0
- package/build/src/host/account_api/account_api_controller.js +8 -6
- package/build/src/host/account_session_sign_in.d.ts +12 -0
- package/build/src/host/account_session_sign_in.js +19 -0
- package/build/src/host/admin_api/admin_users_service.d.ts +5 -2
- package/build/src/host/admin_api/api_clients_controller.d.ts +37 -0
- package/build/src/host/admin_api/api_clients_controller.js +31 -1
- package/build/src/host/admin_api/dto.d.ts +3 -1
- package/build/src/host/admin_api/dto.js +2 -0
- package/build/src/host/admin_authorize.d.ts +8 -0
- package/build/src/host/admin_authorize.js +25 -0
- package/build/src/host/admin_clients_service.d.ts +20 -0
- package/build/src/host/admin_clients_service.js +65 -1
- package/build/src/host/admin_console/admin_console_entry.d.ts +14 -0
- package/build/src/host/admin_console/admin_console_entry.js +80 -0
- package/build/src/host/admin_console/admin_shell_controller.js +8 -0
- package/build/src/host/admin_console/console_clients_controller.d.ts +41 -0
- package/build/src/host/admin_console/console_clients_controller.js +34 -1
- package/build/src/host/admin_console/console_personal_agents_controller.d.ts +1 -6
- package/build/src/host/admin_console/console_personal_agents_controller.js +50 -30
- package/build/src/host/admin_console/console_switcher_controller.d.ts +11 -0
- package/build/src/host/admin_console/console_switcher_controller.js +23 -0
- package/build/src/host/admin_console_warning.d.ts +8 -0
- package/build/src/host/admin_console_warning.js +18 -0
- package/build/src/host/admin_sessions_service.js +8 -5
- package/build/src/host/admin_validators.d.ts +29 -0
- package/build/src/host/admin_validators.js +30 -0
- package/build/src/host/auth_host_config.d.ts +14 -2
- package/build/src/host/console_denied_page.d.ts +6 -0
- package/build/src/host/console_denied_page.js +18 -0
- package/build/src/host/console_sso.d.ts +24 -0
- package/build/src/host/console_sso.js +70 -0
- package/build/src/host/controllers/account_apps_controller.js +15 -11
- package/build/src/host/controllers/account_session_controller.js +10 -1
- package/build/src/host/controllers/console_sso_controller.d.ts +12 -0
- package/build/src/host/controllers/console_sso_controller.js +61 -0
- package/build/src/host/controllers/interaction_controller.js +3 -1
- package/build/src/host/csrf.d.ts +3 -3
- package/build/src/host/csrf.js +3 -13
- package/build/src/host/default_mailer.d.ts +11 -6
- package/build/src/host/default_mailer.js +16 -12
- package/build/src/host/i18n.d.ts +41 -14
- package/build/src/host/i18n.js +55 -13
- package/build/src/host/rate_limit.d.ts +8 -0
- package/build/src/host/rate_limit.js +9 -0
- package/build/src/host/register_auth_host.d.ts +16 -0
- package/build/src/host/register_auth_host.js +230 -88
- package/build/src/host/renderers/edge_renderer.js +5 -0
- package/build/src/host/renderers/inertia_renderer.js +7 -1
- package/build/src/host/screen_props.d.ts +39 -8
- package/build/src/host/ui-dist/assets/index-Dq3dJVBv.js +67 -0
- package/build/src/host/ui-dist/assets/{index-DTSmD4RU.css → index-WNIyCEPA.css} +1 -1
- package/build/src/host/ui-dist/index.html +2 -2
- package/build/src/provider/build_provider.js +43 -6
- package/build/src/provider/oidc_service.d.ts +1 -1
- package/build/src/provider/oidc_service.js +1 -1
- package/build/src/schema/ensure.d.ts +14 -0
- package/build/src/schema/ensure.js +43 -150
- package/build/src/service_tenants/client_metadata.d.ts +39 -0
- package/build/src/service_tenants/client_metadata.js +80 -0
- package/build/src/service_tenants/config.d.ts +44 -0
- package/build/src/service_tenants/config.js +44 -0
- package/build/src/service_tenants/provider_hooks.d.ts +52 -0
- package/build/src/service_tenants/provider_hooks.js +124 -0
- package/build/src/service_tenants/tenant_verifier.d.ts +73 -0
- package/build/src/service_tenants/tenant_verifier.js +104 -0
- package/build/src/tenancy/tenant_labels.d.ts +30 -0
- package/build/src/tenancy/tenant_labels.js +64 -0
- package/build/types.d.ts +3 -1
- package/package.json +12 -9
- package/build/src/agents/poppy/client_registry.d.ts +0 -50
- package/build/src/agents/poppy/client_registry.js +0 -260
- package/build/src/agents/poppy/config.d.ts +0 -396
- package/build/src/agents/poppy/config.js +0 -228
- package/build/src/agents/poppy/dpop.d.ts +0 -61
- package/build/src/agents/poppy/dpop.js +0 -158
- package/build/src/agents/poppy/email_code_sign_in.d.ts +0 -27
- package/build/src/agents/poppy/email_code_sign_in.js +0 -140
- package/build/src/agents/poppy/errors.d.ts +0 -25
- package/build/src/agents/poppy/errors.js +0 -25
- package/build/src/agents/poppy/replay.d.ts +0 -25
- package/build/src/agents/poppy/replay.js +0 -106
- package/build/src/agents/poppy/resource.d.ts +0 -100
- package/build/src/agents/poppy/resource.js +0 -129
- package/build/src/agents/poppy/safe_fetch.d.ts +0 -48
- package/build/src/agents/poppy/safe_fetch.js +0 -262
- package/build/src/agents/poppy/service.d.ts +0 -293
- package/build/src/agents/poppy/service.js +0 -1458
- package/build/src/agents/poppy/session_jwt.d.ts +0 -30
- package/build/src/agents/poppy/session_jwt.js +0 -133
- package/build/src/agents/poppy/store.d.ts +0 -106
- package/build/src/agents/poppy/store.js +0 -261
- package/build/src/agents/poppy/web_session.d.ts +0 -39
- package/build/src/agents/poppy/web_session.js +0 -122
- package/build/src/host/controllers/poppy_consent_controller.d.ts +0 -19
- package/build/src/host/controllers/poppy_consent_controller.js +0 -199
- package/build/src/host/controllers/poppy_controller.d.ts +0 -33
- package/build/src/host/controllers/poppy_controller.js +0 -219
- package/build/src/host/poppy_session_key.d.ts +0 -9
- package/build/src/host/poppy_session_key.js +0 -9
- package/build/src/host/ui-dist/assets/index-D0EIeyKK.js +0 -67
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|