@adonis-agora/authkit-server 0.77.0 → 0.79.0

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 (67) hide show
  1. package/README.md +4 -0
  2. package/build/host/views/agents/consent.edge +13 -2
  3. package/build/host/views/agents/done.edge +3 -0
  4. package/build/index.d.ts +10 -0
  5. package/build/index.js +8 -0
  6. package/build/providers/authkit_server_provider.js +15 -1
  7. package/build/src/agents/config.d.ts +22 -3
  8. package/build/src/agents/config.js +22 -4
  9. package/build/src/agents/delegation_service.js +18 -8
  10. package/build/src/agents/delegation_store.d.ts +16 -0
  11. package/build/src/agents/delegation_store.js +44 -0
  12. package/build/src/agents/middleware.js +2 -1
  13. package/build/src/agents/poppy/client_registry.d.ts +45 -0
  14. package/build/src/agents/poppy/client_registry.js +226 -0
  15. package/build/src/agents/poppy/config.d.ts +365 -0
  16. package/build/src/agents/poppy/config.js +208 -0
  17. package/build/src/agents/poppy/dpop.d.ts +61 -0
  18. package/build/src/agents/poppy/dpop.js +158 -0
  19. package/build/src/agents/poppy/errors.d.ts +20 -0
  20. package/build/src/agents/poppy/errors.js +22 -0
  21. package/build/src/agents/poppy/replay.d.ts +25 -0
  22. package/build/src/agents/poppy/replay.js +106 -0
  23. package/build/src/agents/poppy/resource.d.ts +95 -0
  24. package/build/src/agents/poppy/resource.js +125 -0
  25. package/build/src/agents/poppy/safe_fetch.d.ts +48 -0
  26. package/build/src/agents/poppy/safe_fetch.js +256 -0
  27. package/build/src/agents/poppy/service.d.ts +286 -0
  28. package/build/src/agents/poppy/service.js +1464 -0
  29. package/build/src/agents/poppy/store.d.ts +111 -0
  30. package/build/src/agents/poppy/store.js +269 -0
  31. package/build/src/agents/poppy/web_session.d.ts +32 -0
  32. package/build/src/agents/poppy/web_session.js +104 -0
  33. package/build/src/agents/protocol.d.ts +8 -4
  34. package/build/src/agents/registry_store.d.ts +41 -0
  35. package/build/src/agents/registry_store.js +138 -0
  36. package/build/src/agents/runtime.d.ts +30 -1
  37. package/build/src/agents/runtime.js +221 -14
  38. package/build/src/audit/audit_sink.d.ts +1 -1
  39. package/build/src/audit/audit_sink.js +6 -0
  40. package/build/src/define_config.js +6 -0
  41. package/build/src/host/account_api/account_api_controller.d.ts +12 -1
  42. package/build/src/host/account_api/account_api_controller.js +10 -5
  43. package/build/src/host/admin_api/dto.d.ts +1 -1
  44. package/build/src/host/admin_console/admin_shell_controller.js +7 -0
  45. package/build/src/host/admin_console/console_personal_agents_controller.d.ts +83 -0
  46. package/build/src/host/admin_console/console_personal_agents_controller.js +337 -0
  47. package/build/src/host/admin_sessions_service.js +8 -5
  48. package/build/src/host/auth_host_config.d.ts +2 -1
  49. package/build/src/host/controllers/account_apps_controller.js +13 -3
  50. package/build/src/host/controllers/poppy_consent_controller.d.ts +19 -0
  51. package/build/src/host/controllers/poppy_consent_controller.js +173 -0
  52. package/build/src/host/controllers/poppy_controller.d.ts +25 -0
  53. package/build/src/host/controllers/poppy_controller.js +194 -0
  54. package/build/src/host/csrf.d.ts +7 -0
  55. package/build/src/host/csrf.js +16 -1
  56. package/build/src/host/i18n.d.ts +4 -0
  57. package/build/src/host/i18n.js +4 -0
  58. package/build/src/host/poppy_session_key.d.ts +9 -0
  59. package/build/src/host/poppy_session_key.js +9 -0
  60. package/build/src/host/register_auth_host.js +76 -0
  61. package/build/src/host/ui-dist/assets/index-LrScaR9C.js +155 -0
  62. package/build/src/host/ui-dist/index.html +1 -1
  63. package/build/src/provider/oidc_service.d.ts +5 -0
  64. package/build/src/provider/oidc_service.js +9 -2
  65. package/build/src/schema/ensure.js +152 -0
  66. package/package.json +2 -2
  67. package/build/src/host/ui-dist/assets/index-CIqtoNcO.js +0 -155
