@adonis-agora/authkit-server 0.86.0 → 0.87.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.
- package/README.md +2 -2
- package/build/index.d.ts +1 -2
- package/build/index.js +0 -1
- package/build/src/agents/config.d.ts +1 -1
- package/build/src/agents/delegation_service.js +1 -1
- package/build/src/agents/poppy/client_registry.d.ts +5 -0
- package/build/src/agents/poppy/client_registry.js +35 -1
- package/build/src/agents/poppy/config.d.ts +111 -80
- package/build/src/agents/poppy/config.js +55 -35
- package/build/src/agents/poppy/dpop.d.ts +2 -2
- package/build/src/agents/poppy/dpop.js +3 -3
- package/build/src/agents/poppy/email_code_sign_in.d.ts +4 -4
- package/build/src/agents/poppy/email_code_sign_in.js +5 -5
- package/build/src/agents/poppy/errors.d.ts +7 -2
- package/build/src/agents/poppy/errors.js +5 -2
- package/build/src/agents/poppy/resource.d.ts +30 -25
- package/build/src/agents/poppy/resource.js +24 -20
- package/build/src/agents/poppy/safe_fetch.js +8 -2
- package/build/src/agents/poppy/service.d.ts +69 -62
- package/build/src/agents/poppy/service.js +540 -546
- package/build/src/agents/poppy/session_jwt.d.ts +30 -0
- package/build/src/agents/poppy/session_jwt.js +133 -0
- package/build/src/agents/poppy/store.d.ts +17 -22
- package/build/src/agents/poppy/store.js +22 -30
- package/build/src/agents/poppy/web_session.d.ts +23 -16
- package/build/src/agents/poppy/web_session.js +42 -24
- package/build/src/host/admin_sessions_service.js +3 -3
- package/build/src/host/controllers/account_apps_controller.js +1 -1
- package/build/src/host/controllers/poppy_consent_controller.d.ts +2 -2
- package/build/src/host/controllers/poppy_consent_controller.js +2 -2
- package/build/src/host/controllers/poppy_controller.d.ts +19 -11
- package/build/src/host/controllers/poppy_controller.js +93 -68
- package/build/src/host/csrf.d.ts +2 -4
- package/build/src/host/csrf.js +2 -3
- package/build/src/host/register_auth_host.js +10 -5
- package/build/src/schema/ensure.js +14 -9
- package/package.json +3 -3
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
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
|
|
79
|
-
for (const id of Object.keys(
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
: {
|
|
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
|
-
|
|
193
|
-
|
|
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.
|
|
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.
|
|
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
|
+
* 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.
|
|
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.
|
|
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.
|
|
5
|
-
scopes
|
|
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 `
|
|
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
|
|
27
|
+
export declare function poppyEmailCodeSignIn(options: PoppyEmailCodeSignInOptions): PoppyMediatedConfigInput;
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @experimental Poppy (Personal Agent Protocol) Draft 0.
|
|
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
|
+
* 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
|
-
|
|
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
|
+
* 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' | '
|
|
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
|
-
|
|
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, §
|
|
3
|
-
* request às APIs/conversas do app — e o slot GLOBAL que o `@adonis-agora/agent`
|
|
4
|
-
*
|
|
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
|
-
/**
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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.
|
|
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
|
-
|
|
37
|
-
|
|
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
|
|
58
|
-
*
|
|
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.
|
|
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
|
-
/**
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
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: ['
|
|
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.
|
|
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.
|
|
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:
|
|
23
|
-
error: '
|
|
22
|
+
status: 400,
|
|
23
|
+
error: 'invalid_session',
|
|
24
24
|
description: 'Poppy is not enabled',
|
|
25
|
-
wwwAuthenticate:
|
|
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
|
|
47
|
-
*
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
/**
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
|
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: ['
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
238
|
+
headers: { accept: 'application/json', 'user-agent': 'authkit-poppy/0.3 (insecure-dev)' },
|
|
233
239
|
signal: AbortSignal.timeout(timeoutMs),
|
|
234
240
|
});
|
|
235
241
|
}
|