@adonis-agora/authkit-server 0.86.0 → 0.88.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 (43) hide show
  1. package/README.md +2 -2
  2. package/build/host/views/signup.edge +16 -0
  3. package/build/index.d.ts +1 -2
  4. package/build/index.js +0 -1
  5. package/build/src/agents/config.d.ts +1 -1
  6. package/build/src/agents/delegation_service.js +1 -1
  7. package/build/src/agents/poppy/client_registry.d.ts +5 -0
  8. package/build/src/agents/poppy/client_registry.js +35 -1
  9. package/build/src/agents/poppy/config.d.ts +111 -80
  10. package/build/src/agents/poppy/config.js +55 -35
  11. package/build/src/agents/poppy/dpop.d.ts +2 -2
  12. package/build/src/agents/poppy/dpop.js +3 -3
  13. package/build/src/agents/poppy/email_code_sign_in.d.ts +4 -4
  14. package/build/src/agents/poppy/email_code_sign_in.js +5 -5
  15. package/build/src/agents/poppy/errors.d.ts +7 -2
  16. package/build/src/agents/poppy/errors.js +5 -2
  17. package/build/src/agents/poppy/resource.d.ts +30 -25
  18. package/build/src/agents/poppy/resource.js +24 -20
  19. package/build/src/agents/poppy/safe_fetch.js +8 -2
  20. package/build/src/agents/poppy/service.d.ts +69 -62
  21. package/build/src/agents/poppy/service.js +540 -546
  22. package/build/src/agents/poppy/session_jwt.d.ts +30 -0
  23. package/build/src/agents/poppy/session_jwt.js +133 -0
  24. package/build/src/agents/poppy/store.d.ts +17 -22
  25. package/build/src/agents/poppy/store.js +22 -30
  26. package/build/src/agents/poppy/web_session.d.ts +23 -16
  27. package/build/src/agents/poppy/web_session.js +42 -24
  28. package/build/src/define_config.d.ts +6 -4
  29. package/build/src/host/admin_sessions_service.js +3 -3
  30. package/build/src/host/controllers/account_apps_controller.js +1 -1
  31. package/build/src/host/controllers/interaction_controller.js +32 -4
  32. package/build/src/host/controllers/poppy_consent_controller.d.ts +2 -2
  33. package/build/src/host/controllers/poppy_consent_controller.js +2 -2
  34. package/build/src/host/controllers/poppy_controller.d.ts +19 -11
  35. package/build/src/host/controllers/poppy_controller.js +93 -68
  36. package/build/src/host/controllers/registration_controller.js +30 -7
  37. package/build/src/host/csrf.d.ts +2 -4
  38. package/build/src/host/csrf.js +2 -3
  39. package/build/src/host/register_auth_host.js +10 -5
  40. package/build/src/host/signup_code.d.ts +41 -0
  41. package/build/src/host/signup_code.js +56 -0
  42. package/build/src/schema/ensure.js +14 -9
  43. package/package.json +3 -3
