@adonis-agora/authkit-server 1.0.0-rc.0 → 1.0.0-rc.2

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 (91) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +4 -5
  3. package/build/index.d.ts +7 -11
  4. package/build/index.js +5 -9
  5. package/build/providers/authkit_server_provider.js +14 -8
  6. package/build/src/agents/config.d.ts +9 -13
  7. package/build/src/agents/config.js +17 -11
  8. package/build/src/agents/delegation_service.js +3 -3
  9. package/build/src/agents/delegation_store.d.ts +3 -2
  10. package/build/src/agents/delegation_store.js +6 -5
  11. package/build/src/agents/extension.d.ts +261 -0
  12. package/build/src/agents/extension.js +40 -0
  13. package/build/src/agents/middleware.js +1 -1
  14. package/build/src/agents/protocol.d.ts +4 -4
  15. package/build/src/agents/registry_store.d.ts +3 -2
  16. package/build/src/agents/registry_store.js +1 -1
  17. package/build/src/agents/runtime.d.ts +26 -7
  18. package/build/src/agents/runtime.js +110 -75
  19. package/build/src/audit/audit_sink.js +1 -1
  20. package/build/src/console_authorize.d.ts +56 -0
  21. package/build/src/console_authorize.js +85 -0
  22. package/build/src/define_config.d.ts +20 -14
  23. package/build/src/define_config.js +11 -8
  24. package/build/src/doctor/checks.d.ts +7 -1
  25. package/build/src/doctor/checks.js +47 -1
  26. package/build/src/host/account_api/account_api_controller.js +8 -6
  27. package/build/src/host/account_session_sign_in.d.ts +12 -0
  28. package/build/src/host/account_session_sign_in.js +19 -0
  29. package/build/src/host/admin_clients_service.js +10 -1
  30. package/build/src/host/admin_console/console_personal_agents_controller.d.ts +1 -6
  31. package/build/src/host/admin_console/console_personal_agents_controller.js +50 -30
  32. package/build/src/host/admin_console_warning.d.ts +8 -0
  33. package/build/src/host/admin_console_warning.js +18 -0
  34. package/build/src/host/admin_sessions_service.js +8 -5
  35. package/build/src/host/auth_host_config.d.ts +11 -2
  36. package/build/src/host/console_denied_page.d.ts +6 -0
  37. package/build/src/host/console_denied_page.js +18 -0
  38. package/build/src/host/console_sso.d.ts +24 -0
  39. package/build/src/host/console_sso.js +70 -0
  40. package/build/src/host/controllers/account_apps_controller.js +15 -11
  41. package/build/src/host/controllers/account_session_controller.js +10 -1
  42. package/build/src/host/controllers/console_sso_controller.d.ts +12 -0
  43. package/build/src/host/controllers/console_sso_controller.js +61 -0
  44. package/build/src/host/controllers/interaction_controller.js +3 -1
  45. package/build/src/host/csrf.d.ts +3 -3
  46. package/build/src/host/csrf.js +3 -13
  47. package/build/src/host/default_mailer.d.ts +11 -6
  48. package/build/src/host/default_mailer.js +16 -12
  49. package/build/src/host/i18n.d.ts +3 -14
  50. package/build/src/host/i18n.js +15 -13
  51. package/build/src/host/register_auth_host.d.ts +2 -0
  52. package/build/src/host/register_auth_host.js +110 -64
  53. package/build/src/host/ui-dist/assets/index-BqyJYRVX.js +67 -0
  54. package/build/src/host/ui-dist/index.html +1 -1
  55. package/build/src/provider/build_provider.js +6 -0
  56. package/build/src/provider/oidc_service.d.ts +1 -1
  57. package/build/src/provider/oidc_service.js +1 -1
  58. package/build/src/schema/ensure.d.ts +14 -0
  59. package/build/src/schema/ensure.js +43 -150
  60. package/package.json +5 -5
  61. package/build/src/agents/poppy/client_registry.d.ts +0 -50
  62. package/build/src/agents/poppy/client_registry.js +0 -260
  63. package/build/src/agents/poppy/config.d.ts +0 -396
  64. package/build/src/agents/poppy/config.js +0 -228
  65. package/build/src/agents/poppy/dpop.d.ts +0 -61
  66. package/build/src/agents/poppy/dpop.js +0 -158
  67. package/build/src/agents/poppy/email_code_sign_in.d.ts +0 -27
  68. package/build/src/agents/poppy/email_code_sign_in.js +0 -140
  69. package/build/src/agents/poppy/errors.d.ts +0 -25
  70. package/build/src/agents/poppy/errors.js +0 -25
  71. package/build/src/agents/poppy/replay.d.ts +0 -25
  72. package/build/src/agents/poppy/replay.js +0 -106
  73. package/build/src/agents/poppy/resource.d.ts +0 -100
  74. package/build/src/agents/poppy/resource.js +0 -129
  75. package/build/src/agents/poppy/safe_fetch.d.ts +0 -48
  76. package/build/src/agents/poppy/safe_fetch.js +0 -262
  77. package/build/src/agents/poppy/service.d.ts +0 -293
  78. package/build/src/agents/poppy/service.js +0 -1458
  79. package/build/src/agents/poppy/session_jwt.d.ts +0 -30
  80. package/build/src/agents/poppy/session_jwt.js +0 -133
  81. package/build/src/agents/poppy/store.d.ts +0 -106
  82. package/build/src/agents/poppy/store.js +0 -261
  83. package/build/src/agents/poppy/web_session.d.ts +0 -39
  84. package/build/src/agents/poppy/web_session.js +0 -122
  85. package/build/src/host/controllers/poppy_consent_controller.d.ts +0 -19
  86. package/build/src/host/controllers/poppy_consent_controller.js +0 -199
  87. package/build/src/host/controllers/poppy_controller.d.ts +0 -33
  88. package/build/src/host/controllers/poppy_controller.js +0 -219
  89. package/build/src/host/poppy_session_key.d.ts +0 -9
  90. package/build/src/host/poppy_session_key.js +0 -9
  91. package/build/src/host/ui-dist/assets/index-D0EIeyKK.js +0 -67