@@ -0,0 +1,173 @@
1
+ import '../augmentations.js';
2
+ import { personalAgentsFor } from '../../agents/runtime.js';
3
+ import { ACCOUNT_SESSION_KEY } from '../account_session_key.js';
4
+ import { impersonationState } from '../impersonation_session.js';
5
+ import { redirectExact } from '../redirect_exact.js';
6
+ /** A tela nunca pode ser emoldurada (clickjacking — ver `agent_consent_controller.ts`). */
7
+ function denyFraming(ctx) {
8
+ ctx.response.header('X-Frame-Options', 'DENY');
9
+ ctx.response.header('Content-Security-Policy', "frame-ancestors 'none'");
10
+ }
11
+ function chosenScopes(ctx) {
12
+ const raw = ctx.request.input('scope');
13
+ return (Array.isArray(raw) ? raw : raw ? [raw] : []).map(String);
14
+ }
15
+ /**
16
+ * Telas do usuário no Poppy: o consentimento do Direct Sign-In (o
17
+ * `authorization_endpoint`, §4.5) e a do Device Sign-In (o `verification_uri`,
18
+ * §4.6). Montadas atrás do `accountGuard`: sem sessão, o usuário passa pelo login
19
+ * DESTE app e volta. Abrir o link nunca aprova sozinho — a aprovação é um POST
20
+ * com CSRF. Recusadas durante impersonation.
21
+ */
22
+ export default class PoppyConsentController {
23
+ /** GET {prefix}/oauth/authorize */
24
+ async authorize(ctx) {
25
+ const runtime = await personalAgentsFor(ctx);
26
+ const poppy = runtime?.poppy;
27
+ if (!poppy?.cfg.signIn.direct)
28
+ return ctx.response.notFound();
29
+ denyFraming(ctx);
30
+ const service = await ctx.containerResolver.make('authkit.server');
31
+ const render = service.config.render;
32
+ if (impersonationState(ctx).active) {
33
+ return render(ctx, 'agents/consent', {
34
+ csrfToken: ctx.request.csrfToken,
35
+ action: poppy.urls.authorization,
36
+ request: null,
37
+ account: null,
38
+ error: 'agents.consent.impersonating',
39
+ });
40
+ }
41
+ const accountId = ctx.session.get(ACCOUNT_SESSION_KEY);
42
+ const outcome = await poppy.startAuthorization(ctx.request.qs(), accountId);
43
+ if (outcome.kind === 'redirect')
44
+ return redirectExact(ctx.response, outcome.url);
45
+ if (outcome.kind === 'error') {
46
+ ctx.response.status(400);
47
+ return render(ctx, 'agents/done', { status: 'invalid', agentName: null, scopes: [] });
48
+ }
49
+ const account = await service.config.accountStore.findById(accountId);
50
+ return render(ctx, 'agents/consent', {
51
+ csrfToken: ctx.request.csrfToken,
52
+ action: poppy.urls.authorization,
53
+ request: {
54
+ agentName: outcome.request.agentName,
55
+ agentOrigin: outcome.request.agentOrigin,
56
+ logoUri: outcome.request.logoUri,
57
+ scopes: outcome.request.scopes,
58
+ hidden: [{ name: 'request_id', value: outcome.request.id }],
59
+ },
60
+ account: { email: account?.email ?? null },
61
+ error: null,
62
+ });
63
+ }
64
+ /** POST {prefix}/oauth/authorize — `request_id`, `decision=allow|deny`, `scope[]`. */
65
+ async decideAuthorization(ctx) {
66
+ const runtime = await personalAgentsFor(ctx);
67
+ const poppy = runtime?.poppy;
68
+ if (!poppy?.cfg.signIn.direct)
69
+ return ctx.response.notFound();
70
+ denyFraming(ctx);
71
+ const service = await ctx.containerResolver.make('authkit.server');
72
+ const cfg = service.config;
73
+ const render = cfg.render;
74
+ if (impersonationState(ctx).active) {
75
+ return render(ctx, 'agents/done', { status: 'expired', agentName: null, scopes: [] });
76
+ }
77
+ const accountId = ctx.session.get(ACCOUNT_SESSION_KEY);
78
+ const decided = await poppy.decideAuthorization({
79
+ requestId: String(ctx.request.input('request_id') ?? ''),
80
+ accountId,
81
+ allow: ctx.request.input('decision') === 'allow',
82
+ scopes: chosenScopes(ctx),
83
+ });
84
+ if (!decided) {
85
+ return render(ctx, 'agents/done', { status: 'expired', agentName: null, scopes: [] });
86
+ }
87
+ await cfg.audit?.record({
88
+ type: decided.approved ? 'agent.delegation_approved' : 'agent.delegation_denied',
89
+ accountId,
90
+ clientId: decided.clientId,
91
+ ip: ctx.request.ip?.() ?? null,
92
+ metadata: { protocol: 'poppy', signIn: 'direct', scopes: decided.approved ?? [] },
93
+ });
94
+ return redirectExact(ctx.response, decided.url);
95
+ }
96
+ /** GET {prefix}/device?user_code=XXXX-XXXX */
97
+ async device(ctx) {
98
+ const runtime = await personalAgentsFor(ctx);
99
+ const poppy = runtime?.poppy;
100
+ if (!poppy?.cfg.signIn.device)
101
+ return ctx.response.notFound();
102
+ denyFraming(ctx);
103
+ const service = await ctx.containerResolver.make('authkit.server');
104
+ const render = service.config.render;
105
+ const base = {
106
+ csrfToken: ctx.request.csrfToken,
107
+ action: poppy.urls.device,
108
+ request: null,
109
+ account: null,
110
+ error: null,
111
+ };
112
+ if (impersonationState(ctx).active) {
113
+ return render(ctx, 'agents/consent', { ...base, error: 'agents.consent.impersonating' });
114
+ }
115
+ const input = ctx.request.input('user_code');
116
+ if (!input)
117
+ return render(ctx, 'agents/consent', base);
118
+ const pending = await poppy.pendingDevice(input);
119
+ if (!pending) {
120
+ return render(ctx, 'agents/consent', { ...base, error: 'agents.consent.invalid_code' });
121
+ }
122
+ const accountId = ctx.session.get(ACCOUNT_SESSION_KEY);
123
+ const account = await service.config.accountStore.findById(accountId);
124
+ return render(ctx, 'agents/consent', {
125
+ ...base,
126
+ request: {
127
+ userCode: pending.userCode,
128
+ agentName: pending.agentName,
129
+ agentOrigin: pending.agentOrigin,
130
+ scopes: pending.scopes,
131
+ },
132
+ account: { email: account?.email ?? null },
133
+ });
134
+ }
135
+ /** POST {prefix}/device — `user_code`, `decision`, `scope[]`. */
136
+ async decideDevice(ctx) {
137
+ const runtime = await personalAgentsFor(ctx);
138
+ const poppy = runtime?.poppy;
139
+ if (!poppy?.cfg.signIn.device)
140
+ return ctx.response.notFound();
141
+ denyFraming(ctx);
142
+ const service = await ctx.containerResolver.make('authkit.server');
143
+ const cfg = service.config;
144
+ const render = cfg.render;
145
+ if (impersonationState(ctx).active) {
146
+ return render(ctx, 'agents/done', { status: 'expired', agentName: null, scopes: [] });
147
+ }
148
+ const accountId = ctx.session.get(ACCOUNT_SESSION_KEY);
149
+ const userCode = String(ctx.request.input('user_code') ?? '');
150
+ const pending = await poppy.pendingDevice(userCode);
151
+ const decided = await poppy.decideDevice({
152
+ userCode,
153
+ accountId,
154
+ allow: ctx.request.input('decision') === 'allow',
155
+ scopes: chosenScopes(ctx),
156
+ });
157
+ if (!decided || !pending) {
158
+ return render(ctx, 'agents/done', { status: 'expired', agentName: null, scopes: [] });
159
+ }
160
+ await cfg.audit?.record({
161
+ type: decided.approved ? 'agent.delegation_approved' : 'agent.delegation_denied',
162
+ accountId,
163
+ clientId: decided.clientId,
164
+ ip: ctx.request.ip?.() ?? null,
165
+ metadata: { protocol: 'poppy', signIn: 'device', scopes: decided.approved ?? [] },
166
+ });
167
+ return render(ctx, 'agents/done', {
168
+ status: decided.approved ? 'approved' : 'denied',
169
+ agentName: pending.agentName,
170
+ scopes: decided.approved ? poppy.describe(decided.approved) : [],
171
+ });
172
+ }
173
+ }
@@ -0,0 +1,25 @@
1
+ import type { HttpContext } from '@adonisjs/core/http';
2
+ /**
3
+ * Endpoints server-to-server do Poppy (§3, §4): descoberta, metadata RFC 8414,
4
+ * token, revogação, device authorization, Mediated Sign-In e a sessão de
5
+ * navegador (§5). Todos isentos de CSRF — ver `authkitCsrfExceptions`.
6
+ */
7
+ export default class PoppyController {
8
+ /** `GET /.well-known/poppy.json` — só no domínio da empresa (ignorando `www.`). */
9
+ discovery(ctx: HttpContext): Promise<void>;
10
+ /** `GET /.well-known/oauth-authorization-server{prefix}` (RFC 8414 + `poppy_domains`). */
11
+ metadata(ctx: HttpContext): Promise<void>;
12
+ token(ctx: HttpContext): Promise<void>;
13
+ deviceAuthorization(ctx: HttpContext): Promise<void>;
14
+ /** RFC 7009: 200 também para token desconhecido. */
15
+ revoke(ctx: HttpContext): Promise<void>;
16
+ /** `POST auth.mediated.endpoint` (§4.7) — com o Session Token (DPoP). */
17
+ mediatedStart(ctx: HttpContext): Promise<void>;
18
+ /** `POST {mediated}/{sign_in_id}` (§4.7). */
19
+ mediatedCode(ctx: HttpContext): Promise<void>;
20
+ /**
21
+ * `POST web.browser_session_endpoint` (§5): asserção no corpo → cookie do app
22
+ * para a Session e 303 para `return_to`; qualquer falha → 400, sem cookie.
23
+ */
24
+ browserSession(ctx: HttpContext): Promise<void>;
25
+ }
@@ -0,0 +1,194 @@
1
+ import { PoppyError } from '../../agents/poppy/errors.js';
2
+ import { publicRequestUrl, writePoppyAuthError } from '../../agents/poppy/resource.js';
3
+ import { loginPoppyBrowser } from '../../agents/poppy/web_session.js';
4
+ import { personalAgentsFor } from '../../agents/runtime.js';
5
+ function noStore(ctx) {
6
+ ctx.response.header('Cache-Control', 'no-store');
7
+ ctx.response.header('Pragma', 'no-cache');
8
+ }
9
+ function withNonce(ctx, poppy) {
10
+ const nonce = poppy.nonce();
11
+ if (nonce)
12
+ ctx.response.header('DPoP-Nonce', nonce);
13
+ }
14
+ function oauthError(ctx, error) {
15
+ noStore(ctx);
16
+ for (const [k, v] of Object.entries(error.headers))
17
+ ctx.response.header(k, v);
18
+ if (error.code === 'invalid_dpop_proof' || error.code === 'use_dpop_nonce') {
19
+ ctx.response.header('WWW-Authenticate', `DPoP error="${error.code}"`);
20
+ }
21
+ return ctx.response.status(error.status).json(error.toJSON());
22
+ }
23
+ async function poppyOr404(ctx) {
24
+ const poppy = (await personalAgentsFor(ctx))?.poppy ?? null;
25
+ if (!poppy)
26
+ ctx.response.notFound();
27
+ return poppy;
28
+ }
29
+ /** Formulário (`application/x-www-form-urlencoded`) da request. */
30
+ function form(ctx) {
31
+ return ctx.request.all();
32
+ }
33
+ /**
34
+ * Endpoints server-to-server do Poppy (§3, §4): descoberta, metadata RFC 8414,
35
+ * token, revogação, device authorization, Mediated Sign-In e a sessão de
36
+ * navegador (§5). Todos isentos de CSRF — ver `authkitCsrfExceptions`.
37
+ */
38
+ export default class PoppyController {
39
+ /** `GET /.well-known/poppy.json` — só no domínio da empresa (ignorando `www.`). */
40
+ async discovery(ctx) {
41
+ const poppy = await poppyOr404(ctx);
42
+ if (!poppy)
43
+ return;
44
+ // `host()` mantém a porta (dev em `localhost:3333`): o `browser_session_endpoint` sai dele.
45
+ const doc = poppy.discoveryDocument(ctx.request.host() ?? ctx.request.hostname() ?? '');
46
+ if (!doc)
47
+ return ctx.response.notFound();
48
+ ctx.response.header('Access-Control-Allow-Origin', '*');
49
+ ctx.response.header('Cache-Control', 'public, max-age=300');
50
+ return ctx.response.json(doc);
51
+ }
52
+ /** `GET /.well-known/oauth-authorization-server{prefix}` (RFC 8414 + `poppy_domains`). */
53
+ async metadata(ctx) {
54
+ const poppy = await poppyOr404(ctx);
55
+ if (!poppy)
56
+ return;
57
+ ctx.response.header('Access-Control-Allow-Origin', '*');
58
+ ctx.response.header('Cache-Control', 'public, max-age=300');
59
+ return ctx.response.json(poppy.authorizationServerMetadata());
60
+ }
61
+ async token(ctx) {
62
+ const poppy = await poppyOr404(ctx);
63
+ if (!poppy)
64
+ return;
65
+ withNonce(ctx, poppy);
66
+ try {
67
+ const body = await poppy.token(form(ctx), ctx.request.header('dpop'));
68
+ noStore(ctx);
69
+ return ctx.response.json(body);
70
+ }
71
+ catch (error) {
72
+ if (error instanceof PoppyError)
73
+ return oauthError(ctx, error);
74
+ throw error;
75
+ }
76
+ }
77
+ async deviceAuthorization(ctx) {
78
+ const poppy = await poppyOr404(ctx);
79
+ if (!poppy)
80
+ return;
81
+ try {
82
+ const body = await poppy.deviceAuthorization(form(ctx));
83
+ noStore(ctx);
84
+ return ctx.response.json(body);
85
+ }
86
+ catch (error) {
87
+ if (error instanceof PoppyError)
88
+ return oauthError(ctx, error);
89
+ throw error;
90
+ }
91
+ }
92
+ /** RFC 7009: 200 também para token desconhecido. */
93
+ async revoke(ctx) {
94
+ const poppy = await poppyOr404(ctx);
95
+ if (!poppy)
96
+ return;
97
+ try {
98
+ await poppy.revoke(form(ctx));
99
+ noStore(ctx);
100
+ return ctx.response.status(200).send('');
101
+ }
102
+ catch (error) {
103
+ if (error instanceof PoppyError)
104
+ return oauthError(ctx, error);
105
+ throw error;
106
+ }
107
+ }
108
+ /** `POST auth.mediated.endpoint` (§4.7) — com o Session Token (DPoP). */
109
+ async mediatedStart(ctx) {
110
+ const poppy = await poppyOr404(ctx);
111
+ if (!poppy?.cfg.signIn.mediated)
112
+ return poppy ? ctx.response.notFound() : undefined;
113
+ const auth = await poppy.verifyAccess({
114
+ method: ctx.request.method(),
115
+ url: publicRequestUrl(ctx, poppy),
116
+ headers: ctx.request.headers(),
117
+ });
118
+ if (!auth.ok) {
119
+ const { ok: _ok, ...rest } = auth;
120
+ return writePoppyAuthError(ctx, { ok: false, ...rest });
121
+ }
122
+ try {
123
+ const body = await poppy.mediatedStart(auth.principal, auth.jkt, ctx.request.body(), ctx);
124
+ noStore(ctx);
125
+ return ctx.response.json(body);
126
+ }
127
+ catch (error) {
128
+ if (error instanceof PoppyError)
129
+ return oauthError(ctx, error);
130
+ throw error;
131
+ }
132
+ }
133
+ /** `POST {mediated}/{sign_in_id}` (§4.7). */
134
+ async mediatedCode(ctx) {
135
+ const poppy = await poppyOr404(ctx);
136
+ if (!poppy?.cfg.signIn.mediated)
137
+ return poppy ? ctx.response.notFound() : undefined;
138
+ const auth = await poppy.verifyAccess({
139
+ method: ctx.request.method(),
140
+ url: publicRequestUrl(ctx, poppy),
141
+ headers: ctx.request.headers(),
142
+ });
143
+ if (!auth.ok) {
144
+ const { ok: _ok, ...rest } = auth;
145
+ return writePoppyAuthError(ctx, { ok: false, ...rest });
146
+ }
147
+ try {
148
+ const body = await poppy.mediatedCode(auth.principal, auth.jkt, String(ctx.request.param('id') ?? ''), ctx.request.body());
149
+ noStore(ctx);
150
+ if (!body) {
151
+ return ctx.response
152
+ .status(404)
153
+ .json({ error: 'invalid_request', error_description: 'Unknown sign_in_id' });
154
+ }
155
+ return ctx.response.json(body);
156
+ }
157
+ catch (error) {
158
+ if (error instanceof PoppyError)
159
+ return oauthError(ctx, error);
160
+ throw error;
161
+ }
162
+ }
163
+ /**
164
+ * `POST web.browser_session_endpoint` (§5): asserção no corpo → cookie do app
165
+ * para a Session e 303 para `return_to`; qualquer falha → 400, sem cookie.
166
+ */
167
+ async browserSession(ctx) {
168
+ const poppy = await poppyOr404(ctx);
169
+ if (!poppy?.cfg.web?.browserSession)
170
+ return poppy ? ctx.response.notFound() : undefined;
171
+ noStore(ctx);
172
+ // A asserção vai NO CORPO, nunca na URL (§5).
173
+ if (ctx.request.qs().assertion !== undefined)
174
+ return badBrowserRequest(ctx);
175
+ let result;
176
+ try {
177
+ result = await poppy.verifyBrowserAssertion(ctx.request.input('assertion'), new URL(publicRequestUrl(ctx, poppy)).origin);
178
+ }
179
+ catch (error) {
180
+ if (error instanceof PoppyError)
181
+ return badBrowserRequest(ctx);
182
+ throw error;
183
+ }
184
+ await loginPoppyBrowser(ctx, result);
185
+ ctx.response.header('Location', result.returnTo);
186
+ return ctx.response.status(303).send('');
187
+ }
188
+ }
189
+ function badBrowserRequest(ctx) {
190
+ ctx.response.header('Content-Type', 'text/html; charset=utf-8');
191
+ return ctx.response
192
+ .status(400)
193
+ .send('<!doctype html><html><head><meta charset="utf-8"><title>Bad request</title></head><body><h1>Invalid browser session request</h1></body></html>');
194
+ }
@@ -10,6 +10,13 @@ export interface AuthkitCsrfOptions {
10
10
  * consentimento (`{prefix}/consent`) continua protegida.
11
11
  */
12
12
  personalAgentsPrefix?: string | false;
13
+ /**
14
+ * Prefixo do Poppy: isenta token/revoke/device authorization, o Mediated
15
+ * Sign-In e a sessão de navegador (POST cross-site autenticado pela asserção).
16
+ * Default: o do config. As telas `{prefix}/oauth/authorize` e `{prefix}/device`
17
+ * continuam protegidas. `false` desliga.
18
+ */
19
+ poppyPrefix?: string | false;
13
20
  }
