@adonis-agora/authkit-server 0.77.0 → 0.78.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 (54) 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 +20 -3
  8. package/build/src/agents/config.js +19 -4
  9. package/build/src/agents/delegation_service.js +18 -8
  10. package/build/src/agents/middleware.js +2 -1
  11. package/build/src/agents/poppy/client_registry.d.ts +45 -0
  12. package/build/src/agents/poppy/client_registry.js +226 -0
  13. package/build/src/agents/poppy/config.d.ts +362 -0
  14. package/build/src/agents/poppy/config.js +201 -0
  15. package/build/src/agents/poppy/dpop.d.ts +61 -0
  16. package/build/src/agents/poppy/dpop.js +158 -0
  17. package/build/src/agents/poppy/errors.d.ts +20 -0
  18. package/build/src/agents/poppy/errors.js +22 -0
  19. package/build/src/agents/poppy/replay.d.ts +25 -0
  20. package/build/src/agents/poppy/replay.js +106 -0
  21. package/build/src/agents/poppy/resource.d.ts +95 -0
  22. package/build/src/agents/poppy/resource.js +125 -0
  23. package/build/src/agents/poppy/safe_fetch.d.ts +48 -0
  24. package/build/src/agents/poppy/safe_fetch.js +256 -0
  25. package/build/src/agents/poppy/service.d.ts +280 -0
  26. package/build/src/agents/poppy/service.js +1452 -0
  27. package/build/src/agents/poppy/store.d.ts +109 -0
  28. package/build/src/agents/poppy/store.js +260 -0
  29. package/build/src/agents/poppy/web_session.d.ts +32 -0
  30. package/build/src/agents/poppy/web_session.js +104 -0
  31. package/build/src/agents/protocol.d.ts +8 -4
  32. package/build/src/agents/runtime.d.ts +17 -1
  33. package/build/src/agents/runtime.js +138 -13
  34. package/build/src/define_config.js +6 -0
  35. package/build/src/host/account_api/account_api_controller.d.ts +12 -1
  36. package/build/src/host/account_api/account_api_controller.js +10 -5
  37. package/build/src/host/admin_sessions_service.js +8 -5
  38. package/build/src/host/auth_host_config.d.ts +2 -1
  39. package/build/src/host/controllers/account_apps_controller.js +13 -3
  40. package/build/src/host/controllers/poppy_consent_controller.d.ts +19 -0
  41. package/build/src/host/controllers/poppy_consent_controller.js +173 -0
  42. package/build/src/host/controllers/poppy_controller.d.ts +25 -0
  43. package/build/src/host/controllers/poppy_controller.js +194 -0
  44. package/build/src/host/csrf.d.ts +7 -0
  45. package/build/src/host/csrf.js +16 -1
  46. package/build/src/host/i18n.d.ts +4 -0
  47. package/build/src/host/i18n.js +4 -0
  48. package/build/src/host/poppy_session_key.d.ts +9 -0
  49. package/build/src/host/poppy_session_key.js +9 -0
  50. package/build/src/host/register_auth_host.js +60 -0
  51. package/build/src/provider/oidc_service.d.ts +5 -0
  52. package/build/src/provider/oidc_service.js +9 -2
  53. package/build/src/schema/ensure.js +124 -0
  54. package/package.json +1 -1
@@ -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'),
@@ -508,6 +522,52 @@ export function registerAuthHost(router, opts = {}) {
508
522
  })
509
523
  .use([accountGuard]);
510
524
  }
