@adonis-agora/authkit-server 1.0.0-rc.1 → 1.0.0-rc.11
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 +107 -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 +12 -12
- package/build/index.js +7 -9
- package/build/providers/authkit_server_provider.js +29 -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 +13 -7
- package/build/src/accounts/lucid_account_store.d.ts +3 -3
- package/build/src/accounts/lucid_account_store.js +42 -17
- package/build/src/accounts/lucid_store/mfa.js +3 -3
- package/build/src/accounts/lucid_store/organization_models.d.ts +14 -3
- package/build/src/accounts/lucid_store/organization_models.js +31 -3
- package/build/src/accounts/lucid_store/organizations.d.ts +2 -2
- package/build/src/accounts/lucid_store/organizations.js +40 -31
- package/build/src/accounts/lucid_store/shared.d.ts +11 -3
- package/build/src/accounts/lucid_store/shared.js +7 -5
- package/build/src/accounts/lucid_store/webauthn.js +1 -1
- package/build/src/adapters/factory.d.ts +13 -2
- package/build/src/adapters/factory.js +2 -2
- 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/commands/expire_scan_command.js +1 -1
- package/build/src/commands/settings_commands.js +2 -2
- package/build/src/console_switcher.d.ts +132 -0
- package/build/src/console_switcher.js +206 -0
- package/build/src/define_config.d.ts +38 -13
- package/build/src/define_config.js +32 -10
- 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.js +2 -2
- 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 +55 -0
- 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_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 +9 -2
- package/build/src/host/controllers/account_apps_controller.js +15 -11
- 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 +14 -0
- package/build/src/host/register_auth_host.js +167 -106
- package/build/src/host/renderers/edge_renderer.js +5 -0
- package/build/src/host/renderers/inertia_renderer.js +7 -1
- package/build/src/host/runtime_settings.d.ts +6 -6
- package/build/src/host/runtime_settings.js +4 -3
- 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 +37 -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 +24 -1
- package/build/src/schema/ensure.js +44 -154
- package/build/src/schema/tables_location.d.ts +27 -0
- package/build/src/schema/tables_location.js +22 -0
- 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 +9 -6
- 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,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The console switcher contract, version 1.
|
|
3
|
+
*
|
|
4
|
+
* Every `@adonis-agora` console registers itself into ONE data-only registry bound as a value at
|
|
5
|
+
* `'agora.consoles'` (created by whichever lib boots first). Each console then shows a switcher to
|
|
6
|
+
* the other consoles the viewer may open. The types are declared locally in every lib (TypeScript is
|
|
7
|
+
* structural), so no lib imports another.
|
|
8
|
+
*/
|
|
9
|
+
/** The container binding the registry lives at. */
|
|
10
|
+
export const AGORA_CONSOLES_BINDING = 'agora.consoles';
|
|
11
|
+
const CONSOLE_ID = /^[a-z][a-z0-9-]*$/;
|
|
12
|
+
const ICONS = [
|
|
13
|
+
'workflow',
|
|
14
|
+
'telescope',
|
|
15
|
+
'media',
|
|
16
|
+
'payments',
|
|
17
|
+
'agent',
|
|
18
|
+
'identity',
|
|
19
|
+
'generic',
|
|
20
|
+
];
|
|
21
|
+
/** Default time each console's `authorize` (and `search`) gets before it counts as hidden. */
|
|
22
|
+
export const CONSOLE_PROBE_TIMEOUT_MS = 300;
|
|
23
|
+
/** Hits returned per console by the global search. */
|
|
24
|
+
export const CONSOLE_SEARCH_LIMIT = 5;
|
|
25
|
+
/** A same-origin absolute path: starts with `/`, not `//`, no backslash, no control characters. */
|
|
26
|
+
export function isSameOriginPath(value) {
|
|
27
|
+
return (typeof value === 'string' &&
|
|
28
|
+
value.startsWith('/') &&
|
|
29
|
+
!value.startsWith('//') &&
|
|
30
|
+
!value.includes('\\') &&
|
|
31
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: rejecting control characters is the point
|
|
32
|
+
!/[\u0000-\u001f\u007f]/.test(value));
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Add a console to the shared `'agora.consoles'` registry, creating it when absent. Skips (never
|
|
36
|
+
* throws) a registry of another version; warns and skips a duplicate id. Throws on an invalid `id`
|
|
37
|
+
* or `path`: that is a programming error. Apps can call it to register a console they mount by hand.
|
|
38
|
+
*
|
|
39
|
+
* ```ts
|
|
40
|
+
* await registerAgoraConsole(app, { id: 'reports', title: 'Reports', icon: 'generic', path: '/reports',
|
|
41
|
+
* order: 100, authorize: async (ctx) => ({ allowed: await isStaff(ctx) }) })
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export async function registerAgoraConsole(app, entry, logger) {
|
|
45
|
+
if (typeof entry?.id !== 'string' || !CONSOLE_ID.test(entry.id)) {
|
|
46
|
+
throw new Error(`registerAgoraConsole: console id ${JSON.stringify(entry?.id)} must match /^[a-z][a-z0-9-]*$/`);
|
|
47
|
+
}
|
|
48
|
+
if (!isSameOriginPath(entry.path)) {
|
|
49
|
+
throw new Error(`registerAgoraConsole: console "${entry.id}" path ${JSON.stringify(entry.path)} must be a same-origin absolute path`);
|
|
50
|
+
}
|
|
51
|
+
if (typeof entry.authorize !== 'function') {
|
|
52
|
+
throw new Error(`registerAgoraConsole: console "${entry.id}" needs an authorize(ctx)`);
|
|
53
|
+
}
|
|
54
|
+
const container = app.container;
|
|
55
|
+
if (!container.hasBinding(AGORA_CONSOLES_BINDING)) {
|
|
56
|
+
container.bindValue(AGORA_CONSOLES_BINDING, { version: 1, entries: [] });
|
|
57
|
+
}
|
|
58
|
+
const registry = (await container.make(AGORA_CONSOLES_BINDING));
|
|
59
|
+
if (registry?.version !== 1 || !Array.isArray(registry.entries))
|
|
60
|
+
return false;
|
|
61
|
+
if (registry.entries.some((e) => e.id === entry.id)) {
|
|
62
|
+
logger?.warn(`[@adonis-agora/authkit-server] console id "${entry.id}" already registered`);
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
registry.entries.push(entry);
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
/** The registry, when one is bound and is version 1. */
|
|
69
|
+
export async function readAgoraConsoles(app) {
|
|
70
|
+
if (!app.container.hasBinding(AGORA_CONSOLES_BINDING))
|
|
71
|
+
return null;
|
|
72
|
+
const registry = (await app.container.make(AGORA_CONSOLES_BINDING));
|
|
73
|
+
if (registry?.version !== 1 || !Array.isArray(registry.entries))
|
|
74
|
+
return null;
|
|
75
|
+
return registry;
|
|
76
|
+
}
|
|
77
|
+
/** How many consoles besides `selfId` are registered (the shell's `switcher.others`). */
|
|
78
|
+
export function countOtherConsoles(registry, selfId) {
|
|
79
|
+
return registry ? registry.entries.filter((e) => e.id !== selfId).length : 0;
|
|
80
|
+
}
|
|
81
|
+
function sortEntries(entries) {
|
|
82
|
+
return [...entries].sort((a, b) => a.order - b.order || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
83
|
+
}
|
|
84
|
+
function iconOf(icon) {
|
|
85
|
+
return ICONS.includes(icon) ? icon : 'generic';
|
|
86
|
+
}
|
|
87
|
+
const TIMED_OUT = Symbol('timed out');
|
|
88
|
+
/** Resolves to `TIMED_OUT` when `promise` takes longer than `ms`, or `undefined` when it rejects. */
|
|
89
|
+
async function race(promise, ms) {
|
|
90
|
+
let timer;
|
|
91
|
+
try {
|
|
92
|
+
return await Promise.race([
|
|
93
|
+
Promise.resolve().then(promise),
|
|
94
|
+
new Promise((resolve) => {
|
|
95
|
+
timer = setTimeout(() => resolve(TIMED_OUT), ms);
|
|
96
|
+
}),
|
|
97
|
+
]);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
if (timer)
|
|
104
|
+
clearTimeout(timer);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Whether a console switcher is running this `authorize` on behalf of another console (it only asks
|
|
109
|
+
* whether to list it). The marker is `ctx.agoraProbe === true`, set structurally by every
|
|
110
|
+
* `@adonis-agora` console; an authorizer that audits every call can skip then.
|
|
111
|
+
*/
|
|
112
|
+
export function isAgoraProbe(ctx) {
|
|
113
|
+
return ctx?.agoraProbe === true;
|
|
114
|
+
}
|
|
115
|
+
/** Runs `fn` with `ctx.agoraProbe = true`, restoring the previous value afterwards. */
|
|
116
|
+
async function probing(ctx, fn) {
|
|
117
|
+
const holder = ctx;
|
|
118
|
+
const previous = holder.agoraProbe;
|
|
119
|
+
holder.agoraProbe = true;
|
|
120
|
+
try {
|
|
121
|
+
return await fn();
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
if (previous === undefined)
|
|
125
|
+
delete holder.agoraProbe;
|
|
126
|
+
else
|
|
127
|
+
holder.agoraProbe = previous;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/** Runs every other console's gate in parallel (each raced against `timeoutMs`); keeps the allowed. */
|
|
131
|
+
async function allowedEntries(entries, ctx, timeoutMs, logger) {
|
|
132
|
+
return probing(ctx, async () => {
|
|
133
|
+
const decisions = await Promise.all(entries.map(async (entry) => {
|
|
134
|
+
const decision = await race(() => entry.authorize(ctx), timeoutMs);
|
|
135
|
+
if (decision === TIMED_OUT || decision === undefined) {
|
|
136
|
+
logger?.debug?.(`[@adonis-agora/authkit-server] console "${entry.id}" hidden: authorize ${decision === TIMED_OUT ? 'timed out' : 'failed'}`);
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
return decision?.allowed === true;
|
|
140
|
+
}));
|
|
141
|
+
return entries.filter((_, i) => decisions[i]);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* The consoles (other than `selfId`) the viewer may open: every `authorize` runs in parallel, each
|
|
146
|
+
* raced against 300 ms; a timeout, a rejection or anything but `allowed: true` hides the console.
|
|
147
|
+
*/
|
|
148
|
+
export async function listVisibleConsoles(registry, selfId, ctx, options = {}) {
|
|
149
|
+
const others = sortEntries((registry?.entries ?? []).filter((e) => e.id !== selfId));
|
|
150
|
+
const allowed = await allowedEntries(others, ctx, options.timeoutMs ?? CONSOLE_PROBE_TIMEOUT_MS, options.logger);
|
|
151
|
+
return {
|
|
152
|
+
version: 1,
|
|
153
|
+
current: selfId,
|
|
154
|
+
consoles: allowed
|
|
155
|
+
.filter((e) => isSameOriginPath(e.path))
|
|
156
|
+
.map((e) => ({
|
|
157
|
+
id: e.id,
|
|
158
|
+
title: e.title,
|
|
159
|
+
...(typeof e.package === 'string' ? { package: e.package } : {}),
|
|
160
|
+
icon: iconOf(e.icon),
|
|
161
|
+
href: e.path,
|
|
162
|
+
})),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Global search across the consoles the viewer may open. The current console (`selfId`, already
|
|
167
|
+
* authorized by its own route guard) is searched without re-running its gate; the others only after
|
|
168
|
+
* their `authorize` allowed. Each `search` gets `timeoutMs` and returns at most `limit` hits; a hit
|
|
169
|
+
* whose `href` is not a same-origin path is dropped.
|
|
170
|
+
*/
|
|
171
|
+
export async function searchConsoles(registry, selfId, ctx, query, options = {}) {
|
|
172
|
+
const q = query.trim().slice(0, 200);
|
|
173
|
+
const result = { version: 1, query: q, groups: [] };
|
|
174
|
+
if (q.length === 0 || !registry)
|
|
175
|
+
return result;
|
|
176
|
+
const timeoutMs = options.timeoutMs ?? CONSOLE_PROBE_TIMEOUT_MS;
|
|
177
|
+
const limit = options.limit ?? CONSOLE_SEARCH_LIMIT;
|
|
178
|
+
const searchable = sortEntries(registry.entries.filter((e) => typeof e.search === 'function'));
|
|
179
|
+
const self = searchable.filter((e) => e.id === selfId);
|
|
180
|
+
const others = await allowedEntries(searchable.filter((e) => e.id !== selfId), ctx, timeoutMs, options.logger);
|
|
181
|
+
const targets = sortEntries([...self, ...others]);
|
|
182
|
+
// Real searches by the viewer: run without the probe marker.
|
|
183
|
+
const hits = await Promise.all(targets.map((entry) => race(() => entry.search(ctx, q, limit), timeoutMs)));
|
|
184
|
+
targets.forEach((entry, i) => {
|
|
185
|
+
const found = hits[i];
|
|
186
|
+
if (!Array.isArray(found))
|
|
187
|
+
return;
|
|
188
|
+
const safe = found
|
|
189
|
+
.filter((h) => h && typeof h.title === 'string' && isSameOriginPath(h.href))
|
|
190
|
+
.slice(0, limit)
|
|
191
|
+
.map((h) => ({
|
|
192
|
+
title: h.title,
|
|
193
|
+
...(typeof h.subtitle === 'string' ? { subtitle: h.subtitle } : {}),
|
|
194
|
+
href: h.href,
|
|
195
|
+
}));
|
|
196
|
+
if (safe.length > 0) {
|
|
197
|
+
result.groups.push({
|
|
198
|
+
id: entry.id,
|
|
199
|
+
title: entry.title,
|
|
200
|
+
icon: iconOf(entry.icon),
|
|
201
|
+
hits: safe,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
return result;
|
|
206
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AccessTokenFormat, AccessTokensConfig, JwksConfig, KeystoreStoreConfig, ObservabilityConfig, TtlConfig } from '@adonis-agora/authkit-core';
|
|
2
2
|
import type { HttpContext } from '@adonisjs/core/http';
|
|
3
|
+
import type { AccountConsoleConfig } from './account/runtime.js';
|
|
3
4
|
import type { AccountStore, AuthAccount } from './accounts/account_store.js';
|
|
4
5
|
import { type AdapterFactory, adapters, type OidcAdapterClass } from './adapters/factory.js';
|
|
5
6
|
import { type PersonalAgentsConfigInput, type ResolvedPersonalAgentsConfig } from './agents/config.js';
|
|
@@ -22,6 +23,7 @@ import type { WhatsappCodeSenderBinding } from './host/whatsapp_code_sender.js';
|
|
|
22
23
|
import { type McpOAuthConfigInput, type ResolvedMcpOAuthConfig } from './mcp/mcp_oauth.js';
|
|
23
24
|
import type { PatStore } from './pat/pat_store.js';
|
|
24
25
|
import { type RedirectUriPolicy, type ResolvedRedirectUriPolicy, type ValidateRegistrationHook } from './provider/registration_policy.js';
|
|
26
|
+
import { type ResolvedServiceTenantsConfig, type ServiceTenantsConfigInput } from './service_tenants/config.js';
|
|
25
27
|
export type { AuthAccount };
|
|
26
28
|
export { adapters };
|
|
27
29
|
export type AuthHostRenderer = (ctx: HttpContext, view: string, props: Record<string, unknown>) => unknown;
|
|
@@ -112,18 +114,6 @@ export interface MailHooks {
|
|
|
112
114
|
email: string;
|
|
113
115
|
sudoUrl: string;
|
|
114
116
|
}) => Promise<void>;
|
|
115
|
-
/**
|
|
116
|
-
* Código do Mediated Sign-In pronto do Poppy (`poppyEmailCodeSignIn`): um personal
|
|
117
|
-
* agent quer entrar na conta e a pessoa precisa dar o código a ele. Sem o hook, o
|
|
118
|
-
* host-kit envia pelo mailer default, com branding e i18n.
|
|
119
|
-
*/
|
|
120
|
-
onPoppySignInCode?: (data: {
|
|
121
|
-
email: string;
|
|
122
|
-
code: string;
|
|
123
|
-
/** Nome do agente (metadata dele, ou o host do `client_id`). */
|
|
124
|
-
agentName: string;
|
|
125
|
-
expiresInMinutes: number;
|
|
126
|
-
}) => Promise<void>;
|
|
127
117
|
/**
|
|
128
118
|
* Disparado num login bem-sucedido a partir de um IP NOVO para a conta (nenhum
|
|
129
119
|
* `login.success` anterior desse IP no audit sink), quando `notifications.newLoginEmail`
|
|
@@ -322,6 +312,11 @@ export interface ResolvedRateLimitConfig {
|
|
|
322
312
|
* force ANTES do lockout por interaction (contador persistido no slot do código).
|
|
323
313
|
*/
|
|
324
314
|
otpLogin: RateLimitBucket;
|
|
315
|
+
/**
|
|
316
|
+
* Bucket das actions das páginas de extension do console de conta
|
|
317
|
+
* (`POST /account/<página>/actions/*`), keyed pela CONTA. Default 30/min.
|
|
318
|
+
*/
|
|
319
|
+
accountActions?: RateLimitBucket;
|
|
325
320
|
store?: string;
|
|
326
321
|
}
|
|
327
322
|
export declare function resolveRateLimit(input?: RateLimitConfigInput): ResolvedRateLimitConfig;
|
|
@@ -1125,6 +1120,19 @@ export interface AuthServerConfigInput {
|
|
|
1125
1120
|
* `resources` mapeia resource indicators (RFC 8707) para audiences/scopes/formato/TTL por API.
|
|
1126
1121
|
*/
|
|
1127
1122
|
accessTokens?: AccessTokensConfig;
|
|
1123
|
+
/**
|
|
1124
|
+
* Service tenants: confidential `client_credentials` clients that act as a partition of
|
|
1125
|
+
* a resource server (e.g. a durable control plane). `resources` lists the resource
|
|
1126
|
+
* indicators (RFC 8707) that issue their JWT access tokens (RFC 9068), each with an
|
|
1127
|
+
* optional `expiresIn` (seconds, default 600), `audience` and `scopes`:
|
|
1128
|
+
*
|
|
1129
|
+
* ```ts
|
|
1130
|
+
* serviceTenants: { resources: { 'urn:agora:durable': { expiresIn: 600 } } }
|
|
1131
|
+
* ```
|
|
1132
|
+
*
|
|
1133
|
+
* Turns on the `client_credentials` grant. Default: off.
|
|
1134
|
+
*/
|
|
1135
|
+
serviceTenants?: ServiceTenantsConfigInput;
|
|
1128
1136
|
/**
|
|
1129
1137
|
* Login OAuth de clientes MCP (Claude Code, Claude, ChatGPT, VS Code, Cursor): `true` liga o
|
|
1130
1138
|
* registro dinâmico restrito aos redirects deles, o refresh token desses clientes e os
|
|
@@ -1182,7 +1190,10 @@ export interface AuthServerConfigInput {
|
|
|
1182
1190
|
* adiciona colunas novas (aditivo — nunca dropa nem altera tipos).
|
|
1183
1191
|
* - `autoManage: false`: nada roda no boot; gerencie o schema você mesmo —
|
|
1184
1192
|
* de preferência chamando `ensureAuthkitSchema(this.db)` numa migration.
|
|
1185
|
-
* - `connection`: conexão Lucid
|
|
1193
|
+
* - `connection`: a conexão Lucid de TODAS as tabelas do authkit (default:
|
|
1194
|
+
* primária) — as acima, o `authkit_keystore`, as de extensions e a do
|
|
1195
|
+
* `adapters.database()`/keystore `lucid` quando eles não declaram a própria.
|
|
1196
|
+
* A tabela da conta NÃO entra: fica na conexão do model da conta.
|
|
1186
1197
|
*/
|
|
1187
1198
|
schema?: {
|
|
1188
1199
|
autoManage?: boolean;
|
|
@@ -1249,6 +1260,16 @@ export interface AuthServerConfigInput {
|
|
|
1249
1260
|
accountSession?: {
|
|
1250
1261
|
acceptIdpSession?: boolean;
|
|
1251
1262
|
};
|
|
1263
|
+
/**
|
|
1264
|
+
* Account console extensions: the pages other packages add to `/account/*`.
|
|
1265
|
+
*
|
|
1266
|
+
* - `subject(account, { activeOrg, tenant })`: who the account is in the app (default
|
|
1267
|
+
* `{ type: 'User', id: account.id }`); every extension receives it as `ctx.subject`.
|
|
1268
|
+
* - `extensions`: turn off, rename or reorder pages, by extension id
|
|
1269
|
+
* (`{ payments: { pages: { 'payments.invoices': { segment: 'faturas' } } } }`).
|
|
1270
|
+
* - `loadTimeoutMs`: budget of one page's `load()` (default 5000).
|
|
1271
|
+
*/
|
|
1272
|
+
accountConsole?: AccountConsoleConfig;
|
|
1252
1273
|
}
|
|
1253
1274
|
export interface ResolvedServerConfig {
|
|
1254
1275
|
customLoginMethods?: CustomLoginMethods;
|
|
@@ -1367,6 +1388,8 @@ export interface ResolvedServerConfig {
|
|
|
1367
1388
|
registration: ResolvedRegistrationConfig;
|
|
1368
1389
|
/** Access Tokens resolvido (RFC 9068; default opaque). */
|
|
1369
1390
|
accessTokens: ResolvedAccessTokensConfig;
|
|
1391
|
+
/** Service tenants (always present; `enabled: false` when no resource is declared). */
|
|
1392
|
+
serviceTenants: ResolvedServiceTenantsConfig;
|
|
1370
1393
|
mcp: ResolvedMcpOAuthConfig;
|
|
1371
1394
|
/** Console admin resolvido (sempre presente; default desligado). */
|
|
1372
1395
|
admin: ResolvedAdminConfig;
|
|
@@ -1409,6 +1432,8 @@ export interface ResolvedServerConfig {
|
|
|
1409
1432
|
accountSession: {
|
|
1410
1433
|
acceptIdpSession: boolean;
|
|
1411
1434
|
};
|
|
1435
|
+
/** Account console extensions. Ver {@link AuthServerConfigInput.accountConsole}. */
|
|
1436
|
+
accountConsole?: AccountConsoleConfig;
|
|
1412
1437
|
}
|
|
1413
1438
|
export declare function toSeconds(value: string | number | undefined, fallback: number): number;
|
|
1414
1439
|
/**
|
|
@@ -6,7 +6,7 @@ import { composeAuditSink, resolveEvents } from './events/dispatcher.js';
|
|
|
6
6
|
import { resolveBotProtection, } from './host/bot_protection.js';
|
|
7
7
|
import { resolveBranding } from './host/branding.js';
|
|
8
8
|
import { deriveLockedRouteOptions, deriveLockedSettingKeys, } from './host/config_locks.js';
|
|
9
|
-
import { resolveMessages } from './host/i18n.js';
|
|
9
|
+
import { registerExtensionMessages, resolveMessages, } from './host/i18n.js';
|
|
10
10
|
import { resolveOtpLoginConfig, } from './host/otp_login.js';
|
|
11
11
|
import { edgeRenderer } from './host/renderers/edge_renderer.js';
|
|
12
12
|
import { warnUnsatisfiableSudoConfig } from './host/sudo/satisfiability.js';
|
|
@@ -17,6 +17,8 @@ import { loadEncryptionService } from './keys/keystore_crypto.js';
|
|
|
17
17
|
import { KeystoreManager, resolveKeystoreVault } from './keys/keystore_manager.js';
|
|
18
18
|
import { mcpClientRegistration, resolveMcpOAuth, } from './mcp/mcp_oauth.js';
|
|
19
19
|
import { OPEN_REGISTRATION_REDIRECT_POLICY, resolveRedirectUriPolicy, } from './provider/registration_policy.js';
|
|
20
|
+
import { bindTablesLocation } from './schema/tables_location.js';
|
|
21
|
+
import { resolveServiceTenants, } from './service_tenants/config.js';
|
|
20
22
|
export { adapters };
|
|
21
23
|
const RATE_LIMIT_DEFAULTS = {
|
|
22
24
|
login: { points: 10, duration: '1 min' },
|
|
@@ -26,6 +28,8 @@ const RATE_LIMIT_DEFAULTS = {
|
|
|
26
28
|
sudo: { points: 10, duration: '1 min' },
|
|
27
29
|
// Mais apertado que o login — verificação de código adivinhável.
|
|
28
30
|
otpLogin: { points: 5, duration: '1 min' },
|
|
31
|
+
// Actions das extensions do console de conta, por conta.
|
|
32
|
+
accountActions: { points: 30, duration: '1 min' },
|
|
29
33
|
};
|
|
30
34
|
export function resolveRateLimit(input) {
|
|
31
35
|
const enabled = input?.enabled ?? true;
|
|
@@ -36,6 +40,7 @@ export function resolveRateLimit(input) {
|
|
|
36
40
|
adminIp: RATE_LIMIT_DEFAULTS.adminIp,
|
|
37
41
|
sudo: RATE_LIMIT_DEFAULTS.sudo,
|
|
38
42
|
otpLogin: RATE_LIMIT_DEFAULTS.otpLogin,
|
|
43
|
+
accountActions: RATE_LIMIT_DEFAULTS.accountActions,
|
|
39
44
|
store: input?.store,
|
|
40
45
|
};
|
|
41
46
|
}
|
|
@@ -280,15 +285,20 @@ export function defineConfig(config) {
|
|
|
280
285
|
(!Number.isInteger(requiredFactors) || requiredFactors < 2 || requiredFactors > 8)) {
|
|
281
286
|
throw new Error('mfa.requiredFactors must be an integer between 2 and 8');
|
|
282
287
|
}
|
|
288
|
+
// Toda tabela do AuthKit vive em `schema.connection` — inclusive as que o store da
|
|
289
|
+
// conta lê (auth_mfa, auth_password_history, organizations), que antes seguiam a
|
|
290
|
+
// conexão do model da conta.
|
|
291
|
+
bindTablesLocation(config.accountStore, { connection: config.schema?.connection });
|
|
283
292
|
return configProvider.create(async (app) => {
|
|
284
|
-
const
|
|
293
|
+
const tablesContext = { connection: config.schema?.connection };
|
|
294
|
+
const AdapterClass = await config.adapter.resolver(app, tablesContext);
|
|
285
295
|
// `session.adapter` ausente ⇒ mesma classe do default (back-compat: o
|
|
286
296
|
// dispatcher vira identidade e tudo continua no adapter único de hoje).
|
|
287
297
|
const SessionAdapterClass = config.session?.adapter
|
|
288
|
-
? await config.session.adapter.resolver(app)
|
|
298
|
+
? await config.session.adapter.resolver(app, tablesContext)
|
|
289
299
|
: AdapterClass;
|
|
290
300
|
// `jwks: 'auto'` → resolve env-aware: AUTHKIT_JWKS inline, senão managed em arquivo.
|
|
291
|
-
const
|
|
301
|
+
const jwksInput = config.jwks === 'auto'
|
|
292
302
|
? process.env.AUTHKIT_JWKS
|
|
293
303
|
? {
|
|
294
304
|
source: 'jwks',
|
|
@@ -300,6 +310,14 @@ export function defineConfig(config) {
|
|
|
300
310
|
store: 'tmp/authkit_jwks.json',
|
|
301
311
|
}
|
|
302
312
|
: config.jwks;
|
|
313
|
+
// Keystore no Lucid sem conexão própria fica na conexão das tabelas do authkit.
|
|
314
|
+
const jwksStore = jwksInput.store;
|
|
315
|
+
const jwksConfig = jwksStore?.driver === 'lucid' && !jwksStore.connection && config.schema?.connection
|
|
316
|
+
? {
|
|
317
|
+
...jwksInput,
|
|
318
|
+
store: { ...jwksStore, connection: config.schema.connection },
|
|
319
|
+
}
|
|
320
|
+
: jwksInput;
|
|
303
321
|
if (config.jwks === 'auto' && !process.env.AUTHKIT_JWKS) {
|
|
304
322
|
const warning = jwksAutoFallbackWarning(jwksConfig.store ?? null);
|
|
305
323
|
if (warning) {
|
|
@@ -343,7 +361,7 @@ export function defineConfig(config) {
|
|
|
343
361
|
}
|
|
344
362
|
// Personal agents com delegação assinam tokens e recibos com este keystore,
|
|
345
363
|
// e o PACT só admite ES256/RS256 — falha no boot, não na primeira troca de token.
|
|
346
|
-
const personalAgents = resolvePersonalAgentsConfig(config.personalAgents);
|
|
364
|
+
const personalAgents = resolvePersonalAgentsConfig(config.personalAgents, config.issuer);
|
|
347
365
|
if (personalAgents?.delegation)
|
|
348
366
|
assertDelegationSigningKey(jwks);
|
|
349
367
|
// Debaixo do mountPath, a rota curinga do oidc-provider (`${mount}/*`)
|
|
@@ -355,11 +373,13 @@ export function defineConfig(config) {
|
|
|
355
373
|
(personalAgents.prefix === oidcMount || personalAgents.prefix.startsWith(`${oidcMount}/`))) {
|
|
356
374
|
throw new Error(`authkit: personalAgents.prefix "${personalAgents.prefix}" não pode ficar debaixo do mountPath do OIDC ("${oidcMount}").`);
|
|
357
375
|
}
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
376
|
+
for (const extension of personalAgents?.extensions ?? []) {
|
|
377
|
+
if (extension.messages)
|
|
378
|
+
registerExtensionMessages(extension.messages);
|
|
379
|
+
if (oidcMount &&
|
|
380
|
+
(extension.prefix === oidcMount || extension.prefix.startsWith(`${oidcMount}/`))) {
|
|
381
|
+
throw new Error(`authkit: o prefixo "${extension.prefix}" de personalAgents.extensions "${extension.id}" não pode ficar debaixo do mountPath do OIDC ("${oidcMount}").`);
|
|
382
|
+
}
|
|
363
383
|
}
|
|
364
384
|
// BACKSTOP DE SUDO. Um host cujo `sudo.methods` não tem um único método
|
|
365
385
|
// satisfazível por conta sem senha fica bricado para TODA operação sob
|
|
@@ -473,6 +493,7 @@ export function defineConfig(config) {
|
|
|
473
493
|
: undefined,
|
|
474
494
|
registration: resolveRegistration(config.registration),
|
|
475
495
|
accessTokens: resolveAccessTokens(config.issuer, config.accessTokens),
|
|
496
|
+
serviceTenants: resolveServiceTenants(config.serviceTenants, config.accessTokens?.resources),
|
|
476
497
|
mcp: resolveMcpOAuth(config.mcp),
|
|
477
498
|
admin: resolveAdmin(config.admin),
|
|
478
499
|
adminApi: resolveAdminApi(config.adminApi),
|
|
@@ -498,6 +519,7 @@ export function defineConfig(config) {
|
|
|
498
519
|
// Opt-in: ausente = authkit nunca toca `ctx.auth` (comportamento de sempre).
|
|
499
520
|
adonisAuth: config.adonisAuth,
|
|
500
521
|
accountSession: { acceptIdpSession: config.accountSession?.acceptIdpSession !== false },
|
|
522
|
+
accountConsole: config.accountConsole,
|
|
501
523
|
};
|
|
502
524
|
});
|
|
503
525
|
}
|
|
@@ -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[];
|