14
21
  /**
15
22
  * Retorna `true` quando `url` é uma rota AuthKit que deve ser ISENTA de CSRF
@@ -16,6 +16,7 @@
16
16
  * ```
17
17
  */
18
18
  import { normalizePersonalAgentsPrefix } from '../agents/config.js';
19
+ import { normalizePoppyPrefix } from '../agents/poppy/config.js';
19
20
  import { getAuthHostConfig } from './auth_host_config.js';
20
21
  /**
21
22
  * Retorna `true` quando `url` é uma rota AuthKit que deve ser ISENTA de CSRF
@@ -33,8 +34,22 @@ export function authkitCsrfExceptions(url, options = {}) {
33
34
  ? normalizePersonalAgentsPrefix(options.personalAgentsPrefix)
34
35
  : getAuthHostConfig()?.personalAgents?.prefix;
35
36
  const agentsOAuth = agentsPrefix ? `${agentsPrefix}/oauth/` : null;
37
+ const poppyPrefix = options.poppyPrefix === false
38
+ ? undefined
39
+ : options.poppyPrefix !== undefined
40
+ ? normalizePoppyPrefix(options.poppyPrefix)
41
+ : getAuthHostConfig()?.personalAgents?.poppyPrefix;
42
+ const path = url.split('?')[0];
43
+ const poppyExempt = !!poppyPrefix &&
44
+ (path === `${poppyPrefix}/oauth/token` ||
45
+ path === `${poppyPrefix}/oauth/revoke` ||
46
+ path === `${poppyPrefix}/oauth/device` ||
47
+ path === `${poppyPrefix}/sign-in` ||
48
+ path.startsWith(`${poppyPrefix}/sign-in/`) ||
49
+ path === `${poppyPrefix}/browser-session`);
36
50
  return (url.includes(mountPath) ||
37
51
  url.includes('/authkit/pat') ||
38
52
  (backchannel !== null && url === backchannel) ||
39
- (agentsOAuth !== null && url.startsWith(agentsOAuth)));
53
+ (agentsOAuth !== null && url.startsWith(agentsOAuth)) ||
54
+ poppyExempt);
40
55
  }
@@ -204,6 +204,8 @@ export declare const DEFAULT_MESSAGES: {
204
204
  'agents.done.revoke_hint': string;
205
205
  'agents.done.expired_title': string;
206
206
  'agents.done.expired_body': string;
207
+ 'agents.done.invalid_title': string;
208
+ 'agents.done.invalid_body': string;
207
209
  'account.apps.not_supported': string;
208
210
  'account.orgs.page_title': string;
209
211
  'account.orgs.title': string;
@@ -956,6 +958,8 @@ export declare const PT_BR_MESSAGES: {
956
958
  'agents.done.revoke_hint': string;
957
959
  'agents.done.expired_title': string;
958
960
  'agents.done.expired_body': string;
961
+ 'agents.done.invalid_title': string;
962
+ 'agents.done.invalid_body': string;
959
963
  'account.apps.not_supported': string;
960
964
  'account.orgs.page_title': string;
961
965
  'account.orgs.title': string;
@@ -212,6 +212,8 @@ export const DEFAULT_MESSAGES = {
212
212
  'agents.done.revoke_hint': 'To revoke this access later, go to the apps page of your account.',
213
213
  'agents.done.expired_title': 'Request expired',
214
214
  'agents.done.expired_body': 'This request is no longer valid. Ask your assistant to start again.',
215
+ 'agents.done.invalid_title': 'Invalid request',
216
+ 'agents.done.invalid_body': 'This sign-in link is not valid. Go back to your assistant and start again.',
215
217
  'account.apps.not_supported': 'The configured OIDC adapter does not support enumeration — listing apps is unavailable.',
216
218
  // Console de conta — organizations (account/orgs).
217
219
  'account.orgs.page_title': 'My Organizations',
@@ -1052,6 +1054,8 @@ export const PT_BR_MESSAGES = {
1052
1054
  'agents.done.revoke_hint': 'Para revogar este acesso depois, vá à página de apps da sua conta.',
1053
1055
  'agents.done.expired_title': 'Pedido expirado',
1054
1056
  'agents.done.expired_body': 'Este pedido não vale mais. Peça ao seu assistente para começar de novo.',
1057
+ 'agents.done.invalid_title': 'Pedido inválido',
1058
+ 'agents.done.invalid_body': 'Este link de acesso não é válido. Volte ao seu assistente e comece de novo.',
1055
1059
  'account.apps.not_supported': 'O adapter OIDC configurado não suporta enumeração — a listagem de apps fica indisponível.',
1056
1060
  // Console de conta — organizations (account/orgs).
1057
1061
  'account.orgs.page_title': 'Minhas Organizações',
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Session key que marca a sessão do navegador de um personal agent Poppy (§5):
3
+ * `{ sessionId, clientId, userId }`. Import-free (o barrel a reexporta — ver
4
+ * `account_session_key.ts`).
5
+ *
6
+ * Uma sessão marcada é do AGENTE, não do usuário: o console de conta, o admin e
7
+ * as telas de consentimento a recusam.
8
+ */
9
+ export declare const POPPY_WEB_SESSION_KEY = "authkit_poppy_session";
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Session key que marca a sessão do navegador de um personal agent Poppy (§5):
3
+ * `{ sessionId, clientId, userId }`. Import-free (o barrel a reexporta — ver
4
+ * `account_session_key.ts`).
5
+ *
6
+ * Uma sessão marcada é do AGENTE, não do usuário: o console de conta, o admin e
7
+ * as telas de consentimento a recusam.
8
+ */
9
+ export const POPPY_WEB_SESSION_KEY = 'authkit_poppy_session';
@@ -8,6 +8,7 @@ import { adminApiGuard } from './admin_api/admin_api_guard.js';
8
8
  import { normalizeAdminApiPrefix, normalizeAdminPrefix, setAdminApiPrefix, setAdminPrefix, } from './admin_prefix.js';