525
+ // Personal Agent Protocol (Poppy): descoberta, authorization server (token,
526
+ // revogação, device authorization), Mediated Sign-In e sessão de navegador —
527
+ // server-to-server ou POST cross-site do navegador do agente (sem CSRF, ver
528
+ // `authkitCsrfExceptions`). As telas do usuário (authorize/device) exigem a
529
+ // sessão de conta e mantêm o CSRF.
530
+ const poppyPrefix = hostCfg?.personalAgents?.poppyPrefix;
531
+ if (poppyPrefix) {
532
+ // Nomes explícitos: o Router nomeia pela dupla controller.método, e as duas URLs da metadata
533
+ // (RFC 8414 na raiz e debaixo do issuer) usam o mesmo — "poppy.metadata already exists" no boot.
534
+ router.get('/.well-known/poppy.json', [C.poppy, 'discovery']).as('authkit.poppy.discovery');
535
+ router
536
+ .get(`/.well-known/oauth-authorization-server${poppyPrefix}`, [C.poppy, 'metadata'])
537
+ .as('authkit.poppy.metadata');
538
+ router
539
+ .get(`${poppyPrefix}/.well-known/oauth-authorization-server`, [C.poppy, 'metadata'])
540
+ .as('authkit.poppy.metadata_under_issuer');
541
+ router.post(`${poppyPrefix}/oauth/token`, [C.poppy, 'token']).as('authkit.poppy.token');
542
+ router.post(`${poppyPrefix}/oauth/revoke`, [C.poppy, 'revoke']).as('authkit.poppy.revoke');
543
+ router
544
+ .post(`${poppyPrefix}/oauth/device`, [C.poppy, 'deviceAuthorization'])
545
+ .as('authkit.poppy.device_authorization');
546
+ // Credenciais e códigos de uso único: throttle por IP (§4.7 exige rate limit).
547
+ withLogin(router.post(`${poppyPrefix}/sign-in`, [C.poppy, 'mediatedStart']).as('authkit.poppy.sign_in'));
548
+ withOtpLogin(router
549
+ .post(`${poppyPrefix}/sign-in/:id`, [C.poppy, 'mediatedCode'])
550
+ .as('authkit.poppy.sign_in_code'));
551
+ router
552
+ .post(`${poppyPrefix}/browser-session`, [C.poppy, 'browserSession'])
553
+ .as('authkit.poppy.browser_session');
554
+ router
555
+ .group(() => {
556
+ router
557
+ .get(`${poppyPrefix}/oauth/authorize`, [C.poppyConsent, 'authorize'])
558
+ .as('authkit.poppy.authorize');
559
+ router
560
+ .post(`${poppyPrefix}/oauth/authorize`, [C.poppyConsent, 'decideAuthorization'])
561
+ .as('authkit.poppy.authorize_decide');
562
+ withOtpLogin(router
563
+ .get(`${poppyPrefix}/device`, [C.poppyConsent, 'device'])
564
+ .as('authkit.poppy.device'));
565
+ withOtpLogin(router
566
+ .post(`${poppyPrefix}/device`, [C.poppyConsent, 'decideDevice'])
567
+ .as('authkit.poppy.device_decide'));
568
+ })
569
+ .use([accountGuard]);
570
+ }
511
571
  // Paths do console de conta (configuráveis/localizáveis via `accountRoutes`).
512
572
  // As TELAS vêm de `accountPath(key)` (prefixo + segmento configurável); os
513
573
  // action-subpaths concatenados (`/password`, `/enroll`, ...) são FIXOS —