@@ -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
+ }
@@ -1,5 +1,6 @@
1
1
  import { randomBytes } from 'node:crypto';
2
- import { assertClientMetadata } from './client_metadata.js';
2
+ import { assertClientMetadata, ClientMetadataError, } from './client_metadata.js';
3
+ import { CONSOLE_SSO_CLIENT_ID } from './console_sso.js';
3
4
  /** Métodos públicos (sem segredo) — espelham os "non-secret" do oidc-provider. */
4
5
  const PUBLIC_AUTH_METHODS = new Set(['none']);
5
6
  /** Gera um identificador opaco no estilo do oidc-provider (~43 chars base64url). */
@@ -51,6 +52,7 @@ export class AdminClientsService {
51
52
  */
52
53
  async create(input) {
53
54
  const clientId = (input.clientId ?? '').trim() || randomId();
55
+ assertNotReservedClientId(clientId);
54
56
  const confidential = !PUBLIC_AUTH_METHODS.has(input.tokenEndpointAuthMethod);
55
57
  const clientSecret = confidential ? randomId() : undefined;
56
58
  const payload = this.#buildPayload(clientId, input, clientSecret);
@@ -68,6 +70,7 @@ export class AdminClientsService {
68
70
  */
69
71
  async createWithSecret(input, providedSecret) {
70
72
  const clientId = (input.clientId ?? '').trim() || randomId();
73
+ assertNotReservedClientId(clientId);
71
74
  const confidential = !PUBLIC_AUTH_METHODS.has(input.tokenEndpointAuthMethod);
72
75
  const clientSecret = confidential ? (providedSecret ?? randomId()) : undefined;
73
76
  const payload = this.#buildPayload(clientId, input, clientSecret);
@@ -215,3 +218,9 @@ export class AdminClientsService {
215
218
  return result;
216
219
  }
217
220
  }
221
+ /** O `client_id` do client interno do console (ver `console_sso.ts`) não pode ser cadastrado. */
222
+ function assertNotReservedClientId(clientId) {
223
+ if (clientId === CONSOLE_SSO_CLIENT_ID) {
224
+ throw new ClientMetadataError(`client_id "${clientId}" é reservado pelo authkit`);
225
+ }
226
+ }
@@ -22,7 +22,7 @@ interface AgentEntry {
22
22
  connections: number;
23
23
  }
24
24
  /**
25
- * Endpoints JSON de personal agents (PACT e Poppy) do console admin React.
25
+ * Endpoints JSON de personal agents (PACT e `personalAgents.extensions`) do console admin React.
26
26
  *
27
27
  * GET {prefix}/api/personal-agents → agentes (config + console + conectados)
28
28
  * GET {prefix}/api/personal-agents/connections?clientId=&accountId=&protocol=&page=&size=
@@ -41,11 +41,6 @@ export default class ConsolePersonalAgentsController {
41
41
  /** A lista do config é uma função do host (o console não enxerga). */
42
42
  hostResolver: boolean;
43
43
  } | null;
44
- poppy: {
45
- requireRegistration: boolean;
46
- hostResolver: boolean;
47
- allowInsecureClientIds: boolean;
48
- } | null;
49
44
  };
50
45
  agents: AgentEntry[];
51
46
  }>;
@@ -31,10 +31,21 @@ function enabledProtocols(runtime) {
31
31
  const out = [];
32
32
  if (runtime.config.pact)
33
33
  out.push('pact');
34
- if (runtime.config.poppy)
35
- out.push('poppy');
34
+ out.push(...runtime.extensions.keys());
36
35
  return out;
37
36
  }