@@ -0,0 +1,30 @@
1
+ import type { PoppyClient } from './client_registry.js';
2
+ export declare const SESSION_JWT_TYP = "poppy-session+jwt";
3
+ export declare const DELEGATION_TYP = "pap-delegation+jwt";
4
+ export declare class SessionJwtError extends Error {
5
+ }
6
+ export interface VerifiedSessionJwt {
7
+ clientId: string;
8
+ sid: string;
9
+ userId: string;
10
+ agentId: string | null;
11
+ jti: string;
12
+ iat: number;
13
+ }
14
+ export interface VerifySessionJwtInput {
15
+ /** Valor do(s) header(s) `Poppy-Session`. */
16
+ header: string | string[] | undefined;
17
+ method: string;
18
+ url: string;
19
+ /** Access token da request, ou `null`. */
20
+ accessToken: string | null;
21
+ /** O identificador da empresa: o issuer (§3.2). */
22
+ audience: string;
23
+ /** Janela de aceitação do `iat`, em segundos. */
24
+ maxAge: number;
25
+ /** Resolve o agente pelo `iss` (client metadata + `jwks_uri`). */
26
+ resolveClient: (clientId: unknown) => Promise<PoppyClient>;
27
+ now: Date;
28
+ }
29
+ /** Verifica o JWT (§5.3, passos 1–3). Lança {@link SessionJwtError}. */
30
+ export declare function verifySessionJwt(input: VerifySessionJwtInput): Promise<VerifiedSessionJwt>;
@@ -0,0 +1,133 @@
1
+ /**
2
+ * O header `Poppy-Session` (Draft 0.3, §5.2–§5.3): um JWT que o personal agent assina para uma
3
+ * request, com uma chave delegada. Uma chave do `jwks_uri` assina a delegação
4
+ * (`pap-delegation+jwt`), que vai no header `pap_dlg` do JWT e atesta a chave delegada em `cnf.jwk`.
5
+ *
6
+ * Só as checagens do JWT em si: o `client_id` do token, a Session e o replay ficam no `PoppyService`.
7
+ */
8
+ import { calculateJwkThumbprint, decodeJwt, decodeProtectedHeader, importJWK, jwtVerify, } from 'jose';
9
+ import { accessTokenHash, normalizeHtu } from './dpop.js';
10
+ export const SESSION_JWT_TYP = 'poppy-session+jwt';
11
+ export const DELEGATION_TYP = 'pap-delegation+jwt';
12
+ /** Delegação: `ES256` obrigatório; `PS384` permite a assinatura cega (§5.2, §10.7). */
13
+ const DELEGATION_ALGS = ['ES256', 'PS384'];
14
+ const DELEGATION_MAX_LIFETIME_S = 24 * 3600;
15
+ const CLOCK_SKEW_S = 5;
16
+ /** Mesmos caracteres dos Conversation IDs (§8.2). */
17
+ const SESSION_ID = /^[A-Za-z0-9_-]{1,256}$/;
18
+ /** Tamanho da coluna `user_id`/`agent_id`. */
19
+ const MAX_ID_LENGTH = 255;
20
+ export class SessionJwtError extends Error {
21
+ }
22
+ const fail = (why) => new SessionJwtError(why);
23
+ /** Passos 1–4 de "Delegated keys" (§5.2): devolve a chave delegada, já conferida. */
24
+ async function verifyDelegation(delegation, client, now) {
25
+ if (typeof delegation !== 'string' || !delegation)
26
+ throw fail('pap_dlg is required');
27
+ let payload;
28
+ try {
29
+ ({ payload } = await jwtVerify(delegation, client.keys, {
30
+ algorithms: DELEGATION_ALGS,
31
+ typ: DELEGATION_TYP,
32
+ issuer: client.clientId,
33
+ currentDate: now,
34
+ clockTolerance: CLOCK_SKEW_S,
35
+ requiredClaims: ['iat', 'exp', 'jti'],
36
+ }));
37
+ }
38
+ catch {
39
+ throw fail('delegation verification failed');
40
+ }
41
+ if (payload.exp - payload.iat > DELEGATION_MAX_LIFETIME_S) {
42
+ throw fail('delegation lasts more than 24 hours');
43
+ }
44
+ const jwk = payload.cnf?.jwk;
45
+ if (!jwk || jwk.kty !== 'EC' || jwk.crv !== 'P-256' || 'd' in jwk) {
46
+ throw fail('cnf.jwk must be a public P-256 key');
47
+ }
48
+ return { jwk: jwk, exp: payload.exp };
49
+ }
50
+ /** Verifica o JWT (§5.3, passos 1–3). Lança {@link SessionJwtError}. */
51
+ export async function verifySessionJwt(input) {
52
+ const token = input.header;
53
+ if (typeof token !== 'string' || !token)
54
+ throw fail('exactly one Poppy-Session header required');
55
+ let header;
56
+ let iss;
57
+ try {
58
+ header = decodeProtectedHeader(token);
59
+ iss = decodeJwt(token).iss;
60
+ }
61
+ catch {
62
+ throw fail('malformed Session JWT');
63
+ }
64
+ if (header.typ !== SESSION_JWT_TYP)
65
+ throw fail(`typ must be ${SESSION_JWT_TYP}`);
66
+ if (header.alg !== 'ES256')
67
+ throw fail('alg must be ES256');
68
+ let client;
69
+ try {
70
+ client = await input.resolveClient(iss);
71
+ }
72
+ catch {
73
+ throw fail('the client_id was refused');
74
+ }
75
+ const delegation = await verifyDelegation(header.pap_dlg, client, input.now);
76
+ if (header.kid !== (await calculateJwkThumbprint(delegation.jwk, 'sha256'))) {
77
+ throw fail('kid must be the thumbprint of the delegated key');
78
+ }
79
+ let claims;
80
+ try {
81
+ const key = await importJWK(delegation.jwk, 'ES256');
82
+ ({ payload: claims } = await jwtVerify(token, key, {
83
+ algorithms: ['ES256'],
84
+ typ: SESSION_JWT_TYP,
85
+ issuer: client.clientId,
86
+ currentDate: input.now,
87
+ }));
88
+ }
89
+ catch {
90
+ throw fail('Session JWT signature verification failed');
91
+ }
92
+ if (typeof claims.aud !== 'string' || claims.aud !== input.audience) {
93
+ throw fail('aud must be the Company identifier, as a single string');
94
+ }
95
+ if (claims.htm !== input.method.toUpperCase())
96
+ throw fail('htm does not match the request');
97
+ const htu = typeof claims.htu === 'string' ? normalizeHtu(claims.htu) : null;
98
+ if (htu === null || htu !== normalizeHtu(input.url))
99
+ throw fail('htu does not match the request');
100
+ const nowS = Math.floor(input.now.getTime() / 1000);
101
+ if (typeof claims.iat !== 'number' || Math.abs(nowS - claims.iat) > input.maxAge) {
102
+ throw fail('iat is outside the acceptance window');
103
+ }
104
+ if (claims.iat > delegation.exp)
105
+ throw fail('signed after the delegation expired');
106
+ if (typeof claims.jti !== 'string' || !claims.jti)
107
+ throw fail('jti is required');
108
+ if (typeof claims.sid !== 'string' || !SESSION_ID.test(claims.sid))
109
+ throw fail('invalid sid');
110
+ if (typeof claims.sub !== 'string' || !claims.sub || claims.sub.length > MAX_ID_LENGTH) {
111
+ throw fail('sub (the User ID) is required');
112
+ }
113
+ const agentId = claims.agent_id;
114
+ if (agentId !== undefined &&
115
+ (typeof agentId !== 'string' || !agentId || agentId.length > MAX_ID_LENGTH)) {
116
+ throw fail('invalid agent_id');
117
+ }
118
+ if (input.accessToken === null) {
119
+ if (claims.ath !== undefined)
120
+ throw fail('ath must be absent without an access token');
121
+ }
122
+ else if (claims.ath !== accessTokenHash(input.accessToken)) {
123
+ throw fail('ath does not match the access token');
124
+ }
125
+ return {
126
+ clientId: client.clientId,
127
+ sid: claims.sid,
128
+ userId: claims.sub,
129
+ agentId: agentId ?? null,
130
+ jti: claims.jti,
131
+ iat: claims.iat,
132
+ };
133
+ }
@@ -1,8 +1,7 @@
1
1
  /**
2
- * Persistência do Poppy — tabelas LIB-OWNED (ver `schema/ensure.ts`): Sessions,
3
- * Session Tokens (só o hash), pedidos de sign-in (autorização Direct, códigos,
4
- * device codes, Mediated) e `jti` vistos. Os Account Tokens reaproveitam
5
- * `auth_agent_grants` + `auth_agent_refresh_tokens` (ver `DelegationStore`).
2
+ * Persistência do Poppy — tabelas LIB-OWNED (ver `schema/ensure.ts`): Sessions, access tokens (só o
3
+ * hash), pedidos de sign-in e de entrada de navegador, e `jti` vistos. Os refresh tokens
4
+ * reaproveitam `auth_agent_grants` + `auth_agent_refresh_tokens` (ver `DelegationStore`).
6
5
  *
7
6
  * Mesmas regras do `DelegationStore`: query builder puro, datas comparadas em JS
8
7
  * depois de buscar a linha pela chave, transições de estado atômicas por
@@ -12,12 +11,14 @@ export declare const SESSION_TABLE = "auth_poppy_sessions";
12
11
  export declare const TOKEN_TABLE = "auth_poppy_tokens";
13
12
  export declare const REQUEST_TABLE = "auth_poppy_requests";
14
13
  export interface SessionRow {
14
+ /** Derivado de `client_id` + `sid` (§5.3). */
15
15
  id: string;