9
9
  import { getAuthHostConfig, markAuthHostAutoMounted, wasAuthHostAutoMounted, } from './auth_host_config.js';
10
10
  import { ensureConsoleSession } from './idp_session_bridge.js';
11
+ import { POPPY_WEB_SESSION_KEY } from './poppy_session_key.js';
11
12
  import { createAuthThrottles } from './rate_limit.js';
12
13
  import { redirectExact } from './redirect_exact.js';
13
14
  import { requestPathWithQuery } from './request_url.js';
@@ -113,7 +114,16 @@ function buildLoginRedirect(ctx, extra) {
113
114
  * classe NÃO era aplicada em runtime num grupo, deixando /account/tokens e
114
115
  * /account/mfa acessíveis sem sessão.
115
116
  */
117
+ /** A sessão é do navegador de um personal agent Poppy (§5)? */
118
+ function isPoppyBrowser(ctx) {
119
+ const marker = ctx.session?.get?.(POPPY_WEB_SESSION_KEY);
120
+ return typeof marker === 'object' && marker !== null && typeof marker.sessionId === 'string';
121
+ }
116
122
  const accountGuard = async (ctx, next) => {
123
+ // O navegador de um personal agent Poppy (§5) não entra no console de conta nem
124
+ // nas telas de consentimento: a sessão é do agente, não do usuário.
125
+ if (isPoppyBrowser(ctx))
126
+ return ctx.response.status(403).send('');
117
127
  // Sessão do console — ou, com `accountSession.acceptIdpSession`, a do IdP (SSO).
118
128
  if (!(await ensureConsoleSession(ctx))) {
119
129
  return redirectExact(ctx.response, buildLoginRedirect(ctx));
@@ -145,6 +155,8 @@ export const adminGuard = async (ctx, next) => {
145
155
  if (!cfg.admin.enabled) {
146
156
  return ctx.response.notFound();
147
157
  }
158
+ if (isPoppyBrowser(ctx))
159
+ return ctx.response.status(403).send('');
148
160
  // Com `accountSession.acceptIdpSession`, a sessão do IdP também abre o console.
149
161
  await ensureConsoleSession(ctx);
150
162
  const accountId = ctx.session?.get(ACCOUNT_SESSION_KEY);
@@ -187,6 +199,8 @@ const C = {
187
199
  accountConfirm: () => import('./controllers/account_confirm_controller.js'),
188
200
  agentOAuth: () => import('./controllers/agent_oauth_controller.js'),
189
201
  agentConsent: () => import('./controllers/agent_consent_controller.js'),
202
+ poppy: () => import('./controllers/poppy_controller.js'),
203
+ poppyConsent: () => import('./controllers/poppy_consent_controller.js'),
190
204
  webauthnAsset: () => import('./controllers/webauthn_asset_controller.js'),
191
205
  logoutAsset: () => import('./controllers/logout_asset_controller.js'),
192
206
  passkeyAutofillAsset: () => import('./controllers/passkey_autofill_asset_controller.js'),
@@ -206,6 +220,7 @@ const C = {
206
220
  consoleSettings: () => import('./admin_console/console_settings_controller.js'),
207
221
  consoleKeys: () => import('./admin_console/console_keys_controller.js'),
208
222
  consoleImpersonation: () => import('./admin_console/console_impersonation_controller.js'),
223
+ consolePersonalAgents: () => import('./admin_console/console_personal_agents_controller.js'),
209
224
  apiUsers: () => import('./admin_api/api_users_controller.js'),
210
225
  apiClients: () => import('./admin_api/api_clients_controller.js'),
211
226
  apiMisc: () => import('./admin_api/api_misc_controller.js'),
@@ -508,6 +523,52 @@ export function registerAuthHost(router, opts = {}) {
508
523
  })
509
524
  .use([accountGuard]);
510
525
  }
526
+ // Personal Agent Protocol (Poppy): descoberta, authorization server (token,
527
+ // revogação, device authorization), Mediated Sign-In e sessão de navegador —
528
+ // server-to-server ou POST cross-site do navegador do agente (sem CSRF, ver
529
+ // `authkitCsrfExceptions`). As telas do usuário (authorize/device) exigem a
530
+ // sessão de conta e mantêm o CSRF.
531
+ const poppyPrefix = hostCfg?.personalAgents?.poppyPrefix;
532
+ if (poppyPrefix) {
533
+ // Nomes explícitos: o Router nomeia pela dupla controller.método, e as duas URLs da metadata
534
+ // (RFC 8414 na raiz e debaixo do issuer) usam o mesmo — "poppy.metadata already exists" no boot.
535
+ router.get('/.well-known/poppy.json', [C.poppy, 'discovery']).as('authkit.poppy.discovery');
536
+ router
537
+ .get(`/.well-known/oauth-authorization-server${poppyPrefix}`, [C.poppy, 'metadata'])
538
+ .as('authkit.poppy.metadata');
539
+ router
540
+ .get(`${poppyPrefix}/.well-known/oauth-authorization-server`, [C.poppy, 'metadata'])
541
+ .as('authkit.poppy.metadata_under_issuer');
542
+ router.post(`${poppyPrefix}/oauth/token`, [C.poppy, 'token']).as('authkit.poppy.token');
543
+ router.post(`${poppyPrefix}/oauth/revoke`, [C.poppy, 'revoke']).as('authkit.poppy.revoke');
544
+ router
545
+ .post(`${poppyPrefix}/oauth/device`, [C.poppy, 'deviceAuthorization'])
546
+ .as('authkit.poppy.device_authorization');
547
+ // Credenciais e códigos de uso único: throttle por IP (§4.7 exige rate limit).
548
+ withLogin(router.post(`${poppyPrefix}/sign-in`, [C.poppy, 'mediatedStart']).as('authkit.poppy.sign_in'));
549
+ withOtpLogin(router
550
+ .post(`${poppyPrefix}/sign-in/:id`, [C.poppy, 'mediatedCode'])
551
+ .as('authkit.poppy.sign_in_code'));
552
+ router
553
+ .post(`${poppyPrefix}/browser-session`, [C.poppy, 'browserSession'])
554
+ .as('authkit.poppy.browser_session');
555
+ router
556
+ .group(() => {
557
+ router
558
+ .get(`${poppyPrefix}/oauth/authorize`, [C.poppyConsent, 'authorize'])
559
+ .as('authkit.poppy.authorize');
560
+ router
561
+ .post(`${poppyPrefix}/oauth/authorize`, [C.poppyConsent, 'decideAuthorization'])
562
+ .as('authkit.poppy.authorize_decide');
563
+ withOtpLogin(router
564
+ .get(`${poppyPrefix}/device`, [C.poppyConsent, 'device'])
565
+ .as('authkit.poppy.device'));
566
+ withOtpLogin(router
567
+ .post(`${poppyPrefix}/device`, [C.poppyConsent, 'decideDevice'])
568
+ .as('authkit.poppy.device_decide'));
569
+ })
570
+ .use([accountGuard]);
571
+ }
511
572
  // Paths do console de conta (configuráveis/localizáveis via `accountRoutes`).
512
573
  // As TELAS vêm de `accountPath(key)` (prefixo + segmento configurável); os
513
574
  // action-subpaths concatenados (`/password`, `/enroll`, ...) são FIXOS —
@@ -855,6 +916,21 @@ export function registerAuthHost(router, opts = {}) {
855
916
  // Chave de assinatura managed (status + rotação ao vivo).
856
917
  router.get(`${ap}/api/keys`, [C.consoleKeys, 'status']);
857
918
  router.post(`${ap}/api/keys/rotate`, [C.consoleKeys, 'rotate']);
919
+ // Personal agents (capability-gated: 404 sem `personalAgents` ou sem banco).
920
+ router.get(`${ap}/api/personal-agents`, [C.consolePersonalAgents, 'index']);
921
+ router.get(`${ap}/api/personal-agents/connections`, [
922
+ C.consolePersonalAgents,
923
+ 'connections',
924
+ ]);
925
+ router.post(`${ap}/api/personal-agents/connections/:grantId/revoke`, [
926
+ C.consolePersonalAgents,
927
+ 'revokeConnection',
928
+ ]);
929
+ router.post(`${ap}/api/personal-agents/registry`, [C.consolePersonalAgents, 'upsert']);
930
+ router.delete(`${ap}/api/personal-agents/registry/:id`, [
931
+ C.consolePersonalAgents,
932
+ 'destroy',
933
+ ]);
858
934
  // Impersonation (capability-gated: 404 quando desabilitado ou sem client).
859
935
  router.get(`${ap}/api/impersonation/:userId`, [C.consoleImpersonation, 'handle']);
860
936
  // ─── Shell HTML — POR ÚLTIMO (catch-all) ──────────────────────────────