@adonis-agora/authkit-server 1.0.0-rc.1 → 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 +94 -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 +28 -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_switcher.d.ts +132 -0
- package/build/src/console_switcher.js +206 -0
- package/build/src/define_config.d.ts +34 -12
- package/build/src/define_config.js +15 -7
- 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/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/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 +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 +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
|
@@ -1,13 +1,9 @@
|
|
|
1
|
+
import { getBootedApp } from '../../services/booted_app.js';
|
|
1
2
|
import { supportsAccountStatus } from '../accounts/account_store.js';
|
|
2
3
|
import { findMcpResource } from '../mcp/mcp_oauth.js';
|
|
3
4
|
import { PersonalAgentVerifier } from './agent_identity.js';
|
|
4
5
|
import { PersonalAgentDelegation } from './delegation_service.js';
|
|
5
6
|
import { DelegationStore } from './delegation_store.js';
|
|
6
|
-
import { PoppyClientRegistry } from './poppy/client_registry.js';
|
|
7
|
-
import { hmacNonceSource } from './poppy/dpop.js';
|
|
8
|
-
import { databaseReplayStore, memoryLimiter, memoryReplayStore, redisReplayStore, } from './poppy/replay.js';
|
|
9
|
-
import { PoppyService, poppyUrls } from './poppy/service.js';
|
|
10
|
-
import { PoppyStore } from './poppy/store.js';
|
|
11
7
|
import { AgentRegistryStore } from './registry_store.js';
|
|
12
8
|
import { keystoreSigner } from './signer.js';
|
|
13
9
|
/**
|
|
@@ -27,9 +23,10 @@ export function personalAgentUrls(oidcIssuer, prefix) {
|
|
|
27
23
|
};
|
|
28
24
|
}
|
|
29
25
|
const runtimes = new WeakMap();
|
|
30
|
-
/**
|
|
31
|
-
function
|
|
26
|
+
/** Access tokens do oidc-provider para resources MCP (ver {@link PersonalAgentAccessTokens}). */
|
|
27
|
+
function accessTokens(service) {
|
|
32
28
|
return {
|
|
29
|
+
mcpEnabled: !!service.config.mcp?.enabled,
|
|
33
30
|
isMcpResource(url) {
|
|
34
31
|
try {
|
|
35
32
|
return findMcpResource(url, service.config.issuer, service.config.mcp) !== null;
|
|
@@ -42,11 +39,6 @@ function mcpBridge(service, poppyApis) {
|
|
|
42
39
|
const provider = service.provider;
|
|
43
40
|
if (!provider?.AccessToken || !provider?.Grant)
|
|
44
41
|
return null;
|
|
45
|
-
// Só faz sentido quando a integração MCP do authkit está ligada, ou o MCP é de `apis`.
|
|
46
|
-
if (!service.config.mcp?.enabled &&
|
|
47
|
-
!poppyApis.some((a) => a.type === 'mcp' && a.url.replace(/\/+$/, '') === input.resource.replace(/\/+$/, ''))) {
|
|
48
|
-
return null;
|
|
49
|
-
}
|
|
50
42
|
const remaining = Math.max(60, Math.floor((input.grantExpiresAt.getTime() - Date.now()) / 1000));
|
|
51
43
|
let grant = await provider.Grant.find(input.grantId);
|
|
52
44
|
if (!grant) {
|
|
@@ -63,13 +55,13 @@ function mcpBridge(service, poppyApis) {
|
|
|
63
55
|
clientId: input.clientId,
|
|
64
56
|
grantId: input.grantId,
|
|
65
57
|
scope: input.scope,
|
|
66
|
-
gty:
|
|
58
|
+
gty: input.grantType,
|
|
67
59
|
expiresIn: input.expiresIn,
|
|
68
60
|
});
|
|
69
61
|
at.aud = input.resource;
|
|
70
62
|
return at.save();
|
|
71
63
|
},
|
|
72
|
-
async
|
|
64
|
+
async revokeGrant(grantId) {
|
|
73
65
|
const provider = service.provider;
|
|
74
66
|
if (!provider?.Grant)
|
|
75
67
|
return;
|
|
@@ -80,18 +72,32 @@ function mcpBridge(service, poppyApis) {
|
|
|
80
72
|
},
|
|
81
73
|
};
|
|
82
74
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
75
|
+
/** O ledger de grants visto por uma extension: só os ids com o prefixo dela. */
|
|
76
|
+
function scopedLedger(store, prefix) {
|
|
77
|
+
const mine = (id) => typeof id === 'string' && id.startsWith(prefix);
|
|
78
|
+
return {
|
|
79
|
+
async insertGrant(grant) {
|
|
80
|
+
if (!mine(grant.id)) {
|
|
81
|
+
throw new Error(`authkit: grant "${grant.id}" fora do prefixo "${prefix}" da extension.`);
|
|
82
|
+
}
|
|
83
|
+
await store.insertGrant(grant);
|
|
84
|
+
},
|
|
85
|
+
async findGrant(id) {
|
|
86
|
+
return mine(id) ? store.findGrant(id) : null;
|
|
87
|
+
},
|
|
88
|
+
async listGrants(accountId) {
|
|
89
|
+
return (await store.listGrants(accountId)).filter((g) => mine(g.id));
|
|
90
|
+
},
|
|
91
|
+
async unrevokedGrantIdsOfClient(clientId) {
|
|
92
|
+
return (await store.unrevokedGrantIdsOfClient(clientId)).filter(mine);
|
|
93
|
+
},
|
|
94
|
+
async revokeGrantById(id, at) {
|
|
95
|
+
if (mine(id))
|
|
96
|
+
await store.revokeGrantById(id, at);
|
|
97
|
+
},
|
|
98
|
+
insertRefreshToken: (token) => store.insertRefreshToken(token),
|
|
99
|
+
findRefreshToken: (tokenHash) => store.findRefreshToken(tokenHash),
|
|
100
|
+
};
|
|
95
101
|
}
|
|
96
102
|
/**
|
|
97
103
|
* Config com o registro do console por cima: um agente BLOQUEADO no console é
|
|
@@ -112,21 +118,7 @@ function withRegistry(config, registry) {
|
|
|
112
118
|
}
|
|
113
119
|
return null;
|
|
114
120
|
};
|
|
115
|
-
|
|
116
|
-
return {
|
|
117
|
-
...config,
|
|
118
|
-
resolveAgent,
|
|
119
|
-
poppy: poppy && {
|
|
120
|
-
...poppy,
|
|
121
|
-
clients: {
|
|
122
|
-
...poppy.clients,
|
|
123
|
-
isBlocked: async (id) => (await poppy.clients.isBlocked(id)) ||
|
|
124
|
-
(await registry.find('poppy', id))?.status === 'blocked',
|
|
125
|
-
isRegistered: async (id) => (await poppy.clients.isRegistered(id)) ||
|
|
126
|
-
(await registry.find('poppy', id))?.status === 'allowed',
|
|
127
|
-
},
|
|
128
|
-
},
|
|
129
|
-
};
|
|
121
|
+
return { ...config, resolveAgent };
|
|
130
122
|
}
|
|
131
123
|
/**
|
|
132
124
|
* Monta (uma vez por `OidcService`) o runtime de personal agents. `null` quando
|
|
@@ -149,7 +141,7 @@ export function buildPersonalAgentsRuntime(service, makeDb, options = {}) {
|
|
|
149
141
|
};
|
|
150
142
|
let conn = null;
|
|
151
143
|
const connection = service.config.schema.connection;
|
|
152
|
-
if (config.delegation || config.
|
|
144
|
+
if (config.delegation || (config.extensions ?? []).length > 0) {
|
|
153
145
|
const db = await makeDb();
|
|
154
146
|
conn = () => (connection ? db.connection(connection) : db.connection());
|
|
155
147
|
}
|
|
@@ -177,39 +169,53 @@ export function buildPersonalAgentsRuntime(service, makeDb, options = {}) {
|
|
|
177
169
|
isAccountActive,
|
|
178
170
|
});
|
|
179
171
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
:
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
172
|
+
const extensions = new Map();
|
|
173
|
+
const now = () => options.now?.() ?? new Date();
|
|
174
|
+
if (store && conn && registry) {
|
|
175
|
+
const db = conn;
|
|
176
|
+
const tokens = accessTokens(service);
|
|
177
|
+
for (const extension of config.extensions ?? []) {
|
|
178
|
+
const runtime = await extension.setup({
|
|
179
|
+
protocol: extension.id,
|
|
180
|
+
issuer: service.config.issuer,
|
|
181
|
+
db: () => db(),
|
|
182
|
+
grants: scopedLedger(store, extension.grantIdPrefix),
|
|
183
|
+
registryStatus: async (identifier) => (await registry.find(extension.id, identifier))?.status ?? null,
|
|
184
|
+
deriveSecret: (label) => service.deriveSecret(label),
|
|
185
|
+
isAccountActive,
|
|
186
|
+
make: async (binding) => {
|
|
187
|
+
if (!options.make) {
|
|
188
|
+
throw new Error(`authkit: o binding "${binding}" exige o container do app.`);
|
|
189
|
+
}
|
|
190
|
+
return options.make(binding);
|
|
191
|
+
},
|
|
192
|
+
accessTokens: tokens,
|
|
193
|
+
now,
|
|
194
|
+
});
|
|
195
|
+
extensions.set(extension.id, { extension, runtime });
|
|
196
|
+
}
|
|
202
197
|
}
|
|
198
|
+
/** A extension dona de um grant, pelo prefixo do id. */
|
|
199
|
+
const ownerOf = (grantId) => {
|
|
200
|
+
for (const entry of extensions.values()) {
|
|
201
|
+
if (grantId.startsWith(entry.extension.grantIdPrefix))
|
|
202
|
+
return entry;
|
|
203
|
+
}
|
|
204
|
+
return null;
|
|
205
|
+
};
|
|
206
|
+
const isExtensionGrant = (grantId) => (config.extensions ?? []).some((e) => grantId.startsWith(e.grantIdPrefix));
|
|
203
207
|
const revokeAgentGrant = async (accountId, grantId) => {
|
|
204
|
-
if (grantId
|
|
205
|
-
|
|
208
|
+
if (isExtensionGrant(grantId)) {
|
|
209
|
+
const owner = ownerOf(grantId);
|
|
210
|
+
return owner ? owner.runtime.revokeAccountGrant(accountId, grantId) : false;
|
|
206
211
|
}
|
|
207
212
|
return delegation ? delegation.revokeGrant(accountId, grantId) : false;
|
|
208
213
|
};
|
|
209
214
|
const revokeAllAgentGrants = async (accountId) => {
|
|
210
215
|
let count = 0;
|
|
211
|
-
|
|
212
|
-
count += await
|
|
216
|
+
for (const { runtime } of extensions.values()) {
|
|
217
|
+
count += await runtime.revokeAllAccountGrants(accountId);
|
|
218
|
+
}
|
|
213
219
|
if (delegation)
|
|
214
220
|
count += await delegation.revokeAllGrants(accountId);
|
|
215
221
|
return count;
|
|
@@ -222,10 +228,11 @@ export function buildPersonalAgentsRuntime(service, makeDb, options = {}) {
|
|
|
222
228
|
// Expirado já não vale: revogar não muda nada (e não vira evento de auditoria).
|
|
223
229
|
if (!grant || grant.revokedAt || grant.expiresAt <= now)
|
|
224
230
|
return null;
|
|
225
|
-
if (grantId
|
|
226
|
-
|
|
231
|
+
if (isExtensionGrant(grantId)) {
|
|
232
|
+
const owner = ownerOf(grantId);
|
|
233
|
+
if (!owner)
|
|
227
234
|
return null;
|
|
228
|
-
await
|
|
235
|
+
await owner.runtime.revokeGrant(grantId);
|
|
229
236
|
}
|
|
230
237
|
else {
|
|
231
238
|
await store.revokeGrantById(grantId, now);
|
|
@@ -235,9 +242,11 @@ export function buildPersonalAgentsRuntime(service, makeDb, options = {}) {
|
|
|
235
242
|
const disconnectAgent = async (protocol, identifier) => {
|
|
236
243
|
if (!store)
|
|
237
244
|
return 0;
|
|
238
|
-
if (protocol
|
|
239
|
-
|
|
240
|
-
|
|
245
|
+
if (protocol !== 'pact') {
|
|
246
|
+
const entry = extensions.get(protocol);
|
|
247
|
+
return entry ? entry.runtime.disconnectAgent(identifier) : 0;
|
|
248
|
+
}
|
|
249
|
+
const ids = (await store.unrevokedGrantIdsOfClient(identifier)).filter((id) => !isExtensionGrant(id));
|
|
241
250
|
const at = options.now?.() ?? new Date();
|
|
242
251
|
for (const id of ids)
|
|
243
252
|
await store.revokeGrantById(id, at);
|
|
@@ -248,7 +257,7 @@ export function buildPersonalAgentsRuntime(service, makeDb, options = {}) {
|
|
|
248
257
|
urls,
|
|
249
258
|
verifier: new PersonalAgentVerifier(effective),
|
|
250
259
|
delegation,
|
|
251
|
-
|
|
260
|
+
extensions,
|
|
252
261
|
revokeAgentGrant,
|
|
253
262
|
revokeAllAgentGrants,
|
|
254
263
|
registry,
|
|
@@ -270,3 +279,29 @@ export async function personalAgentsFor(ctx) {
|
|
|
270
279
|
make: (binding) => ctx.containerResolver.make(binding),
|
|
271
280
|
});
|
|
272
281
|
}
|
|
282
|
+
/**
|
|
283
|
+
* The runtime an extension's `setup` built (`personalAgents.extensions`), by protocol id. `null`
|
|
284
|
+
* when the extension is not configured (or there is no database). Takes the request, an explicit
|
|
285
|
+
* source (tests, scripts) or nothing (the booted app).
|
|
286
|
+
*
|
|
287
|
+
* Stable for 1.x — see `extension.ts`.
|
|
288
|
+
*/
|
|
289
|
+
export async function personalAgentExtensionRuntime(id, from) {
|
|
290
|
+
let runtime;
|
|
291
|
+
if (from && 'containerResolver' in from) {
|
|
292
|
+
runtime = await personalAgentsFor(from);
|
|
293
|
+
}
|
|
294
|
+
else if (from) {
|
|
295
|
+
const source = from;
|
|
296
|
+
runtime = await buildPersonalAgentsRuntime(source.service, () => source.db(), {
|
|
297
|
+
make: source.make ? (binding) => source.make(binding) : undefined,
|
|
298
|
+
now: source.now ? () => source.now() : undefined,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
const app = getBootedApp();
|
|
303
|
+
const service = (await app.container.make('authkit.server'));
|
|
304
|
+
runtime = await buildPersonalAgentsRuntime(service, () => app.container.make('lucid.db'), { make: (binding) => app.container.make(binding) });
|
|
305
|
+
}
|
|
306
|
+
return runtime?.extensions.get(id)?.runtime ?? null;
|
|
307
|
+
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* varre os `audit.record({ type: ... })` do `src/` e falha quando um evento
|
|
9
9
|
* emitido não está aqui.
|
|
10
10
|
*/
|
|
11
|
-
export declare const AUDIT_EVENT_TYPES: readonly ['login.success', 'login.failure', 'signup', 'password_reset.issued', 'password_reset.consumed', 'pat.issued', 'pat.revoked', 'pat.used', 'impersonation', 'impersonation.panel_viewed', 'impersonation.stopped', 'mfa.enabled', 'mfa.disabled', 'mfa.recovery_codes_regenerated', 'account.locked', 'passkey.registered', 'passkey.removed', 'email_verification.issued', 'email_verification.consumed', 'client.created', 'client.updated', 'client.deleted', 'session.revoked_all', 'password.changed', 'password.rehashed', 'email.change_requested', 'email.changed', 'login.new_ip_notified', 'login.new_device', 'login.otp_sent', 'login.otp_verified', 'login.otp_failed', 'login.otp_invalidated', 'bot_protection.rejected', 'grant.revoked_by_user', 'user.created', 'user.password_reset_sent', 'user.disabled', 'user.enabled', 'user.deleted', 'profile.updated', 'account.deleted', 'account.exported', 'keys.rotated', 'organization.created', 'organization.updated', 'organization.deleted', 'organization.member_added', 'organization.member_removed', 'organization.member_role_changed', 'organization.member_role_updated', 'organization.switched', 'organization.deactivated', 'organization.invitation_sent', 'organization.invitation_accepted', 'organization.invitation_revoked', 'email_change.requested', 'email_change.confirmed', 'email_change.cancelled', 'security_notice.sent', 'settings.updated', 'maintenance.enabled', 'maintenance.disabled', 'trusted_device.revoked', 'password.expired_change_forced', 'otp.locked', 'otp.unlocked', 'otp.unlock_failed', 'sudo.confirmed', 'session.single_enforced', 'account.expired_login_blocked', 'account.expiration_warned', 'login.magic_link_sent', 'session.revoked', 'account.signed_out_all', 'client.secret_regenerated', 'roles_catalog.updated', 'agent.delegation_approved', 'agent.delegation_denied', 'agent.grant_revoked', 'agent.grant_revoked_by_admin', 'agent.allowed', 'agent.blocked', 'agent.registry_removed'];
|
|
11
|
+
export declare const AUDIT_EVENT_TYPES: readonly ['login.success', 'login.failure', 'signup', 'password_reset.issued', 'password_reset.consumed', 'pat.issued', 'pat.revoked', 'pat.used', 'impersonation', 'impersonation.panel_viewed', 'impersonation.stopped', 'mfa.enabled', 'mfa.disabled', 'mfa.recovery_codes_regenerated', 'account.locked', 'passkey.registered', 'passkey.removed', 'email_verification.issued', 'email_verification.consumed', 'client.created', 'client.updated', 'client.deleted', 'session.revoked_all', 'password.changed', 'password.rehashed', 'email.change_requested', 'email.changed', 'login.new_ip_notified', 'login.new_device', 'login.otp_sent', 'login.otp_verified', 'login.otp_failed', 'login.otp_invalidated', 'bot_protection.rejected', 'grant.revoked_by_user', 'user.created', 'user.password_reset_sent', 'user.disabled', 'user.enabled', 'user.deleted', 'profile.updated', 'account.deleted', 'account.exported', 'keys.rotated', 'organization.created', 'organization.updated', 'organization.deleted', 'organization.member_added', 'organization.member_removed', 'organization.member_role_changed', 'organization.member_role_updated', 'organization.switched', 'organization.deactivated', 'organization.invitation_sent', 'organization.invitation_accepted', 'organization.invitation_revoked', 'email_change.requested', 'email_change.confirmed', 'email_change.cancelled', 'security_notice.sent', 'settings.updated', 'maintenance.enabled', 'maintenance.disabled', 'trusted_device.revoked', 'password.expired_change_forced', 'otp.locked', 'otp.unlocked', 'otp.unlock_failed', 'sudo.confirmed', 'session.single_enforced', 'account.expired_login_blocked', 'account.expiration_warned', 'login.magic_link_sent', 'session.revoked', 'account.signed_out_all', 'client.secret_regenerated', 'client.disabled', 'client.enabled', 'service_tenant.token_issued', 'roles_catalog.updated', 'agent.delegation_approved', 'agent.delegation_denied', 'agent.grant_revoked', 'agent.grant_revoked_by_admin', 'agent.allowed', 'agent.blocked', 'agent.registry_removed', 'account.extension.redirect', 'account.extension.action'];
|
|
12
12
|
/** Tipos de eventos de auditoria relevantes para segurança emitidos pelo IdP. */
|
|
13
13
|
export type AuditEventType = (typeof AUDIT_EVENT_TYPES)[number];
|
|
14
14
|
/**
|
|
@@ -107,6 +107,11 @@ export const AUDIT_EVENT_TYPES = [
|
|
|
107
107
|
'session.revoked',
|
|
108
108
|
'account.signed_out_all',
|
|
109
109
|
'client.secret_regenerated',
|
|
110
|
+
// A client was disabled (no new tokens) or enabled again.
|
|
111
|
+
'client.disabled',
|
|
112
|
+
'client.enabled',
|
|
113
|
+
// A service tenant got a token (`partition`, `resource`, `jti` in the metadata).
|
|
114
|
+
'service_tenant.token_issued',
|
|
110
115
|
'roles_catalog.updated',
|
|
111
116
|
// Personal agents (PACT §5): o usuário aprovou/negou a delegação pedida por um
|
|
112
117
|
// agente, ou revogou um grant no console. `clientId` = issuer do agente.
|
|
@@ -114,9 +119,13 @@ export const AUDIT_EVENT_TYPES = [
|
|
|
114
119
|
'agent.delegation_denied',
|
|
115
120
|
'agent.grant_revoked',
|
|
116
121
|
// Console admin: um admin revogou a conexão de um usuário, ou liberou/bloqueou um
|
|
117
|
-
// agente (PACT pelo `iss
|
|
122
|
+
// agente (PACT pelo `iss`; uma extension pelo identificador dela).
|
|
118
123
|
'agent.grant_revoked_by_admin',
|
|
119
124
|
'agent.allowed',
|
|
120
125
|
'agent.blocked',
|
|
121
126
|
'agent.registry_removed',
|
|
127
|
+
// Console de conta: uma action de extension mandou o usuário para fora do app
|
|
128
|
+
// (`redirectTo.external`), ou uma action sensível (sudo/danger) rodou.
|
|
129
|
+
'account.extension.redirect',
|
|
130
|
+
'account.extension.action',
|
|
122
131
|
];
|
|
@@ -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
|
+
}
|