@@ -25,6 +25,11 @@ export declare class OidcService {
25
25
  get signingJwks(): {
26
26
  keys: Record<string, any>[];
27
27
  };
28
+ /**
29
+ * @internal Segredo derivado da app key para um uso (`label`) — HMAC dos nonces
30
+ * DPoP do Poppy, por exemplo. Igual em todas as instâncias, sem expor a chave.
31
+ */
32
+ deriveSecret(label: string): Buffer;
28
33
  /** Pathname do issuer sem barra final (ex.: `/oidc`). Vazio quando montado na raiz. */
29
34
  readonly mountPath: string;
30
35
  readonly recorder: MetricsRecorder;
@@ -1,4 +1,4 @@
1
- import { timingSafeEqual } from 'node:crypto';
1
+ import { createHmac, timingSafeEqual } from 'node:crypto';
2
2
  import { NoopRecorder } from '@adonis-agora/authkit-core';
3
3
  import Koa from 'koa';
4
4
  import mount from 'koa-mount';
@@ -41,6 +41,13 @@ export class OidcService {
41
41
  get signingJwks() {
42
42
  return this.#signingJwks;
43
43
  }
44
+ /**
45
+ * @internal Segredo derivado da app key para um uso (`label`) — HMAC dos nonces
46
+ * DPoP do Poppy, por exemplo. Igual em todas as instâncias, sem expor a chave.
47
+ */
48
+ deriveSecret(label) {
49
+ return createHmac('sha256', this.#appKey).update(`authkit:${label}`).digest();
50
+ }
44
51
  /** Pathname do issuer sem barra final (ex.: `/oidc`). Vazio quando montado na raiz. */
45
52
  mountPath;
46
53
  recorder;
@@ -325,7 +332,7 @@ export class OidcService {
325
332
  /** Verifica client_id + client_secret contra os clients da config (p/ endpoints custom como introspecção de PAT). */
326
333
  verifyClientCredentials(clientId, clientSecret) {
327
334
  const client = this.#clients.find((c) => c.clientId === clientId);
328
- if (!client || !client.clientSecret)
335
+ if (!client?.clientSecret)
329
336
  return false;
330
337
  const a = Buffer.from(client.clientSecret);
331
338
  const b = Buffer.from(clientSecret);
@@ -295,6 +295,130 @@ const TABLES = [
295
295
  created_at: (t) => t.dateTime('created_at', { useTz: true, precision: 3 }).nullable(),
296
296
  },
297
297
  },
298
+ /*
299
+ * Personal Agent Protocol (Poppy). Os Account Tokens são grants de
300
+ * `auth_agent_grants` (prefixo `pgrant_`) com o hash em
301
+ * `auth_agent_refresh_tokens`; aqui ficam Sessions, Session Tokens, pedidos de
302
+ * sign-in e os `jti` vistos. Mesmas regras de data das tabelas acima.
303
+ */
304
+ {
305
+ name: 'auth_poppy_sessions',
306
+ /**
307
+ * Uma Session: um usuário do agente (`user_id`, opaco) num agente
308
+ * (`client_id`). `account_id` é a conta a que ficou PRESA ao logar (não troca);
309
+ * `grant_id` o Account Token com que está logada agora (`null` = deslogada).
310
+ */
311
+ create: (t) => {
312
+ t.string('id').primary();
313
+ t.string('client_id', 2048).notNullable();
314
+ t.string('user_id').notNullable();
315
+ t.string('account_id').nullable().index();
316
+ t.string('grant_id').nullable().index();
317
+ t.text('scope').notNullable();
318
+ t.dateTime('expires_at', { useTz: true, precision: 3 }).notNullable();
319
+ t.dateTime('ended_at', { useTz: true, precision: 3 }).nullable();
320
+ t.dateTime('created_at', { useTz: true, precision: 3 }).notNullable();
321
+ t.dateTime('updated_at', { useTz: true, precision: 3 }).notNullable();
322
+ },
323
+ columns: {
324
+ client_id: (t) => t.string('client_id', 2048),
325
+ user_id: (t) => t.string('user_id'),
326
+ account_id: (t) => t.string('account_id').nullable().index(),
327
+ grant_id: (t) => t.string('grant_id').nullable().index(),
328
+ scope: (t) => t.text('scope'),
329
+ expires_at: (t) => t.dateTime('expires_at', { useTz: true, precision: 3 }).nullable(),
330
+ ended_at: (t) => t.dateTime('ended_at', { useTz: true, precision: 3 }).nullable(),
331
+ created_at: (t) => t.dateTime('created_at', { useTz: true, precision: 3 }).nullable(),
332
+ updated_at: (t) => t.dateTime('updated_at', { useTz: true, precision: 3 }).nullable(),
333
+ },
334
+ },
335
+ {
336
+ name: 'auth_poppy_tokens',
337
+ /** Session Tokens — só o hash. `jkt` = chave DPoP; `null` = Bearer (MCP). */
338
+ create: (t) => {
339
+ t.string('token_hash', 64).primary();
340
+ t.string('session_id').notNullable().index();
341
+ t.string('client_id', 2048).notNullable();
342
+ t.string('user_id').notNullable();
343
+ t.string('account_id').nullable();
344
+ t.string('grant_id').nullable().index();
345
+ t.text('scope').notNullable();
346
+ t.string('resource', 2048).nullable();
347
+ t.string('jkt', 64).nullable();
348
+ t.dateTime('expires_at', { useTz: true, precision: 3 }).notNullable().index();
349
+ t.dateTime('created_at', { useTz: true, precision: 3 }).notNullable();
350
+ },
351
+ columns: {
352
+ session_id: (t) => t.string('session_id').index(),
353
+ client_id: (t) => t.string('client_id', 2048),
354
+ user_id: (t) => t.string('user_id'),
355
+ account_id: (t) => t.string('account_id').nullable(),
356
+ grant_id: (t) => t.string('grant_id').nullable().index(),
357
+ scope: (t) => t.text('scope'),
358
+ resource: (t) => t.string('resource', 2048).nullable(),
359
+ jkt: (t) => t.string('jkt', 64).nullable(),
360
+ expires_at: (t) => t.dateTime('expires_at', { useTz: true, precision: 3 }).nullable(),
361
+ created_at: (t) => t.dateTime('created_at', { useTz: true, precision: 3 }).nullable(),
362
+ },
363
+ },
364
+ {
365
+ name: 'auth_poppy_requests',
366
+ /**
367
+ * Pedidos de sign-in: `authorize` (Direct — vira código após o consentimento),
368
+ * `device` (RFC 8628) e `mediated` (§4.7, aguardando o código de uso único).
369
+ * Códigos só como hash. Linhas expiradas são apagadas no próximo pedido.
370
+ */
371
+ create: (t) => {
372
+ t.string('id').primary();
373
+ t.string('kind', 16).notNullable();
374
+ t.string('code_hash', 64).nullable().unique();
375
+ t.string('user_code', 16).nullable().unique();
376
+ t.string('client_id', 2048).notNullable();
377
+ t.string('session_id').nullable();
378
+ t.string('account_id').nullable();
379
+ t.text('requested_scope').notNullable();
380
+ t.text('granted_scope').nullable();
381
+ t.string('redirect_uri', 2048).nullable();
382
+ t.string('code_challenge', 128).nullable();
383
+ t.string('status', 16).notNullable();
384
+ t.integer('attempts').notNullable().defaultTo(0);
385
+ t.integer('interval_seconds').nullable();
386
+ t.dateTime('last_polled_at', { useTz: true, precision: 3 }).nullable();
387
+ t.text('data').nullable();
388
+ t.dateTime('expires_at', { useTz: true, precision: 3 }).notNullable().index();
389
+ t.dateTime('created_at', { useTz: true, precision: 3 }).notNullable();
390
+ },
391
+ columns: {
392
+ kind: (t) => t.string('kind', 16),
393
+ code_hash: (t) => t.string('code_hash', 64).nullable(),
394
+ user_code: (t) => t.string('user_code', 16).nullable(),
395
+ client_id: (t) => t.string('client_id', 2048),
396
+ session_id: (t) => t.string('session_id').nullable(),
397
+ account_id: (t) => t.string('account_id').nullable(),
398
+ requested_scope: (t) => t.text('requested_scope'),
399
+ granted_scope: (t) => t.text('granted_scope').nullable(),
400
+ redirect_uri: (t) => t.string('redirect_uri', 2048).nullable(),
401
+ code_challenge: (t) => t.string('code_challenge', 128).nullable(),
402
+ status: (t) => t.string('status', 16),
403
+ attempts: (t) => t.integer('attempts').defaultTo(0),
404
+ interval_seconds: (t) => t.integer('interval_seconds').nullable(),
405
+ last_polled_at: (t) => t.dateTime('last_polled_at', { useTz: true, precision: 3 }).nullable(),
406
+ data: (t) => t.text('data').nullable(),
407
+ expires_at: (t) => t.dateTime('expires_at', { useTz: true, precision: 3 }).nullable(),
408
+ created_at: (t) => t.dateTime('created_at', { useTz: true, precision: 3 }).nullable(),
409
+ },
410
+ },
411
+ {
412
+ name: 'auth_poppy_jtis',
413
+ /** `jti` já aceitos (asserções, provas DPoP), até expirarem — anti-replay entre instâncias. */
414
+ create: (t) => {
415
+ t.string('key', 100).primary();
416
+ t.dateTime('expires_at', { useTz: true, precision: 3 }).notNullable().index();
417
+ },
418
+ columns: {
419
+ expires_at: (t) => t.dateTime('expires_at', { useTz: true, precision: 3 }).nullable(),
420
+ },
421
+ },
298
422
  ];
299
423
  /**
300
424
  * Probe searchPath-aware: `schema.hasTable` no Postgres ignora o
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adonis-agora/authkit-server",
3
- "version": "0.77.0",
3
+ "version": "0.78.0",
4
4
  "description": "AdonisJS OIDC/OAuth2 provider (Identity Provider) toolkit: ejectable auth server with sessions, rate-limiting, MFA/TOTP, audit log, federated logout and OpenTelemetry metrics.",
5
5
  "license": "MIT",
6
6
  "author": "dudousxd",