@adonis-agora/authkit-server 1.0.0-rc.1 → 1.0.0-rc.11

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 (181) hide show
  1. package/CHANGELOG.md +107 -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 +29 -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 +13 -7
  37. package/build/src/accounts/lucid_account_store.d.ts +3 -3
  38. package/build/src/accounts/lucid_account_store.js +42 -17
  39. package/build/src/accounts/lucid_store/mfa.js +3 -3
  40. package/build/src/accounts/lucid_store/organization_models.d.ts +14 -3
  41. package/build/src/accounts/lucid_store/organization_models.js +31 -3
  42. package/build/src/accounts/lucid_store/organizations.d.ts +2 -2
  43. package/build/src/accounts/lucid_store/organizations.js +40 -31
  44. package/build/src/accounts/lucid_store/shared.d.ts +11 -3
  45. package/build/src/accounts/lucid_store/shared.js +7 -5
  46. package/build/src/accounts/lucid_store/webauthn.js +1 -1
  47. package/build/src/adapters/factory.d.ts +13 -2
  48. package/build/src/adapters/factory.js +2 -2
  49. package/build/src/agents/config.d.ts +9 -13
  50. package/build/src/agents/config.js +17 -11
  51. package/build/src/agents/delegation_service.js +3 -3
  52. package/build/src/agents/delegation_store.d.ts +3 -2
  53. package/build/src/agents/delegation_store.js +6 -5
  54. package/build/src/agents/extension.d.ts +261 -0
  55. package/build/src/agents/extension.js +40 -0
  56. package/build/src/agents/middleware.js +1 -1
  57. package/build/src/agents/protocol.d.ts +4 -4
  58. package/build/src/agents/registry_store.d.ts +3 -2
  59. package/build/src/agents/registry_store.js +1 -1
  60. package/build/src/agents/runtime.d.ts +26 -7
  61. package/build/src/agents/runtime.js +110 -75
  62. package/build/src/audit/audit_sink.d.ts +1 -1
  63. package/build/src/audit/audit_sink.js +10 -1
  64. package/build/src/commands/expire_scan_command.js +1 -1
  65. package/build/src/commands/settings_commands.js +2 -2
  66. package/build/src/console_switcher.d.ts +132 -0
  67. package/build/src/console_switcher.js +206 -0
  68. package/build/src/define_config.d.ts +38 -13
  69. package/build/src/define_config.js +32 -10
  70. package/build/src/dev_tools/contract.d.ts +72 -0
  71. package/build/src/dev_tools/contract.js +10 -0
  72. package/build/src/dev_tools/gate.d.ts +17 -0
  73. package/build/src/dev_tools/gate.js +69 -0
  74. package/build/src/dev_tools/register.d.ts +11 -0
  75. package/build/src/dev_tools/register.js +25 -0
  76. package/build/src/dev_tools/tools.d.ts +24 -0
  77. package/build/src/dev_tools/tools.js +384 -0
  78. package/build/src/doctor/agora_checks.d.ts +15 -0
  79. package/build/src/doctor/agora_checks.js +150 -0
  80. package/build/src/doctor/agora_doctor.d.ts +115 -0
  81. package/build/src/doctor/agora_doctor.js +692 -0
  82. package/build/src/doctor/checks.js +2 -2
  83. package/build/src/doctor/doctor_input.d.ts +27 -0
  84. package/build/src/doctor/doctor_input.js +86 -0
  85. package/build/src/doctor/index.d.ts +5 -0
  86. package/build/src/doctor/index.js +4 -0
  87. package/build/src/host/access_token_verifier.js +7 -0
  88. package/build/src/host/account_api/account_api_controller.js +8 -6
  89. package/build/src/host/account_session_sign_in.d.ts +12 -0
  90. package/build/src/host/account_session_sign_in.js +19 -0
  91. package/build/src/host/admin_api/admin_users_service.d.ts +5 -2
  92. package/build/src/host/admin_api/api_clients_controller.d.ts +37 -0
  93. package/build/src/host/admin_api/api_clients_controller.js +31 -1
  94. package/build/src/host/admin_api/dto.d.ts +3 -1
  95. package/build/src/host/admin_api/dto.js +2 -0
  96. package/build/src/host/admin_authorize.d.ts +8 -0
  97. package/build/src/host/admin_authorize.js +25 -0
  98. package/build/src/host/admin_clients_service.d.ts +20 -0
  99. package/build/src/host/admin_clients_service.js +55 -0
  100. package/build/src/host/admin_console/admin_console_entry.d.ts +14 -0
  101. package/build/src/host/admin_console/admin_console_entry.js +80 -0
  102. package/build/src/host/admin_console/admin_shell_controller.js +8 -0
  103. package/build/src/host/admin_console/console_clients_controller.d.ts +41 -0
  104. package/build/src/host/admin_console/console_clients_controller.js +34 -1
  105. package/build/src/host/admin_console/console_personal_agents_controller.d.ts +1 -6
  106. package/build/src/host/admin_console/console_personal_agents_controller.js +50 -30
  107. package/build/src/host/admin_console/console_switcher_controller.d.ts +11 -0
  108. package/build/src/host/admin_console/console_switcher_controller.js +23 -0
  109. package/build/src/host/admin_sessions_service.js +8 -5
  110. package/build/src/host/admin_validators.d.ts +29 -0
  111. package/build/src/host/admin_validators.js +30 -0
  112. package/build/src/host/auth_host_config.d.ts +9 -2
  113. package/build/src/host/controllers/account_apps_controller.js +15 -11
  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 +14 -0
  123. package/build/src/host/register_auth_host.js +167 -106
  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/runtime_settings.d.ts +6 -6
  127. package/build/src/host/runtime_settings.js +4 -3
  128. package/build/src/host/screen_props.d.ts +39 -8
  129. package/build/src/host/ui-dist/assets/index-Dq3dJVBv.js +67 -0
  130. package/build/src/host/ui-dist/assets/{index-DTSmD4RU.css → index-WNIyCEPA.css} +1 -1
  131. package/build/src/host/ui-dist/index.html +2 -2
  132. package/build/src/provider/build_provider.js +37 -6
  133. package/build/src/provider/oidc_service.d.ts +1 -1
  134. package/build/src/provider/oidc_service.js +1 -1
  135. package/build/src/schema/ensure.d.ts +24 -1
  136. package/build/src/schema/ensure.js +44 -154
  137. package/build/src/schema/tables_location.d.ts +27 -0
  138. package/build/src/schema/tables_location.js +22 -0
  139. package/build/src/service_tenants/client_metadata.d.ts +39 -0
  140. package/build/src/service_tenants/client_metadata.js +80 -0
  141. package/build/src/service_tenants/config.d.ts +44 -0
  142. package/build/src/service_tenants/config.js +44 -0
  143. package/build/src/service_tenants/provider_hooks.d.ts +52 -0
  144. package/build/src/service_tenants/provider_hooks.js +124 -0
  145. package/build/src/service_tenants/tenant_verifier.d.ts +73 -0
  146. package/build/src/service_tenants/tenant_verifier.js +104 -0
  147. package/build/src/tenancy/tenant_labels.d.ts +30 -0
  148. package/build/src/tenancy/tenant_labels.js +64 -0
  149. package/build/types.d.ts +3 -1
  150. package/package.json +9 -6
  151. package/build/src/agents/poppy/client_registry.d.ts +0 -50
  152. package/build/src/agents/poppy/client_registry.js +0 -260
  153. package/build/src/agents/poppy/config.d.ts +0 -396
  154. package/build/src/agents/poppy/config.js +0 -228
  155. package/build/src/agents/poppy/dpop.d.ts +0 -61
  156. package/build/src/agents/poppy/dpop.js +0 -158
  157. package/build/src/agents/poppy/email_code_sign_in.d.ts +0 -27
  158. package/build/src/agents/poppy/email_code_sign_in.js +0 -140
  159. package/build/src/agents/poppy/errors.d.ts +0 -25
  160. package/build/src/agents/poppy/errors.js +0 -25
  161. package/build/src/agents/poppy/replay.d.ts +0 -25
  162. package/build/src/agents/poppy/replay.js +0 -106
  163. package/build/src/agents/poppy/resource.d.ts +0 -100
  164. package/build/src/agents/poppy/resource.js +0 -129
  165. package/build/src/agents/poppy/safe_fetch.d.ts +0 -48
  166. package/build/src/agents/poppy/safe_fetch.js +0 -262
  167. package/build/src/agents/poppy/service.d.ts +0 -293
  168. package/build/src/agents/poppy/service.js +0 -1458
  169. package/build/src/agents/poppy/session_jwt.d.ts +0 -30
  170. package/build/src/agents/poppy/session_jwt.js +0 -133
  171. package/build/src/agents/poppy/store.d.ts +0 -106
  172. package/build/src/agents/poppy/store.js +0 -261
  173. package/build/src/agents/poppy/web_session.d.ts +0 -39
  174. package/build/src/agents/poppy/web_session.js +0 -122
  175. package/build/src/host/controllers/poppy_consent_controller.d.ts +0 -19
  176. package/build/src/host/controllers/poppy_consent_controller.js +0 -199
  177. package/build/src/host/controllers/poppy_controller.d.ts +0 -33
  178. package/build/src/host/controllers/poppy_controller.js +0 -219
  179. package/build/src/host/poppy_session_key.d.ts +0 -9
  180. package/build/src/host/poppy_session_key.js +0 -9
  181. package/build/src/host/ui-dist/assets/index-D0EIeyKK.js +0 -67
