@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
@@ -1,11 +1,15 @@
1
- /** Scopes amplos definidos pelo protocolo (§4.4). */
2
- export const POPPY_READ = 'poppy:read';
3
- export const POPPY_WRITE = 'poppy:write';
4
- /** Descrições default dos scopes do protocolo — o que a tela de consentimento mostra. */
5
- export const POPPY_SCOPE_DESCRIPTIONS = {
6
- [POPPY_READ]: 'View your account information, such as orders and bookings',
7
- [POPPY_WRITE]: 'Make changes on your account, such as exchanging an item or updating a booking',
8
- };
1
+ /** Parâmetros que um campo do Mediated Sign-In não pode usar (extensão, §2). */
2
+ const RESERVED_FIELDS = new Set([
3
+ 'grant_type',
4
+ 'scope',
5
+ 'resource',
6
+ 'client_id',
7
+ 'client_secret',
8
+ 'client_assertion',
9
+ 'client_assertion_type',
10
+ 'sign_in_id',
11
+ 'one_time_code',
12
+ ]);
9
13
  const SCOPE_ID = /^[\x21\x23-\x5B\x5D-\x7E]+$/; // RFC 6749 §3.3 scope-token
10
14
  const DOMAIN = /^(?=.{1,253}$)([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)(\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$/;
11
15
  function fail(message) {
@@ -30,11 +34,6 @@ function absoluteUrl(value, what) {
30
34
  fail(`${what} precisa ser http(s) (recebeu "${value}").`);
31
35
  }
32
36
  }
33
- /** `www.example.com` → `example.com`, minúsculo. */
34
- export function stripWww(host) {
35
- const lower = host.toLowerCase().replace(/\.$/, '');
36
- return lower.startsWith('www.') ? lower.slice(4) : lower;
37
- }
38
37
  export function normalizePoppyPrefix(prefix) {
39
38
  const trimmed = (prefix ?? '/poppy').trim().replace(/\/+$/, '');
40
39
  if (!trimmed)
@@ -70,30 +69,27 @@ export function resolvePoppyConfig(input, fallbackScopes) {
70
69
  const org = input.organization;
71
70
  if (!org?.name?.trim())
72
71
  fail('organization.name é obrigatório.');
73
- const domains = [org.domain, ...(org.aliases ?? [])].map((d) => stripWww(d ?? ''));
72
+ // Domínios comparados só ignorando caixa, sem nenhuma outra normalização (§3.1).
73
+ const domains = [org.domain, ...(org.aliases ?? [])].map((d) => (d ?? '').toLowerCase().replace(/\.$/, ''));
74
74
  for (const d of domains) {
75
75
  if (!DOMAIN.test(d))
76
76
  fail(`organization — domínio inválido "${d}" (só o host, sem esquema).`);
77
77
  }
78
- const customScopes = { ...(input.scopes ?? fallbackScopes ?? {}) };
79
- for (const id of Object.keys(customScopes)) {
78
+ const scopes = { ...(input.scopes ?? fallbackScopes ?? {}) };
79
+ for (const id of Object.keys(scopes)) {
80
80
  if (!SCOPE_ID.test(id))
81
81
  fail(`scopes — id inválido "${id}" (sem espaços/aspas).`);
82
- if (id.startsWith('poppy:')) {
83
- fail(`scopes — "${id}": nomes com "poppy:" são reservados ao protocolo (§4.4).`);
84
- }
85
82
  }
86
- const broad = input.broadScopes !== false;
87
- const scopes = {
88
- ...(broad
89
- ? {
90
- [POPPY_READ]: input.broadScopeDescriptions?.[POPPY_READ] ?? POPPY_SCOPE_DESCRIPTIONS[POPPY_READ],
91
- [POPPY_WRITE]: input.broadScopeDescriptions?.[POPPY_WRITE] ?? POPPY_SCOPE_DESCRIPTIONS[POPPY_WRITE],
92
- }
93
- : {}),
94
- ...customScopes,
95
- };
96
83
  const all = Object.keys(scopes);
84
+ const scopeHints = { ...(input.scopeHints ?? {}) };
85
+ for (const [id, hint] of Object.entries(scopeHints)) {
86
+ if (!['none', 'reversible', 'irreversible'].includes(hint?.changes)) {
87
+ fail(`scopeHints.${id}.changes precisa ser none, reversible ou irreversible.`);
88
+ }
89
+ if (typeof hint.personal_data !== 'boolean') {
90
+ fail(`scopeHints.${id}.personal_data é obrigatório (boolean).`);
91
+ }
92
+ }
97
93
  const signInInput = input.signIn ?? {};
98
94
  let mediated = null;
99
95
  if (signInInput.mediated) {
@@ -106,13 +102,18 @@ export function resolvePoppyConfig(input, fallbackScopes) {
106
102
  if (!f?.name || !f.label || typeof f.secret !== 'boolean') {
107
103
  fail('signIn.mediated.fields — cada campo precisa de name, label e secret.');
108
104
  }
105
+ if (RESERVED_FIELDS.has(f.name)) {
106
+ fail(`signIn.mediated.fields — "${f.name}" é um parâmetro reservado.`);
107
+ }
109
108
  if (names.has(f.name))
110
109
  fail(`signIn.mediated.fields — campo duplicado "${f.name}".`);
111
110
  names.add(f.name);
112
111
  }
113
112
  if (typeof m.verify !== 'function')
114
113
  fail('signIn.mediated.verify é obrigatório.');
115
- const mediatedScopes = typeScopes({ scopes: m.scopes ?? (broad ? [POPPY_READ] : all) }, all, 'mediated').scopes;
114
+ if (!Array.isArray(m.scopes))
115
+ fail('signIn.mediated.scopes é obrigatório.');
116
+ const mediatedScopes = typeScopes({ scopes: m.scopes }, all, 'mediated').scopes;
116
117
  mediated = {
117
118
  fields: m.fields.map((f) => ({ name: f.name, label: f.label, secret: f.secret })),
118
119
  scopes: mediatedScopes,
@@ -123,7 +124,7 @@ export function resolvePoppyConfig(input, fallbackScopes) {
123
124
  };
124
125
  }
125
126
  if (all.length === 0 && (signInInput.direct !== false || signInInput.device !== false)) {
126
- fail('nenhum scope: com broadScopes: false, declare `scopes`.');
127
+ fail('nenhum scope: declare `scopes` ou desligue signIn.direct e signIn.device.');
127
128
  }
128
129
  for (const entry of input.agent?.protocols ?? []) {
129
130
  if (!entry.type)
@@ -131,6 +132,9 @@ export function resolvePoppyConfig(input, fallbackScopes) {
131
132
  absoluteUrl(entry.endpoint, 'agent.protocols[].endpoint');
132
133
  if (entry.resource)
133
134
  absoluteUrl(entry.resource, 'agent.protocols[].resource');
135
+ if (entry.access && entry.access !== 'session' && entry.access !== 'signed_in') {
136
+ fail('agent.protocols[].access precisa ser session ou signed_in.');
137
+ }
134
138
  }
135
139
  for (const api of input.apis ?? []) {
136
140
  if (!api.type)
@@ -141,6 +145,14 @@ export function resolvePoppyConfig(input, fallbackScopes) {
141
145
  if (typeof api.description !== 'string')
142
146
  fail('apis[].description é obrigatório.');
143
147
  }
148
+ if (input.feedback)
149
+ absoluteUrl(input.feedback.endpoint, 'feedback.endpoint');
150
+ if (input.excludedTasks)
151
+ absoluteUrl(input.excludedTasks.url, 'excludedTasks.url');
152
+ const redirectTo = input.web ? (input.web.redirectTo ?? '/') : '/';
153
+ if (!redirectTo.startsWith('/') || redirectTo.startsWith('//')) {
154
+ fail('web.redirectTo precisa ser um path do site.');
155
+ }
144
156
  for (const name of Object.keys(input.extensions ?? {})) {
145
157
  if (typeof input.extensions[name]?.version !== 'string') {
146
158
  fail(`extensions.${name}.version é obrigatório (string).`);
@@ -169,7 +181,7 @@ export function resolvePoppyConfig(input, fallbackScopes) {
169
181
  organization: { name: org.name, domain: domains[0], aliases: domains.slice(1) },
170
182
  prefix: normalizePoppyPrefix(input.prefix),
171
183
  scopes,
172
- customScopes,
184
+ scopeHints,
173
185
  signIn: {
174
186
  direct: typeScopes(signInInput.direct, all, 'direct'),
175
187
  device: typeScopes(signInInput.device, all, 'device'),
@@ -177,9 +189,15 @@ export function resolvePoppyConfig(input, fallbackScopes) {
177
189
  },
178
190
  agent: input.agent?.protocols?.length ? { protocols: [...input.agent.protocols] } : null,
179
191
  apis: [...(input.apis ?? [])],
192
+ feedback: input.feedback ?? null,
193
+ excludedTasks: input.excludedTasks ?? null,
180
194
  web: input.web === false
181
195
  ? null
182
- : { browserSession: input.web?.browserSession !== false, login: input.web?.login },
196
+ : {
197
+ browserSession: input.web?.browserSession !== false,
198
+ login: input.web?.login,
199
+ redirectTo,
200
+ },
183
201
  extensions: { ...(input.extensions ?? {}) },
184
202
  clients: {
185
203
  requireRegistration: input.clients?.requireRegistration === true,
@@ -189,14 +207,16 @@ export function resolvePoppyConfig(input, fallbackScopes) {
189
207
  blockedList: listOf(input.clients?.blocked),
190
208
  },
191
209
  allowInsecureClientIds,
192
- sessionTokenTtl: positive(input.sessionTokenTtl, 3600, 'sessionTokenTtl'),
193
- accountTokenTtl: positive(input.accountTokenTtl, 30 * 24 * 3600, 'accountTokenTtl'),
210
+ accessTokenTtl: positive(input.accessTokenTtl, 3600, 'accessTokenTtl'),
211
+ refreshTokenTtl: positive(input.refreshTokenTtl, 30 * 24 * 3600, 'refreshTokenTtl'),
194
212
  sessionTtl: positive(input.sessionTtl, 30 * 24 * 3600, 'sessionTtl'),
195
213
  authorizationCodeTtl: positive(input.authorizationCodeTtl, 60, 'authorizationCodeTtl'),
196
214
  deviceCodeTtl: positive(input.deviceCodeTtl, 600, 'deviceCodeTtl'),
197
215
  pollInterval: positive(input.pollInterval, 5, 'pollInterval'),
198
216
  assertionMaxLifetime: positive(input.assertionMaxLifetime, 300, 'assertionMaxLifetime'),
217
+ sessionJwt: { maxAge: positive(input.sessionJwt?.maxAge, 60, 'sessionJwt.maxAge') },
199
218
  dpop: {
219
+ required: input.dpop?.required === true,
200
220
  requireNonce: input.dpop?.requireNonce === true,
201
221
  maxAge: positive(input.dpop?.maxAge, 60, 'dpop.maxAge'),
202
222
  algorithms: [...dpopAlgs],
@@ -19,7 +19,7 @@ export interface DpopNonceSource {
19
19
  * Nonces sem estado: `base64url(ts).base64url(hmac(ts))`, válidos por
20
20
  * `ttlSeconds` (default 300). Qualquer instância com o mesmo segredo valida.
21
21
  *
22
- * @experimental Poppy (Personal Agent Protocol) Draft 0.1 — acompanha a spec em desenvolvimento e PODE MUDAR
22
+ * @experimental Poppy (Personal Agent Protocol) Draft 0.3 — acompanha a spec em desenvolvimento e PODE MUDAR
23
23
  * de forma incompatível fora de majors enquanto ela for draft.
24
24
  */
25
25
  export declare function hmacNonceSource(secret: Buffer | string, options?: {
@@ -56,6 +56,6 @@ export interface VerifyDpopInput {
56
56
  now?: () => number;
57
57
  }
58
58
  /**
59
- * @experimental Poppy (Personal Agent Protocol) Draft 0.1 — pode mudar de forma incompatível enquanto a spec for draft.
59
+ * @experimental Poppy (Personal Agent Protocol) Draft 0.3 — pode mudar de forma incompatível enquanto a spec for draft.
60
60
  */
61
61
  export declare function verifyDpopProof(input: VerifyDpopInput): Promise<DpopResult>;
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Verificação de provas DPoP (RFC 9449) — utilitário genérico para o token
3
- * endpoint e para resource servers (§4.3 do Poppy).
3
+ * endpoint e para resource servers (§4.5 do Poppy).
4
4
  *
5
5
  * Confere: um único header `DPoP`; `typ` `dpop+jwt`; algoritmo permitido; `jwk`
6
6
  * público no header e a assinatura com ele; `jti`, `htm`, `htu` (sem query nem
@@ -15,7 +15,7 @@ import { replayKey } from './replay.js';
15
15
  * Nonces sem estado: `base64url(ts).base64url(hmac(ts))`, válidos por
16
16
  * `ttlSeconds` (default 300). Qualquer instância com o mesmo segredo valida.
17
17
  *
18
- * @experimental Poppy (Personal Agent Protocol) Draft 0.1 — acompanha a spec em desenvolvimento e PODE MUDAR
18
+ * @experimental Poppy (Personal Agent Protocol) Draft 0.3 — acompanha a spec em desenvolvimento e PODE MUDAR
19
19
  * de forma incompatível fora de majors enquanto ela for draft.
20
20
  */
21
21
  export function hmacNonceSource(secret, options = {}) {
@@ -66,7 +66,7 @@ function failure(error, description) {
66
66
  return { ok: false, error, description };
67
67
  }
68
68
  /**
69
- * @experimental Poppy (Personal Agent Protocol) Draft 0.1 — pode mudar de forma incompatível enquanto a spec for draft.
69
+ * @experimental Poppy (Personal Agent Protocol) Draft 0.3 — pode mudar de forma incompatível enquanto a spec for draft.
70
70
  */
71
71
  export async function verifyDpopProof(input) {
72
72
  const raw = input.proof;
@@ -1,11 +1,11 @@
1
1
  import { type AuthAccount } from '../../accounts/account_store.js';
2
2
  import type { PoppyMediatedConfigInput } from './config.js';
3
3
  export interface PoppyEmailCodeSignInOptions {
4
- /** Scopes que este sign-in pode conceder. Default: `['poppy:read']` (o default do Mediated). */
5
- scopes?: string[];
4
+ /** Scopes que este sign-in pode conceder. */
5
+ scopes: string[];
6
6
  /** Validade do código, em segundos. Default: 600. */
7
7
  codeTtl?: number;
8
- /** Tentativas de código antes de `failed`. Default: 5. */
8
+ /** Tentativas de código antes de `invalid_grant`. Default: 5. */
9
9
  maxCodeAttempts?: number;
10
10
  /**
11
11
  * Quem pode entrar por aqui (ex.: só pacientes, não profissionais). `false` = recebe a
@@ -24,4 +24,4 @@ export interface PoppyEmailCodeSignInOptions {
24
24
  }
25
25
  /** `ana.souza@gmail.com` → `a***@gmail.com`: o bastante para a pessoa reconhecer. */
26
26
  export declare function maskEmail(email: string): string;
27
- export declare function poppyEmailCodeSignIn(options?: PoppyEmailCodeSignInOptions): PoppyMediatedConfigInput;
27
+ export declare function poppyEmailCodeSignIn(options: PoppyEmailCodeSignInOptions): PoppyMediatedConfigInput;
@@ -1,14 +1,14 @@
1
1
  /**
2
- * @experimental Poppy (Personal Agent Protocol) Draft 0.1 — pode mudar de forma incompatível enquanto a spec for draft.
2
+ * @experimental Poppy (Personal Agent Protocol) Draft 0.3 — pode mudar de forma incompatível enquanto a spec for draft.
3
3
  *
4
- * Mediated Sign-In pronto (§4.7): o agente pede o e-mail da conta, o authkit manda
4
+ * Mediated Sign-In pronto (extensão `mediated-sign-in`): o agente pede o e-mail da conta, o authkit manda
5
5
  * um código de 6 dígitos para ele e o agente devolve o código. Usa as contas
6
6
  * (`accountStore`) e o e-mail (hook `mail.onPoppySignInCode` ou o mailer default)
7
7
  * do próprio authkit — o app só liga:
8
8
  *
9
9
  * ```ts
10
10
  * personalAgents: {
11
- * poppy: { signIn: { mediated: poppyEmailCodeSignIn() } },
11
+ * poppy: { signIn: { mediated: poppyEmailCodeSignIn({ scopes: ['orders:read'] }) } },
12
12
  * }
13
13
  * ```
14
14
  *
@@ -33,7 +33,7 @@ export function maskEmail(email) {
33
33
  const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
34
34
  const HOUR_MS = 3600 * 1000;
35
35
  const MAX_TRACKED = 10_000;
36
- export function poppyEmailCodeSignIn(options = {}) {
36
+ export function poppyEmailCodeSignIn(options) {
37
37
  const positive = (value, fallback, what) => {
38
38
  if (value === undefined)
39
39
  return fallback;
@@ -85,7 +85,7 @@ export function poppyEmailCodeSignIn(options = {}) {
85
85
  };
86
86
  return {
87
87
  fields: [{ name: 'email', label: options.label ?? 'Email address', secret: false }],
88
- ...(options.scopes ? { scopes: options.scopes } : {}),
88
+ scopes: options.scopes,
89
89
  codeTtl,
90
90
  maxCodeAttempts,
91
91
  async verify({ credentials, clientId, ctx }) {
@@ -1,17 +1,22 @@
1
1
  /**
2
- * Erros OAuth do Poppy (§4.2, §4.4, §4.6, RFC 6749 §5.2, RFC 8628 §3.5, RFC 9449)
2
+ * Erros OAuth do Poppy (§4.7, RFC 6749 §5.2, RFC 8628 §3.5, RFC 9449, extensão Mediated Sign-In)
3
3
  * — viram `{ error, error_description }` com o status da spec.
4
4
  */
5
- export type PoppyErrorCode = 'invalid_request' | 'invalid_client' | 'invalid_grant' | 'invalid_session' | 'account_mismatch' | 'invalid_dpop_proof' | 'use_dpop_nonce' | 'rate_limited' | 'invalid_scope' | 'invalid_target' | 'unsupported_grant_type' | 'unsupported_token_type' | 'unauthorized_client' | 'authorization_pending' | 'slow_down' | 'access_denied' | 'expired_token';
5
+ export type PoppyErrorCode = 'invalid_request' | 'invalid_client' | 'invalid_grant' | 'invalid_session' | 'invalid_dpop_proof' | 'use_dpop_nonce'
6
+ /** Interno: HTTP 429 com `Retry-After`, sem corpo (§5.6). */
7
+ | 'too_many_requests' | 'code_required' | 'invalid_scope' | 'invalid_target' | 'unsupported_grant_type' | 'unsupported_token_type' | 'unauthorized_client' | 'authorization_pending' | 'slow_down' | 'access_denied' | 'expired_token';
6
8
  export declare class PoppyError extends Error {
7
9
  readonly code: PoppyErrorCode;
8
10
  readonly description: string;
9
11
  readonly status: number;
10
12
  /** Headers extras da resposta (`DPoP-Nonce`, `Retry-After`). */
11
13
  readonly headers: Record<string, string>;
14
+ /** Campos extras do corpo (`sign_in_id`, `sent_to`, `expires_in` do `code_required`). */
15
+ readonly extra: Record<string, unknown>;
12
16
  constructor(code: PoppyErrorCode, description: string, options?: {
13
17
  status?: number;
14
18
  headers?: Record<string, string>;
19
+ extra?: Record<string, unknown>;
15
20
  });
16
21
  toJSON(): {
17
22
  error: PoppyErrorCode;
@@ -1,6 +1,6 @@
1
1
  const STATUS = {
2
2
  invalid_client: 401,
3
- rate_limited: 429,
3
+ too_many_requests: 429,
4
4
  };
5
5
  export class PoppyError extends Error {
6
6
  code;
@@ -8,6 +8,8 @@ export class PoppyError extends Error {
8
8
  status;
9
9
  /** Headers extras da resposta (`DPoP-Nonce`, `Retry-After`). */
10
10
  headers;
11
+ /** Campos extras do corpo (`sign_in_id`, `sent_to`, `expires_in` do `code_required`). */
12
+ extra;
11
13
  constructor(code, description, options = {}) {
12
14
  super(description);
13
15
  this.code = code;
@@ -15,8 +17,9 @@ export class PoppyError extends Error {
15
17
  this.name = 'PoppyError';
16
18
  this.status = options.status ?? STATUS[code] ?? 400;
17
19
  this.headers = options.headers ?? {};
20
+ this.extra = options.extra ?? {};
18
21
  }
19
22
  toJSON() {
20
- return { error: this.code, error_description: this.description };
23
+ return { error: this.code, error_description: this.description, ...this.extra };
21
24
  }
22
25
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Lado resource server do Poppy (§4.3, §6): verificar o Session Token de uma
3
- * request às APIs/conversas do app — e o slot GLOBAL que o `@adonis-agora/agent`
4
- * usa para autenticar o endpoint de conversas sem importar este pacote.
2
+ * Lado resource server do Poppy (§4.5, §5.3, §7): verificar o header `Poppy-Session` e o access
3
+ * token de uma request às APIs/conversas do app — e o slot GLOBAL que o `@adonis-agora/agent` usa
4
+ * para autenticar o endpoint de conversas sem importar este pacote.
5
5
  *
6
6
  * Contrato do slot (o mesmo padrão do `Symbol.for('@adonis-agora/oauth:resources')`):
7
7
  *
@@ -16,31 +16,34 @@ import type { PoppyPrincipalBase } from './config.js';
16
16
  import type { PoppyAuthError, PoppyService, PoppyVerifyOptions } from './service.js';
17
17
  /** Símbolo do slot global de autenticação Poppy. */
18
18
  export declare const POPPY_AUTHENTICATE_SLOT: unique symbol;
19
- /** O principal de um Session Token válido. *
20
- /** O principal de um Session Token válido. * @experimental Poppy (Personal Agent Protocol) Draft 0.1 — acompanha a spec em desenvolvimento e PODE MUDAR
21
- /** O principal de um Session Token válido. * de forma incompatível fora de majors enquanto ela for draft.
22
- /** O principal de um Session Token válido. */
19
+ /**
20
+ * O principal de uma request verificada.
21
+ *
22
+ * @experimental Poppy (Personal Agent Protocol) Draft 0.3 — acompanha a spec em desenvolvimento e PODE MUDAR
23
+ * de forma incompatível fora de majors enquanto ela for draft.
24
+ */
23
25
  export interface PoppyPrincipal extends PoppyPrincipalBase {
24
26
  /** O que `personalAgents.poppy.toActor` devolveu, quando configurado. */
25
27
  actor?: unknown;
26
28
  }
27
29
  /**
28
- * @experimental Poppy (Personal Agent Protocol) Draft 0.1 — pode mudar de forma incompatível enquanto a spec for draft.
30
+ * @experimental Poppy (Personal Agent Protocol) Draft 0.3 — pode mudar de forma incompatível enquanto a spec for draft.
29
31
  */
30
32
  export type PoppyAuthResult = {
31
33
  ok: true;
32
34
  principal: PoppyPrincipal;
33
35
  } | {
34
36
  ok: false;
35
- status: 401 | 403;
36
- error: PoppyAuthError;
37
- /** Scopes exigidos (`insufficient_scope`). */
37
+ status: 400 | 401 | 403 | 429;
38
+ /** `null`: challenge sem código (falta o access token, §7) ou limite de taxa (§5.6). */
39
+ error: PoppyAuthError | null;
40
+ /** Scopes exigidos (challenge e `insufficient_scope`). */
38
41
  scope?: string;
39
- /** Valor pronto do header `WWW-Authenticate`. */
40
- wwwAuthenticate: string;
42
+ /** Valor pronto do header `WWW-Authenticate`; `null` em `invalid_session` e no 429. */
43
+ wwwAuthenticate: string | null;
41
44
  /** Texto para logs/corpo — nunca contém o token. */
42
45
  description: string;
43
- /** Headers extras a devolver (`DPoP-Nonce`). */
46
+ /** Headers extras a devolver (`DPoP-Nonce`, `Retry-After`). */
44
47
  headers: Record<string, string>;
45
48
  };
46
49
  /** Request no formato do slot (sem `HttpContext`). */
@@ -54,10 +57,10 @@ export type PoppyAuthenticate = (request: PoppySlotRequest, options?: PoppyVerif
54
57
  /** URL pública da request: atrás de proxy TLS, o esquema segue o do issuer. */
55
58
  export declare function publicRequestUrl(ctx: HttpContext, poppy: PoppyService): string;
56
59
  /**
57
- * Verifica o Session Token de uma request (DPoP; Bearer só no MCP para o qual
58
- * foi emitido). Aceita o `HttpContext` ou `{ method, url, headers }`.
60
+ * Verifica o header `Poppy-Session` de uma request e, se houver, o access token. Aceita o
61
+ * `HttpContext` ou `{ method, url, headers }`.
59
62
  *
60
- * @experimental Poppy (Personal Agent Protocol) Draft 0.1 — acompanha a spec em desenvolvimento e PODE MUDAR
63
+ * @experimental Poppy (Personal Agent Protocol) Draft 0.3 — acompanha a spec em desenvolvimento e PODE MUDAR
61
64
  * de forma incompatível fora de majors enquanto ela for draft.
62
65
  */
63
66
  export declare function verifyPoppyRequest(input: HttpContext | PoppySlotRequest, options?: PoppyVerifyOptions): Promise<PoppyAuthResult>;
@@ -65,22 +68,24 @@ export declare function verifyPoppyRequest(input: HttpContext | PoppySlotRequest
65
68
  export declare function writePoppyAuthError(ctx: HttpContext, result: Extract<PoppyAuthResult, {
66
69
  ok: false;
67
70
  }>): void;
68
- /** O principal Poppy da request — preenchido por {@link poppyAuth}. *
69
- /** O principal Poppy da request — preenchido por {@link poppyAuth}. * @experimental Poppy (Personal Agent Protocol) Draft 0.1 — acompanha a spec em desenvolvimento e PODE MUDAR
70
- /** O principal Poppy da request — preenchido por {@link poppyAuth}. * de forma incompatível fora de majors enquanto ela for draft.
71
- /** O principal Poppy da request — preenchido por {@link poppyAuth}. */
71
+ /**
72
+ * O principal Poppy da request — preenchido por {@link poppyAuth}.
73
+ *
74
+ * @experimental Poppy (Personal Agent Protocol) Draft 0.3 — acompanha a spec em desenvolvimento e PODE MUDAR
75
+ * de forma incompatível fora de majors enquanto ela for draft.
76
+ */
72
77
  export declare function poppyOf(ctx: HttpContext): PoppyPrincipal | null;
73
78
  /**
74
- * Middleware das APIs/rotas que aceitam Session Tokens Poppy.
79
+ * Middleware das APIs/rotas que usam a Session Poppy.
75
80
  *
76
81
  * ```ts
77
82
  * router.post('/orders/:id/exchanges', [Orders, 'exchange'])
78
- * .use(poppyAuth({ scopes: ['poppy:write'] }))
83
+ * .use(poppyAuth({ scopes: ['orders:write'] }))
79
84
  * ```
80
85
  *
81
86
  * Isente essas rotas do CSRF (são chamadas por agentes, sem navegador).
82
87
  *
83
- * @experimental Poppy (Personal Agent Protocol) Draft 0.1 — acompanha a spec em desenvolvimento e PODE MUDAR
88
+ * @experimental Poppy (Personal Agent Protocol) Draft 0.3 — acompanha a spec em desenvolvimento e PODE MUDAR
84
89
  * de forma incompatível fora de majors enquanto ela for draft.
85
90
  */
86
91
  export declare function poppyAuth(options?: PoppyVerifyOptions): (ctx: HttpContext, next: () => Promise<void>) => Promise<void>;
@@ -89,7 +94,7 @@ export declare function poppyAuth(options?: PoppyVerifyOptions): (ctx: HttpConte
89
94
  * `Symbol.for('@adonis-agora/poppy:issuer')`). Chamado no boot do provider
90
95
  * quando `personalAgents.poppy` está no config.
91
96
  *
92
- * @experimental Poppy (Personal Agent Protocol) Draft 0.1 — acompanha a spec em desenvolvimento e PODE MUDAR
97
+ * @experimental Poppy (Personal Agent Protocol) Draft 0.3 — acompanha a spec em desenvolvimento e PODE MUDAR
93
98
  * de forma incompatível fora de majors enquanto ela for draft.
94
99
  */
95
100
  export declare function installPoppySlots(issuer: string): void;
@@ -19,10 +19,10 @@ function requestFromCtx(ctx, poppy) {
19
19
  }
20
20
  const NOT_ENABLED = {
21
21
  ok: false,
22
- status: 401,
23
- error: 'invalid_token',
22
+ status: 400,
23
+ error: 'invalid_session',
24
24
  description: 'Poppy is not enabled',
25
- wwwAuthenticate: 'DPoP error="invalid_token"',
25
+ wwwAuthenticate: null,
26
26
  headers: {},
27
27
  };
28
28
  async function verifyWith(poppy, input, options) {
@@ -43,10 +43,10 @@ async function poppyFromBootedApp() {
43
43
  return runtime?.poppy ?? null;
44
44
  }
45
45
  /**
46
- * Verifica o Session Token de uma request (DPoP; Bearer só no MCP para o qual
47
- * foi emitido). Aceita o `HttpContext` ou `{ method, url, headers }`.
46
+ * Verifica o header `Poppy-Session` de uma request e, se houver, o access token. Aceita o
47
+ * `HttpContext` ou `{ method, url, headers }`.
48
48
  *
49
- * @experimental Poppy (Personal Agent Protocol) Draft 0.1 — acompanha a spec em desenvolvimento e PODE MUDAR
49
+ * @experimental Poppy (Personal Agent Protocol) Draft 0.3 — acompanha a spec em desenvolvimento e PODE MUDAR
50
50
  * de forma incompatível fora de majors enquanto ela for draft.
51
51
  */
52
52
  export async function verifyPoppyRequest(input, options = {}) {
@@ -64,36 +64,40 @@ export async function verifyPoppyRequest(input, options = {}) {
64
64
  }
65
65
  /** Escreve a resposta de erro de um {@link PoppyAuthResult} recusado. */
66
66
  export function writePoppyAuthError(ctx, result) {
67
- ctx.response.header('WWW-Authenticate', result.wwwAuthenticate);
67
+ if (result.wwwAuthenticate)
68
+ ctx.response.header('WWW-Authenticate', result.wwwAuthenticate);
68
69
  for (const [k, v] of Object.entries(result.headers))
69
70
  ctx.response.header(k, v);
70
71
  ctx.response.header('Cache-Control', 'no-store');
71
- return ctx.response.status(result.status).json({
72
- error: result.error,
73
- error_description: result.description,
74
- ...(result.scope ? { scope: result.scope } : {}),
75
- });
72
+ // Erros do access token vão no header, como no OAuth; `invalid_session` vai no corpo (§7).
73
+ if (result.error !== 'invalid_session')
74
+ return ctx.response.status(result.status).send('');
75
+ return ctx.response
76
+ .status(result.status)
77
+ .json({ error: result.error, error_description: result.description });
76
78
  }
77
79
  /** Guardado fora do `HttpContext` (sem augmentation — ver `augmentation_isolation.spec.ts`). */
78
80
  const principals = new WeakMap();
79
- /** O principal Poppy da request — preenchido por {@link poppyAuth}. *
80
- /** O principal Poppy da request — preenchido por {@link poppyAuth}. * @experimental Poppy (Personal Agent Protocol) Draft 0.1 — acompanha a spec em desenvolvimento e PODE MUDAR
81
- /** O principal Poppy da request — preenchido por {@link poppyAuth}. * de forma incompatível fora de majors enquanto ela for draft.
82
- /** O principal Poppy da request — preenchido por {@link poppyAuth}. */
81
+ /**
82
+ * O principal Poppy da request — preenchido por {@link poppyAuth}.
83
+ *
84
+ * @experimental Poppy (Personal Agent Protocol) Draft 0.3 — acompanha a spec em desenvolvimento e PODE MUDAR
85
+ * de forma incompatível fora de majors enquanto ela for draft.
86
+ */
83
87
  export function poppyOf(ctx) {
84
88
  return principals.get(ctx) ?? null;
85
89
  }
86
90
  /**
87
- * Middleware das APIs/rotas que aceitam Session Tokens Poppy.
91
+ * Middleware das APIs/rotas que usam a Session Poppy.
88
92
  *
89
93
  * ```ts
90
94
  * router.post('/orders/:id/exchanges', [Orders, 'exchange'])
91
- * .use(poppyAuth({ scopes: ['poppy:write'] }))
95
+ * .use(poppyAuth({ scopes: ['orders:write'] }))
92
96
  * ```
93
97
  *
94
98
  * Isente essas rotas do CSRF (são chamadas por agentes, sem navegador).
95
99
  *
96
- * @experimental Poppy (Personal Agent Protocol) Draft 0.1 — acompanha a spec em desenvolvimento e PODE MUDAR
100
+ * @experimental Poppy (Personal Agent Protocol) Draft 0.3 — acompanha a spec em desenvolvimento e PODE MUDAR
97
101
  * de forma incompatível fora de majors enquanto ela for draft.
98
102
  */
99
103
  export function poppyAuth(options = {}) {
@@ -110,7 +114,7 @@ export function poppyAuth(options = {}) {
110
114
  * `Symbol.for('@adonis-agora/poppy:issuer')`). Chamado no boot do provider
111
115
  * quando `personalAgents.poppy` está no config.
112
116
  *
113
- * @experimental Poppy (Personal Agent Protocol) Draft 0.1 — acompanha a spec em desenvolvimento e PODE MUDAR
117
+ * @experimental Poppy (Personal Agent Protocol) Draft 0.3 — acompanha a spec em desenvolvimento e PODE MUDAR
114
118
  * de forma incompatível fora de majors enquanto ela for draft.
115
119
  */
116
120
  export function installPoppySlots(issuer) {
@@ -77,6 +77,12 @@ export function isBlockedAddress(ip) {
77
77
  return true; // ::
78
78
  if (g.slice(0, 7).every((x) => x === 0) && g[7] === 1)
79
79
  return true; // ::1
80
+ // IPv4-translated (::ffff:0:a.b.c.d) e NAT64 (64:ff9b::a.b.c.d) também chegam a um IPv4.
81
+ const translated = g.slice(0, 4).every((x) => x === 0) && g[4] === 0xffff && g[5] === 0;
82
+ const nat64 = g[0] === 0x64 && g[1] === 0xff9b && g.slice(2, 6).every((x) => x === 0);
83
+ if (translated || nat64) {
84
+ return isBlockedAddress(`${g[6] >> 8}.${g[6] & 255}.${g[7] >> 8}.${g[7] & 255}`);
85
+ }
80
86
  // IPv4-mapped (::ffff:a.b.c.d) e IPv4-compatible: decide pelo IPv4.
81
87
  if (g.slice(0, 5).every((x) => x === 0) && (g[5] === 0xffff || g[5] === 0)) {
82
88
  const v4 = `${g[6] >> 8}.${g[6] & 255}.${g[7] >> 8}.${g[7] & 255}`;
@@ -160,7 +166,7 @@ export function safeFetchJson(url, options = {}) {
160
166
  };
161
167
  const req = httpsRequest(parsed, {
162
168
  method: 'GET',
163
- headers: { accept: 'application/json', 'user-agent': 'authkit-poppy/0.1' },
169
+ headers: { accept: 'application/json', 'user-agent': 'authkit-poppy/0.3' },
164
170
  lookup: guardedLookup,
165
171
  // IP literal: o `lookup` não roda — já conferido acima.
166
172
  }, (res) => {
@@ -229,7 +235,7 @@ export async function insecureDevFetchJson(url, options = {}) {
229
235
  try {
230
236
  res = await fetch(parsed, {
231
237
  redirect: 'manual',
232
- headers: { accept: 'application/json', 'user-agent': 'authkit-poppy/0.1 (insecure-dev)' },
238
+ headers: { accept: 'application/json', 'user-agent': 'authkit-poppy/0.3 (insecure-dev)' },
233
239
  signal: AbortSignal.timeout(timeoutMs),
234
240
  });
235
241
  }