16
16
  clientId: string;
17
17
  userId: string;
18
- /** Conta a que a Session está PRESA (uma vez logada, não troca — §4.4). */
18
+ agentId: string | null;
19
+ /** Conta que a Session já usou: recusamos outra com `invalid_session` (§5.4). */
19
20
  accountId: string | null;
20
- /** Grant (Account Token) com que está logada agora; `null` = deslogada. */
21
+ /** Colunas da Draft 0.1, sem uso: o acesso à conta não passa pela Session. */
21
22
  grantId: string | null;
22
23
  scope: string;
23
24
  expiresAt: Date;
@@ -25,21 +26,20 @@ export interface SessionRow {
25
26
  createdAt: Date;
26
27
  updatedAt: Date;
27
28
  }
29
+ /** Access token (§4.5). */
28
30
  export interface TokenRow {
29
31
  tokenHash: string;
30
- sessionId: string;
31
32
  clientId: string;
32
- userId: string;
33
33
  accountId: string | null;
34
34
  grantId: string | null;
35
35
  scope: string;
36
36
  resource: string | null;
37
- /** Thumbprint da chave DPoP; `null` = Bearer (só MCP). */
37
+ /** Thumbprint da chave DPoP; `null` = Bearer. */
38
38
  jkt: string | null;
39
39
  expiresAt: Date;
40
40
  createdAt: Date;
41
41
  }