@@ -553,8 +553,8 @@ export function checkSettings(input) {
553
553
  // Table absent — opt-in feature, silently ok.
554
554
  return null;
555
555
  }
556
- // Informa a conexão usada quando nomeada (ajuda a diagnosticar bugs de searchPath).
557
- const connectionName = cfg.accountStore?.connectionName;
556
+ // Informa a conexão usada quando nomeada (a das tabelas do authkit).
557
+ const connectionName = cfg.schema?.connection;
558
558
  const connectionHint = connectionName ? ` (connection: '${connectionName}')` : '';
559
559
  // Table present — check if botProtection.verify is configured.
560
560
  const hasBotVerify = typeof cfg.botProtection?.verify === 'function';
@@ -0,0 +1,27 @@
1
+ import type { ApplicationService } from '@adonisjs/core/types';
2
+ import { type DoctorInput, type Finding } from './checks.js';
3
+ /** Logger mínimo para reportar peers instalados-mas-quebrados. */
4
+ export interface CanImportLogger {
5
+ warning(msg: string): void;
6
+ }
7
+ /**
8
+ * Tenta importar um peer; `true` se importável.
9
+ *
10
+ * Distingue "não instalado" de "instalado mas quebrado": só um erro de
11
+ * módulo-não-encontrado (`ERR_MODULE_NOT_FOUND`/`MODULE_NOT_FOUND`) é tratado como
12
+ * peer ausente (silencioso — é o caso esperado). Qualquer outro erro significa que
13
+ * o peer ESTÁ presente mas falhou ao carregar (import circular, erro de sintaxe,
14
+ * config inválida) — isso o doctor precisa gritar, senão um peer quebrado aparece
15
+ * como "ausente" e o diagnóstico mente.
16
+ */
17
+ export declare function canImport(specifier: string, logger?: CanImportLogger): Promise<boolean>;
18
+ /**
19
+ * Monta o `DoctorInput` a partir do app (config `authkit` resolvida, `session`, peers). Usado por
20
+ * `authkit:doctor` e pelos checks do `agora:doctor`. Lança se `config/authkit.ts` não resolver.
21
+ */
22
+ export declare function buildDoctorInput(app: ApplicationService, logger?: CanImportLogger): Promise<DoctorInput>;
23
+ /**
24
+ * O finding de idade da chave de assinatura managed (best-effort): `null` quando o jwks não é
25
+ * `managed` com store, ou quando o keystore não pôde ser lido.
26
+ */
27
+ export declare function managedSigningKeyAgeFinding(app: ApplicationService, authkitConfig: Record<string, any> | null): Promise<Finding | null>;
@@ -0,0 +1,86 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
9
+ import { DatabaseAdapter } from '../adapters/database_adapter.js';
10
+ import { resolveAuthkitConfig } from '../commands/resolve_config.js';
11
+ import { defaultEncryptForStore } from '../define_config.js';
12
+ import { signingKeyAgeDays } from '../keys/keystore.js';
13
+ import { KeystoreCodec } from '../keys/keystore_codec.js';
14
+ import { loadEncryptionService } from '../keys/keystore_crypto.js';
15
+ import { KeystoreManager, resolveKeystoreVault } from '../keys/keystore_manager.js';
16
+ import { signingKeyAgeFinding } from './checks.js';
17
+ /**
18
+ * Tenta importar um peer; `true` se importável.
19
+ *
20
+ * Distingue "não instalado" de "instalado mas quebrado": só um erro de
21
+ * módulo-não-encontrado (`ERR_MODULE_NOT_FOUND`/`MODULE_NOT_FOUND`) é tratado como
22
+ * peer ausente (silencioso — é o caso esperado). Qualquer outro erro significa que
23
+ * o peer ESTÁ presente mas falhou ao carregar (import circular, erro de sintaxe,
24
+ * config inválida) — isso o doctor precisa gritar, senão um peer quebrado aparece
25
+ * como "ausente" e o diagnóstico mente.
26
+ */
27
+ export async function canImport(specifier, logger) {
28
+ try {
29
+ await import(__rewriteRelativeImportExtension(specifier));
30
+ return true;
31
+ }
32
+ catch (err) {
33
+ const code = err?.code;
34
+ if (code !== 'ERR_MODULE_NOT_FOUND' && code !== 'MODULE_NOT_FOUND') {
35
+ logger?.warning(`⚠️ ${specifier} está instalado mas falhou ao carregar: ${err.message}`);
36
+ }
37
+ return false;
38
+ }
39
+ }
40
+ /**
41
+ * Monta o `DoctorInput` a partir do app (config `authkit` resolvida, `session`, peers). Usado por
42
+ * `authkit:doctor` e pelos checks do `agora:doctor`. Lança se `config/authkit.ts` não resolver.
43
+ */
44
+ export async function buildDoctorInput(app, logger) {
45
+ const config = await app.container.make('config');
46
+ // `defineConfig` exporta um config provider — resolve antes de inspecionar,
47
+ // senão todo campo aparece como "ausente" (provider cru não tem issuer etc.).
48
+ const authkitConfig = await resolveAuthkitConfig(app, config.get('authkit', null));
49
+ const sessionConfig = config.get('session', null) ?? null;
50
+ return {
51
+ authkitConfig,
52
+ sessionConfig,
53
+ peers: {
54
+ session: await canImport('@adonisjs/session', logger),
55
+ shield: await canImport('@adonisjs/shield', logger),
56
+ ally: await canImport('@adonisjs/ally', logger),
57
+ limiter: await canImport('@adonisjs/limiter', logger),
58
+ },
59
+ __adapterClasses: { DatabaseAdapter },
60
+ };
61
+ }
62
+ /**
63
+ * O finding de idade da chave de assinatura managed (best-effort): `null` quando o jwks não é
64
+ * `managed` com store, ou quando o keystore não pôde ser lido.
65
+ */
66
+ export async function managedSigningKeyAgeFinding(app, authkitConfig) {
67
+ const jwksInput = (authkitConfig?.jwksConfig ?? authkitConfig?.jwks);
68
+ if (jwksInput?.source !== 'managed' || !jwksInput?.store)
69
+ return null;
70
+ try {
71
+ const vault = resolveKeystoreVault(jwksInput.store, {
72
+ makePath: (p) => app.makePath(p),
73
+ container: app.container,
74
+ });
75
+ const encrypt = jwksInput.encrypt ?? defaultEncryptForStore(jwksInput.store);
76
+ const enc = encrypt ? await loadEncryptionService().catch(() => undefined) : undefined;
77
+ const mgr = new KeystoreManager(vault, new KeystoreCodec({ encrypt, enc }), jwksInput.algorithm ?? 'RS256');
78
+ const store = await mgr.read().catch(() => null);
79
+ const maxAge = jwksInput.rotationDays ?? 90;
80
+ return signingKeyAgeFinding(signingKeyAgeDays(store), maxAge);
81
+ }
82
+ catch {
83
+ /* idade é best-effort no doctor */
84
+ return null;
85
+ }
86
+ }
@@ -0,0 +1,5 @@
1
+ import type { AgoraDoctorCheck } from './agora_doctor.js';
2
+ export type { AgoraDoctorCheck, AgoraDoctorRegistry, AgoraDoctorReport, AgoraDoctorResult, AgoraDoctorStatus, } from './agora_doctor.js';
3
+ export { AGORA_DOCTOR_BINDING, registerDoctorChecks } from './agora_doctor.js';
4
+ /** authkit's checks, for `node ace agora:doctor --no-boot` (which imports this entry instead of booting). */
5
+ export declare const agoraDoctorChecks: AgoraDoctorCheck[];
@@ -0,0 +1,4 @@
1
+ import { authkitDoctorChecks } from './agora_checks.js';
2
+ export { AGORA_DOCTOR_BINDING, registerDoctorChecks } from './agora_doctor.js';
3
+ /** authkit's checks, for `node ace agora:doctor --no-boot` (which imports this entry instead of booting). */
4
+ export const agoraDoctorChecks = authkitDoctorChecks();
@@ -41,6 +41,10 @@ function fromJwtPayload(payload) {
41
41
  if (typeof payload.sub !== 'string' || !payload.sub)
42
42
  return null;
43
43
  const clientId = payload.client_id;
44
+ // No user behind it: a `client_credentials` token (a service tenant's, for one) has
45
+ // `sub` = the client id (RFC 9068 §2.2) and never authenticates an account.
46
+ if (payload.sub === clientId)
47
+ return null;
44
48
  return {
45
49
  format: 'jwt',
46
50
  sub: payload.sub,
@@ -209,6 +213,9 @@ export function remoteAccessTokenVerifier(options) {
209
213
  return null;
210
214
  if (typeof body.sub !== 'string' || !body.sub)
211
215
  return null;
216
+ // `client_credentials` token: no account behind it.
217
+ if (body.sub === body.client_id)
218
+ return null;
212
219
  if (typeof body.exp === 'number' && body.exp <= Math.floor(Date.now() / 1000))
213
220
  return null;
214
221
  return {
@@ -611,7 +611,7 @@ export default class AccountApiController {
611
611
  /** Personal agents com delegação ativa na conta (scopes aprovados). */
612
612
  async listAgents(ctx) {
613
613
  const runtime = await personalAgentsFor(ctx);
614
- if (!runtime?.delegation && !runtime?.poppy) {
614
+ if (!runtime || (!runtime.delegation && runtime.extensions.size === 0)) {
615
615
  return ctx.response
616
616
  .status(404)
617
617
  .send(apiErr('capability_unsupported', 'Personal agent delegation is not enabled.'));
@@ -620,16 +620,18 @@ export default class AccountApiController {
620
620
  const pact = runtime.delegation
621
621
  ? (await runtime.delegation.listGrants(userId)).map((g) => ({ ...g, protocol: 'pact' }))
622
622
  : [];
623
- const poppy = runtime.poppy
624
- ? (await runtime.poppy.listGrants(userId)).map((g) => ({ ...g, protocol: 'poppy' }))
625
- : [];
626
- return { agents: [...pact, ...poppy] };
623
+ const others = [];
624
+ for (const [protocol, { runtime: extension }] of runtime.extensions) {
625
+ for (const g of await extension.listAccountGrants(userId))
626
+ others.push({ ...g, protocol });
627
+ }
628
+ return { agents: [...pact, ...others] };
627
629
  }
628
630
  // ─── DELETE /account/api/agents/:id ──────────────────────────────────────
629
631
  /** Revoga a delegação de um personal agent (efeito imediato nos tokens). */
630
632
  async revokeAgent(ctx) {
631
633
  const runtime = await personalAgentsFor(ctx);
632
- if (!runtime?.delegation && !runtime?.poppy) {
634
+ if (!runtime || (!runtime.delegation && runtime.extensions.size === 0)) {
633
635
  return ctx.response
634
636
  .status(404)
635
637
  .send(apiErr('capability_unsupported', 'Personal agent delegation is not enabled.'));
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Abrir e fechar a sessão de CONTA do authkit (o cookie do console, `ACCOUNT_SESSION_KEY`) a partir
3
+ * de código de fora do host-kit — o navegador de um personal agent que entrou na conta, por
4
+ * exemplo. Sincroniza `ctx.auth` (`adonisAuth.guard`) como o login do próprio console.
5
+ *
6
+ * Estável no 1.x (usado pelas `personalAgents.extensions`).
7
+ */
8
+ import type { HttpContext } from '@adonisjs/core/http';
9
+ /** Liga a sessão de conta a `accountId` e sincroniza `ctx.auth`. Não regenera a sessão. */
10
+ export declare function signInAccountSession(ctx: HttpContext, accountId: string): Promise<void>;
11
+ /** Desliga a sessão de conta e sincroniza `ctx.auth`. Não regenera a sessão. */
12
+ export declare function signOutAccountSession(ctx: HttpContext): Promise<void>;
@@ -0,0 +1,19 @@
1
+ import { ACCOUNT_SESSION_KEY } from './account_session_key.js';
2
+ import { syncAdonisAuthLogin, syncAdonisAuthLogout } from './adonis_auth_sync.js';
3
+ /** Liga a sessão de conta a `accountId` e sincroniza `ctx.auth`. Não regenera a sessão. */
4
+ export async function signInAccountSession(ctx, accountId) {
5
+ // Sem augmentation no barrel (ver `augmentation_isolation.spec.ts`): a sessão é lida sem tipo.
6
+ const session = ctx.session;
7
+ const service = await ctx.containerResolver.make('authkit.server');
8
+ session.put(ACCOUNT_SESSION_KEY, accountId);
9
+ const account = await service.config.accountStore.findById(accountId);
10
+ if (account)
11
+ await syncAdonisAuthLogin(ctx, service.config, account);
12
+ }
13
+ /** Desliga a sessão de conta e sincroniza `ctx.auth`. Não regenera a sessão. */
14
+ export async function signOutAccountSession(ctx) {
15
+ const session = ctx.session;
16
+ const service = await ctx.containerResolver.make('authkit.server');
17
+ session.forget(ACCOUNT_SESSION_KEY);
18
+ await syncAdonisAuthLogout(ctx, service.config);
19
+ }
@@ -8,8 +8,11 @@ import type { SettingsCapability } from '../runtime_settings.js';
8
8
  export interface AdminActor {
9
9
  actorId: string | null;
10
10
  ip: string | null;
11
- /** Marca metadata da auditoria — 'admin-api' nas escritas via REST, 'admin' no console HTML. */
12
- source?: 'admin-api' | 'admin';
11
+ /**
12
+ * Marca metadata da auditoria — 'admin-api' nas escritas via REST, 'admin' no console HTML,
13
+ * 'mcp' nas ações dos dev tools (endpoint MCP do Agora).
14
+ */
15
+ source?: 'admin-api' | 'admin' | 'mcp';
13
16
  }
14
17
  /** Resultado da deleção via admin: false quando o store não suporta hard delete. */
15
18
  export type DeleteUserResult = {
@@ -6,6 +6,7 @@ import type { HttpContext } from '@adonisjs/core/http';
6
6
  * em create/regenerate. Audita create/update/delete.
7
7
  */
8
8
  export default class ApiClientsController {
9
+ #private;
9
10
  index(ctx: HttpContext): Promise<{
10
11
  data: {
11
12
  clientId: string;
@@ -19,6 +20,8 @@ export default class ApiClientsController {
19
20
  backchannelLogoutSessionRequired: boolean;
20
21
  clientName: string | null;
21
22
  logoUri: string | null;
23
+ disabled: boolean;
24
+ serviceTenant: import("../../service_tenants/client_metadata.ts").ServiceTenantInfo | null;
22
25
  }[];
23
26
  canList: boolean;
24
27
  }>;
@@ -34,6 +37,8 @@ export default class ApiClientsController {
34
37
  backchannelLogoutSessionRequired: boolean;
35
38
  clientName: string | null;
36
39
  logoUri: string | null;
40
+ disabled: boolean;
41
+ serviceTenant: import("../../service_tenants/client_metadata.ts").ServiceTenantInfo | null;
37
42
  }>;
38
43
  store(ctx: HttpContext): Promise<void | {
39
44
  clientId: string;
@@ -51,11 +56,43 @@ export default class ApiClientsController {
51
56
  backchannelLogoutSessionRequired: boolean;
52
57
  clientName: string | null;
53
58
  logoUri: string | null;
59
+ disabled: boolean;
60
+ serviceTenant: import("../../service_tenants/client_metadata.ts").ServiceTenantInfo | null;
54
61
  }>;
55
62
  regenerateSecret(ctx: HttpContext): Promise<void | {
56
63
  clientId: any;
57
64
  clientSecret: string;
58
65
  }>;
66
+ disable(ctx: HttpContext): Promise<void | {
67
+ clientId: string;
68
+ confidential: boolean;
69
+ grantTypes: string[];
70
+ redirectUris: string[];
71
+ postLogoutRedirectUris: string[];
72
+ tokenEndpointAuthMethod: string;
73
+ applicationType: import("../client_metadata.js").ApplicationType;
74
+ backchannelLogoutUri: string | null;
75
+ backchannelLogoutSessionRequired: boolean;
76
+ clientName: string | null;
77
+ logoUri: string | null;
78
+ disabled: boolean;
79
+ serviceTenant: import("../../service_tenants/client_metadata.ts").ServiceTenantInfo | null;
80
+ }>;
81
+ enable(ctx: HttpContext): Promise<void | {
82
+ clientId: string;
83
+ confidential: boolean;
84
+ grantTypes: string[];
85
+ redirectUris: string[];
86
+ postLogoutRedirectUris: string[];
87
+ tokenEndpointAuthMethod: string;
88
+ applicationType: import("../client_metadata.js").ApplicationType;
89
+ backchannelLogoutUri: string | null;
90
+ backchannelLogoutSessionRequired: boolean;
91
+ clientName: string | null;
92
+ logoUri: string | null;
93
+ disabled: boolean;
94
+ serviceTenant: import("../../service_tenants/client_metadata.ts").ServiceTenantInfo | null;
95
+ }>;
59
96
  destroy(ctx: HttpContext): Promise<{
60
97
  clientId: any;
61
98
  deleted: boolean;
@@ -85,11 +85,16 @@ export default class ApiClientsController {
85
85
  const id = ctx.request.param('id');
86
86
  try {
87
87
  const secret = await svc.regenerateSecret(id);
88
+ const tenant = (await svc.find(id))?.serviceTenant;
88
89
  await service.config.audit?.record({
89
90
  type: 'client.updated',
90
91
  clientId: id,
91
92
  ip: ctx.request.ip?.() ?? null,
92
- metadata: { actor: 'admin-api', action: 'regenerate_secret' },
93
+ metadata: {
94
+ actor: 'admin-api',
95
+ action: 'regenerate_secret',
96
+ ...(tenant ? { partition: tenant.partition } : {}),
97
+ },
93
98
  });
94
99
  return { clientId: id, clientSecret: secret };
95
100
  }
@@ -97,6 +102,31 @@ export default class ApiClientsController {
97
102
  return ctx.response.conflict(apiError('cannot_regenerate', e.message));
98
103
  }
99
104
  }
105
+ async disable(ctx) {
106
+ return this.#setDisabled(ctx, true);
107
+ }
108
+ async enable(ctx) {
109
+ return this.#setDisabled(ctx, false);
110
+ }
111
+ /** Disabled clients get `invalid_client` from the next request on. */
112
+ async #setDisabled(ctx, disabled) {
113
+ const { service, svc } = await clientsService(ctx);
114
+ const id = ctx.request.param('id');
115
+ if (!(await svc.setDisabled(id, disabled))) {
116
+ return ctx.response.notFound(apiError('not_found', 'Client não encontrado.'));
117
+ }
118
+ const client = await svc.find(id);
119
+ await service.config.audit?.record({
120
+ type: disabled ? 'client.disabled' : 'client.enabled',
121
+ clientId: id,
122
+ ip: ctx.request.ip?.() ?? null,
123
+ metadata: {
124
+ actor: 'admin-api',
125
+ ...(client?.serviceTenant ? { partition: client.serviceTenant.partition } : {}),
126
+ },
127
+ });
128
+ return clientDto(client);
129
+ }
100
130
  async destroy(ctx) {
101
131
  const { service, svc } = await clientsService(ctx);
102
132
  const id = ctx.request.param('id');
@@ -40,6 +40,8 @@ export declare function clientDto(client: AdminClient): {
40
40
  backchannelLogoutSessionRequired: boolean;
41
41
  clientName: string | null;
42
42
  logoUri: string | null;
43
+ disabled: boolean;
44
+ serviceTenant: import("../../service_tenants/client_metadata.ts").ServiceTenantInfo | null;
43
45
  };
44
46
  /** Inclui o secret (mostrado UMA vez) em create/regenerate. */
45
47
  export declare function createdClientDto(created: CreatedClient): {
@@ -67,7 +69,7 @@ export declare function grantDto(grant: AdminGrant): {
67
69
  };
68
70
  export declare function auditDto(event: StoredAuditEvent): {
69
71
  id: string;
70
- type: "account.deleted" | "account.expiration_warned" | "account.expired_login_blocked" | "account.exported" | "account.locked" | "account.signed_out_all" | "agent.allowed" | "agent.blocked" | "agent.delegation_approved" | "agent.delegation_denied" | "agent.grant_revoked" | "agent.grant_revoked_by_admin" | "agent.registry_removed" | "bot_protection.rejected" | "client.created" | "client.deleted" | "client.secret_regenerated" | "client.updated" | "email.change_requested" | "email.changed" | "email_change.cancelled" | "email_change.confirmed" | "email_change.requested" | "email_verification.consumed" | "email_verification.issued" | "grant.revoked_by_user" | "impersonation" | "impersonation.panel_viewed" | "impersonation.stopped" | "keys.rotated" | "login.failure" | "login.magic_link_sent" | "login.new_device" | "login.new_ip_notified" | "login.otp_failed" | "login.otp_invalidated" | "login.otp_sent" | "login.otp_verified" | "login.success" | "maintenance.disabled" | "maintenance.enabled" | "mfa.disabled" | "mfa.enabled" | "mfa.recovery_codes_regenerated" | "organization.created" | "organization.deactivated" | "organization.deleted" | "organization.invitation_accepted" | "organization.invitation_revoked" | "organization.invitation_sent" | "organization.member_added" | "organization.member_removed" | "organization.member_role_changed" | "organization.member_role_updated" | "organization.switched" | "organization.updated" | "otp.locked" | "otp.unlock_failed" | "otp.unlocked" | "passkey.registered" | "passkey.removed" | "password.changed" | "password.expired_change_forced" | "password.rehashed" | "password_reset.consumed" | "password_reset.issued" | "pat.issued" | "pat.revoked" | "pat.used" | "profile.updated" | "roles_catalog.updated" | "security_notice.sent" | "session.revoked" | "session.revoked_all" | "session.single_enforced" | "settings.updated" | "signup" | "sudo.confirmed" | "trusted_device.revoked" | "user.created" | "user.deleted" | "user.disabled" | "user.enabled" | "user.password_reset_sent";
72
+ type: "account.deleted" | "account.expiration_warned" | "account.expired_login_blocked" | "account.exported" | "account.extension.action" | "account.extension.redirect" | "account.locked" | "account.signed_out_all" | "agent.allowed" | "agent.blocked" | "agent.delegation_approved" | "agent.delegation_denied" | "agent.grant_revoked" | "agent.grant_revoked_by_admin" | "agent.registry_removed" | "bot_protection.rejected" | "client.created" | "client.deleted" | "client.disabled" | "client.enabled" | "client.secret_regenerated" | "client.updated" | "email.change_requested" | "email.changed" | "email_change.cancelled" | "email_change.confirmed" | "email_change.requested" | "email_verification.consumed" | "email_verification.issued" | "grant.revoked_by_user" | "impersonation" | "impersonation.panel_viewed" | "impersonation.stopped" | "keys.rotated" | "login.failure" | "login.magic_link_sent" | "login.new_device" | "login.new_ip_notified" | "login.otp_failed" | "login.otp_invalidated" | "login.otp_sent" | "login.otp_verified" | "login.success" | "maintenance.disabled" | "maintenance.enabled" | "mfa.disabled" | "mfa.enabled" | "mfa.recovery_codes_regenerated" | "organization.created" | "organization.deactivated" | "organization.deleted" | "organization.invitation_accepted" | "organization.invitation_revoked" | "organization.invitation_sent" | "organization.member_added" | "organization.member_removed" | "organization.member_role_changed" | "organization.member_role_updated" | "organization.switched" | "organization.updated" | "otp.locked" | "otp.unlock_failed" | "otp.unlocked" | "passkey.registered" | "passkey.removed" | "password.changed" | "password.expired_change_forced" | "password.rehashed" | "password_reset.consumed" | "password_reset.issued" | "pat.issued" | "pat.revoked" | "pat.used" | "profile.updated" | "roles_catalog.updated" | "security_notice.sent" | "service_tenant.token_issued" | "session.revoked" | "session.revoked_all" | "session.single_enforced" | "settings.updated" | "signup" | "sudo.confirmed" | "trusted_device.revoked" | "user.created" | "user.deleted" | "user.disabled" | "user.enabled" | "user.password_reset_sent";
71
73
  accountId: string | null;
72
74
  email: string | null;
73
75
  clientId: string | null;
@@ -32,6 +32,8 @@ export function clientDto(client) {
32
32
  backchannelLogoutSessionRequired: client.backchannelLogoutSessionRequired ?? false,
33
33
  clientName: client.clientName,
34
34
  logoUri: client.logoUri,
35
+ disabled: client.disabled,
36
+ serviceTenant: client.serviceTenant,
35
37
  };
36
38
  }
37
39
  /** Inclui o secret (mostrado UMA vez) em create/regenerate. */
@@ -0,0 +1,8 @@
1
+ import { type ConsoleAuthorize, type ConsoleAuthorizeDecision } from '../console_authorize.js';
2
+ /**
3
+ * Whether the app runs in production. Unknown (the provider did not register the
4
+ * app) counts as production, so an omitted `admin.authorize` fails closed.
5
+ */
6
+ export declare function appInProduction(): boolean;
7
+ /** The resolved `admin.authorize` checker (memoized per configured value). Never throws. */
8
+ export declare function adminAuthorizeChecker(authorize: ConsoleAuthorize): (ctx: any) => Promise<ConsoleAuthorizeDecision>;
@@ -0,0 +1,25 @@
1
+ import { getBootedApp } from '../../services/booted_app.js';
2
+ import { resolveConsoleAuthorize, } from '../console_authorize.js';
3
+ /**
4
+ * Whether the app runs in production. Unknown (the provider did not register the
5
+ * app) counts as production, so an omitted `admin.authorize` fails closed.
6
+ */
7
+ export function appInProduction() {
8
+ try {
9
+ return getBootedApp().inProduction !== false;
10
+ }
11
+ catch {
12
+ return true;
13
+ }
14
+ }
15
+ /** One checker per configured `admin.authorize`, so a lazy import runs once. */
16
+ const adminAuthorizeCheckers = new WeakMap();
17
+ /** The resolved `admin.authorize` checker (memoized per configured value). Never throws. */
18
+ export function adminAuthorizeChecker(authorize) {
19
+ let checker = adminAuthorizeCheckers.get(authorize);
20
+ if (!checker) {
21
+ checker = resolveConsoleAuthorize(authorize);
22
+ adminAuthorizeCheckers.set(authorize, checker);
23
+ }
24
+ return checker;
25
+ }
@@ -1,4 +1,5 @@
1
1
  import type { OidcService } from '../provider/oidc_service.js';
2
+ import { type ServiceTenantInfo } from '../service_tenants/client_metadata.js';
2
3
  import { type ApplicationType } from './client_metadata.js';
3
4
  /** Métodos de autenticação no token endpoint suportados pelo formulário admin. */
4
5
  export type TokenEndpointAuthMethod = 'client_secret_basic' | 'client_secret_post' | 'none';
@@ -30,6 +31,15 @@ export interface ClientInput {
30
31
  * `backchannelLogoutUri` não está definido.
31
32
  */
32
33
  backchannelLogoutSessionRequired?: boolean;
34
+ /**
35
+ * Makes the client a service tenant: confidential, `client_credentials` only, no
36
+ * redirect URIs; its tokens carry `partition` and `capabilities`. On update, `null`
37
+ * turns it back into a regular client; absent keeps the current value.
38
+ */
39
+ serviceTenant?: {
40
+ partition: string;
41
+ capabilities?: string[];
42
+ } | null;
33
43
  }
34
44
  /** Client persistido, apresentado ao console admin. */
35
45
  export interface AdminClient {
@@ -49,6 +59,10 @@ export interface AdminClient {
49
59
  clientName: string | null;
50
60
  /** `logo_uri` registrado, ou `null`. */
51
61
  logoUri: string | null;
62
+ /** Disabled clients get no tokens (`invalid_client`) until enabled again. */
63
+ disabled: boolean;
64
+ /** Service-tenant settings, or `null` for a regular client. */
65
+ serviceTenant: ServiceTenantInfo | null;
52
66
  }
53
67
  /** Resultado de uma criação: o client + o secret em claro (mostrado UMA vez). */
54
68
  export interface CreatedClient {
@@ -94,6 +108,12 @@ export declare class AdminClientsService {
94
108
  * PRESERVANDO o client_secret existente. Lança se o client não existe.
95
109
  */
96
110
  update(clientId: string, input: Partial<ClientInput>): Promise<void>;
111
+ /**
112
+ * Disables or re-enables a client. A disabled client gets `invalid_client` on every
113
+ * endpoint from the next request on (tokens already issued stay valid until they
114
+ * expire). Returns `false` when the client does not exist.
115
+ */
116
+ setDisabled(clientId: string, disabled: boolean): Promise<boolean>;
97
117
  /**
98
118
  * Regenera o client_secret de um client confidencial, preservando o resto da
99
119
  * metadata. Retorna o novo secret em claro (mostrado UMA vez). Lança se o
@@ -1,4 +1,5 @@
1
1
  import { randomBytes } from 'node:crypto';
2
+ import { CLIENT_DISABLED_KEY, isClientDisabled, normalizeServiceTenant, SERVICE_TENANT_KEY, serviceTenantOf, } from '../service_tenants/client_metadata.js';
2
3
  import { assertClientMetadata, ClientMetadataError, } from './client_metadata.js';
3
4
  import { CONSOLE_SSO_CLIENT_ID } from './console_sso.js';
4
5
  /** Métodos públicos (sem segredo) — espelham os "non-secret" do oidc-provider. */
@@ -111,14 +112,37 @@ export class AdminClientsService {
111
112
  logoUri: input.logoUri === null
112
113
  ? undefined
113
114
  : (input.logoUri ?? existing.logo_uri),
115
+ serviceTenant: input.serviceTenant === undefined
116
+ ? serviceTenantOf(existing)
117
+ : input.serviceTenant,
114
118
  };
115
119
  const confidential = !PUBLIC_AUTH_METHODS.has(merged.tokenEndpointAuthMethod);
116
120
  // Mantém o secret atual se continua confidencial; se virou public, remove-o.
117
121
  const clientSecret = confidential ? (previousSecret ?? randomId()) : undefined;
118
122
  const payload = this.#buildPayload(clientId, merged, clientSecret);
123
+ if (isClientDisabled(existing))
124
+ payload[CLIENT_DISABLED_KEY] = true;
119
125
  await this.#adapter.upsert(clientId, payload, 0);
120
126
  await this.oidc.evictDynamicClientCache();
121
127
  }
128
+ /**
129
+ * Disables or re-enables a client. A disabled client gets `invalid_client` on every
130
+ * endpoint from the next request on (tokens already issued stay valid until they
131
+ * expire). Returns `false` when the client does not exist.
132
+ */
133
+ async setDisabled(clientId, disabled) {
134
+ const existing = await this.#adapter.find(clientId);
135
+ if (!existing)
136
+ return false;
137
+ const payload = { ...existing };
138
+ if (disabled)
139
+ payload[CLIENT_DISABLED_KEY] = true;
140
+ else
141
+ delete payload[CLIENT_DISABLED_KEY];
142
+ await this.#adapter.upsert(clientId, payload, 0);
143
+ await this.oidc.evictDynamicClientCache();
144
+ return true;
145
+ }
122
146
  /**
123
147
  * Regenera o client_secret de um client confidencial, preservando o resto da
124
148
  * metadata. Retorna o novo secret em claro (mostrado UMA vez). Lança se o
@@ -148,6 +172,8 @@ export class AdminClientsService {
148
172
  * `client_metadata.ts`). Lança `ClientMetadataError`.
149
173
  */
150
174
  #assertValid(input) {
175
+ if (input.serviceTenant)
176
+ this.#assertServiceTenant(input);
151
177
  assertClientMetadata({
152
178
  applicationType: input.applicationType ?? 'web',
153
179
  tokenEndpointAuthMethod: input.tokenEndpointAuthMethod,
@@ -156,6 +182,27 @@ export class AdminClientsService {
156
182
  postLogoutRedirectUris: input.postLogoutRedirectUris,
157
183
  });
158
184
  }
185
+ /** Rules of a service-tenant client (see {@link ClientInput.serviceTenant}). */
186
+ #assertServiceTenant(input) {
187
+ if (!this.oidc.config.serviceTenants?.enabled) {
188
+ throw new ClientMetadataError('service tenants are not enabled: declare serviceTenants.resources in config/authkit.ts');
189
+ }
190
+ if (PUBLIC_AUTH_METHODS.has(input.tokenEndpointAuthMethod)) {
191
+ throw new ClientMetadataError('a service tenant must be confidential (it has a secret)');
192
+ }
193
+ if ((input.applicationType ?? 'web') !== 'web') {
194
+ throw new ClientMetadataError('a service tenant must be a web (server) client');
195
+ }
196
+ if (input.grantTypes.length !== 1 || input.grantTypes[0] !== 'client_credentials') {
197
+ throw new ClientMetadataError('a service tenant can only use the client_credentials grant');
198
+ }
199
+ if (input.redirectUris.length || input.postLogoutRedirectUris.length) {
200
+ throw new ClientMetadataError('a service tenant has no redirect URIs');
201
+ }
202
+ if (input.backchannelLogoutUri) {
203
+ throw new ClientMetadataError('a service tenant has no back-channel logout URI');
204
+ }
205
+ }
159
206
  /**
160
207
  * Monta o payload na forma snake_case que o oidc-provider espera/persiste —
161
208
  * verificada contra o que o registro dinâmico (RFC 7591) grava. As chaves de
@@ -193,6 +240,12 @@ export class AdminClientsService {
193
240
  payload.backchannel_logout_session_required = input.backchannelLogoutSessionRequired;
194
241
  }
195
242
  }
243
+ if (input.serviceTenant) {
244
+ const tenant = normalizeServiceTenant(input.serviceTenant);
245
+ if (typeof tenant === 'string')
246
+ throw new ClientMetadataError(tenant);
247
+ payload[SERVICE_TENANT_KEY] = tenant;
248
+ }
196
249
  return payload;
197
250
  }
198
251
  /** Projeta um payload persistido para a forma exibida no console admin. */
@@ -209,6 +262,8 @@ export class AdminClientsService {
209
262
  applicationType: p.application_type === 'native' ? 'native' : 'web',
210
263
  clientName: typeof p.client_name === 'string' && p.client_name ? p.client_name : null,
211
264
  logoUri: typeof p.logo_uri === 'string' && p.logo_uri ? p.logo_uri : null,
265
+ disabled: isClientDisabled(p),
266
+ serviceTenant: serviceTenantOf(p),
212
267
  };
213
268
  if (p.backchannel_logout_uri) {
214
269
  result.backchannelLogoutUri = p.backchannel_logout_uri;
@@ -0,0 +1,14 @@
1
+ import type { HttpContext } from '@adonisjs/core/http';
2
+ import { type AgoraConsoleEntry, type AgoraConsoleRegistry, type AgoraConsoleSearchHit } from '../../console_switcher.js';
3
+ /** The admin console's id in the `'agora.consoles'` registry. */
4
+ export declare const ADMIN_CONSOLE_ID = "authkit";
5
+ /** The `'agora.consoles'` registry, or `null` (none bound, or no booted app). */
6
+ export declare function consoleRegistry(): Promise<AgoraConsoleRegistry | null>;
7
+ /**
8
+ * Global search of the admin console: users by email (as the account store's `listAccounts`
9
+ * search matches them), and organizations by name or slug.
10
+ * Each hit opens the matching drawer in the console.
11
+ */
12
+ export declare function searchAdminConsole(ctx: HttpContext, adminPrefix: string, query: string, limit: number): Promise<AgoraConsoleSearchHit[]>;
13
+ /** The admin console's registry entry, for the shell mounted at `adminPrefix`. */
14
+ export declare function adminConsoleEntry(adminPrefix: string, authorize: AgoraConsoleEntry['authorize']): AgoraConsoleEntry;