@adonis-agora/authkit-server 1.0.0-rc.7 → 1.0.0-rc.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/build/host/views/account/apps.edge +1 -5
  3. package/build/host/views/account/extension.edge +77 -0
  4. package/build/host/views/account/mfa.edge +2 -0
  5. package/build/host/views/account/partials/extension_actions.edge +14 -0
  6. package/build/host/views/account/partials/extension_block.edge +197 -0
  7. package/build/host/views/account/partials/extension_value.edge +8 -0
  8. package/build/host/views/account/partials/nav.edge +14 -0
  9. package/build/host/views/account/security.edge +1 -5
  10. package/build/host/views/account/tokens.edge +2 -2
  11. package/build/host/views/partials/styles.edge +1 -1
  12. package/build/index.d.ts +2 -0
  13. package/build/index.js +1 -0
  14. package/build/providers/authkit_server_provider.js +5 -0
  15. package/build/src/account/extension.d.ts +323 -0
  16. package/build/src/account/extension.js +36 -0
  17. package/build/src/account/http.d.ts +71 -0
  18. package/build/src/account/http.js +369 -0
  19. package/build/src/account/mount.d.ts +35 -0
  20. package/build/src/account/mount.js +99 -0
  21. package/build/src/account/registry.d.ts +48 -0
  22. package/build/src/account/registry.js +229 -0
  23. package/build/src/account/runtime.d.ts +277 -0
  24. package/build/src/account/runtime.js +623 -0
  25. package/build/src/audit/audit_sink.d.ts +1 -1
  26. package/build/src/audit/audit_sink.js +4 -0
  27. package/build/src/console_switcher.d.ts +132 -0
  28. package/build/src/console_switcher.js +206 -0
  29. package/build/src/define_config.d.ts +18 -0
  30. package/build/src/define_config.js +4 -0
  31. package/build/src/host/admin_api/dto.d.ts +1 -1
  32. package/build/src/host/admin_console/admin_console_entry.d.ts +14 -0
  33. package/build/src/host/admin_console/admin_console_entry.js +80 -0
  34. package/build/src/host/admin_console/admin_shell_controller.js +8 -0
  35. package/build/src/host/admin_console/console_switcher_controller.d.ts +11 -0
  36. package/build/src/host/admin_console/console_switcher_controller.js +23 -0
  37. package/build/src/host/auth_host_config.d.ts +3 -0
  38. package/build/src/host/i18n.d.ts +36 -0
  39. package/build/src/host/i18n.js +38 -0
  40. package/build/src/host/rate_limit.d.ts +8 -0
  41. package/build/src/host/rate_limit.js +9 -0
  42. package/build/src/host/register_auth_host.d.ts +14 -0
  43. package/build/src/host/register_auth_host.js +140 -29
  44. package/build/src/host/renderers/edge_renderer.js +5 -0
  45. package/build/src/host/renderers/inertia_renderer.js +7 -1
  46. package/build/src/host/screen_props.d.ts +28 -0
  47. package/build/src/host/ui-dist/assets/index-Dq3dJVBv.js +67 -0
  48. package/build/src/host/ui-dist/assets/{index-DTSmD4RU.css → index-WNIyCEPA.css} +1 -1
  49. package/build/src/host/ui-dist/index.html +2 -2
  50. package/build/types.d.ts +3 -1
  51. package/package.json +3 -2
  52. package/build/src/host/ui-dist/assets/index-u0bMxkLg.js +0 -67
