@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
package/build/index.js
CHANGED
|
@@ -10,15 +10,11 @@ export { lucidStores } from './src/accounts/lucid_stores.js';
|
|
|
10
10
|
export { ProviderIdentities, ProviderIdentitiesUnsupportedError, } from './src/accounts/provider_identities.js';
|
|
11
11
|
export { PersonalAgentVerifier } from './src/agents/agent_identity.js';
|
|
12
12
|
export { personalAgentAuth, personalAgentOf, personalAgentReceipt, personalAgentSecurity, personalAgentStepUp, } from './src/agents/middleware.js';
|
|
13
|
-
export { accessTokenHash, hmacNonceSource, verifyDpopProof, } from './src/agents/poppy/dpop.js';
|
|
14
|
-
export { maskEmail, poppyEmailCodeSignIn, } from './src/agents/poppy/email_code_sign_in.js';
|
|
15
|
-
export { PoppyError } from './src/agents/poppy/errors.js';
|
|
16
|
-
export { databaseReplayStore, memoryReplayStore, redisReplayStore, } from './src/agents/poppy/replay.js';
|
|
17
|
-
export { installPoppySlots, POPPY_AUTHENTICATE_SLOT, poppyAuth, poppyOf, verifyPoppyRequest, writePoppyAuthError, } from './src/agents/poppy/resource.js';
|
|
18
|
-
export { isBlockedAddress, SafeFetchError, safeFetchJson, } from './src/agents/poppy/safe_fetch.js';
|
|
19
|
-
export { poppyWebSession, poppyWebSessionOf, } from './src/agents/poppy/web_session.js';
|
|
20
13
|
export { pact, pactProtocol } from './src/agents/protocol.js';
|
|
14
|
+
export { personalAgentExtensionRuntime, } from './src/agents/runtime.js';
|
|
21
15
|
export { lucidAuditSink } from './src/audit/lucid_audit_sink.js';
|
|
16
|
+
export { authorizeByToken } from './src/console_authorize.js';
|
|
17
|
+
export { AGORA_CONSOLES_BINDING, isAgoraProbe, registerAgoraConsole, } from './src/console_switcher.js';
|
|
22
18
|
export { adapters, defineConfig, resolveAdmin, resolveAdminApi, resolveAuthMethodsConfig, resolveDynamicRegistration, resolveLogin, resolveNotifications, resolveOrganizations, resolvePasswordless, resolveRateLimit, resolveRegistration, resolveWebauthn, toSeconds, } from './src/define_config.js';
|
|
23
19
|
export { buildWebhookBody, composeAuditSink, resolveEvents, signWebhookBody, } from './src/events/dispatcher.js';
|
|
24
20
|
export { inProcessAccessTokenVerifier, remoteAccessTokenVerifier, } from './src/host/access_token_verifier.js';
|
|
@@ -38,6 +34,7 @@ export { AccountExportService } from './src/host/account_export_service.js';
|
|
|
38
34
|
*/
|
|
39
35
|
export { accountPath, accountPrefix, joinAccountPath, } from './src/host/account_paths.js';
|
|
40
36
|
export { ACCOUNT_SESSION_KEY } from './src/host/account_session_key.js';
|
|
37
|
+
export { signInAccountSession, signOutAccountSession, } from './src/host/account_session_sign_in.js';
|
|
41
38
|
// Organizations (multi-tenancy)
|
|
42
39
|
export { ACTIVE_ORG_COOKIE, ACTIVE_ORG_COOKIE_TTL, decodeActiveOrgCookie, encodeActiveOrgCookie, readActiveOrgFromKoaCtx, } from './src/host/active_org_cookie.js';
|
|
43
40
|
export { AdminOrgsService } from './src/host/admin_api/admin_orgs_service.js';
|
|
@@ -64,6 +61,7 @@ export { consoleLoginUrl, getAccountId, hasAccountSession, realAccountId, } from
|
|
|
64
61
|
export { authkitCsrfExceptions } from './src/host/csrf.js';
|
|
65
62
|
export { authenticateCustomLogin, beginCustomLogin, completeCustomLogin, } from './src/host/custom_login.js';
|
|
66
63
|
export { beginCustomMfa, customMfaViewProps, verifyCustomMfa } from './src/host/custom_mfa.js';
|
|
64
|
+
export { sendAuthkitCodeEmail } from './src/host/default_mailer.js';
|
|
67
65
|
export { normalizeEmailIdentifier } from './src/host/email_identifier.js';
|
|
68
66
|
export { GEO_RESOLVE_TIMEOUT_MS, resolveGeoSafe } from './src/host/geo.js';
|
|
69
67
|
export { BUILTIN_MESSAGES, DEFAULT_LOCALE, DEFAULT_MESSAGES, PT_BR_MESSAGES, resolveMessages, translate, } from './src/host/i18n.js';
|
|
@@ -88,7 +86,6 @@ export { OidcRpGuard, oidcRpGuard } from './src/host/oidc_rp_guard.js';
|
|
|
88
86
|
// Login por OTP (código digitável): config + helpers puros.
|
|
89
87
|
export { evaluateLoginOtp, generateOtpCode, OTP_LOGIN_DEFAULTS, resolveOtpLoginConfig, } from './src/host/otp_login.js';
|
|
90
88
|
export { endRpSession } from './src/host/persistent_rp_session.js';
|
|
91
|
-
export { POPPY_WEB_SESSION_KEY } from './src/host/poppy_session_key.js';
|
|
92
89
|
export { createAuthThrottles } from './src/host/rate_limit.js';
|
|
93
90
|
export { registerAuthHost } from './src/host/register_auth_host.js';
|
|
94
91
|
export { edgeRenderer } from './src/host/renderers/edge_renderer.js';
|
|
@@ -139,5 +136,7 @@ export { generatePatToken, hashPatToken } from './src/pat/pat_tokens.js';
|
|
|
139
136
|
export { OidcService } from './src/provider/oidc_service.js';
|
|
140
137
|
export { checkClientRegistration, classifyRedirect, RegistrationPolicyError, } from './src/provider/registration_policy.js';
|
|
141
138
|
export { registerOidcRoutes } from './src/register_routes.js';
|
|
142
|
-
export { ensureAuthkitSchema } from './src/schema/ensure.js';
|
|
139
|
+
export { ensureAuthkitSchema, ensureAuthkitTables } from './src/schema/ensure.js';
|
|
140
|
+
export { AuthkitTenantVerifier, authkitTenantVerifier, } from './src/service_tenants/tenant_verifier.js';
|
|
141
|
+
export { authkitTenantLabels, } from './src/tenancy/tenant_labels.js';
|
|
143
142
|
export { stubsRoot } from './stubs/main.js';
|
|
@@ -16,7 +16,10 @@ import { fileURLToPath } from 'node:url';
|
|
|
16
16
|
import { configProvider } from '@adonisjs/core';
|
|
17
17
|
import { RuntimeException } from '@adonisjs/core/exceptions';
|
|
18
18
|
import { setBootedApp } from '../services/booted_app.js';
|
|
19
|
+
import { accountExtensionRegistry } from '../src/account/mount.js';
|
|
19
20
|
import { defaultEncryptForStore } from '../src/define_config.js';
|
|
21
|
+
import { authkitDoctorChecks } from '../src/doctor/agora_checks.js';
|
|
22
|
+
import { registerDoctorChecks } from '../src/doctor/agora_doctor.js';
|
|
20
23
|
import { resolveAppKey } from '../src/host/app_key.js';
|
|
21
24
|
import { resolveEffectiveKeyRotation } from '../src/host/key_rotation.js';
|
|
22
25
|
import { RuntimeSettings } from '../src/host/runtime_settings.js';
|
|
@@ -33,6 +36,9 @@ export default class AuthkitServerProvider {
|
|
|
33
36
|
this.app = app;
|
|
34
37
|
}
|
|
35
38
|
async boot() {
|
|
39
|
+
// `node ace agora:doctor` reads these after boot, from the registry every @adonis-agora lib
|
|
40
|
+
// shares. Registered here rather than in `register()`, which AdonisJS does not await.
|
|
41
|
+
await registerDoctorChecks(this.app.container, authkitDoctorChecks());
|
|
36
42
|
// EAGER + LOUD (fail-fast): se o host configurou `config/authkit.ts`, a
|
|
37
43
|
// `appKey` é indispensável — o oidc-provider a usa para assinar TODOS os
|
|
38
44
|
// cookies (keygrip). Antes, essa validação só rodava dentro do factory
|
|
@@ -130,11 +136,13 @@ export default class AuthkitServerProvider {
|
|
|
130
136
|
// Defaults estruturais de `config.routes` (o argumento ainda vence).
|
|
131
137
|
routes: typeof config.routes === 'object' ? config.routes : undefined,
|
|
132
138
|
lockedRouteOptions: config.lockedRouteOptions,
|
|
139
|
+
consoleNeedsLoginUrl: config.accountSession?.acceptIdpSession === false && config.authMethods?.password === false,
|
|
140
|
+
accountConsole: config.accountConsole,
|
|
133
141
|
personalAgents: config.personalAgents
|
|
134
142
|
? {
|
|
135
143
|
prefix: config.personalAgents.prefix,
|
|
136
|
-
...(config.personalAgents.
|
|
137
|
-
? {
|
|
144
|
+
...(config.personalAgents.extensions?.length
|
|
145
|
+
? { extensions: config.personalAgents.extensions }
|
|
138
146
|
: {}),
|
|
139
147
|
}
|
|
140
148
|
: undefined,
|
|
@@ -146,12 +154,28 @@ export default class AuthkitServerProvider {
|
|
|
146
154
|
}
|
|
147
155
|
: undefined,
|
|
148
156
|
});
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
if (config.
|
|
152
|
-
const {
|
|
153
|
-
const
|
|
154
|
-
|
|
157
|
+
// Admin console without `admin.authorize`: open outside production, 403 in production.
|
|
158
|
+
// Said once, at boot.
|
|
159
|
+
if (config.admin.enabled && !config.admin.authorize) {
|
|
160
|
+
const { warnAdminConsoleUnauthorized } = await import('../src/host/admin_console_warning.js');
|
|
161
|
+
const logger = await this.app.container.make('logger').catch(() => null);
|
|
162
|
+
warnAdminConsoleUnauthorized(logger);
|
|
163
|
+
}
|
|
164
|
+
// Agora dev tools over MCP: the admin console's tools join the hub `@adonis-agora/agent`
|
|
165
|
+
// binds at `'agora.devTools'` (no import of the agent; nothing happens without it).
|
|
166
|
+
if (config.admin.enabled) {
|
|
167
|
+
const { registerAuthkitDevTools } = await import('../src/dev_tools/register.js');
|
|
168
|
+
try {
|
|
169
|
+
await registerAuthkitDevTools(this.app, config);
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
const logger = await this.app.container.make('logger').catch(() => null);
|
|
173
|
+
logger?.warn({ err: error }, 'authkit: could not register the Agora dev tools');
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// `personalAgents.extensions`: o boot de cada uma (slots globais, etc.).
|
|
177
|
+
for (const extension of config.personalAgents?.extensions ?? []) {
|
|
178
|
+
await extension.boot?.();
|
|
155
179
|
}
|
|
156
180
|
// Auto-montagem das rotas (`config.routes`). Chama a MESMA função exportada que o
|
|
157
181
|
// `start/routes.ts` chamaria; não há segunda implementação. "As rotas não subiram" não pode
|
|
@@ -169,6 +193,9 @@ export default class AuthkitServerProvider {
|
|
|
169
193
|
// que o `bin/server` bootou — dual-package hazard do core (ver
|
|
170
194
|
// `services/booted_app.ts`).
|
|
171
195
|
setBootedApp(this.app);
|
|
196
|
+
// Account console extensions: other packages register pages here from their `boot()`. No
|
|
197
|
+
// config to resolve, so `hasBinding` + `make` never boots the auth server.
|
|
198
|
+
this.app.container.singleton('authkit.accountExtensions', async () => accountExtensionRegistry());
|
|
172
199
|
this.app.container.singleton('authkit.server', async () => {
|
|
173
200
|
const configProviderValue = this.app.config.get('authkit');
|
|
174
201
|
const config = (await configProvider.resolve(this.app, configProviderValue));
|
|
@@ -260,6 +287,7 @@ export default class AuthkitServerProvider {
|
|
|
260
287
|
const report = await ensureAuthkitSchema(db, {
|
|
261
288
|
connection: config.schema.connection,
|
|
262
289
|
accountTable: config.accountStore?.accountTable,
|
|
290
|
+
tables: config.personalAgents?.extensions?.flatMap((e) => e.tables ?? []),
|
|
263
291
|
});
|
|
264
292
|
const logger = await this.app.container.make('logger').catch(() => null);
|
|
265
293
|
if (report.created.length > 0) {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Account console extensions — the surface another package (or the app) uses to add pages to the
|
|
3
|
+
* signed-in user's own console (`/account/*`).
|
|
4
|
+
*
|
|
5
|
+
* **Stable for all of 1.x.** Types and constants only (the same ones `@adonis-agora/authkit-client`
|
|
6
|
+
* binds for an app whose IdP runs elsewhere), so a contributor can `import type` it from an
|
|
7
|
+
* internal module without a runtime dependency on authkit.
|
|
8
|
+
*/
|
|
9
|
+
export type { AccountAction, AccountActionField, AccountActionResult, AccountActiveOrg, AccountBlockKind, AccountExtension, AccountExtensionContext, AccountExtensionOverride, AccountExtensionRegistry, AccountPage, AccountPageNav, AccountPageView, AccountSubject, AccountTenantLevel, AccountTenantRef, ActionRef, Block, ComponentScript, ListItem, Text, Value, } from '@adonis-agora/authkit-core';
|
|
10
|
+
export { ACCOUNT_BLOCK_KINDS, ACCOUNT_EXTENSION_CONTRACT, ACCOUNT_EXTENSIONS_BINDING, } from '@adonis-agora/authkit-core';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Account console extensions — the surface another package (or the app) uses to add pages to the
|
|
3
|
+
* signed-in user's own console (`/account/*`).
|
|
4
|
+
*
|
|
5
|
+
* **Stable for all of 1.x.** Types and constants only (the same ones `@adonis-agora/authkit-client`
|
|
6
|
+
* binds for an app whose IdP runs elsewhere), so a contributor can `import type` it from an
|
|
7
|
+
* internal module without a runtime dependency on authkit.
|
|
8
|
+
*/
|
|
9
|
+
export { ACCOUNT_BLOCK_KINDS, ACCOUNT_EXTENSION_CONTRACT, ACCOUNT_EXTENSIONS_BINDING, } from '@adonis-agora/authkit-core';
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { HttpContext } from '@adonisjs/core/http';
|
|
2
|
+
import type { AccountAction, Text } from './extension.js';
|
|
3
|
+
import type { RegisteredAccountPage } from './registry.js';
|
|
4
|
+
/** The confirmation step of an action, as the view renders it. */
|
|
5
|
+
export interface ResolvedActionStep {
|
|
6
|
+
action: string;
|
|
7
|
+
title: string;
|
|
8
|
+
text: string | null;
|
|
9
|
+
tone: 'default' | 'danger';
|
|
10
|
+
sudo: boolean;
|
|
11
|
+
/** POST target of the form. */
|
|
12
|
+
href: string;
|
|
13
|
+
target: string | null;
|
|
14
|
+
cancelHref: string;
|
|
15
|
+
submitLabel: string;
|
|
16
|
+
fields: Array<{
|
|
17
|
+
name: string;
|
|
18
|
+
label: string;
|
|
19
|
+
kind: 'text' | 'select' | 'textarea';
|
|
20
|
+
required: boolean;
|
|
21
|
+
maxLength: number | null;
|
|
22
|
+
options: Array<{
|
|
23
|
+
value: string;
|
|
24
|
+
label: string;
|
|
25
|
+
}>;
|
|
26
|
+
value: string;
|
|
27
|
+
}>;
|
|
28
|
+
}
|
|
29
|
+
/** An action as the JSON API describes it. */
|
|
30
|
+
export interface ResolvedActionDefinition {
|
|
31
|
+
label: string;
|
|
32
|
+
tone: 'default' | 'danger';
|
|
33
|
+
confirm: string | null;
|
|
34
|
+
sudo: boolean;
|
|
35
|
+
fields: ResolvedActionStep['fields'];
|
|
36
|
+
}
|
|
37
|
+
/** Validates the posted fields against the action's declaration. */
|
|
38
|
+
export declare function validateActionInput(action: AccountAction, raw: {
|
|
39
|
+
target?: unknown;
|
|
40
|
+
fields?: unknown;
|
|
41
|
+
}, t: (text: Text | undefined | null) => string): {
|
|
42
|
+
ok: true;
|
|
43
|
+
target: string | null;
|
|
44
|
+
fields: Record<string, string>;
|
|
45
|
+
} | {
|
|
46
|
+
ok: false;
|
|
47
|
+
error: string;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* The `<script>` tag of a `component` block's script: the response's CSP nonce when shield set
|
|
51
|
+
* one, `integrity` + `crossorigin` when the block gave one, `type="module"` when asked.
|
|
52
|
+
*/
|
|
53
|
+
export declare function componentScriptTag(script: {
|
|
54
|
+
src: string;
|
|
55
|
+
integrity: string | null;
|
|
56
|
+
module: boolean;
|
|
57
|
+
}, nonce: string | null): string;
|
|
58
|
+
/** `GET {prefix}/{segment}` — renders an extension page. */
|
|
59
|
+
export declare function showPage(ctx: HttpContext, page: RegisteredAccountPage): Promise<unknown>;
|
|
60
|
+
/** `GET {prefix}/{segment}/actions/:action/confirm` — the no-JS confirmation step. */
|
|
61
|
+
export declare function showActionStep(ctx: HttpContext, page: RegisteredAccountPage): Promise<unknown>;
|
|
62
|
+
/** The CSP nonce shield put on this response, when there is one. */
|
|
63
|
+
export declare function cspNonceOf(ctx: HttpContext): string | null;
|
|
64
|
+
/** `POST {prefix}/{segment}/actions/:action` — a form submission (`_csrf` checked by shield). */
|
|
65
|
+
export declare function postAction(ctx: HttpContext, page: RegisteredAccountPage): Promise<unknown>;
|
|
66
|
+
/** `GET {prefix}/api/extensions` — the navigation for React hosts. */
|
|
67
|
+
export declare function apiExtensions(ctx: HttpContext): Promise<void>;
|
|
68
|
+
/** `GET {prefix}/api/x/:pageId` — one page, resolved. */
|
|
69
|
+
export declare function apiPage(ctx: HttpContext): Promise<void>;
|
|
70
|
+
/** `POST {prefix}/api/x/:pageId/actions/:action` — runs an action, answers JSON. */
|
|
71
|
+
export declare function apiAction(ctx: HttpContext): Promise<void>;
|
|
72
|
+
/** A remote app's page (mounted on the catch-all route; `undefined` when it went away). */
|
|
73
|
+
export declare function showRemotePage(ctx: HttpContext, page: RegisteredAccountPage | undefined): Promise<unknown>;
|
|
74
|
+
/** The confirmation step of a remote app's action. */
|
|
75
|
+
export declare function showRemoteActionStep(ctx: HttpContext, page: RegisteredAccountPage | undefined): Promise<unknown>;
|
|
76
|
+
/** Runs a remote app's action. */
|
|
77
|
+
export declare function postRemoteAction(ctx: HttpContext, page: RegisteredAccountPage | undefined): Promise<unknown>;
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import { accountPath } from '../host/account_paths.js';
|
|
2
|
+
import { redirectExact } from '../host/redirect_exact.js';
|
|
3
|
+
import { resolveRuntimeSettings } from '../host/runtime_settings.js';
|
|
4
|
+
import { isSudoSatisfied } from '../host/sudo_mode.js';
|
|
5
|
+
import { accountExtensionPages, buildAccountNav, consoleRequest, extensionContext, findPageById, isPageVisible, loadResolvedPage, logExtensionError, pageHref, resolveAction, safeHref, translatorFor, validatedQuery, withTimeout, } from './runtime.js';
|
|
6
|
+
/** Flash key of an action's outcome. */
|
|
7
|
+
const FLASH_KEY = 'authkit_account_extension';
|
|
8
|
+
function apiError(code, message, extra = {}) {
|
|
9
|
+
return { error: { code, message, ...extra } };
|
|
10
|
+
}
|
|
11
|
+
function notFound(ctx, json) {
|
|
12
|
+
if (json)
|
|
13
|
+
return ctx.response.status(404).send(apiError('not_found', 'Not found.'));
|
|
14
|
+
return ctx.response.status(404).send('Not found');
|
|
15
|
+
}
|
|
16
|
+
async function requestOr404(ctx, page, json) {
|
|
17
|
+
const request = await consoleRequest(ctx);
|
|
18
|
+
if (!request || !page || !(await isPageVisible(request, page))) {
|
|
19
|
+
notFound(ctx, json);
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
return { request, page };
|
|
23
|
+
}
|
|
24
|
+
function actionOf(page, name) {
|
|
25
|
+
const actions = page.page.actions ?? {};
|
|
26
|
+
return Object.hasOwn(actions, name) ? (actions[name] ?? null) : null;
|
|
27
|
+
}
|
|
28
|
+
function resolveFields(action, t, values = {}) {
|
|
29
|
+
return (action.fields ?? []).map((field) => ({
|
|
30
|
+
name: field.name,
|
|
31
|
+
label: t(field.label),
|
|
32
|
+
kind: field.kind,
|
|
33
|
+
required: field.required === true,
|
|
34
|
+
maxLength: typeof field.maxLength === 'number' ? field.maxLength : null,
|
|
35
|
+
options: (field.options ?? []).map((o) => ({ value: o.value, label: t(o.label) })),
|
|
36
|
+
value: values[field.name] ?? '',
|
|
37
|
+
}));
|
|
38
|
+
}
|
|
39
|
+
function definitionOf(action, t) {
|
|
40
|
+
return {
|
|
41
|
+
label: t(action.label),
|
|
42
|
+
tone: action.tone === 'danger' ? 'danger' : 'default',
|
|
43
|
+
confirm: action.confirm !== undefined ? t(action.confirm) : null,
|
|
44
|
+
sudo: action.sudo === true,
|
|
45
|
+
fields: resolveFields(action, t),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
const DEFAULT_MAX_LENGTH = 2000;
|
|
49
|
+
/** Validates the posted fields against the action's declaration. */
|
|
50
|
+
export function validateActionInput(action, raw, t) {
|
|
51
|
+
const target = typeof raw.target === 'string' && raw.target !== '' && raw.target.length <= 512
|
|
52
|
+
? raw.target
|
|
53
|
+
: null;
|
|
54
|
+
const posted = (raw.fields && typeof raw.fields === 'object' ? raw.fields : {});
|
|
55
|
+
const fields = {};
|
|
56
|
+
for (const field of action.fields ?? []) {
|
|
57
|
+
const value = posted[field.name];
|
|
58
|
+
const text = typeof value === 'string' ? value : '';
|
|
59
|
+
const name = t(field.label);
|
|
60
|
+
if (field.required && text.trim() === '') {
|
|
61
|
+
return {
|
|
62
|
+
ok: false,
|
|
63
|
+
error: t({ key: 'account.extension.field_required', params: { field: name } }),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
if (text.length > (field.maxLength ?? DEFAULT_MAX_LENGTH)) {
|
|
67
|
+
return {
|
|
68
|
+
ok: false,
|
|
69
|
+
error: t({ key: 'account.extension.field_too_long', params: { field: name } }),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (field.kind === 'select' &&
|
|
73
|
+
text !== '' &&
|
|
74
|
+
!(field.options ?? []).some((o) => o.value === text)) {
|
|
75
|
+
return {
|
|
76
|
+
ok: false,
|
|
77
|
+
error: t({ key: 'account.extension.field_invalid', params: { field: name } }),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
fields[field.name] = text;
|
|
81
|
+
}
|
|
82
|
+
return { ok: true, target, fields };
|
|
83
|
+
}
|
|
84
|
+
/** The page's flash (set by the last action), read once. */
|
|
85
|
+
function takeFlash(ctx) {
|
|
86
|
+
try {
|
|
87
|
+
const flash = ctx.session?.flashMessages?.get(FLASH_KEY);
|
|
88
|
+
if (flash && typeof flash.text === 'string') {
|
|
89
|
+
return { tone: flash.tone === 'danger' ? 'danger' : 'success', text: flash.text };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
/* no session */
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
function flash(ctx, tone, text) {
|
|
98
|
+
try {
|
|
99
|
+
ctx.session.flash(FLASH_KEY, { tone, text });
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
/* no session */
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function escapeAttr(value) {
|
|
106
|
+
return value
|
|
107
|
+
.replace(/&/g, '&')
|
|
108
|
+
.replace(/"/g, '"')
|
|
109
|
+
.replace(/</g, '<')
|
|
110
|
+
.replace(/>/g, '>');
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* The `<script>` tag of a `component` block's script: the response's CSP nonce when shield set
|
|
114
|
+
* one, `integrity` + `crossorigin` when the block gave one, `type="module"` when asked.
|
|
115
|
+
*/
|
|
116
|
+
export function componentScriptTag(script, nonce) {
|
|
117
|
+
const attrs = [`src="${escapeAttr(script.src)}"`];
|
|
118
|
+
if (script.module)
|
|
119
|
+
attrs.push('type="module"');
|
|
120
|
+
else
|
|
121
|
+
attrs.push('defer');
|
|
122
|
+
if (nonce)
|
|
123
|
+
attrs.push(`nonce="${escapeAttr(nonce)}"`);
|
|
124
|
+
if (script.integrity) {
|
|
125
|
+
attrs.push(`integrity="${escapeAttr(script.integrity)}"`, 'crossorigin="anonymous"');
|
|
126
|
+
}
|
|
127
|
+
return `<script ${attrs.join(' ')}></script>`;
|
|
128
|
+
}
|
|
129
|
+
async function render(ctx, request, props) {
|
|
130
|
+
const render = request.cfg.render;
|
|
131
|
+
const nonce = cspNonceOf(ctx);
|
|
132
|
+
return render(ctx, 'account/extension', {
|
|
133
|
+
csrfToken: ctx.request.csrfToken ?? '',
|
|
134
|
+
cspNonce: nonce,
|
|
135
|
+
htmlLang: request.locale.toLowerCase(),
|
|
136
|
+
scriptTag: (script) => componentScriptTag(script, nonce),
|
|
137
|
+
...props,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
/** `GET {prefix}/{segment}` — renders an extension page. */
|
|
141
|
+
export async function showPage(ctx, page) {
|
|
142
|
+
const found = await requestOr404(ctx, page, false);
|
|
143
|
+
if (!found)
|
|
144
|
+
return;
|
|
145
|
+
const query = validatedQuery(ctx.request.qs());
|
|
146
|
+
const resolved = await loadResolvedPage(found.request, found.page, query);
|
|
147
|
+
return render(ctx, found.request, {
|
|
148
|
+
page: resolved,
|
|
149
|
+
step: null,
|
|
150
|
+
flash: takeFlash(ctx),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
/** `GET {prefix}/{segment}/actions/:action/confirm` — the no-JS confirmation step. */
|
|
154
|
+
export async function showActionStep(ctx, page) {
|
|
155
|
+
const found = await requestOr404(ctx, page, false);
|
|
156
|
+
if (!found)
|
|
157
|
+
return;
|
|
158
|
+
const name = String(ctx.request.param('action') ?? '');
|
|
159
|
+
const action = actionOf(found.page, name);
|
|
160
|
+
if (!action)
|
|
161
|
+
return notFound(ctx, false);
|
|
162
|
+
const t = translatorFor(found.request, found.page);
|
|
163
|
+
const target = typeof ctx.request.qs().target === 'string' ? ctx.request.qs().target : null;
|
|
164
|
+
const button = resolveAction(found.page, { action: name, ...(target ? { target } : {}) }, t);
|
|
165
|
+
const step = {
|
|
166
|
+
action: name,
|
|
167
|
+
title: t(action.label),
|
|
168
|
+
text: action.confirm !== undefined ? t(action.confirm) : null,
|
|
169
|
+
tone: action.tone === 'danger' ? 'danger' : 'default',
|
|
170
|
+
sudo: action.sudo === true,
|
|
171
|
+
href: button ? button.href.replace(/\/confirm(\?.*)?$/, '') : pageHref(found.page),
|
|
172
|
+
target,
|
|
173
|
+
cancelHref: pageHref(found.page),
|
|
174
|
+
submitLabel: t(action.label),
|
|
175
|
+
fields: resolveFields(action, t),
|
|
176
|
+
};
|
|
177
|
+
const resolved = {
|
|
178
|
+
id: found.page.page.id,
|
|
179
|
+
extension: found.page.extensionId,
|
|
180
|
+
title: t(found.page.page.title),
|
|
181
|
+
href: pageHref(found.page),
|
|
182
|
+
tabs: [],
|
|
183
|
+
blocks: [],
|
|
184
|
+
};
|
|
185
|
+
return render(ctx, found.request, {
|
|
186
|
+
page: resolved,
|
|
187
|
+
step,
|
|
188
|
+
flash: takeFlash(ctx),
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
/** The CSP nonce shield put on this response, when there is one. */
|
|
192
|
+
export function cspNonceOf(ctx) {
|
|
193
|
+
const nonce = ctx.response?.nonce;
|
|
194
|
+
return typeof nonce === 'string' && nonce !== '' ? nonce : null;
|
|
195
|
+
}
|
|
196
|
+
async function runAction(ctx, request, page, name, raw, json) {
|
|
197
|
+
const t = translatorFor(request, page);
|
|
198
|
+
const action = actionOf(page, name);
|
|
199
|
+
const back = pageHref(page);
|
|
200
|
+
if (!action) {
|
|
201
|
+
const text = t({ key: 'account.extension.action_not_found' });
|
|
202
|
+
return {
|
|
203
|
+
status: 404,
|
|
204
|
+
body: { ok: false, error: apiError('not_found', text).error },
|
|
205
|
+
location: back,
|
|
206
|
+
flash: { tone: 'danger', text },
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
const sensitive = action.sudo === true || action.tone === 'danger';
|
|
210
|
+
if (sensitive && request.base.impersonatedBy !== null) {
|
|
211
|
+
const text = t({ key: 'account.extension.impersonation_refused' });
|
|
212
|
+
return {
|
|
213
|
+
status: 403,
|
|
214
|
+
body: { ok: false, error: apiError('impersonation_refused', text).error },
|
|
215
|
+
location: back,
|
|
216
|
+
flash: { tone: 'danger', text },
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
if (action.sudo === true) {
|
|
220
|
+
const settings = await resolveRuntimeSettings(ctx);
|
|
221
|
+
if (!(await isSudoSatisfied(ctx, settings)))
|
|
222
|
+
return 'sudo';
|
|
223
|
+
}
|
|
224
|
+
const input = validateActionInput(action, raw, t);
|
|
225
|
+
// The step keeps the posted target even when the fields were refused, so the user can fix them.
|
|
226
|
+
const rawTarget = typeof raw.target === 'string' && raw.target !== '' && raw.target.length <= 512
|
|
227
|
+
? raw.target
|
|
228
|
+
: null;
|
|
229
|
+
const button = resolveAction(page, { action: name, ...(rawTarget ? { target: rawTarget } : {}) }, t);
|
|
230
|
+
const stepHref = button?.step ? button.href : back;
|
|
231
|
+
if (!input.ok) {
|
|
232
|
+
return {
|
|
233
|
+
status: 422,
|
|
234
|
+
body: { ok: false, error: apiError('invalid_input', input.error).error },
|
|
235
|
+
location: stepHref,
|
|
236
|
+
flash: { tone: 'danger', text: input.error },
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
let result;
|
|
240
|
+
try {
|
|
241
|
+
result = await withTimeout(action.handle(extensionContext(request, {}), { target: input.target, fields: input.fields }), request.config.actionTimeoutMs ?? 15_000, 'handle()');
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
logExtensionError(page, `actions.${name}.handle`, error);
|
|
245
|
+
result = { ok: false, error: { key: 'account.extension.action_failed' } };
|
|
246
|
+
}
|
|
247
|
+
const audit = async (type, extra = {}) => {
|
|
248
|
+
try {
|
|
249
|
+
await request.cfg.audit?.record({
|
|
250
|
+
type,
|
|
251
|
+
accountId: request.base.account.id,
|
|
252
|
+
ip: ctx.request.ip?.() ?? null,
|
|
253
|
+
metadata: { page: page.page.id, action: name, target: input.target, ...extra },
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
/* best effort */
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
if (sensitive)
|
|
261
|
+
await audit('account.extension.action', { ok: result?.ok === true });
|
|
262
|
+
if (!result || result.ok !== true) {
|
|
263
|
+
const failure = result;
|
|
264
|
+
const text = t(failure?.error ?? { key: 'account.extension.action_failed' });
|
|
265
|
+
const status = [400, 403, 404, 409].includes(failure?.status ?? 0)
|
|
266
|
+
? failure?.status
|
|
267
|
+
: 400;
|
|
268
|
+
return {
|
|
269
|
+
status,
|
|
270
|
+
body: { ok: false, error: apiError('action_failed', text).error },
|
|
271
|
+
location: json ? back : button?.step ? stepHref : back,
|
|
272
|
+
flash: { tone: 'danger', text },
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
let location = back;
|
|
276
|
+
let external = false;
|
|
277
|
+
const redirectTo = result.redirectTo;
|
|
278
|
+
if (redirectTo && 'page' in redirectTo) {
|
|
279
|
+
const other = findPageById(redirectTo.page);
|
|
280
|
+
if (other)
|
|
281
|
+
location = pageHref(other);
|
|
282
|
+
}
|
|
283
|
+
else if (redirectTo && 'external' in redirectTo) {
|
|
284
|
+
const href = safeHref(redirectTo.external, true);
|
|
285
|
+
if (href) {
|
|
286
|
+
location = href;
|
|
287
|
+
external = true;
|
|
288
|
+
await audit('account.extension.redirect', { url: href });
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
const text = t(result.flash ?? { key: 'account.extension.done' });
|
|
292
|
+
return {
|
|
293
|
+
status: 200,
|
|
294
|
+
body: { ok: true, flash: text, redirectTo: location === back ? null : location, external },
|
|
295
|
+
location,
|
|
296
|
+
flash: external ? null : { tone: 'success', text },
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
function sudoUrl(returnTo) {
|
|
300
|
+
return `${accountPath('confirm')}?return_to=${encodeURIComponent(returnTo)}`;
|
|
301
|
+
}
|
|
302
|
+
/** `POST {prefix}/{segment}/actions/:action` — a form submission (`_csrf` checked by shield). */
|
|
303
|
+
export async function postAction(ctx, page) {
|
|
304
|
+
const found = await requestOr404(ctx, page, false);
|
|
305
|
+
if (!found)
|
|
306
|
+
return;
|
|
307
|
+
const name = String(ctx.request.param('action') ?? '');
|
|
308
|
+
const body = ctx.request.body();
|
|
309
|
+
const fields = {};
|
|
310
|
+
for (const [key, value] of Object.entries(body)) {
|
|
311
|
+
if (key.startsWith('field_'))
|
|
312
|
+
fields[key.slice('field_'.length)] = value;
|
|
313
|
+
}
|
|
314
|
+
const outcome = await runAction(ctx, found.request, found.page, name, { target: body.target, fields }, false);
|
|
315
|
+
if (outcome === 'sudo') {
|
|
316
|
+
const t = translatorFor(found.request, found.page);
|
|
317
|
+
const target = typeof body.target === 'string' ? body.target : undefined;
|
|
318
|
+
const button = resolveAction(found.page, { action: name, ...(target ? { target } : {}) }, t);
|
|
319
|
+
return redirectExact(ctx.response, sudoUrl(button?.step ? button.href : pageHref(found.page)));
|
|
320
|
+
}
|
|
321
|
+
if (outcome.flash)
|
|
322
|
+
flash(ctx, outcome.flash.tone, outcome.flash.text);
|
|
323
|
+
return redirectExact(ctx.response, outcome.location);
|
|
324
|
+
}
|
|
325
|
+
// ── JSON API ──────────────────────────────────────────────────────────────────────────────────────
|
|
326
|
+
/** `GET {prefix}/api/extensions` — the navigation for React hosts. */
|
|
327
|
+
export async function apiExtensions(ctx) {
|
|
328
|
+
const nav = await buildAccountNav(ctx, null);
|
|
329
|
+
const ids = new Set(accountExtensionPages().map((p) => p.page.id));
|
|
330
|
+
return ctx.response.send({
|
|
331
|
+
nav,
|
|
332
|
+
pages: nav
|
|
333
|
+
.filter((entry) => ids.has(entry.id))
|
|
334
|
+
.map(({ id, label, href, order, group }) => ({ id, label, href, order, group })),
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
/** `GET {prefix}/api/x/:pageId` — one page, resolved. */
|
|
338
|
+
export async function apiPage(ctx) {
|
|
339
|
+
const found = await requestOr404(ctx, findPageById(String(ctx.request.param('pageId'))), true);
|
|
340
|
+
if (!found)
|
|
341
|
+
return;
|
|
342
|
+
const query = validatedQuery(ctx.request.qs());
|
|
343
|
+
const page = await loadResolvedPage(found.request, found.page, query);
|
|
344
|
+
const t = translatorFor(found.request, found.page);
|
|
345
|
+
const actions = {};
|
|
346
|
+
for (const [name, action] of Object.entries(found.page.page.actions ?? {})) {
|
|
347
|
+
actions[name] = definitionOf(action, t);
|
|
348
|
+
}
|
|
349
|
+
return ctx.response.header('cache-control', 'private, no-store').send({ page, actions });
|
|
350
|
+
}
|
|
351
|
+
/** `POST {prefix}/api/x/:pageId/actions/:action` — runs an action, answers JSON. */
|
|
352
|
+
export async function apiAction(ctx) {
|
|
353
|
+
const found = await requestOr404(ctx, findPageById(String(ctx.request.param('pageId'))), true);
|
|
354
|
+
if (!found)
|
|
355
|
+
return;
|
|
356
|
+
const name = String(ctx.request.param('action') ?? '');
|
|
357
|
+
const body = ctx.request.body();
|
|
358
|
+
const outcome = await runAction(ctx, found.request, found.page, name, body, true);
|
|
359
|
+
if (outcome === 'sudo') {
|
|
360
|
+
// Back to the action's confirmation step (target included) when it has one, like the forms.
|
|
361
|
+
const t = translatorFor(found.request, found.page);
|
|
362
|
+
const target = typeof body.target === 'string' && body.target !== '' ? body.target : undefined;
|
|
363
|
+
const button = resolveAction(found.page, { action: name, ...(target ? { target } : {}) }, t);
|
|
364
|
+
return ctx.response.status(403).send(apiError('sudo_required', 'Identity confirmation required.', {
|
|
365
|
+
confirmUrl: sudoUrl(button?.step ? button.href : pageHref(found.page)),
|
|
366
|
+
}));
|
|
367
|
+
}
|
|
368
|
+
return ctx.response.status(outcome.status).send(outcome.body);
|
|
369
|
+
}
|
|
370
|
+
/** A remote app's page (mounted on the catch-all route; `undefined` when it went away). */
|
|
371
|
+
export async function showRemotePage(ctx, page) {
|
|
372
|
+
return page ? showPage(ctx, page) : notFound(ctx, false);
|
|
373
|
+
}
|
|
374
|
+
/** The confirmation step of a remote app's action. */
|
|
375
|
+
export async function showRemoteActionStep(ctx, page) {
|
|
376
|
+
return page ? showActionStep(ctx, page) : notFound(ctx, false);
|
|
377
|
+
}
|
|
378
|
+
/** Runs a remote app's action. */
|
|
379
|
+
export async function postRemoteAction(ctx, page) {
|
|
380
|
+
return page ? postAction(ctx, page) : notFound(ctx, false);
|
|
381
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { HttpContext } from '@adonisjs/core/http';
|
|
2
|
+
import type { AccountExtensionOverride } from './extension.js';
|
|
3
|
+
import { AccountExtensionRegistryImpl } from './registry.js';
|
|
4
|
+
import { type RemoteAccountExtensionSource } from './remote.js';
|
|
5
|
+
import { type BuiltinNavId } from './runtime.js';
|
|
6
|
+
type Middleware = (ctx: HttpContext, next: () => Promise<void>) => Promise<unknown>;
|
|
7
|
+
/** The process registry bound at `authkit.accountExtensions`. */
|
|
8
|
+
export declare function accountExtensionRegistry(): AccountExtensionRegistryImpl;
|
|
9
|
+
/** A fresh registry (tests). */
|
|
10
|
+
export declare function resetAccountExtensionRegistry(next?: AccountExtensionRegistryImpl): void;
|
|
11
|
+
/** One extension page in {@link AuthHostRouteMap}. */
|
|
12
|
+
export interface AccountExtensionRouteEntry {
|
|
13
|
+
id: string;
|
|
14
|
+
href: string;
|
|
15
|
+
label: string;
|
|
16
|
+
order: number;
|
|
17
|
+
}
|
|
18
|
+
export interface MountAccountExtensionsOptions {
|
|
19
|
+
guard: Middleware;
|
|
20
|
+
/** Per-account bucket for action POSTs: `[html, json]` variants (same count). */
|
|
21
|
+
throttle?: {
|
|
22
|
+
html: Middleware;
|
|
23
|
+
json: Middleware;
|
|
24
|
+
};
|
|
25
|
+
overrides?: Readonly<Record<string, AccountExtensionOverride>>;
|
|
26
|
+
builtins: Partial<Record<BuiltinNavId, string>>;
|
|
27
|
+
/** Filled in place once the routes are mounted. */
|
|
28
|
+
entries: AccountExtensionRouteEntry[];
|
|
29
|
+
/** `accountConsole.remote`: apps that serve pages for this console. */
|
|
30
|
+
remote?: readonly RemoteAccountExtensionSource[];
|
|
31
|
+
/** `accountConsole.actionTimeoutMs`. */
|
|
32
|
+
actionTimeoutMs?: number;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Mounts the JSON API of the extensions now, and the page routes once every provider booted
|
|
36
|
+
* (contributors register from their `boot()`; pages mount when the app starts), then freezes the
|
|
37
|
+
* registry.
|
|
38
|
+
*/
|
|
39
|
+
export declare function mountAccountExtensions(router: any, options: MountAccountExtensionsOptions): void;
|
|
40
|
+
export {};
|