@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
package/build/index.js
CHANGED
|
@@ -10,16 +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';
|
|
22
16
|
export { authorizeByToken } from './src/console_authorize.js';
|
|
17
|
+
export { AGORA_CONSOLES_BINDING, isAgoraProbe, registerAgoraConsole, } from './src/console_switcher.js';
|
|
23
18
|
export { adapters, defineConfig, resolveAdmin, resolveAdminApi, resolveAuthMethodsConfig, resolveDynamicRegistration, resolveLogin, resolveNotifications, resolveOrganizations, resolvePasswordless, resolveRateLimit, resolveRegistration, resolveWebauthn, toSeconds, } from './src/define_config.js';
|
|
24
19
|
export { buildWebhookBody, composeAuditSink, resolveEvents, signWebhookBody, } from './src/events/dispatcher.js';
|
|
25
20
|
export { inProcessAccessTokenVerifier, remoteAccessTokenVerifier, } from './src/host/access_token_verifier.js';
|
|
@@ -39,6 +34,7 @@ export { AccountExportService } from './src/host/account_export_service.js';
|
|
|
39
34
|
*/
|
|
40
35
|
export { accountPath, accountPrefix, joinAccountPath, } from './src/host/account_paths.js';
|
|
41
36
|
export { ACCOUNT_SESSION_KEY } from './src/host/account_session_key.js';
|
|
37
|
+
export { signInAccountSession, signOutAccountSession, } from './src/host/account_session_sign_in.js';
|
|
42
38
|
// Organizations (multi-tenancy)
|
|
43
39
|
export { ACTIVE_ORG_COOKIE, ACTIVE_ORG_COOKIE_TTL, decodeActiveOrgCookie, encodeActiveOrgCookie, readActiveOrgFromKoaCtx, } from './src/host/active_org_cookie.js';
|
|
44
40
|
export { AdminOrgsService } from './src/host/admin_api/admin_orgs_service.js';
|
|
@@ -65,6 +61,7 @@ export { consoleLoginUrl, getAccountId, hasAccountSession, realAccountId, } from
|
|
|
65
61
|
export { authkitCsrfExceptions } from './src/host/csrf.js';
|
|
66
62
|
export { authenticateCustomLogin, beginCustomLogin, completeCustomLogin, } from './src/host/custom_login.js';
|
|
67
63
|
export { beginCustomMfa, customMfaViewProps, verifyCustomMfa } from './src/host/custom_mfa.js';
|
|
64
|
+
export { sendAuthkitCodeEmail } from './src/host/default_mailer.js';
|
|
68
65
|
export { normalizeEmailIdentifier } from './src/host/email_identifier.js';
|
|
69
66
|
export { GEO_RESOLVE_TIMEOUT_MS, resolveGeoSafe } from './src/host/geo.js';
|
|
70
67
|
export { BUILTIN_MESSAGES, DEFAULT_LOCALE, DEFAULT_MESSAGES, PT_BR_MESSAGES, resolveMessages, translate, } from './src/host/i18n.js';
|
|
@@ -89,7 +86,6 @@ export { OidcRpGuard, oidcRpGuard } from './src/host/oidc_rp_guard.js';
|
|
|
89
86
|
// Login por OTP (código digitável): config + helpers puros.
|
|
90
87
|
export { evaluateLoginOtp, generateOtpCode, OTP_LOGIN_DEFAULTS, resolveOtpLoginConfig, } from './src/host/otp_login.js';
|
|
91
88
|
export { endRpSession } from './src/host/persistent_rp_session.js';
|
|
92
|
-
export { POPPY_WEB_SESSION_KEY } from './src/host/poppy_session_key.js';
|
|
93
89
|
export { createAuthThrottles } from './src/host/rate_limit.js';
|
|
94
90
|
export { registerAuthHost } from './src/host/register_auth_host.js';
|
|
95
91
|
export { edgeRenderer } from './src/host/renderers/edge_renderer.js';
|
|
@@ -140,5 +136,7 @@ export { generatePatToken, hashPatToken } from './src/pat/pat_tokens.js';
|
|
|
140
136
|
export { OidcService } from './src/provider/oidc_service.js';
|
|
141
137
|
export { checkClientRegistration, classifyRedirect, RegistrationPolicyError, } from './src/provider/registration_policy.js';
|
|
142
138
|
export { registerOidcRoutes } from './src/register_routes.js';
|
|
143
|
-
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';
|
|
144
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
|
|
@@ -131,11 +137,12 @@ export default class AuthkitServerProvider {
|
|
|
131
137
|
routes: typeof config.routes === 'object' ? config.routes : undefined,
|
|
132
138
|
lockedRouteOptions: config.lockedRouteOptions,
|
|
133
139
|
consoleNeedsLoginUrl: config.accountSession?.acceptIdpSession === false && config.authMethods?.password === false,
|
|
140
|
+
accountConsole: config.accountConsole,
|
|
134
141
|
personalAgents: config.personalAgents
|
|
135
142
|
? {
|
|
136
143
|
prefix: config.personalAgents.prefix,
|
|
137
|
-
...(config.personalAgents.
|
|
138
|
-
? {
|
|
144
|
+
...(config.personalAgents.extensions?.length
|
|
145
|
+
? { extensions: config.personalAgents.extensions }
|
|
139
146
|
: {}),
|
|
140
147
|
}
|
|
141
148
|
: undefined,
|
|
@@ -154,12 +161,21 @@ export default class AuthkitServerProvider {
|
|
|
154
161
|
const logger = await this.app.container.make('logger').catch(() => null);
|
|
155
162
|
warnAdminConsoleUnauthorized(logger);
|
|
156
163
|
}
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
if (config.
|
|
160
|
-
const {
|
|
161
|
-
|
|
162
|
-
|
|
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?.();
|
|
163
179
|
}
|
|
164
180
|
// Auto-montagem das rotas (`config.routes`). Chama a MESMA função exportada que o
|
|
165
181
|
// `start/routes.ts` chamaria; não há segunda implementação. "As rotas não subiram" não pode
|
|
@@ -177,6 +193,9 @@ export default class AuthkitServerProvider {
|
|
|
177
193
|
// que o `bin/server` bootou — dual-package hazard do core (ver
|
|
178
194
|
// `services/booted_app.ts`).
|
|
179
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());
|
|
180
199
|
this.app.container.singleton('authkit.server', async () => {
|
|
181
200
|
const configProviderValue = this.app.config.get('authkit');
|
|
182
201
|
const config = (await configProvider.resolve(this.app, configProviderValue));
|
|
@@ -268,6 +287,7 @@ export default class AuthkitServerProvider {
|
|
|
268
287
|
const report = await ensureAuthkitSchema(db, {
|
|
269
288
|
connection: config.schema.connection,
|
|
270
289
|
accountTable: config.accountStore?.accountTable,
|
|
290
|
+
tables: config.personalAgents?.extensions?.flatMap((e) => e.tables ?? []),
|
|
271
291
|
});
|
|
272
292
|
const logger = await this.app.container.make('logger').catch(() => null);
|
|
273
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 {};
|