42
- export type RequestKind = 'authorize' | 'device' | 'mediated';
42
+ export type RequestKind = 'authorize' | 'device' | 'mediated' | 'browser';
43
43
  export type RequestStatus = 'pending' | 'approved' | 'denied' | 'consumed' | 'failed';
44
44
  export interface RequestRow {
45
45
  id: string;
@@ -70,19 +70,11 @@ export declare class PoppyStore {
70
70
  /** Estende a vida da Session (uso). */
71
71
  touchSession(id: string, expiresAt: Date, at: Date): Promise<void>;
72
72
  /**
73
- * Loga a Session numa conta. Atômico: só se ainda estiver deslogada ou presa
74
- * à MESMA conta. `false` = presa a outra conta (ou terminou) nesse meio-tempo.
73
+ * Registra a conta que a Session usa (§5.4). Atômico: só se nenhuma foi registrada ou é a MESMA.
74
+ * `false` = outra conta chegou nesse meio-tempo.
75
75
  */
76
- signInSession(id: string, patch: {
77
- accountId: string;
78
- grantId: string;
79
- scope: string;
80
- expiresAt: Date;
81
- at: Date;
82
- }): Promise<boolean>;
83
- /** Sign-out (§4.9): as Sessions logadas com o grant continuam, deslogadas. */
84
- signOutSessionsOfGrant(grantId: string, at: Date): Promise<number>;
85
- /** Encerra todas as Sessions de um agente (bloqueio no console): os tokens param na hora. */
76
+ bindSessionAccount(id: string, accountId: string, at: Date): Promise<boolean>;
77
+ /** Encerra todas as Sessions de um agente (bloqueio no console): param na hora. */
86
78
  endSessionsOfClient(clientId: string, at: Date): Promise<number>;
87
79
  insertToken(row: TokenRow): Promise<void>;
88
80
  findToken(hash: string): Promise<TokenRow | null>;
@@ -102,6 +94,9 @@ export declare class PoppyStore {
102
94
  codeHash?: string;
103
95
  attempts?: number;
104
96
  expiresAt?: Date;
97
+ clientId?: string;
98
+ sessionId?: string;
99
+ data?: Record<string, unknown>;
105
100
  }): Promise<boolean>;
106
101
  /** Conta uma tentativa errada (atômico sobre o valor lido). */
107
102
  bumpAttempts(id: string, from: number): Promise<boolean>;
@@ -1,8 +1,7 @@
1
1
  /**
2
- * Persistência do Poppy — tabelas LIB-OWNED (ver `schema/ensure.ts`): Sessions,
3
- * Session Tokens (só o hash), pedidos de sign-in (autorização Direct, códigos,
4
- * device codes, Mediated) e `jti` vistos. Os Account Tokens reaproveitam
5
- * `auth_agent_grants` + `auth_agent_refresh_tokens` (ver `DelegationStore`).
2
+ * Persistência do Poppy — tabelas LIB-OWNED (ver `schema/ensure.ts`): Sessions, access tokens (só o
3
+ * hash), pedidos de sign-in e de entrada de navegador, e `jti` vistos. Os refresh tokens
4
+ * reaproveitam `auth_agent_grants` + `auth_agent_refresh_tokens` (ver `DelegationStore`).
6
5
  *
7
6
  * Mesmas regras do `DelegationStore`: query builder puro, datas comparadas em JS
8
7
  * depois de buscar a linha pela chave, transições de estado atômicas por
@@ -35,6 +34,7 @@ function toSession(row) {
35
34
  id: row.id,
36
35
  clientId: row.client_id,
37
36
  userId: row.user_id,
37
+ agentId: row.agent_id ?? null,
38
38
  accountId: row.account_id ?? null,
39
39
  grantId: row.grant_id ?? null,
40
40
  scope: row.scope ?? '',
@@ -47,9 +47,7 @@ function toSession(row) {
47
47
  function toToken(row) {
48
48
  return {
49
49
  tokenHash: row.token_hash,
50
- sessionId: row.session_id,
51
50
  clientId: row.client_id,
52
- userId: row.user_id,
53
51
  accountId: row.account_id ?? null,
54
52
  grantId: row.grant_id ?? null,
55
53
  scope: row.scope ?? '',
@@ -103,6 +101,7 @@ export class PoppyStore {
103
101
  id: row.id,
104
102
  client_id: row.clientId,
105
103
  user_id: row.userId,
104
+ agent_id: row.agentId,
106
105
  account_id: row.accountId,
107
106
  grant_id: row.grantId,
108
107
  scope: row.scope,
@@ -126,33 +125,18 @@ export class PoppyStore {
126
125
  .update({ expires_at: expiresAt, updated_at: at });
127
126
  }
128
127
  /**
129
- * Loga a Session numa conta. Atômico: só se ainda estiver deslogada ou presa
130
- * à MESMA conta. `false` = presa a outra conta (ou terminou) nesse meio-tempo.
128
+ * Registra a conta que a Session usa (§5.4). Atômico: só se nenhuma foi registrada ou é a MESMA.
129
+ * `false` = outra conta chegou nesse meio-tempo.
131
130
  */
132
- async signInSession(id, patch) {
131
+ async bindSessionAccount(id, accountId, at) {
133
132
  const result = await this.conn()
134
133
  .from(SESSION_TABLE)
135
134
  .where('id', id)
136
- .whereNull('ended_at')
137
- .where((q) => q.whereNull('account_id').orWhere('account_id', patch.accountId))
138
- .update({
139
- account_id: patch.accountId,
140
- grant_id: patch.grantId,
141
- scope: patch.scope,
142
- expires_at: patch.expiresAt,
143
- updated_at: patch.at,
144
- });
135
+ .where((q) => q.whereNull('account_id').orWhere('account_id', accountId))
136
+ .update({ account_id: accountId, updated_at: at });
145
137
  return affected(result) > 0;
146
138
  }
147
- /** Sign-out (§4.9): as Sessions logadas com o grant continuam, deslogadas. */
148
- async signOutSessionsOfGrant(grantId, at) {
149
- const result = await this.conn()
150
- .from(SESSION_TABLE)
151
- .where('grant_id', grantId)
152
- .update({ grant_id: null, scope: '', updated_at: at });
153
- return affected(result);
154
- }
155
- /** Encerra todas as Sessions de um agente (bloqueio no console): os tokens param na hora. */
139
+ /** Encerra todas as Sessions de um agente (bloqueio no console): param na hora. */
156
140
  async endSessionsOfClient(clientId, at) {
157
141
  const result = await this.conn()
158
142
  .from(SESSION_TABLE)
@@ -161,13 +145,15 @@ export class PoppyStore {
161
145
  .update({ ended_at: at, grant_id: null, scope: '', updated_at: at });
162
146
  return affected(result);
163
147
  }
164
- // ─── session tokens ───────────────────────────────────────────────────────
148
+ // ─── access tokens ────────────────────────────────────────────────────────
165
149
  async insertToken(row) {
166
150
  await this.conn().table(TOKEN_TABLE).insert({
167
151
  token_hash: row.tokenHash,
168
- session_id: row.sessionId,
152
+ // `session_id`/`user_id` são NOT NULL desde a Draft 0.1 e o schema só é aditivo; o sign-in
153
+ // não conhece a Session nem o User ID (§4.4).
154
+ session_id: '',
169
155
  client_id: row.clientId,
170
- user_id: row.userId,
156
+ user_id: '',
171
157
  account_id: row.accountId,
172
158
  grant_id: row.grantId,
173
159
  scope: row.scope,
@@ -238,6 +224,12 @@ export class PoppyStore {
238
224
  update.attempts = patch.attempts;
239
225
  if (patch.expiresAt !== undefined)
240
226
  update.expires_at = patch.expiresAt;
227
+ if (patch.clientId !== undefined)
228
+ update.client_id = patch.clientId;
229
+ if (patch.sessionId !== undefined)
230
+ update.session_id = patch.sessionId;
231
+ if (patch.data !== undefined)
232
+ update.data = JSON.stringify(patch.data);
241
233
  const result = await this.conn()
242
234
  .from(REQUEST_TABLE)
243
235
  .where('id', id)
@@ -1,32 +1,39 @@
1
1
  /**
2
- * O cookie do site para o navegador de um personal agent (§5): login pela
3
- * asserção de navegador, e o acompanhamento do estado da Session a cada request.
2
+ * O cookie do site para o navegador de um personal agent (§6): a entrada na Session, e o
3
+ * acompanhamento do acesso à conta autorizado nela a cada request.
4
4
  */
5
5
  import type { HttpContext } from '@adonisjs/core/http';
6
- import type { PoppyBrowserSessionResult, PoppySessionState } from './service.js';
6
+ import type { PoppyBrowserJoin, PoppySessionState } from './service.js';
7
7
  export interface PoppyWebSessionMarker {
8
8
  sessionId: string;
9
9
  clientId: string;
10
10
  userId: string;
11
+ /** Conta, grant e scopes autorizados na entrada (§6, passo 2); `null` = entrada deslogada. */
12
+ accountId: string | null;
13
+ grantId: string | null;
14
+ scopes: string[];
11
15
  }
12
16
  /**
13
- * Liga o navegador à Session: regenera a sessão do app (session fixation),
14
- * marca-a como do agente e, se a Session está logada, faz o login com o
15
- * mecanismo do app — o hook `web.login`, ou a sessão de conta do authkit +
16
- * `adonisAuth.guard`.
17
+ * Liga o navegador à Session (§6, passo 3): regenera a sessão do app (session fixation) e a marca
18
+ * como do agente. Uma entrada com access token faz o login na conta autorizada, no lugar de qualquer
19
+ * outro, com o mecanismo do app — o hook `web.login`, ou a sessão de conta do authkit +
20
+ * `adonisAuth.guard`. Uma entrada sem access token não mexe no login do navegador — a não ser que
21
+ * uma entrada anterior o tenha logado: aí o sign-in dela termina aqui.
22
+ */
23
+ export declare function loginPoppyBrowser(httpCtx: HttpContext, result: PoppyBrowserJoin): Promise<void>;
24
+ /**
25
+ * Estado da Session Poppy deste navegador (preenchido por {@link poppyWebSession}).
26
+ *
27
+ * @experimental Poppy (Personal Agent Protocol) Draft 0.3 — acompanha a spec em desenvolvimento e PODE MUDAR
28
+ * de forma incompatível fora de majors enquanto ela for draft.
17
29
  */
18
- export declare function loginPoppyBrowser(httpCtx: HttpContext, result: PoppyBrowserSessionResult): Promise<void>;
19
- /** Estado da Session Poppy deste navegador (preenchido por {@link poppyWebSession}). *
20
- /** Estado da Session Poppy deste navegador (preenchido por {@link poppyWebSession}). * @experimental Poppy (Personal Agent Protocol) Draft 0.1 — acompanha a spec em desenvolvimento e PODE MUDAR
21
- /** Estado da Session Poppy deste navegador (preenchido por {@link poppyWebSession}). * de forma incompatível fora de majors enquanto ela for draft.
22
- /** Estado da Session Poppy deste navegador (preenchido por {@link poppyWebSession}). */
23
30
  export declare function poppyWebSessionOf(ctx: HttpContext): PoppySessionState | null;
24
31
  /**
25
- * Middleware do site: um navegador de agente segue o estado ATUAL da Session
26
- * (§5 — logou/deslogou/mudou de scopes, o próximo request reflete; o cookie não
27
- * sobrevive à Session). Coloque-o depois do middleware de sessão.
32
+ * Middleware do site: o cookie age pela conta e pelos scopes autorizados na entrada e deixa de agir
33
+ * quando o sign-in termina (sign-out, agente desconectado); não sobrevive à Session (§6). Um login
34
+ * antigo não volta. Coloque-o depois do middleware de sessão.
28
35
  *
29
- * @experimental Poppy (Personal Agent Protocol) Draft 0.1 — acompanha a spec em desenvolvimento e PODE MUDAR
36
+ * @experimental Poppy (Personal Agent Protocol) Draft 0.3 — acompanha a spec em desenvolvimento e PODE MUDAR
30
37
  * de forma incompatível fora de majors enquanto ela for draft.
31
38
  */
32
39
  export declare function poppyWebSession(): (httpCtx: HttpContext, next: () => Promise<void>) => Promise<void>;
@@ -3,10 +3,11 @@ import { syncAdonisAuthLogin, syncAdonisAuthLogout } from '../../host/adonis_aut
3
3
  import { POPPY_WEB_SESSION_KEY } from '../../host/poppy_session_key.js';
4
4
  import { personalAgentsFor } from '../runtime.js';
5
5
  /**
6
- * Liga o navegador à Session: regenera a sessão do app (session fixation),
7
- * marca-a como do agente e, se a Session está logada, faz o login com o
8
- * mecanismo do app — o hook `web.login`, ou a sessão de conta do authkit +
9
- * `adonisAuth.guard`.
6
+ * Liga o navegador à Session (§6, passo 3): regenera a sessão do app (session fixation) e a marca
7
+ * como do agente. Uma entrada com access token faz o login na conta autorizada, no lugar de qualquer
8
+ * outro, com o mecanismo do app — o hook `web.login`, ou a sessão de conta do authkit +
9
+ * `adonisAuth.guard`. Uma entrada sem access token não mexe no login do navegador — a não ser que
10
+ * uma entrada anterior o tenha logado: aí o sign-in dela termina aqui.
10
11
  */
11
12
  export async function loginPoppyBrowser(httpCtx, result) {
12
13
  // Sem augmentation no barrel (ver `augmentation_isolation.spec.ts`): a sessão é lida sem tipo.
@@ -14,12 +15,15 @@ export async function loginPoppyBrowser(httpCtx, result) {
14
15
  const service = await ctx.containerResolver.make('authkit.server');
15
16
  const cfg = service.config;
16
17
  const login = cfg.personalAgents?.poppy?.web?.login;
18
+ const previous = ctx.session.get(POPPY_WEB_SESSION_KEY);
17
19
  await ctx.session.regenerate();
18
- ctx.session.forget(ACCOUNT_SESSION_KEY);
19
20
  const marker = {
20
21
  sessionId: result.sessionId,
21
22
  clientId: result.clientId,
22
23
  userId: result.userId,
24
+ accountId: result.accountId,
25
+ grantId: result.grantId,
26
+ scopes: result.scopes,
23
27
  };
24
28
  ctx.session.put(POPPY_WEB_SESSION_KEY, marker);
25
29
  if (login) {
@@ -38,24 +42,27 @@ export async function loginPoppyBrowser(httpCtx, result) {
38
42
  if (account)
39
43
  await syncAdonisAuthLogin(ctx, cfg, account);
40
44
  }
41
- else {
45
+ else if (previous?.accountId) {
46
+ ctx.session.forget(ACCOUNT_SESSION_KEY);
42
47
  await syncAdonisAuthLogout(ctx, cfg);
43
48
  }
44
49
  }
45
50
  const states = new WeakMap();
46
- /** Estado da Session Poppy deste navegador (preenchido por {@link poppyWebSession}). *
47
- /** Estado da Session Poppy deste navegador (preenchido por {@link poppyWebSession}). * @experimental Poppy (Personal Agent Protocol) Draft 0.1 — acompanha a spec em desenvolvimento e PODE MUDAR
48
- /** Estado da Session Poppy deste navegador (preenchido por {@link poppyWebSession}). * de forma incompatível fora de majors enquanto ela for draft.
49
- /** Estado da Session Poppy deste navegador (preenchido por {@link poppyWebSession}). */
51
+ /**
52
+ * Estado da Session Poppy deste navegador (preenchido por {@link poppyWebSession}).
53
+ *
54
+ * @experimental Poppy (Personal Agent Protocol) Draft 0.3 — acompanha a spec em desenvolvimento e PODE MUDAR
55
+ * de forma incompatível fora de majors enquanto ela for draft.
56
+ */
50
57
  export function poppyWebSessionOf(ctx) {
51
58
  return states.get(ctx) ?? null;
52
59
  }
53
60
  /**
54
- * Middleware do site: um navegador de agente segue o estado ATUAL da Session
55
- * (§5 — logou/deslogou/mudou de scopes, o próximo request reflete; o cookie não
56
- * sobrevive à Session). Coloque-o depois do middleware de sessão.
61
+ * Middleware do site: o cookie age pela conta e pelos scopes autorizados na entrada e deixa de agir
62
+ * quando o sign-in termina (sign-out, agente desconectado); não sobrevive à Session (§6). Um login
63
+ * antigo não volta. Coloque-o depois do middleware de sessão.
57
64
  *
58
- * @experimental Poppy (Personal Agent Protocol) Draft 0.1 — acompanha a spec em desenvolvimento e PODE MUDAR
65
+ * @experimental Poppy (Personal Agent Protocol) Draft 0.3 — acompanha a spec em desenvolvimento e PODE MUDAR
59
66
  * de forma incompatível fora de majors enquanto ela for draft.
60
67
  */
61
68
  export function poppyWebSession() {
@@ -65,16 +72,18 @@ export function poppyWebSession() {
65
72
  if (!marker?.sessionId)
66
73
  return next();
67
74
  const runtime = await personalAgentsFor(ctx);
68
- const state = runtime?.poppy ? await runtime.poppy.sessionState(marker.sessionId) : null;
75
+ const state = runtime?.poppy ? await runtime.poppy.browserState(marker) : null;
69
76
  const service = await ctx.containerResolver.make('authkit.server');
70
77
  const cfg = service.config;
71
78
  const login = cfg.personalAgents?.poppy?.web?.login;
72
79
  if (!state?.active || state.clientId !== marker.clientId) {
73
- // A Session terminou: o cookie também.
80
+ // A Session terminou: o cookie também — e o login dela, não o do próprio navegador.
74
81
  ctx.session.forget(POPPY_WEB_SESSION_KEY);
75
- ctx.session.forget(ACCOUNT_SESSION_KEY);
82
+ if (marker.accountId) {
83
+ ctx.session.forget(ACCOUNT_SESSION_KEY);
84
+ await syncAdonisAuthLogout(ctx, cfg);
85
+ }
76
86
  await ctx.session.regenerate();
77
- await syncAdonisAuthLogout(ctx, cfg);
78
87
  return next();
79
88
  }
80
89
  states.set(ctx, state);
@@ -89,13 +98,22 @@ export function poppyWebSession() {
89
98
  return next();
90
99
  }
91
100
  const current = ctx.session.get(ACCOUNT_SESSION_KEY);
92
- if (state.signedIn && state.accountId && current !== state.accountId) {
93
- ctx.session.put(ACCOUNT_SESSION_KEY, state.accountId);
94
- const account = await cfg.accountStore.findById(state.accountId);
95
- if (account)
96
- await syncAdonisAuthLogin(ctx, cfg, account);
101
+ if (state.signedIn && state.accountId) {
102
+ if (current !== state.accountId) {
103
+ ctx.session.put(ACCOUNT_SESSION_KEY, state.accountId);
104
+ const account = await cfg.accountStore.findById(state.accountId);
105
+ if (account)
106
+ await syncAdonisAuthLogin(ctx, cfg, account);
107
+ }
97
108
  }
98
- else if (!state.signedIn && current) {
109
+ else if (marker.accountId) {
110
+ // O sign-in da entrada terminou: o navegador sai, e a Session segue deslogada.
111
+ ctx.session.put(POPPY_WEB_SESSION_KEY, {
112
+ ...marker,
113
+ accountId: null,
114
+ grantId: null,
115
+ scopes: [],
116
+ });
99
117
  ctx.session.forget(ACCOUNT_SESSION_KEY);
100
118
  await syncAdonisAuthLogout(ctx, cfg);
101
119
  }
@@ -76,8 +76,8 @@ export interface MailHooks {
76
76
  magicUrl: string;
77
77
  token: string;
78
78
  /**
79
- * Código OTP de login, presente APENAS quando `login.otp.enabled` está
80
- * ligado. O host pode montar o próprio e-mail com link E código. Ausente no
79
+ * Código OTP, de login ou de cadastro (`kind: 'signup'`), presente APENAS quando
80
+ * `login.otp.enabled` está ligado. O host pode montar o próprio e-mail com link E código. Ausente no
81
81
  * fluxo só-magic-link (back-compat).
82
82
  */
83
83
  code?: string;
@@ -90,7 +90,7 @@ export interface MailHooks {
90
90
  channel?: 'code' | 'link';
91
91
  /**
92
92
  * `'signup'`: link de confirmação do cadastro passwordless (a conta é criada
93
- * ao abri-lo). Ausente = login por magic link. O host pode trocar o texto do
93
+ * ao abri-lo, ou ao confirmar o `code`). Ausente = login por magic link. O host pode trocar o texto do
94
94
  * e-mail ("confirme seu cadastro"); ignorar o campo continua funcionando.
95
95
  */
96
96
  kind?: 'signup';
@@ -523,7 +523,9 @@ export interface PasswordlessConfigInput {
523
523
  * senha) e envia um link de confirmação (`onMagicLink` com `kind: 'signup'`).
524
524
  * A conta só é criada quando o dono do e-mail abre o link: nasce com o e-mail
525
525
  * verificado e uma senha aleatória inutilizável, e o login segue pelo magic
526
- * link. E-mail que já tem conta recebe um magic link de login. Default: false.
526
+ * link. E-mail que já tem conta recebe um magic link de login. Com `login.otp`
527
+ * ligado, os dois casos também levam um código para concluir no mesmo navegador.
528
+ * Default: false.
527
529
  * Exige que o accountStore implemente {@link MagicLinkCapability} e o serviço de
528
530
  * encryption do app (APP_KEY), que assina o link; sem a capacidade o cadastro
529
531
  * passwordless fica indisponível e o fluxo de senha segue valendo.
@@ -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 Account Tokens do Poppy). Sem
90
+ * agente agindo na conta (delegações do PACT e refresh tokens do Poppy). 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,9 +142,9 @@ 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 Account Tokens do Poppy. */
145
+ /** Grants da conta, exceto os espelhos `pgrant_` de refresh tokens do Poppy. */
146
146
  async #accountGrants(accountId) {
147
- // Os grants `pgrant_` são o espelho de um Account Token do Poppy (Bearer de MCP) — aparecem e
147
+ // Os grants `pgrant_` são o espelho de um refresh token do Poppy (Bearer de MCP) — aparecem e
148
148
  // são revogados como personal agent, não como app OIDC.
149
149
  return (await this.#listModelOfAccount('Grant', accountId)).filter((r) => !r.id.startsWith('pgrant_'));
150
150
  }
@@ -38,7 +38,7 @@ export default class AccountAppsController {
38
38
  scopes: g.scopes,
39
39
  })))
40
40
  : null;
41
- // Account Tokens do Poppy: o agente é desconectado pela mesma lista (§4.9).
41
+ // Refresh tokens do Poppy: o agente é desconectado pela mesma lista (§5.5).
42
42
  const poppy = agentsRuntime?.poppy;
43
43
  const poppyAgents = poppy
44
44
  ? await Promise.all((await poppy.listGrants(accountId)).map(async (g) => ({