@@ -0,0 +1,132 @@
1
+ import type { HttpContext } from '@adonisjs/core/http';
2
+ /**
3
+ * The console switcher contract, version 1.
4
+ *
5
+ * Every `@adonis-agora` console registers itself into ONE data-only registry bound as a value at
6
+ * `'agora.consoles'` (created by whichever lib boots first). Each console then shows a switcher to
7
+ * the other consoles the viewer may open. The types are declared locally in every lib (TypeScript is
8
+ * structural), so no lib imports another.
9
+ */
10
+ /** The container binding the registry lives at. */
11
+ export declare const AGORA_CONSOLES_BINDING = "agora.consoles";
12
+ /** Bound as a VALUE under `'agora.consoles'`. Data only, so every lib's copy is identical. */
13
+ export interface AgoraConsoleRegistry {
14
+ readonly version: 1;
15
+ readonly entries: AgoraConsoleEntry[];
16
+ }
17
+ export type AgoraConsoleIcon = 'workflow' | 'telescope' | 'media' | 'payments' | 'agent' | 'identity' | 'generic';
18
+ /** The decision a console's gate returns. Never throws. */
19
+ export interface AgoraConsoleDecision {
20
+ allowed: boolean;
21
+ reason?: string;
22
+ redirectTo?: string;
23
+ }
24
+ /** One search hit. `href` is a same-origin absolute path. */
25
+ export interface AgoraConsoleSearchHit {
26
+ title: string;
27
+ subtitle?: string;
28
+ href: string;
29
+ }
30
+ export interface AgoraConsoleEntry {
31
+ /** `/^[a-z][a-z0-9-]*$/`: `'durable'`, `'telescope'`, `'authkit'`, … */
32
+ id: string;
33
+ /** Product title (`'Identity'`). */
34
+ title: string;
35
+ /** Package name, shown as secondary text (`'@adonis-agora/authkit-server'`). */
36
+ package?: string;
37
+ /** Unknown values render as `'generic'`. */
38
+ icon: AgoraConsoleIcon;
39
+ /** Same-origin absolute path of the console shell (`'/admin'`). */
40
+ path: string;
41
+ /** Lower first. durable 10, telescope 20, agent 30, payments 40, media 50, authkit 90. */
42
+ order: number;
43
+ /**
44
+ * The console's resolved gate (the one its own routes run). While a switcher runs it for another
45
+ * console, ``ctx.agoraProbe` is `true` (see `isAgoraProbe`), so an authorizer that writes an audit entry per call can skip.
46
+ */
47
+ authorize(ctx: HttpContext): Promise<AgoraConsoleDecision>;
48
+ /** Global search provider. Only called after `authorize` allowed the viewer. */
49
+ search?(ctx: HttpContext, query: string, limit: number): Promise<AgoraConsoleSearchHit[]>;
50
+ }
51
+ /** What `GET <console api>/consoles` answers. */
52
+ export interface VisibleConsoles {
53
+ version: 1;
54
+ current: string;
55
+ consoles: Array<{
56
+ id: string;
57
+ title: string;
58
+ package?: string;
59
+ icon: AgoraConsoleIcon;
60
+ href: string;
61
+ }>;
62
+ }
63
+ /** What `GET <console api>/consoles/search?q=` answers. */
64
+ export interface ConsoleSearchResults {
65
+ version: 1;
66
+ query: string;
67
+ groups: Array<{
68
+ id: string;
69
+ title: string;
70
+ icon: AgoraConsoleIcon;
71
+ hits: AgoraConsoleSearchHit[];
72
+ }>;
73
+ }
74
+ /** Default time each console's `authorize` (and `search`) gets before it counts as hidden. */
75
+ export declare const CONSOLE_PROBE_TIMEOUT_MS = 300;
76
+ /** Hits returned per console by the global search. */
77
+ export declare const CONSOLE_SEARCH_LIMIT = 5;
78
+ interface ContainerLike {
79
+ hasBinding(binding: string): boolean;
80
+ bindValue(binding: string, value: unknown): void;
81
+ make(binding: string): Promise<unknown>;
82
+ }
83
+ interface AppLike {
84
+ container: ContainerLike;
85
+ }
86
+ interface LoggerLike {
87
+ warn(message: string): void;
88
+ debug?(message: string): void;
89
+ }
90
+ /** A same-origin absolute path: starts with `/`, not `//`, no backslash, no control characters. */
91
+ export declare function isSameOriginPath(value: unknown): value is string;
92
+ /**
93
+ * Add a console to the shared `'agora.consoles'` registry, creating it when absent. Skips (never
94
+ * throws) a registry of another version; warns and skips a duplicate id. Throws on an invalid `id`
95
+ * or `path`: that is a programming error. Apps can call it to register a console they mount by hand.
96
+ *
97
+ * ```ts
98
+ * await registerAgoraConsole(app, { id: 'reports', title: 'Reports', icon: 'generic', path: '/reports',
99
+ * order: 100, authorize: async (ctx) => ({ allowed: await isStaff(ctx) }) })
100
+ * ```
101
+ */
102
+ export declare function registerAgoraConsole(app: AppLike, entry: AgoraConsoleEntry, logger?: LoggerLike): Promise<boolean>;
103
+ /** The registry, when one is bound and is version 1. */
104
+ export declare function readAgoraConsoles(app: AppLike): Promise<AgoraConsoleRegistry | null>;
105
+ /** How many consoles besides `selfId` are registered (the shell's `switcher.others`). */
106
+ export declare function countOtherConsoles(registry: AgoraConsoleRegistry | null, selfId: string): number;
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 declare function isAgoraProbe(ctx: unknown): boolean;
113
+ /**
114
+ * The consoles (other than `selfId`) the viewer may open: every `authorize` runs in parallel, each
115
+ * raced against 300 ms; a timeout, a rejection or anything but `allowed: true` hides the console.
116
+ */
117
+ export declare function listVisibleConsoles(registry: AgoraConsoleRegistry | null, selfId: string, ctx: HttpContext, options?: {
118
+ timeoutMs?: number;
119
+ logger?: LoggerLike;
120
+ }): Promise<VisibleConsoles>;
121
+ /**
122
+ * Global search across the consoles the viewer may open. The current console (`selfId`, already
123
+ * authorized by its own route guard) is searched without re-running its gate; the others only after
124
+ * their `authorize` allowed. Each `search` gets `timeoutMs` and returns at most `limit` hits; a hit
125
+ * whose `href` is not a same-origin path is dropped.
126
+ */
127
+ export declare function searchConsoles(registry: AgoraConsoleRegistry | null, selfId: string, ctx: HttpContext, query: string, options?: {
128
+ timeoutMs?: number;
129
+ limit?: number;
130
+ logger?: LoggerLike;
131
+ }): Promise<ConsoleSearchResults>;
132
+ export {};
@@ -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';
@@ -311,6 +312,11 @@ export interface ResolvedRateLimitConfig {
311
312
  * force ANTES do lockout por interaction (contador persistido no slot do código).
312
313
  */
313
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;
314
320
  store?: string;
315
321
  }
