@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.
Files changed (177) hide show
  1. package/CHANGELOG.md +111 -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 +14 -12
  22. package/build/index.js +8 -9
  23. package/build/providers/authkit_server_provider.js +36 -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_authorize.d.ts +56 -0
  54. package/build/src/console_authorize.js +85 -0
  55. package/build/src/console_switcher.d.ts +132 -0
  56. package/build/src/console_switcher.js +206 -0
  57. package/build/src/define_config.d.ts +54 -14
  58. package/build/src/define_config.js +17 -8
  59. package/build/src/dev_tools/contract.d.ts +72 -0
  60. package/build/src/dev_tools/contract.js +10 -0
  61. package/build/src/dev_tools/gate.d.ts +17 -0
  62. package/build/src/dev_tools/gate.js +69 -0
  63. package/build/src/dev_tools/register.d.ts +11 -0
  64. package/build/src/dev_tools/register.js +25 -0
  65. package/build/src/dev_tools/tools.d.ts +24 -0
  66. package/build/src/dev_tools/tools.js +384 -0
  67. package/build/src/doctor/agora_checks.d.ts +15 -0
  68. package/build/src/doctor/agora_checks.js +150 -0
  69. package/build/src/doctor/agora_doctor.d.ts +115 -0
  70. package/build/src/doctor/agora_doctor.js +692 -0
  71. package/build/src/doctor/checks.d.ts +7 -1
  72. package/build/src/doctor/checks.js +47 -1
  73. package/build/src/doctor/doctor_input.d.ts +27 -0
  74. package/build/src/doctor/doctor_input.js +86 -0
  75. package/build/src/doctor/index.d.ts +5 -0
  76. package/build/src/doctor/index.js +4 -0
  77. package/build/src/host/access_token_verifier.js +7 -0
  78. package/build/src/host/account_api/account_api_controller.js +8 -6
  79. package/build/src/host/account_session_sign_in.d.ts +12 -0
  80. package/build/src/host/account_session_sign_in.js +19 -0
  81. package/build/src/host/admin_api/admin_users_service.d.ts +5 -2
  82. package/build/src/host/admin_api/api_clients_controller.d.ts +37 -0
  83. package/build/src/host/admin_api/api_clients_controller.js +31 -1
  84. package/build/src/host/admin_api/dto.d.ts +3 -1
  85. package/build/src/host/admin_api/dto.js +2 -0
  86. package/build/src/host/admin_authorize.d.ts +8 -0
  87. package/build/src/host/admin_authorize.js +25 -0
  88. package/build/src/host/admin_clients_service.d.ts +20 -0
  89. package/build/src/host/admin_clients_service.js +65 -1
  90. package/build/src/host/admin_console/admin_console_entry.d.ts +14 -0
  91. package/build/src/host/admin_console/admin_console_entry.js +80 -0
  92. package/build/src/host/admin_console/admin_shell_controller.js +8 -0
  93. package/build/src/host/admin_console/console_clients_controller.d.ts +41 -0
  94. package/build/src/host/admin_console/console_clients_controller.js +34 -1
  95. package/build/src/host/admin_console/console_personal_agents_controller.d.ts +1 -6
  96. package/build/src/host/admin_console/console_personal_agents_controller.js +50 -30
  97. package/build/src/host/admin_console/console_switcher_controller.d.ts +11 -0
  98. package/build/src/host/admin_console/console_switcher_controller.js +23 -0
  99. package/build/src/host/admin_console_warning.d.ts +8 -0
  100. package/build/src/host/admin_console_warning.js +18 -0
  101. package/build/src/host/admin_sessions_service.js +8 -5
  102. package/build/src/host/admin_validators.d.ts +29 -0
  103. package/build/src/host/admin_validators.js +30 -0
  104. package/build/src/host/auth_host_config.d.ts +14 -2
  105. package/build/src/host/console_denied_page.d.ts +6 -0
  106. package/build/src/host/console_denied_page.js +18 -0
  107. package/build/src/host/console_sso.d.ts +24 -0
  108. package/build/src/host/console_sso.js +70 -0
  109. package/build/src/host/controllers/account_apps_controller.js +15 -11
  110. package/build/src/host/controllers/account_session_controller.js +10 -1
  111. package/build/src/host/controllers/console_sso_controller.d.ts +12 -0
  112. package/build/src/host/controllers/console_sso_controller.js +61 -0
  113. package/build/src/host/controllers/interaction_controller.js +3 -1
  114. package/build/src/host/csrf.d.ts +3 -3
  115. package/build/src/host/csrf.js +3 -13
  116. package/build/src/host/default_mailer.d.ts +11 -6
  117. package/build/src/host/default_mailer.js +16 -12
  118. package/build/src/host/i18n.d.ts +41 -14
  119. package/build/src/host/i18n.js +55 -13
  120. package/build/src/host/rate_limit.d.ts +8 -0
  121. package/build/src/host/rate_limit.js +9 -0
  122. package/build/src/host/register_auth_host.d.ts +16 -0
  123. package/build/src/host/register_auth_host.js +230 -88
  124. package/build/src/host/renderers/edge_renderer.js +5 -0
  125. package/build/src/host/renderers/inertia_renderer.js +7 -1
  126. package/build/src/host/screen_props.d.ts +39 -8
  127. package/build/src/host/ui-dist/assets/index-Dq3dJVBv.js +67 -0
  128. package/build/src/host/ui-dist/assets/{index-DTSmD4RU.css → index-WNIyCEPA.css} +1 -1
  129. package/build/src/host/ui-dist/index.html +2 -2
  130. package/build/src/provider/build_provider.js +43 -6
  131. package/build/src/provider/oidc_service.d.ts +1 -1
  132. package/build/src/provider/oidc_service.js +1 -1
  133. package/build/src/schema/ensure.d.ts +14 -0
  134. package/build/src/schema/ensure.js +43 -150
  135. package/build/src/service_tenants/client_metadata.d.ts +39 -0
  136. package/build/src/service_tenants/client_metadata.js +80 -0
  137. package/build/src/service_tenants/config.d.ts +44 -0
  138. package/build/src/service_tenants/config.js +44 -0
  139. package/build/src/service_tenants/provider_hooks.d.ts +52 -0
  140. package/build/src/service_tenants/provider_hooks.js +124 -0
  141. package/build/src/service_tenants/tenant_verifier.d.ts +73 -0
  142. package/build/src/service_tenants/tenant_verifier.js +104 -0
  143. package/build/src/tenancy/tenant_labels.d.ts +30 -0
  144. package/build/src/tenancy/tenant_labels.js +64 -0
  145. package/build/types.d.ts +3 -1
  146. package/package.json +12 -9
  147. package/build/src/agents/poppy/client_registry.d.ts +0 -50
  148. package/build/src/agents/poppy/client_registry.js +0 -260
  149. package/build/src/agents/poppy/config.d.ts +0 -396
  150. package/build/src/agents/poppy/config.js +0 -228
  151. package/build/src/agents/poppy/dpop.d.ts +0 -61
  152. package/build/src/agents/poppy/dpop.js +0 -158
  153. package/build/src/agents/poppy/email_code_sign_in.d.ts +0 -27
  154. package/build/src/agents/poppy/email_code_sign_in.js +0 -140
  155. package/build/src/agents/poppy/errors.d.ts +0 -25
  156. package/build/src/agents/poppy/errors.js +0 -25
  157. package/build/src/agents/poppy/replay.d.ts +0 -25
  158. package/build/src/agents/poppy/replay.js +0 -106
  159. package/build/src/agents/poppy/resource.d.ts +0 -100
  160. package/build/src/agents/poppy/resource.js +0 -129
  161. package/build/src/agents/poppy/safe_fetch.d.ts +0 -48
  162. package/build/src/agents/poppy/safe_fetch.js +0 -262
  163. package/build/src/agents/poppy/service.d.ts +0 -293
  164. package/build/src/agents/poppy/service.js +0 -1458
  165. package/build/src/agents/poppy/session_jwt.d.ts +0 -30
  166. package/build/src/agents/poppy/session_jwt.js +0 -133
  167. package/build/src/agents/poppy/store.d.ts +0 -106
  168. package/build/src/agents/poppy/store.js +0 -261
  169. package/build/src/agents/poppy/web_session.d.ts +0 -39
  170. package/build/src/agents/poppy/web_session.js +0 -122
  171. package/build/src/host/controllers/poppy_consent_controller.d.ts +0 -19
  172. package/build/src/host/controllers/poppy_consent_controller.js +0 -199
  173. package/build/src/host/controllers/poppy_controller.d.ts +0 -33
  174. package/build/src/host/controllers/poppy_controller.js +0 -219
  175. package/build/src/host/poppy_session_key.d.ts +0 -9
  176. package/build/src/host/poppy_session_key.js +0 -9
  177. 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.poppy
137
- ? { poppyPrefix: config.personalAgents.poppy.prefix }
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
- // Poppy: o slot global que o `@adonis-agora/agent` usa para autenticar o endpoint de
150
- // conversas (e o MCP) sem importar este pacote — o contrato é o símbolo.
151
- if (config.personalAgents?.poppy) {
152
- const { installPoppySlots } = await import('../src/agents/poppy/resource.js');
153
- const { poppyUrls } = await import('../src/agents/poppy/service.js');
154
- installPoppySlots(poppyUrls(config.issuer, config.personalAgents.poppy.prefix).issuer);
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, '&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 {};