@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.
Files changed (163) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/README.md +4 -5
  3. package/build/commands/agora_doctor.d.ts +22 -0
  4. package/build/commands/agora_doctor.js +109 -0
  5. package/build/commands/clients_create.d.ts +4 -0
  6. package/build/commands/clients_create.js +97 -0
  7. package/build/commands/commands.json +78 -0
  8. package/build/commands/doctor.js +8 -74
  9. package/build/host/views/account/apps.edge +1 -5
  10. package/build/host/views/account/extension.edge +77 -0
  11. package/build/host/views/account/mfa.edge +2 -0
  12. package/build/host/views/account/partials/extension_actions.edge +14 -0
  13. package/build/host/views/account/partials/extension_block.edge +197 -0
  14. package/build/host/views/account/partials/extension_value.edge +8 -0
  15. package/build/host/views/account/partials/nav.edge +14 -0
  16. package/build/host/views/account/security.edge +1 -5
  17. package/build/host/views/account/tokens.edge +2 -2
  18. package/build/host/views/agents/consent.edge +11 -2
  19. package/build/host/views/agents/done.edge +1 -1
  20. package/build/host/views/partials/styles.edge +1 -1
  21. package/build/index.d.ts +12 -12
  22. package/build/index.js +7 -9
  23. package/build/providers/authkit_server_provider.js +28 -8
  24. package/build/src/account/extension.d.ts +10 -0
  25. package/build/src/account/extension.js +9 -0
  26. package/build/src/account/http.d.ts +77 -0
  27. package/build/src/account/http.js +381 -0
  28. package/build/src/account/mount.d.ts +40 -0
  29. package/build/src/account/mount.js +171 -0
  30. package/build/src/account/registry.d.ts +41 -0
  31. package/build/src/account/registry.js +81 -0
  32. package/build/src/account/remote.d.ts +72 -0
  33. package/build/src/account/remote.js +406 -0
  34. package/build/src/account/runtime.d.ts +289 -0
  35. package/build/src/account/runtime.js +641 -0
  36. package/build/src/accounts/account_store.d.ts +6 -0
  37. package/build/src/accounts/lucid_store/organizations.js +6 -0
  38. package/build/src/agents/config.d.ts +9 -13
  39. package/build/src/agents/config.js +17 -11
  40. package/build/src/agents/delegation_service.js +3 -3
  41. package/build/src/agents/delegation_store.d.ts +3 -2
  42. package/build/src/agents/delegation_store.js +6 -5
  43. package/build/src/agents/extension.d.ts +261 -0
  44. package/build/src/agents/extension.js +40 -0
  45. package/build/src/agents/middleware.js +1 -1
  46. package/build/src/agents/protocol.d.ts +4 -4
  47. package/build/src/agents/registry_store.d.ts +3 -2
  48. package/build/src/agents/registry_store.js +1 -1
  49. package/build/src/agents/runtime.d.ts +26 -7
  50. package/build/src/agents/runtime.js +110 -75
  51. package/build/src/audit/audit_sink.d.ts +1 -1
  52. package/build/src/audit/audit_sink.js +10 -1
  53. package/build/src/console_switcher.d.ts +132 -0
  54. package/build/src/console_switcher.js +206 -0
  55. package/build/src/define_config.d.ts +34 -12
  56. package/build/src/define_config.js +15 -7
  57. package/build/src/dev_tools/contract.d.ts +72 -0
  58. package/build/src/dev_tools/contract.js +10 -0
  59. package/build/src/dev_tools/gate.d.ts +17 -0
  60. package/build/src/dev_tools/gate.js +69 -0
  61. package/build/src/dev_tools/register.d.ts +11 -0
  62. package/build/src/dev_tools/register.js +25 -0
  63. package/build/src/dev_tools/tools.d.ts +24 -0
  64. package/build/src/dev_tools/tools.js +384 -0
  65. package/build/src/doctor/agora_checks.d.ts +15 -0
  66. package/build/src/doctor/agora_checks.js +150 -0
  67. package/build/src/doctor/agora_doctor.d.ts +115 -0
  68. package/build/src/doctor/agora_doctor.js +692 -0
  69. package/build/src/doctor/doctor_input.d.ts +27 -0
  70. package/build/src/doctor/doctor_input.js +86 -0
  71. package/build/src/doctor/index.d.ts +5 -0
  72. package/build/src/doctor/index.js +4 -0
  73. package/build/src/host/access_token_verifier.js +7 -0
  74. package/build/src/host/account_api/account_api_controller.js +8 -6
  75. package/build/src/host/account_session_sign_in.d.ts +12 -0
  76. package/build/src/host/account_session_sign_in.js +19 -0
  77. package/build/src/host/admin_api/admin_users_service.d.ts +5 -2
  78. package/build/src/host/admin_api/api_clients_controller.d.ts +37 -0
  79. package/build/src/host/admin_api/api_clients_controller.js +31 -1
  80. package/build/src/host/admin_api/dto.d.ts +3 -1
  81. package/build/src/host/admin_api/dto.js +2 -0
  82. package/build/src/host/admin_authorize.d.ts +8 -0
  83. package/build/src/host/admin_authorize.js +25 -0
  84. package/build/src/host/admin_clients_service.d.ts +20 -0
  85. package/build/src/host/admin_clients_service.js +55 -0
  86. package/build/src/host/admin_console/admin_console_entry.d.ts +14 -0
  87. package/build/src/host/admin_console/admin_console_entry.js +80 -0
  88. package/build/src/host/admin_console/admin_shell_controller.js +8 -0
  89. package/build/src/host/admin_console/console_clients_controller.d.ts +41 -0
  90. package/build/src/host/admin_console/console_clients_controller.js +34 -1
  91. package/build/src/host/admin_console/console_personal_agents_controller.d.ts +1 -6
  92. package/build/src/host/admin_console/console_personal_agents_controller.js +50 -30
  93. package/build/src/host/admin_console/console_switcher_controller.d.ts +11 -0
  94. package/build/src/host/admin_console/console_switcher_controller.js +23 -0
  95. package/build/src/host/admin_sessions_service.js +8 -5
  96. package/build/src/host/admin_validators.d.ts +29 -0
  97. package/build/src/host/admin_validators.js +30 -0
  98. package/build/src/host/auth_host_config.d.ts +9 -2
  99. package/build/src/host/controllers/account_apps_controller.js +15 -11
  100. package/build/src/host/csrf.d.ts +3 -3
  101. package/build/src/host/csrf.js +3 -13
  102. package/build/src/host/default_mailer.d.ts +11 -6
  103. package/build/src/host/default_mailer.js +16 -12
  104. package/build/src/host/i18n.d.ts +41 -14
  105. package/build/src/host/i18n.js +55 -13
  106. package/build/src/host/rate_limit.d.ts +8 -0
  107. package/build/src/host/rate_limit.js +9 -0
  108. package/build/src/host/register_auth_host.d.ts +14 -0
  109. package/build/src/host/register_auth_host.js +167 -106
  110. package/build/src/host/renderers/edge_renderer.js +5 -0
  111. package/build/src/host/renderers/inertia_renderer.js +7 -1
  112. package/build/src/host/screen_props.d.ts +39 -8
  113. package/build/src/host/ui-dist/assets/index-Dq3dJVBv.js +67 -0
  114. package/build/src/host/ui-dist/assets/{index-DTSmD4RU.css → index-WNIyCEPA.css} +1 -1
  115. package/build/src/host/ui-dist/index.html +2 -2
  116. package/build/src/provider/build_provider.js +37 -6
  117. package/build/src/provider/oidc_service.d.ts +1 -1
  118. package/build/src/provider/oidc_service.js +1 -1
  119. package/build/src/schema/ensure.d.ts +14 -0
  120. package/build/src/schema/ensure.js +43 -150
  121. package/build/src/service_tenants/client_metadata.d.ts +39 -0
  122. package/build/src/service_tenants/client_metadata.js +80 -0
  123. package/build/src/service_tenants/config.d.ts +44 -0
  124. package/build/src/service_tenants/config.js +44 -0
  125. package/build/src/service_tenants/provider_hooks.d.ts +52 -0
  126. package/build/src/service_tenants/provider_hooks.js +124 -0
  127. package/build/src/service_tenants/tenant_verifier.d.ts +73 -0
  128. package/build/src/service_tenants/tenant_verifier.js +104 -0
  129. package/build/src/tenancy/tenant_labels.d.ts +30 -0
  130. package/build/src/tenancy/tenant_labels.js +64 -0
  131. package/build/types.d.ts +3 -1
  132. package/package.json +9 -6
  133. package/build/src/agents/poppy/client_registry.d.ts +0 -50
  134. package/build/src/agents/poppy/client_registry.js +0 -260
  135. package/build/src/agents/poppy/config.d.ts +0 -396
  136. package/build/src/agents/poppy/config.js +0 -228
  137. package/build/src/agents/poppy/dpop.d.ts +0 -61
  138. package/build/src/agents/poppy/dpop.js +0 -158
  139. package/build/src/agents/poppy/email_code_sign_in.d.ts +0 -27
  140. package/build/src/agents/poppy/email_code_sign_in.js +0 -140
  141. package/build/src/agents/poppy/errors.d.ts +0 -25
  142. package/build/src/agents/poppy/errors.js +0 -25
  143. package/build/src/agents/poppy/replay.d.ts +0 -25
  144. package/build/src/agents/poppy/replay.js +0 -106
  145. package/build/src/agents/poppy/resource.d.ts +0 -100
  146. package/build/src/agents/poppy/resource.js +0 -129
  147. package/build/src/agents/poppy/safe_fetch.d.ts +0 -48
  148. package/build/src/agents/poppy/safe_fetch.js +0 -262
  149. package/build/src/agents/poppy/service.d.ts +0 -293
  150. package/build/src/agents/poppy/service.js +0 -1458
  151. package/build/src/agents/poppy/session_jwt.d.ts +0 -30
  152. package/build/src/agents/poppy/session_jwt.js +0 -133
  153. package/build/src/agents/poppy/store.d.ts +0 -106
  154. package/build/src/agents/poppy/store.js +0 -261
  155. package/build/src/agents/poppy/web_session.d.ts +0 -39
  156. package/build/src/agents/poppy/web_session.js +0 -122
  157. package/build/src/host/controllers/poppy_consent_controller.d.ts +0 -19
  158. package/build/src/host/controllers/poppy_consent_controller.js +0 -199
  159. package/build/src/host/controllers/poppy_controller.d.ts +0 -33
  160. package/build/src/host/controllers/poppy_controller.js +0 -219
  161. package/build/src/host/poppy_session_key.d.ts +0 -9
  162. package/build/src/host/poppy_session_key.js +0 -9
  163. 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.poppy
138
- ? { poppyPrefix: config.personalAgents.poppy.prefix }
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
- // Poppy: o slot global que o `@adonis-agora/agent` usa para autenticar o endpoint de
158
- // conversas (e o MCP) sem importar este pacote — o contrato é o símbolo.
159
- if (config.personalAgents?.poppy) {
160
- const { installPoppySlots } = await import('../src/agents/poppy/resource.js');
161
- const { poppyUrls } = await import('../src/agents/poppy/service.js');
162
- installPoppySlots(poppyUrls(config.issuer, config.personalAgents.poppy.prefix).issuer);
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, '&amp;')
108
+ .replace(/"/g, '&quot;')
109
+ .replace(/</g, '&lt;')
110
+ .replace(/>/g, '&gt;');
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 {};