316
322
  export declare function resolveRateLimit(input?: RateLimitConfigInput): ResolvedRateLimitConfig;
@@ -1251,6 +1257,16 @@ export interface AuthServerConfigInput {
1251
1257
  accountSession?: {
1252
1258
  acceptIdpSession?: boolean;
1253
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;
1254
1270
  }
1255
1271
  export interface ResolvedServerConfig {
1256
1272
  customLoginMethods?: CustomLoginMethods;
@@ -1413,6 +1429,8 @@ export interface ResolvedServerConfig {
1413
1429
  accountSession: {
1414
1430
  acceptIdpSession: boolean;
1415
1431
  };
1432
+ /** Account console extensions. Ver {@link AuthServerConfigInput.accountConsole}. */
1433
+ accountConsole?: AccountConsoleConfig;
1416
1434
  }
1417
1435
  export declare function toSeconds(value: string | number | undefined, fallback: number): number;
1418
1436
  /**
@@ -27,6 +27,8 @@ const RATE_LIMIT_DEFAULTS = {
27
27
  sudo: { points: 10, duration: '1 min' },
28
28
  // Mais apertado que o login — verificação de código adivinhável.
29
29
  otpLogin: { points: 5, duration: '1 min' },
30
+ // Actions das extensions do console de conta, por conta.
31
+ accountActions: { points: 30, duration: '1 min' },
30
32
  };
31
33
  export function resolveRateLimit(input) {
32
34
  const enabled = input?.enabled ?? true;
@@ -37,6 +39,7 @@ export function resolveRateLimit(input) {
37
39
  adminIp: RATE_LIMIT_DEFAULTS.adminIp,
38
40
  sudo: RATE_LIMIT_DEFAULTS.sudo,
39
41
  otpLogin: RATE_LIMIT_DEFAULTS.otpLogin,
42
+ accountActions: RATE_LIMIT_DEFAULTS.accountActions,
40
43
  store: input?.store,
41
44
  };
42
45
  }
@@ -502,6 +505,7 @@ export function defineConfig(config) {
502
505
  // Opt-in: ausente = authkit nunca toca `ctx.auth` (comportamento de sempre).
503
506
  adonisAuth: config.adonisAuth,
504
507
  accountSession: { acceptIdpSession: config.accountSession?.acceptIdpSession !== false },
508
+ accountConsole: config.accountConsole,
505
509
  };
506
510
  });
507
511
  }
@@ -69,7 +69,7 @@ export declare function grantDto(grant: AdminGrant): {
69
69
  };
70
70
  export declare function auditDto(event: StoredAuditEvent): {
71
71
  id: string;
72
- type: "account.deleted" | "account.expiration_warned" | "account.expired_login_blocked" | "account.exported" | "account.locked" | "account.signed_out_all" | "agent.allowed" | "agent.blocked" | "agent.delegation_approved" | "agent.delegation_denied" | "agent.grant_revoked" | "agent.grant_revoked_by_admin" | "agent.registry_removed" | "bot_protection.rejected" | "client.created" | "client.deleted" | "client.disabled" | "client.enabled" | "client.secret_regenerated" | "client.updated" | "email.change_requested" | "email.changed" | "email_change.cancelled" | "email_change.confirmed" | "email_change.requested" | "email_verification.consumed" | "email_verification.issued" | "grant.revoked_by_user" | "impersonation" | "impersonation.panel_viewed" | "impersonation.stopped" | "keys.rotated" | "login.failure" | "login.magic_link_sent" | "login.new_device" | "login.new_ip_notified" | "login.otp_failed" | "login.otp_invalidated" | "login.otp_sent" | "login.otp_verified" | "login.success" | "maintenance.disabled" | "maintenance.enabled" | "mfa.disabled" | "mfa.enabled" | "mfa.recovery_codes_regenerated" | "organization.created" | "organization.deactivated" | "organization.deleted" | "organization.invitation_accepted" | "organization.invitation_revoked" | "organization.invitation_sent" | "organization.member_added" | "organization.member_removed" | "organization.member_role_changed" | "organization.member_role_updated" | "organization.switched" | "organization.updated" | "otp.locked" | "otp.unlock_failed" | "otp.unlocked" | "passkey.registered" | "passkey.removed" | "password.changed" | "password.expired_change_forced" | "password.rehashed" | "password_reset.consumed" | "password_reset.issued" | "pat.issued" | "pat.revoked" | "pat.used" | "profile.updated" | "roles_catalog.updated" | "security_notice.sent" | "service_tenant.token_issued" | "session.revoked" | "session.revoked_all" | "session.single_enforced" | "settings.updated" | "signup" | "sudo.confirmed" | "trusted_device.revoked" | "user.created" | "user.deleted" | "user.disabled" | "user.enabled" | "user.password_reset_sent";
72
+ type: "account.deleted" | "account.expiration_warned" | "account.expired_login_blocked" | "account.exported" | "account.extension.action" | "account.extension.redirect" | "account.locked" | "account.signed_out_all" | "agent.allowed" | "agent.blocked" | "agent.delegation_approved" | "agent.delegation_denied" | "agent.grant_revoked" | "agent.grant_revoked_by_admin" | "agent.registry_removed" | "bot_protection.rejected" | "client.created" | "client.deleted" | "client.disabled" | "client.enabled" | "client.secret_regenerated" | "client.updated" | "email.change_requested" | "email.changed" | "email_change.cancelled" | "email_change.confirmed" | "email_change.requested" | "email_verification.consumed" | "email_verification.issued" | "grant.revoked_by_user" | "impersonation" | "impersonation.panel_viewed" | "impersonation.stopped" | "keys.rotated" | "login.failure" | "login.magic_link_sent" | "login.new_device" | "login.new_ip_notified" | "login.otp_failed" | "login.otp_invalidated" | "login.otp_sent" | "login.otp_verified" | "login.success" | "maintenance.disabled" | "maintenance.enabled" | "mfa.disabled" | "mfa.enabled" | "mfa.recovery_codes_regenerated" | "organization.created" | "organization.deactivated" | "organization.deleted" | "organization.invitation_accepted" | "organization.invitation_revoked" | "organization.invitation_sent" | "organization.member_added" | "organization.member_removed" | "organization.member_role_changed" | "organization.member_role_updated" | "organization.switched" | "organization.updated" | "otp.locked" | "otp.unlock_failed" | "otp.unlocked" | "passkey.registered" | "passkey.removed" | "password.changed" | "password.expired_change_forced" | "password.rehashed" | "password_reset.consumed" | "password_reset.issued" | "pat.issued" | "pat.revoked" | "pat.used" | "profile.updated" | "roles_catalog.updated" | "security_notice.sent" | "service_tenant.token_issued" | "session.revoked" | "session.revoked_all" | "session.single_enforced" | "settings.updated" | "signup" | "sudo.confirmed" | "trusted_device.revoked" | "user.created" | "user.deleted" | "user.disabled" | "user.enabled" | "user.password_reset_sent";
73
73
  accountId: string | null;
74
74
  email: string | null;
75
75
  clientId: string | null;
@@ -0,0 +1,14 @@
1
+ import type { HttpContext } from '@adonisjs/core/http';
2
+ import { type AgoraConsoleEntry, type AgoraConsoleRegistry, type AgoraConsoleSearchHit } from '../../console_switcher.js';
3
+ /** The admin console's id in the `'agora.consoles'` registry. */
4
+ export declare const ADMIN_CONSOLE_ID = "authkit";
5
+ /** The `'agora.consoles'` registry, or `null` (none bound, or no booted app). */
6
+ export declare function consoleRegistry(): Promise<AgoraConsoleRegistry | null>;
7
+ /**
8
+ * Global search of the admin console: users by email (as the account store's `listAccounts`
9
+ * search matches them), and organizations by name or slug.
10
+ * Each hit opens the matching drawer in the console.
11
+ */
12
+ export declare function searchAdminConsole(ctx: HttpContext, adminPrefix: string, query: string, limit: number): Promise<AgoraConsoleSearchHit[]>;
13
+ /** The admin console's registry entry, for the shell mounted at `adminPrefix`. */
14
+ export declare function adminConsoleEntry(adminPrefix: string, authorize: AgoraConsoleEntry['authorize']): AgoraConsoleEntry;
@@ -0,0 +1,80 @@
1
+ import { getBootedApp } from '../../../services/booted_app.js';
2
+ import { supportsOrganizations } from '../../accounts/account_store.js';
3
+ import { readAgoraConsoles, } from '../../console_switcher.js';
4
+ /** The admin console's id in the `'agora.consoles'` registry. */
5
+ export const ADMIN_CONSOLE_ID = 'authkit';
6
+ /** The `'agora.consoles'` registry, or `null` (none bound, or no booted app). */
7
+ export async function consoleRegistry() {
8
+ try {
9
+ return await readAgoraConsoles(getBootedApp());
10
+ }
11
+ catch {
12
+ return null;
13
+ }
14
+ }
15
+ const ORG_CACHE_MS = 30_000;
16
+ const orgCache = new WeakMap();
17
+ /** The store's organizations, cached for 30 s per store so a search session does one query. */
18
+ function cachedOrgs(store, load) {
19
+ const hit = orgCache.get(store);
20
+ if (hit && Date.now() - hit.at < ORG_CACHE_MS)
21
+ return hit.rows;
22
+ const rows = load();
23
+ orgCache.set(store, { at: Date.now(), rows });
24
+ rows.catch(() => orgCache.delete(store));
25
+ return rows;
26
+ }
27
+ /**
28
+ * Global search of the admin console: users by email (as the account store's `listAccounts`
29
+ * search matches them), and organizations by name or slug.
30
+ * Each hit opens the matching drawer in the console.
31
+ */
32
+ export async function searchAdminConsole(ctx, adminPrefix, query, limit) {
33
+ const service = await ctx.containerResolver.make('authkit.server');
34
+ const store = service.config.accountStore;
35
+ const hits = [];
36
+ const accounts = await store.listAccounts({ search: query, page: 1, size: limit });
37
+ for (const account of accounts.data.slice(0, limit)) {
38
+ hits.push({
39
+ title: account.email,
40
+ subtitle: account.name ? `User · ${account.name}` : 'User',
41
+ href: `${adminPrefix}?view=users&user=${encodeURIComponent(account.id)}`,
42
+ });
43
+ }
44
+ // Organizations only from a store that lists them in one query, for queries of 2+ characters,
45
+ // from a list cached briefly per process: search runs on every (debounced) keystroke.
46
+ if (hits.length < limit && query.length >= 2 && supportsOrganizations(store)) {
47
+ const listAll = store
48
+ .listAllOrgsWithMemberCount;
49
+ if (typeof listAll === 'function') {
50
+ const needle = query.toLowerCase();
51
+ const orgs = await cachedOrgs(store, () => listAll.call(store));
52
+ for (const org of orgs) {
53
+ if (hits.length >= limit)
54
+ break;
55
+ if (!org.name.toLowerCase().includes(needle) && !org.slug.toLowerCase().includes(needle)) {
56
+ continue;
57
+ }
58
+ hits.push({
59
+ title: org.name,
60
+ subtitle: `Organization · ${org.slug}`,
61
+ href: `${adminPrefix}?view=orgs&org=${encodeURIComponent(org.id)}`,
62
+ });
63
+ }
64
+ }
65
+ }
66
+ return hits;
67
+ }
68
+ /** The admin console's registry entry, for the shell mounted at `adminPrefix`. */
69
+ export function adminConsoleEntry(adminPrefix, authorize) {
70
+ return {
71
+ id: ADMIN_CONSOLE_ID,
72
+ title: 'Identity',
73
+ package: '@adonis-agora/authkit-server',
74
+ icon: 'identity',
75
+ path: adminPrefix,
76
+ order: 90,
77
+ authorize,
78
+ search: (ctx, query, limit) => searchAdminConsole(ctx, adminPrefix, query, limit),
79
+ };
80
+ }
@@ -1,8 +1,10 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import { extname } from 'node:path';
3
+ import { countOtherConsoles } from '../../console_switcher.js';
3
4
  import { accountPath } from '../account_paths.js';
4
5
  import { resolveAccountRoles, rolesManagedExternally } from '../account_roles.js';
5
6
  import { getAdminPrefix } from '../admin_prefix.js';
7
+ import { ADMIN_CONSOLE_ID, consoleRegistry } from './admin_console_entry.js';
6
8
  // ─── Shell HTML cache ─────────────────────────────────────────────────────────
7
9
  /**
8
10
  * Points to the Vite-built index.html inside build/host/ui-dist/.
@@ -141,6 +143,9 @@ export default class AdminShellController {
141
143
  }
142
144
  }
143
145
  const adminBase = getAdminPrefix();
146
+ // Console switcher: only announced when other `@adonis-agora` consoles are registered; the SPA
147
+ // then asks the endpoint which of them this viewer may open.
148
+ const others = countOtherConsoles(await consoleRegistry(), ADMIN_CONSOLE_ID);
144
149
  const authkitConfig = {
145
150
  adminBase,
146
151
  csrfToken: ctx.request.csrfToken ?? null,
@@ -152,6 +157,9 @@ export default class AdminShellController {
152
157
  // Confirmação de identidade (sudo) das ações destrutivas.
153
158
  confirm: accountPath('confirm'),
154
159
  },
160
+ ...(others > 0 ? { switcher: { endpoint: `${adminBase}/api/consoles`, others } } : {}),
161
+ // Global search (Ctrl/Cmd-K) across this console and every other one the viewer may open.
162
+ search: { endpoint: `${adminBase}/api/consoles/search` },
155
163
  features: {
156
164
  // A página de personal agents só aparece com `personalAgents` no config.
157
165
  personalAgents: !!cfg.personalAgents,
@@ -0,0 +1,11 @@
1
+ import type { HttpContext } from '@adonisjs/core/http';
2
+ /**
3
+ * The console switcher endpoints of the admin console (behind the admin console's own guard).
4
+ *
5
+ * GET {prefix}/api/consoles → the other consoles this viewer may open
6
+ * GET {prefix}/api/consoles/search?q= → hits from every console this viewer may open
7
+ */
8
+ export default class ConsoleSwitcherController {
9
+ index(ctx: HttpContext): Promise<import("../../console_switcher.js").VisibleConsoles>;
10
+ search(ctx: HttpContext): Promise<import("../../console_switcher.js").ConsoleSearchResults>;
11
+ }
@@ -0,0 +1,23 @@
1
+ import { listVisibleConsoles, searchConsoles } from '../../console_switcher.js';
2
+ import { ADMIN_CONSOLE_ID, consoleRegistry } from './admin_console_entry.js';
3
+ /**
4
+ * The console switcher endpoints of the admin console (behind the admin console's own guard).
5
+ *
6
+ * GET {prefix}/api/consoles → the other consoles this viewer may open
7
+ * GET {prefix}/api/consoles/search?q= → hits from every console this viewer may open
8
+ */
9
+ export default class ConsoleSwitcherController {
10
+ async index(ctx) {
11
+ const body = await listVisibleConsoles(await consoleRegistry(), ADMIN_CONSOLE_ID, ctx, {
12
+ logger: ctx.logger,
13
+ });
14
+ ctx.response.header('Cache-Control', 'private, no-store');
15
+ return body;
16
+ }
17
+ async search(ctx) {
18
+ const q = ctx.request.input('q', '');
19
+ const body = await searchConsoles(await consoleRegistry(), ADMIN_CONSOLE_ID, ctx, typeof q === 'string' ? q : '', { logger: ctx.logger });
20
+ ctx.response.header('Cache-Control', 'private, no-store');
21
+ return body;
22
+ }
23
+ }
@@ -1,4 +1,5 @@
1
1
  import type { HttpContext } from '@adonisjs/core/http';
2
+ import type { AccountConsoleConfig } from '../account/runtime.js';
2
3
  import type { ResolvedPersonalAgentExtension } from '../agents/extension.js';
3
4
  import type { AuthSocialConfig, ResolvedRateLimitConfig } from '../define_config.js';
4
5
  import type { PolicyRouteOption } from './config_locks.js';
@@ -49,6 +50,8 @@ export interface AuthHostRuntimeConfig {
49
50
  * o console só entra por um `accountLoginUrl` do host (`registerAuthHost` recusa sem ele).
50
51
  */
51
52
  consoleNeedsLoginUrl?: boolean;
53
+ /** `accountConsole` do config: os overrides das páginas de extension. */
54
+ accountConsole?: AccountConsoleConfig;
52
55
  /**
53
56
  * API headless (Clerk-style). Presente quando o host a declarou — o provider o
54
57
  * stash no boot para `registerAuthHost` montar as rotas sem reler o config inteiro.
@@ -174,6 +174,24 @@ export declare const DEFAULT_MESSAGES: {
174
174
  'account.delete.deleted': string;
175
175
  'account.delete.invalid_confirmation': string;
176
176
  'account.delete.not_supported': string;
177
+ 'account.extension.logout': string;
178
+ 'account.extension.nav_label': string;
179
+ 'account.extension.unknown_block': string;
180
+ 'account.extension.unsafe_link': string;
181
+ 'account.extension.unsafe_script': string;
182
+ 'account.extension.load_failed': string;
183
+ 'account.extension.next_page': string;
184
+ 'account.extension.confirm_title': string;
185
+ 'account.extension.confirm_submit': string;
186
+ 'account.extension.cancel': string;
187
+ 'account.extension.action_failed': string;
188
+ 'account.extension.action_not_found': string;
189
+ 'account.extension.impersonation_refused': string;
190
+ 'account.extension.field_required': string;
191
+ 'account.extension.field_too_long': string;
192
+ 'account.extension.field_invalid': string;
193
+ 'account.extension.done': string;
194
+ 'account.extension.provider_loading': string;
177
195
  'account.apps.page_title': string;
178
196
  'account.apps.title': string;
179
197
  'account.apps.intro': string;
@@ -928,6 +946,24 @@ export declare const PT_BR_MESSAGES: {
928
946
  'account.delete.deleted': string;
929
947
  'account.delete.invalid_confirmation': string;
930
948
  'account.delete.not_supported': string;
949
+ 'account.extension.logout': string;
950
+ 'account.extension.nav_label': string;
951
+ 'account.extension.unknown_block': string;
952
+ 'account.extension.unsafe_link': string;
953
+ 'account.extension.unsafe_script': string;
954
+ 'account.extension.load_failed': string;
955
+ 'account.extension.next_page': string;
956
+ 'account.extension.confirm_title': string;
957
+ 'account.extension.confirm_submit': string;
958
+ 'account.extension.cancel': string;
959
+ 'account.extension.action_failed': string;
960
+ 'account.extension.action_not_found': string;
961
+ 'account.extension.impersonation_refused': string;
962
+ 'account.extension.field_required': string;
963
+ 'account.extension.field_too_long': string;
964
+ 'account.extension.field_invalid': string;
965
+ 'account.extension.done': string;
966
+ 'account.extension.provider_loading': string;
931
967
  'account.apps.page_title': string;
932
968
  'account.apps.title': string;
933
969
  'account.apps.intro': string;