@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,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,9 +1,11 @@
|
|
|
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';
|
|
6
7
|
import type { AuditSink } from './audit/audit_sink.js';
|
|
8
|
+
import type { ConsoleAuthorize } from './console_authorize.js';
|
|
7
9
|
import { type EventsConfigInput } from './events/dispatcher.js';
|
|
8
10
|
import { type BotProtectionConfigInput, type ResolvedBotProtectionConfig } from './host/bot_protection.js';
|
|
9
11
|
import { type BrandingConfig } from './host/branding.js';
|
|
@@ -21,6 +23,7 @@ import type { WhatsappCodeSenderBinding } from './host/whatsapp_code_sender.js';
|
|
|
21
23
|
import { type McpOAuthConfigInput, type ResolvedMcpOAuthConfig } from './mcp/mcp_oauth.js';
|
|
22
24
|
import type { PatStore } from './pat/pat_store.js';
|
|
23
25
|
import { type RedirectUriPolicy, type ResolvedRedirectUriPolicy, type ValidateRegistrationHook } from './provider/registration_policy.js';
|
|
26
|
+
import { type ResolvedServiceTenantsConfig, type ServiceTenantsConfigInput } from './service_tenants/config.js';
|
|
24
27
|
export type { AuthAccount };
|
|
25
28
|
export { adapters };
|
|
26
29
|
export type AuthHostRenderer = (ctx: HttpContext, view: string, props: Record<string, unknown>) => unknown;
|
|
@@ -111,18 +114,6 @@ export interface MailHooks {
|
|
|
111
114
|
email: string;
|
|
112
115
|
sudoUrl: string;
|
|
113
116
|
}) => Promise<void>;
|
|
114
|
-
/**
|
|
115
|
-
* Código do Mediated Sign-In pronto do Poppy (`poppyEmailCodeSignIn`): um personal
|
|
116
|
-
* agent quer entrar na conta e a pessoa precisa dar o código a ele. Sem o hook, o
|
|
117
|
-
* host-kit envia pelo mailer default, com branding e i18n.
|
|
118
|
-
*/
|
|
119
|
-
onPoppySignInCode?: (data: {
|
|
120
|
-
email: string;
|
|
121
|
-
code: string;
|
|
122
|
-
/** Nome do agente (metadata dele, ou o host do `client_id`). */
|
|
123
|
-
agentName: string;
|
|
124
|
-
expiresInMinutes: number;
|
|
125
|
-
}) => Promise<void>;
|
|
126
117
|
/**
|
|
127
118
|
* Disparado num login bem-sucedido a partir de um IP NOVO para a conta (nenhum
|
|
128
119
|
* `login.success` anterior desse IP no audit sink), quando `notifications.newLoginEmail`
|
|
@@ -321,6 +312,11 @@ export interface ResolvedRateLimitConfig {
|
|
|
321
312
|
* force ANTES do lockout por interaction (contador persistido no slot do código).
|
|
322
313
|
*/
|
|
323
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;
|
|
324
320
|
store?: string;
|
|
325
321
|
}
|
|
326
322
|
export declare function resolveRateLimit(input?: RateLimitConfigInput): ResolvedRateLimitConfig;
|
|
@@ -708,12 +704,21 @@ export interface AdminConfigInput {
|
|
|
708
704
|
* default é decisão de major, mesma regra de `impersonation`).
|
|
709
705
|
*/
|
|
710
706
|
impersonateAdmins?: boolean;
|
|
707
|
+
/**
|
|
708
|
+
* Access check for the console, run after `roles` (both must pass), on the shell
|
|
709
|
+
* and on every `{prefix}/api/*` route. An inline function, an authorizer class
|
|
710
|
+
* (`@inject()` works), a lazy import (`() => import('#policies/...')`) or
|
|
711
|
+
* `authorizeByToken({ token })`. Omitted: open outside production, 403 in production.
|
|
712
|
+
*/
|
|
713
|
+
authorize?: ConsoleAuthorize;
|
|
711
714
|
}
|
|
712
715
|
export interface ResolvedAdminConfig {
|
|
713
716
|
enabled: boolean;
|
|
714
717
|
roles: string[];
|
|
715
718
|
impersonation: boolean;
|
|
716
719
|
impersonateAdmins: boolean;
|
|
720
|
+
/** `admin.authorize` as configured, or `null` when omitted. */
|
|
721
|
+
authorize: ConsoleAuthorize | null;
|
|
717
722
|
}
|
|
718
723
|
export declare function resolveAdmin(input?: AdminConfigInput): ResolvedAdminConfig;
|
|
719
724
|
/**
|
|
@@ -1115,6 +1120,19 @@ export interface AuthServerConfigInput {
|
|
|
1115
1120
|
* `resources` mapeia resource indicators (RFC 8707) para audiences/scopes/formato/TTL por API.
|
|
1116
1121
|
*/
|
|
1117
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;
|
|
1118
1136
|
/**
|
|
1119
1137
|
* Login OAuth de clientes MCP (Claude Code, Claude, ChatGPT, VS Code, Cursor): `true` liga o
|
|
1120
1138
|
* registro dinâmico restrito aos redirects deles, o refresh token desses clientes e os
|
|
@@ -1225,12 +1243,30 @@ export interface AuthServerConfigInput {
|
|
|
1225
1243
|
* expiração), e o "Sair" do console encerra também a sessão do IdP. Contas
|
|
1226
1244
|
* desabilitadas não entram. Operações sensíveis continuam pedindo sudo.
|
|
1227
1245
|
*
|
|
1228
|
-
* Default `
|
|
1229
|
-
*
|
|
1246
|
+
* Default `true`: o console é mais um app do IdP. Sem sessão, `/account/*` e o
|
|
1247
|
+
* console admin mandam o visitante para o login do IdP (`/auth/console/sso`,
|
|
1248
|
+
* client interno `authkit-console`) e voltam com o console aberto. Um único
|
|
1249
|
+
* login, como no Keycloak, Okta e Google; o que protege as operações
|
|
1250
|
+
* sensíveis é o sudo, não um segundo login.
|
|
1251
|
+
*
|
|
1252
|
+
* `false`: o console só aceita a própria sessão (`POST /account/login`, por
|
|
1253
|
+
* senha). Com `authMethods: { password: false }` isso deixa o console sem
|
|
1254
|
+
* nenhum jeito de entrar, então o boot recusa a combinação, a menos que
|
|
1255
|
+
* `routes.accountLoginUrl` aponte para um login do próprio host.
|
|
1230
1256
|
*/
|
|
1231
1257
|
accountSession?: {
|
|
1232
1258
|
acceptIdpSession?: boolean;
|
|
1233
1259
|
};
|
|
1260
|
+
/**
|
|
1261
|
+
* Account console extensions: the pages other packages add to `/account/*`.
|
|
1262
|
+
*
|
|
1263
|
+
* - `subject(account, { activeOrg, tenant })`: who the account is in the app (default
|
|
1264
|
+
* `{ type: 'User', id: account.id }`); every extension receives it as `ctx.subject`.
|
|
1265
|
+
* - `extensions`: turn off, rename or reorder pages, by extension id
|
|
1266
|
+
* (`{ payments: { pages: { 'payments.invoices': { segment: 'faturas' } } } }`).
|
|
1267
|
+
* - `loadTimeoutMs`: budget of one page's `load()` (default 5000).
|
|
1268
|
+
*/
|
|
1269
|
+
accountConsole?: AccountConsoleConfig;
|
|
1234
1270
|
}
|
|
1235
1271
|
export interface ResolvedServerConfig {
|
|
1236
1272
|
customLoginMethods?: CustomLoginMethods;
|
|
@@ -1349,6 +1385,8 @@ export interface ResolvedServerConfig {
|
|
|
1349
1385
|
registration: ResolvedRegistrationConfig;
|
|
1350
1386
|
/** Access Tokens resolvido (RFC 9068; default opaque). */
|
|
1351
1387
|
accessTokens: ResolvedAccessTokensConfig;
|
|
1388
|
+
/** Service tenants (always present; `enabled: false` when no resource is declared). */
|
|
1389
|
+
serviceTenants: ResolvedServiceTenantsConfig;
|
|
1352
1390
|
mcp: ResolvedMcpOAuthConfig;
|
|
1353
1391
|
/** Console admin resolvido (sempre presente; default desligado). */
|
|
1354
1392
|
admin: ResolvedAdminConfig;
|
|
@@ -1391,6 +1429,8 @@ export interface ResolvedServerConfig {
|
|
|
1391
1429
|
accountSession: {
|
|
1392
1430
|
acceptIdpSession: boolean;
|
|
1393
1431
|
};
|
|
1432
|
+
/** Account console extensions. Ver {@link AuthServerConfigInput.accountConsole}. */
|
|
1433
|
+
accountConsole?: AccountConsoleConfig;
|
|
1394
1434
|
}
|
|
1395
1435
|
export declare function toSeconds(value: string | number | undefined, fallback: number): number;
|
|
1396
1436
|
/**
|
|
@@ -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,7 @@ 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 { resolveServiceTenants, } from './service_tenants/config.js';
|
|
20
21
|
export { adapters };
|
|
21
22
|
const RATE_LIMIT_DEFAULTS = {
|
|
22
23
|
login: { points: 10, duration: '1 min' },
|
|
@@ -26,6 +27,8 @@ const RATE_LIMIT_DEFAULTS = {
|
|
|
26
27
|
sudo: { points: 10, duration: '1 min' },
|
|
27
28
|
// Mais apertado que o login — verificação de código adivinhável.
|
|
28
29
|
otpLogin: { points: 5, duration: '1 min' },
|
|
30
|
+
// Actions das extensions do console de conta, por conta.
|
|
31
|
+
accountActions: { points: 30, duration: '1 min' },
|
|
29
32
|
};
|
|
30
33
|
export function resolveRateLimit(input) {
|
|
31
34
|
const enabled = input?.enabled ?? true;
|
|
@@ -36,6 +39,7 @@ export function resolveRateLimit(input) {
|
|
|
36
39
|
adminIp: RATE_LIMIT_DEFAULTS.adminIp,
|
|
37
40
|
sudo: RATE_LIMIT_DEFAULTS.sudo,
|
|
38
41
|
otpLogin: RATE_LIMIT_DEFAULTS.otpLogin,
|
|
42
|
+
accountActions: RATE_LIMIT_DEFAULTS.accountActions,
|
|
39
43
|
store: input?.store,
|
|
40
44
|
};
|
|
41
45
|
}
|
|
@@ -193,6 +197,7 @@ export function resolveAdmin(input) {
|
|
|
193
197
|
// registrado). Ver o docblock de `AdminConfigInput.impersonation`.
|
|
194
198
|
impersonation: input?.impersonation !== false,
|
|
195
199
|
impersonateAdmins: input?.impersonateAdmins !== false,
|
|
200
|
+
authorize: input?.authorize ?? null,
|
|
196
201
|
};
|
|
197
202
|
}
|
|
198
203
|
/** Lê API keys de `AUTHKIT_ADMIN_API_KEY` (uma ou várias, separadas por vírgula). */
|
|
@@ -342,7 +347,7 @@ export function defineConfig(config) {
|
|
|
342
347
|
}
|
|
343
348
|
// Personal agents com delegação assinam tokens e recibos com este keystore,
|
|
344
349
|
// e o PACT só admite ES256/RS256 — falha no boot, não na primeira troca de token.
|
|
345
|
-
const personalAgents = resolvePersonalAgentsConfig(config.personalAgents);
|
|
350
|
+
const personalAgents = resolvePersonalAgentsConfig(config.personalAgents, config.issuer);
|
|
346
351
|
if (personalAgents?.delegation)
|
|
347
352
|
assertDelegationSigningKey(jwks);
|
|
348
353
|
// Debaixo do mountPath, a rota curinga do oidc-provider (`${mount}/*`)
|
|
@@ -354,11 +359,13 @@ export function defineConfig(config) {
|
|
|
354
359
|
(personalAgents.prefix === oidcMount || personalAgents.prefix.startsWith(`${oidcMount}/`))) {
|
|
355
360
|
throw new Error(`authkit: personalAgents.prefix "${personalAgents.prefix}" não pode ficar debaixo do mountPath do OIDC ("${oidcMount}").`);
|
|
356
361
|
}
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
+
for (const extension of personalAgents?.extensions ?? []) {
|
|
363
|
+
if (extension.messages)
|
|
364
|
+
registerExtensionMessages(extension.messages);
|
|
365
|
+
if (oidcMount &&
|
|
366
|
+
(extension.prefix === oidcMount || extension.prefix.startsWith(`${oidcMount}/`))) {
|
|
367
|
+
throw new Error(`authkit: o prefixo "${extension.prefix}" de personalAgents.extensions "${extension.id}" não pode ficar debaixo do mountPath do OIDC ("${oidcMount}").`);
|
|
368
|
+
}
|
|
362
369
|
}
|
|
363
370
|
// BACKSTOP DE SUDO. Um host cujo `sudo.methods` não tem um único método
|
|
364
371
|
// satisfazível por conta sem senha fica bricado para TODA operação sob
|
|
@@ -472,6 +479,7 @@ export function defineConfig(config) {
|
|
|
472
479
|
: undefined,
|
|
473
480
|
registration: resolveRegistration(config.registration),
|
|
474
481
|
accessTokens: resolveAccessTokens(config.issuer, config.accessTokens),
|
|
482
|
+
serviceTenants: resolveServiceTenants(config.serviceTenants, config.accessTokens?.resources),
|
|
475
483
|
mcp: resolveMcpOAuth(config.mcp),
|
|
476
484
|
admin: resolveAdmin(config.admin),
|
|
477
485
|
adminApi: resolveAdminApi(config.adminApi),
|
|
@@ -496,7 +504,8 @@ export function defineConfig(config) {
|
|
|
496
504
|
lockedRouteOptions: deriveLockedRouteOptions(config),
|
|
497
505
|
// Opt-in: ausente = authkit nunca toca `ctx.auth` (comportamento de sempre).
|
|
498
506
|
adonisAuth: config.adonisAuth,
|
|
499
|
-
accountSession: { acceptIdpSession: config.accountSession?.acceptIdpSession
|
|
507
|
+
accountSession: { acceptIdpSession: config.accountSession?.acceptIdpSession !== false },
|
|
508
|
+
accountConsole: config.accountConsole,
|
|
500
509
|
};
|
|
501
510
|
});
|
|
502
511
|
}
|
|
@@ -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
|
+
}
|