37
+ /** O protocolo dono de um grant, pelo prefixo do id. */
38
+ function protocolOfGrant(runtime, grantId) {
39
+ for (const [id, { extension }] of runtime.extensions) {
40
+ if (grantId.startsWith(extension.grantIdPrefix))
41
+ return id;
42
+ }
43
+ return 'pact';
44
+ }
45
+ /** Prefixo de id dos grants de um protocolo (`agrant_` = PACT). */
46
+ function grantPrefixOf(runtime, protocol) {
47
+ return runtime.extensions.get(protocol)?.extension.grantIdPrefix ?? 'agrant_';
48
+ }
38
49
  function isUrl(value, allowHttp) {
39
50
  if (typeof value !== 'string' || value.length > 2048)
40
51
  return false;
@@ -52,14 +63,14 @@ function isUrl(value, allowHttp) {
52
63
  async function agentName(runtime, protocol, identifier, fallback) {
53
64
  if (fallback)
54
65
  return fallback;
55
- if (protocol === 'poppy' && runtime.poppy) {
56
- return runtime.poppy.registry.displayName(identifier);
57
- }
66
+ const extension = runtime.extensions.get(protocol);
67
+ if (extension)
68
+ return extension.runtime.displayName(identifier);
58
69
  const registration = await runtime.config.resolveAgent(identifier);
59
70
  return displayNameFor(registration ?? { issuer: identifier, jwksUri: '' });
60
71
  }
61
72
  /**
62
- * Endpoints JSON de personal agents (PACT e Poppy) do console admin React.
73
+ * Endpoints JSON de personal agents (PACT e `personalAgents.extensions`) do console admin React.
63
74
  *
64
75
  * GET {prefix}/api/personal-agents → agentes (config + console + conectados)
65
76
  * GET {prefix}/api/personal-agents/connections?clientId=&accountId=&protocol=&page=&size=
@@ -105,17 +116,20 @@ export default class ConsolePersonalAgentsController {
105
116
  e.jwksUri = agent.jwksUri;
106
117
  names.set(keyOf('pact', agent.issuer), agent.name ?? null);
107
118
  }
108
- for (const [clientId, count] of await runtime.grants.countLiveGrantsByClient(now, 'pact')) {
119
+ for (const [clientId, count] of await runtime.grants.countLiveGrantsByClient(now, 'agrant_')) {
109
120
  entry('pact', clientId).connections = count;
110
121
  }
111
122
  }
112
- if (cfg.poppy) {
113
- for (const id of cfg.poppy.clients.registeredList ?? [])
114
- entry('poppy', id).config = 'allowed';
115
- for (const id of cfg.poppy.clients.blockedList ?? [])
116
- entry('poppy', id).config = 'blocked';
117
- for (const [clientId, count] of await runtime.grants.countLiveGrantsByClient(now, 'poppy')) {
118
- entry('poppy', clientId).connections = count;
123
+ const summaries = {};
124
+ for (const [protocol, { extension, runtime: ext }] of runtime.extensions) {
125
+ const summary = ext.consoleSummary();
126
+ summaries[protocol] = { label: extension.label, ...summary };
127
+ for (const id of summary.configured.allowed)
128
+ entry(protocol, id).config = 'allowed';
129
+ for (const id of summary.configured.blocked)
130
+ entry(protocol, id).config = 'blocked';
131
+ for (const [clientId, count] of await runtime.grants.countLiveGrantsByClient(now, extension.grantIdPrefix)) {
132
+ entry(protocol, clientId).connections = count;
119
133
  }
120
134
  }
121
135
  for (const row of await runtime.registry.list()) {
@@ -153,13 +167,17 @@ export default class ConsolePersonalAgentsController {
153
167
  hostResolver: cfg.staticAgents === null,
154
168
  }
155
169
  : null,
156
- poppy: cfg.poppy
157
- ? {
158
- requireRegistration: cfg.poppy.clients.requireRegistration,
159
- hostResolver: cfg.poppy.clients.registeredList === null || cfg.poppy.clients.blockedList === null,
160
- allowInsecureClientIds: cfg.poppy.allowInsecureClientIds,
161
- }
162
- : null,
170
+ ...Object.fromEntries(Object.entries(summaries).map(([protocol, summary]) => [
171
+ protocol,
172
+ {
173
+ label: summary.label,
174
+ requireRegistration: summary.requireRegistration,
175
+ hostResolver: summary.hostResolver,
176
+ allowHttpIdentifiers: summary.allowHttpIdentifiers,
177
+ identifierLabel: summary.identifierLabel,
178
+ identifierPlaceholder: summary.identifierPlaceholder ?? null,
179
+ },
180
+ ])),
163
181
  },
164
182
  agents,
165
183
  };
@@ -172,11 +190,9 @@ export default class ConsolePersonalAgentsController {
172
190
  const qs = ctx.request.qs();
173
191
  const page = parseListPage(qs.page);
174
192
  const size = parseListSize(qs.size);
175
- const protocol = qs.protocol === 'pact' || qs.protocol === 'poppy'
176
- ? qs.protocol
177
- : undefined;
193
+ const protocol = qs.protocol && enabledProtocols(runtime).includes(qs.protocol) ? qs.protocol : undefined;
178
194
  const filter = {
179
- protocol,
195
+ idPrefix: protocol ? grantPrefixOf(runtime, protocol) : undefined,
180
196
  clientId: qs.clientId?.trim() || undefined,
181
197
  accountId: qs.accountId?.trim() || undefined,
182
198
  };
@@ -189,7 +205,7 @@ export default class ConsolePersonalAgentsController {
189
205
  const emails = new Map();
190
206
  const nameCache = new Map();
191
207
  const data = await Promise.all(rows.map(async (g) => {
192
- const rowProtocol = g.id.startsWith('pgrant_') ? 'poppy' : 'pact';
208
+ const rowProtocol = protocolOfGrant(runtime, g.id);
193
209
  if (!emails.has(g.accountId)) {
194
210
  const account = await store.findById(g.accountId).catch(() => null);
195
211
  emails.set(g.accountId, account?.email ?? null);
@@ -248,17 +264,21 @@ export default class ConsolePersonalAgentsController {
248
264
  const invalid = (message) => ctx.response.unprocessableEntity(apiError('invalid_request', message));
249
265
  const protocol = body.protocol;
250
266
  if (!enabledProtocols(runtime).includes(protocol)) {
251
- return invalid('`protocol` precisa ser um protocolo ligado (`pact` ou `poppy`).');
267
+ return invalid(`\`protocol\` precisa ser um protocolo ligado (${enabledProtocols(runtime)
268
+ .map((p) => `\`${p}\``)
269
+ .join(', ')}).`);
252
270
  }
253
271
  const status = body.status;
254
272
  if (status !== 'allowed' && status !== 'blocked') {
255
273
  return invalid('`status` precisa ser `allowed` ou `blocked`.');
256
274
  }
257
- const allowHttp = protocol === 'poppy' && !!runtime.config.poppy?.allowInsecureClientIds;
275
+ const extension = runtime.extensions.get(protocol);
276
+ const summary = extension?.runtime.consoleSummary();
277
+ const allowHttp = !!summary?.allowHttpIdentifiers;
258
278
  const identifier = typeof body.identifier === 'string' ? body.identifier.trim() : '';
259
279
  if (!isUrl(identifier, allowHttp)) {
260
- return invalid(protocol === 'poppy'
261
- ? '`identifier` precisa ser o `client_id` do agente (URL HTTPS do documento de metadata).'
280
+ return invalid(summary
281
+ ? `\`identifier\` precisa ser uma URL ${allowHttp ? 'http(s)' : 'HTTPS'}: ${summary.identifierLabel}.`
262
282
  : '`identifier` precisa ser o `iss` do agente (URL HTTPS).');
263
283
  }
264
284
  const name = typeof body.name === 'string' && body.name.trim() ? body.name.trim().slice(0, 200) : null;
@@ -0,0 +1,8 @@
1
+ /** The boot warning for an admin console with no `admin.authorize`. Same wording in every console. */
2
+ export declare const ADMIN_CONSOLE_NO_AUTHORIZE_WARNING = "[@adonis-agora/authkit-server] console has no `authorize` configured: it is open outside production and denies every request in production. Set `authorize` in config/authkit.ts.";
3
+ /** Logs {@link ADMIN_CONSOLE_NO_AUTHORIZE_WARNING} once per process. */
4
+ export declare function warnAdminConsoleUnauthorized(logger: {
5
+ warn: (message: string) => unknown;
6
+ } | null | undefined): boolean;
7
+ /** Test helper: lets the warning fire again. */
8
+ export declare function resetAdminConsoleWarning(): void;
@@ -0,0 +1,18 @@
1
+ /** The boot warning for an admin console with no `admin.authorize`. Same wording in every console. */
2
+ export const ADMIN_CONSOLE_NO_AUTHORIZE_WARNING = '[@adonis-agora/authkit-server] console has no `authorize` configured: it is open outside production and denies every request in production. Set `authorize` in config/authkit.ts.';
3
+ let warned = false;
4
+ /** Logs {@link ADMIN_CONSOLE_NO_AUTHORIZE_WARNING} once per process. */
5
+ export function warnAdminConsoleUnauthorized(logger) {
6
+ if (warned)
7
+ return false;
8
+ warned = true;
9
+ if (logger)
10
+ logger.warn(ADMIN_CONSOLE_NO_AUTHORIZE_WARNING);
11
+ else
12
+ console.warn(ADMIN_CONSOLE_NO_AUTHORIZE_WARNING);
13
+ return true;
14
+ }
15
+ /** Test helper: lets the warning fire again. */
16
+ export function resetAdminConsoleWarning() {
17
+ warned = false;
18
+ }
@@ -87,7 +87,7 @@ export class AdminSessionsService {
87
87
  /**
88
88
  * Revogação total também corta os personal agents: "sair de todas as sessões",
89
89
  * reset de senha, a ação do admin e a exclusão de conta não podem deixar um
90
- * agente agindo na conta (delegações do PACT e refresh tokens do Poppy). Sem
90
+ * agente agindo na conta (delegações do PACT e conexões das extensions). Sem
91
91
  * `personalAgents` é no-op — e aí nem resolve o banco. Diferente do
92
92
  * `recordSubRevocation`, uma falha AQUI propaga: não há outra camada que corte
93
93
  * esses tokens.
@@ -142,11 +142,14 @@ export class AdminSessionsService {
142
142
  return [];
143
143
  return (await adapter.list()).filter((r) => grantIds.has(r.payload.grantId));
144
144
  }
145
- /** Grants da conta, exceto os espelhos `pgrant_` de refresh tokens do Poppy. */
145
+ /**
146
+ * Grants da conta, exceto os espelhos dos grants das `personalAgents.extensions` (os access
147
+ * tokens de MCP que elas emitem pelo oidc-provider usam o id do grant delas) — aparecem e são
148
+ * revogados como personal agent, não como app OIDC.
149
+ */
146
150
  async #accountGrants(accountId) {
147
- // Os grants `pgrant_` são o espelho de um refresh token do Poppy (Bearer de MCP) — aparecem e
148
- // são revogados como personal agent, não como app OIDC.
149
- return (await this.#listModelOfAccount('Grant', accountId)).filter((r) => !r.id.startsWith('pgrant_'));
151
+ const prefixes = (this.#oidc.config.personalAgents?.extensions ?? []).map((e) => e.grantIdPrefix);
152
+ return (await this.#listModelOfAccount('Grant', accountId)).filter((r) => !prefixes.some((prefix) => r.id.startsWith(prefix)));
150
153
  }
151
154
  /**
152
155
  * Conta TODAS as sessões ativas do IdP (todas as contas). 0 quando o adapter não
@@ -1,4 +1,5 @@
1
1
  import type { HttpContext } from '@adonisjs/core/http';
2
+ import type { ResolvedPersonalAgentExtension } from '../agents/extension.js';
2
3
  import type { AuthSocialConfig, ResolvedRateLimitConfig } from '../define_config.js';
3
4
  import type { PolicyRouteOption } from './config_locks.js';
4
5
  import type { AuthHostOptions } from './register_auth_host.js';
@@ -35,11 +36,19 @@ export interface AuthHostRuntimeConfig {
35
36
  * Ver `deriveLockedRouteOptions`.
36
37
  */
37
38
  lockedRouteOptions?: PolicyRouteOption[];
38
- /** Personal agents ligados → prefixo das rotas deles (`/agents`) e do Poppy (`/poppy`). */
39
+ /**
40
+ * Personal agents ligados → prefixo das rotas do PACT (`/agents`) e as
41
+ * `personalAgents.extensions` resolvidas (rotas, isenções de CSRF, navegador do agente).
42
+ */
39
43
  personalAgents?: {
40
44
  prefix: string;
41
- poppyPrefix?: string;
45
+ extensions?: readonly ResolvedPersonalAgentExtension[];
42
46
  };
47
+ /**
48
+ * `accountSession.acceptIdpSession: false` com `authMethods: { password: false }`:
49
+ * o console só entra por um `accountLoginUrl` do host (`registerAuthHost` recusa sem ele).
50
+ */
51
+ consoleNeedsLoginUrl?: boolean;
43
52
  /**
44
53
  * API headless (Clerk-style). Presente quando o host a declarou — o provider o
45
54
  * stash no boot para `registerAuthHost` montar as rotas sem reler o config inteiro.
@@ -0,0 +1,6 @@
1
+ /**
2
+ * The page a browser gets when `admin.authorize` denies the admin console
3
+ * (403), or asks for a sign-in it cannot do itself (401). Self-contained: no
4
+ * script and no external asset, so any CSP renders it.
5
+ */
6
+ export declare function consoleDeniedPage(status: 401 | 403): string;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The page a browser gets when `admin.authorize` denies the admin console
3
+ * (403), or asks for a sign-in it cannot do itself (401). Self-contained: no
4
+ * script and no external asset, so any CSP renders it.
5
+ */
6
+ export function consoleDeniedPage(status) {
7
+ const title = status === 401 ? 'Sign in required' : 'Access denied';
8
+ const body = status === 401
9
+ ? 'This console needs credentials this request did not carry.'
10
+ : 'You do not have access to this console.';
11
+ return `<!doctype html>
12
+ <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
13
+ <title>${title}</title>
14
+ <style>body{font-family:system-ui,sans-serif;background:#f9fafb;color:#111827;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0;padding:16px}
15
+ .box{max-width:420px;text-align:center}h1{font-size:20px;margin:0 0 8px}p{color:#4b5563;font-size:14px;line-height:1.6;margin:0}
16
+ .code{font-size:12px;color:#9ca3af;margin-bottom:8px}</style>
17
+ </head><body><main class="box"><p class="code">${status}</p><h1>${title}</h1><p>${body}</p></main></body></html>`;
18
+ }
@@ -0,0 +1,24 @@
1
+ /** `client_id` do client interno do console. Reservado: não pode ser cadastrado. */
2
+ export declare const CONSOLE_SSO_CLIENT_ID = "authkit-console";
3
+ /** Rota que inicia o login do console pelo IdP. */
4
+ export declare const CONSOLE_SSO_START_PATH = "/auth/console/sso";
5
+ /** `redirect_uri` do client interno: valida o `state` e abre o console. */
6
+ export declare const CONSOLE_SSO_CALLBACK_PATH = "/auth/console/callback";
7
+ /** Chave da sessão Adonis com o `state` e o destino de um login em andamento. */
8
+ export declare const CONSOLE_SSO_STATE_KEY = "authkit_console_sso";
9
+ /** O que fica na sessão entre o início e o callback. */
10
+ export interface ConsoleSsoState {
11
+ state: string;
12
+ returnTo: string | null;
13
+ }
14
+ /** Metadata do client interno para o `clients` estático do oidc-provider. */
15
+ export declare function consoleSsoClientMetadata(issuer: string): Record<string, unknown>;
16
+ /** Novo `state` opaco para um login do console. */
17
+ export declare function newConsoleSsoState(): string;
18
+ /**
19
+ * Path do authorize do provider. O issuer termina no `mountPath` (garantido por
20
+ * `registerAuthHost`), então o mount é o path do issuer.
21
+ */
22
+ export declare function authorizationPath(issuer: string, provider?: any): string;
23
+ /** URL do login do console pelo IdP, preservando o destino como `return_to`. */
24
+ export declare function consoleSsoUrl(returnTo?: string | null): string;
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Login do console pelo IdP (SSO), o jeito padrão de entrar em `/account/*` e no
3
+ * console admin.
4
+ *
5
+ * O console é tratado como mais um app do IdP, como o Keycloak faz com o
6
+ * `account-console` e o `security-admin-console`: sem sessão, o visitante vai
7
+ * para a tela de login do IdP (a mesma interaction de qualquer client: senha,
8
+ * magic link, OTP, passkey, social), volta, e a ponte de
9
+ * `idp_session_bridge.ts` abre o console para a conta da sessão do IdP.
10
+ *
11
+ * Para isso existe um client interno, `authkit-console`, injetado como client
12
+ * ESTÁTICO do provider (não fica no adapter, então não aparece na lista de
13
+ * clients do console nem pode ser editado):
14
+ *
15
+ * - `response_types: ['none']`: o authorize não emite code nem token. O client
16
+ * só usa o IdP para autenticar; o que abre o console é a sessão do IdP que o
17
+ * login criou, lida pela ponte. Não há segredo, PKCE ou troca de code.
18
+ * - o consent é concedido automaticamente (é a própria lib).
19
+ * - a única `redirect_uri` é `/auth/console/callback`, no MESMO host do issuer.
20
+ *
21
+ * Só existe com `accountSession.acceptIdpSession` ligado (o default).
22
+ */
23
+ import { randomBytes } from 'node:crypto';
24
+ /** `client_id` do client interno do console. Reservado: não pode ser cadastrado. */
25
+ export const CONSOLE_SSO_CLIENT_ID = 'authkit-console';
26
+ /** Rota que inicia o login do console pelo IdP. */
27
+ export const CONSOLE_SSO_START_PATH = '/auth/console/sso';
28
+ /** `redirect_uri` do client interno: valida o `state` e abre o console. */
29
+ export const CONSOLE_SSO_CALLBACK_PATH = '/auth/console/callback';
30
+ /** Chave da sessão Adonis com o `state` e o destino de um login em andamento. */
31
+ export const CONSOLE_SSO_STATE_KEY = 'authkit_console_sso';
32
+ /** Metadata do client interno para o `clients` estático do oidc-provider. */
33
+ export function consoleSsoClientMetadata(issuer) {
34
+ const origin = new URL(issuer).origin;
35
+ return {
36
+ client_id: CONSOLE_SSO_CLIENT_ID,
37
+ client_name: 'Account console',
38
+ application_type: 'web',
39
+ token_endpoint_auth_method: 'none',
40
+ response_types: ['none'],
41
+ grant_types: [],
42
+ redirect_uris: [`${origin}${CONSOLE_SSO_CALLBACK_PATH}`],
43
+ };
44
+ }
45
+ /** Novo `state` opaco para um login do console. */
46
+ export function newConsoleSsoState() {
47
+ return randomBytes(24).toString('base64url');
48
+ }
49
+ /**
50
+ * Path do authorize do provider. O issuer termina no `mountPath` (garantido por
51
+ * `registerAuthHost`), então o mount é o path do issuer.
52
+ */
53
+ export function authorizationPath(issuer, provider) {
54
+ const mount = new URL(issuer).pathname.replace(/\/+$/, '');
55
+ if (provider && typeof provider.pathFor === 'function') {
56
+ try {
57
+ return provider.pathFor('authorization', { mountPath: mount });
58
+ }
59
+ catch {
60
+ // cai no default do oidc-provider abaixo
61
+ }
62
+ }
63
+ return `${mount}/auth`;
64
+ }
65
+ /** URL do login do console pelo IdP, preservando o destino como `return_to`. */
66
+ export function consoleSsoUrl(returnTo) {
67
+ if (!returnTo)
68
+ return CONSOLE_SSO_START_PATH;
69
+ return `${CONSOLE_SSO_START_PATH}?return_to=${encodeURIComponent(returnTo)}`;
70
+ }
@@ -38,16 +38,19 @@ export default class AccountAppsController {
38
38
  scopes: g.scopes,
39
39
  })))
40
40
  : null;
41
- // Refresh tokens do Poppy: o agente é desconectado pela mesma lista (§5.5).
42
- const poppy = agentsRuntime?.poppy;
43
- const poppyAgents = poppy
44
- ? await Promise.all((await poppy.listGrants(accountId)).map(async (g) => ({
45
- id: g.id,
46
- name: await poppy.registry.displayName(g.clientId),
47
- scopes: g.scopes,
48
- })))
49
- : null;
50
- const agents = pactAgents || poppyAgents ? [...(pactAgents ?? []), ...(poppyAgents ?? [])] : null;
41
+ // Conexões das `personalAgents.extensions`: desconectadas pela mesma lista.
42
+ let extensionAgents = null;
43
+ for (const { runtime } of agentsRuntime?.extensions.values() ?? []) {
44
+ extensionAgents ??= [];
45
+ for (const g of await runtime.listAccountGrants(accountId)) {
46
+ extensionAgents.push({
47
+ id: g.id,
48
+ name: await runtime.displayName(g.clientId),
49
+ scopes: g.scopes,
50
+ });
51
+ }
52
+ }
53
+ const agents = pactAgents || extensionAgents ? [...(pactAgents ?? []), ...(extensionAgents ?? [])] : null;
51
54
  return render(ctx, 'account/apps', {
52
55
  csrfToken: ctx.request.csrfToken,
53
56
  supported,
@@ -89,8 +92,9 @@ export default class AccountAppsController {
89
92
  /** POST /account/apps/agents/:grantId/revoke — revoga a delegação de um personal agent. */
90
93
  async revokeAgent(ctx) {
91
94
  const runtime = await personalAgentsFor(ctx);
92
- if (!runtime?.delegation && !runtime?.poppy)
95
+ if (!runtime || (!runtime.delegation && runtime.extensions.size === 0)) {
93
96
  return ctx.response.notFound();
97
+ }
94
98
  const service = await ctx.containerResolver.make('authkit.server');
95
99
  const cfg = service.config;
96
100
  const accountId = ctx.session.get(ACCOUNT_SESSION_KEY);
@@ -3,6 +3,7 @@ import { accountHome } from '../account_home.js';
3
3
  import { getAccountLoginUrl } from '../account_login_url.js';
4
4
  import { ACCOUNT_SESSION_KEY } from '../account_session_key.js';
5
5
  import { syncAdonisAuthLogin, syncAdonisAuthLogout } from '../adonis_auth_sync.js';
6
+ import { consoleSsoUrl } from '../console_sso.js';
6
7
  import { normalizeEmailIdentifier } from '../email_identifier.js';
7
8
  import { translate } from '../i18n.js';
8
9
  import { endBridgedIdpSession } from '../idp_session_bridge.js';
@@ -49,8 +50,16 @@ export default class AccountSessionController {
49
50
  return ctx.response.redirect(accountHome(cfg));
50
51
  }
51
52
  // Lê e valida o return_to da query-string — descarta valores inválidos (open-redirect).
52
- const rawReturnTo = ctx.request.qs?.()?.return_to ?? ctx.request.input?.('return_to');
53
+ const qs = (ctx.request.qs?.() ?? {});
54
+ const rawReturnTo = qs.return_to ?? ctx.request.input?.('return_to');
53
55
  const returnTo = validateReturnTo(rawReturnTo);
56
+ // SSO (default): o console entra pelo login do IdP, a mesma tela de qualquer
57
+ // app. O formulário de senha daqui fica como porta alternativa
58
+ // (`?method=password`), e só quando a senha não foi desligada no config.
59
+ const passwordForm = qs.method === 'password' && cfg.authMethods?.password !== false;
60
+ if (cfg.accountSession?.acceptIdpSession === true && !passwordForm) {
61
+ return redirectExact(ctx.response, consoleSsoUrl(returnTo));
62
+ }
54
63
  const props = {
55
64
  csrfToken: ctx.request.csrfToken,
56
65
  returnTo,
@@ -0,0 +1,12 @@
1
+ import '../augmentations.js';
2
+ import type { HttpContext } from '@adonisjs/core/http';
3
+ /**
4
+ * Login do console pelo IdP (ver `console_sso.ts`).
5
+ *
6
+ * GET /auth/console/sso?return_to=… → authorize do client interno
7
+ * GET /auth/console/callback?state=… → valida o state, abre o console pela ponte
8
+ */
9
+ export default class ConsoleSsoController {
10
+ start(ctx: HttpContext): Promise<unknown>;
11
+ callback(ctx: HttpContext): Promise<unknown>;
12
+ }
@@ -0,0 +1,61 @@
1
+ import '../augmentations.js';
2
+ import { accountHome } from '../account_home.js';
3
+ import { getAccountLoginUrl } from '../account_login_url.js';
4
+ import { authorizationPath, CONSOLE_SSO_CALLBACK_PATH, CONSOLE_SSO_CLIENT_ID, CONSOLE_SSO_STATE_KEY, newConsoleSsoState, } from '../console_sso.js';
5
+ import { ensureConsoleSession } from '../idp_session_bridge.js';
6
+ import { redirectExact } from '../redirect_exact.js';
7
+ import { validateReturnTo } from './account_session_controller.js';
8
+ /**
9
+ * Login do console pelo IdP (ver `console_sso.ts`).
10
+ *
11
+ * GET /auth/console/sso?return_to=… → authorize do client interno
12
+ * GET /auth/console/callback?state=… → valida o state, abre o console pela ponte
13
+ */
14
+ export default class ConsoleSsoController {
15
+ async start(ctx) {
16
+ const service = await ctx.containerResolver.make('authkit.server');
17
+ const cfg = service.config;
18
+ const returnTo = validateReturnTo(ctx.request.qs?.()?.return_to ?? ctx.request.input?.('return_to'));
19
+ // SSO desligado (`acceptIdpSession: false`): o console só tem o login próprio.
20
+ if (cfg.accountSession?.acceptIdpSession !== true) {
21
+ const login = getAccountLoginUrl();
22
+ const sep = login.includes('?') ? '&' : '?';
23
+ return redirectExact(ctx.response, returnTo ? `${login}${sep}return_to=${encodeURIComponent(returnTo)}` : login);
24
+ }
25
+ // Já tem sessão (de console ou do IdP): não precisa passar pelo IdP.
26
+ if (await ensureConsoleSession(ctx)) {
27
+ return redirectExact(ctx.response, returnTo ?? accountHome(cfg));
28
+ }
29
+ const state = newConsoleSsoState();
30
+ ctx.session.put(CONSOLE_SSO_STATE_KEY, { state, returnTo });
31
+ const origin = new URL(cfg.issuer).origin;
32
+ const query = new URLSearchParams({
33
+ client_id: CONSOLE_SSO_CLIENT_ID,
34
+ response_type: 'none',
35
+ scope: 'openid',
36
+ redirect_uri: `${origin}${CONSOLE_SSO_CALLBACK_PATH}`,
37
+ state,
38
+ });
39
+ return redirectExact(ctx.response, `${authorizationPath(cfg.issuer, service.provider)}?${query.toString()}`);
40
+ }
41
+ async callback(ctx) {
42
+ const service = await ctx.containerResolver.make('authkit.server');
43
+ const cfg = service.config;
44
+ const pending = ctx.session.get(CONSOLE_SSO_STATE_KEY);
45
+ ctx.session.forget(CONSOLE_SSO_STATE_KEY);
46
+ const qs = (ctx.request.qs?.() ?? {});
47
+ // `state` amarra o callback ao navegador que iniciou o login (anti-CSRF de login).
48
+ if (!pending || typeof qs.state !== 'string' || qs.state !== pending.state) {
49
+ return ctx.response.status(400).send('Invalid or expired sign-in request.');
50
+ }
51
+ // O IdP recusou (o usuário cancelou, conta desabilitada, …): sem console.
52
+ // Vai para a raiz, e não de volta ao console, para não entrar em loop.
53
+ if (typeof qs.error === 'string') {
54
+ return redirectExact(ctx.response, '/');
55
+ }
56
+ if (!(await ensureConsoleSession(ctx))) {
57
+ return redirectExact(ctx.response, '/');
58
+ }
59
+ return redirectExact(ctx.response, pending.returnTo ?? accountHome(cfg));
60
+ }
61
+ }