@adonis-agora/authkit-server 0.76.0 → 0.78.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +149 -0
- package/build/host/views/agents/consent.edge +13 -2
- package/build/host/views/agents/done.edge +3 -0
- package/build/host/views/mfa-challenge.edge +42 -3
- package/build/index.d.ts +19 -1
- package/build/index.js +13 -1
- package/build/providers/authkit_server_provider.js +15 -1
- package/build/src/accounts/account_store.d.ts +2 -0
- package/build/src/accounts/lucid_account_store.js +2 -0
- package/build/src/adapters/adapter_contract.d.ts +2 -0
- package/build/src/adapters/database_adapter.d.ts +1 -0
- package/build/src/adapters/database_adapter.js +26 -0
- package/build/src/adapters/redis_adapter.d.ts +1 -0
- package/build/src/adapters/redis_adapter.js +30 -0
- package/build/src/agents/config.d.ts +20 -3
- package/build/src/agents/config.js +19 -4
- package/build/src/agents/delegation_service.js +18 -8
- package/build/src/agents/middleware.js +2 -1
- package/build/src/agents/poppy/client_registry.d.ts +45 -0
- package/build/src/agents/poppy/client_registry.js +226 -0
- package/build/src/agents/poppy/config.d.ts +362 -0
- package/build/src/agents/poppy/config.js +201 -0
- package/build/src/agents/poppy/dpop.d.ts +61 -0
- package/build/src/agents/poppy/dpop.js +158 -0
- package/build/src/agents/poppy/errors.d.ts +20 -0
- package/build/src/agents/poppy/errors.js +22 -0
- package/build/src/agents/poppy/replay.d.ts +25 -0
- package/build/src/agents/poppy/replay.js +106 -0
- package/build/src/agents/poppy/resource.d.ts +95 -0
- package/build/src/agents/poppy/resource.js +125 -0
- package/build/src/agents/poppy/safe_fetch.d.ts +48 -0
- package/build/src/agents/poppy/safe_fetch.js +256 -0
- package/build/src/agents/poppy/service.d.ts +280 -0
- package/build/src/agents/poppy/service.js +1452 -0
- package/build/src/agents/poppy/store.d.ts +109 -0
- package/build/src/agents/poppy/store.js +260 -0
- package/build/src/agents/poppy/web_session.d.ts +32 -0
- package/build/src/agents/poppy/web_session.js +104 -0
- package/build/src/agents/protocol.d.ts +8 -4
- package/build/src/agents/runtime.d.ts +17 -1
- package/build/src/agents/runtime.js +138 -13
- package/build/src/define_config.d.ts +24 -0
- package/build/src/define_config.js +14 -0
- package/build/src/host/account_api/account_api_controller.d.ts +12 -1
- package/build/src/host/account_api/account_api_controller.js +10 -5
- package/build/src/host/admin_sessions_service.js +8 -5
- package/build/src/host/auth_host_config.d.ts +2 -1
- package/build/src/host/controllers/account_apps_controller.js +13 -3
- package/build/src/host/controllers/account_security_controller.js +8 -4
- package/build/src/host/controllers/interaction_controller.d.ts +13 -1
- package/build/src/host/controllers/interaction_controller.js +352 -24
- package/build/src/host/controllers/poppy_consent_controller.d.ts +19 -0
- package/build/src/host/controllers/poppy_consent_controller.js +173 -0
- package/build/src/host/controllers/poppy_controller.d.ts +25 -0
- package/build/src/host/controllers/poppy_controller.js +194 -0
- package/build/src/host/csrf.d.ts +7 -0
- package/build/src/host/csrf.js +16 -1
- package/build/src/host/custom_login.d.ts +36 -0
- package/build/src/host/custom_login.js +76 -0
- package/build/src/host/custom_login_completion.d.ts +4 -0
- package/build/src/host/custom_login_completion.js +33 -0
- package/build/src/host/custom_mfa.d.ts +77 -0
- package/build/src/host/custom_mfa.js +268 -0
- package/build/src/host/i18n.d.ts +8 -0
- package/build/src/host/i18n.js +14 -6
- package/build/src/host/idp_session_bridge.d.ts +5 -3
- package/build/src/host/idp_session_bridge.js +5 -3
- package/build/src/host/oidc_rp_guard.d.ts +5 -1
- package/build/src/host/oidc_rp_guard.js +15 -2
- package/build/src/host/persistent_rp_session.d.ts +8 -0
- package/build/src/host/persistent_rp_session.js +74 -0
- package/build/src/host/poppy_session_key.d.ts +9 -0
- package/build/src/host/poppy_session_key.js +9 -0
- package/build/src/host/register_auth_host.js +72 -0
- package/build/src/host/whatsapp_code_sender.d.ts +20 -0
- package/build/src/host/whatsapp_code_sender.js +9 -0
- package/build/src/host/whatsapp_senders/http.d.ts +5 -0
- package/build/src/host/whatsapp_senders/http.js +33 -0
- package/build/src/host/whatsapp_senders/meta_whatsapp_code_sender.d.ts +20 -0
- package/build/src/host/whatsapp_senders/meta_whatsapp_code_sender.js +51 -0
- package/build/src/host/whatsapp_senders/whatsmiau_code_sender.d.ts +14 -0
- package/build/src/host/whatsapp_senders/whatsmiau_code_sender.js +36 -0
- package/build/src/provider/oidc_service.d.ts +5 -0
- package/build/src/provider/oidc_service.js +15 -4
- package/build/src/schema/ensure.js +124 -0
- package/build/stubs/ui/react/pages/mfa-challenge.tsx +207 -37
- package/package.json +2 -1
- package/stubs/ui/react/pages/mfa-challenge.tsx +207 -37
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @experimental Draft 0.1 da spec (https://personalagentprotocol.org/docs/spec) — pode mudar de forma
|
|
3
|
+
* incompatível fora de majors enquanto a spec for draft.
|
|
4
|
+
*
|
|
5
|
+
* Personal Agent Protocol ("Poppy", Draft 0.1 — https://personalagentprotocol.org) — config.
|
|
6
|
+
*
|
|
7
|
+
* Poppy é o segundo protocolo de personal agents ao lado do PACT. O modelo é
|
|
8
|
+
* outro: em vez de um JWT do agente em cada request + um token de delegação, a
|
|
9
|
+
* empresa (este app) roda um authorization server OAuth com SESSÕES — o agente
|
|
10
|
+
* se identifica pelo `client_id` (uma URL HTTPS com o documento de metadata
|
|
11
|
+
* dele), começa uma Session deslogada com uma asserção JWT, e o usuário entra
|
|
12
|
+
* na conta por Direct (código + PKCE), Device (RFC 8628) ou Mediated Sign-In.
|
|
13
|
+
* Os tokens de sessão são opacos e presos a uma chave por DPoP (RFC 9449);
|
|
14
|
+
* Bearer só no MCP.
|
|
15
|
+
*
|
|
16
|
+
* Reaproveita o núcleo de delegação: o Account Token é um grant na tabela de
|
|
17
|
+
* grants de personal agents (`auth_agent_grants`) — lista e revogação no console
|
|
18
|
+
* de conta são as mesmas do PACT — e os scopes aprovados viram os `scopes` do
|
|
19
|
+
* principal (que o `@adonis-agora/agent` mapeia para roles `scope:<id>`).
|
|
20
|
+
*/
|
|
21
|
+
import type { HttpContext } from '@adonisjs/core/http';
|
|
22
|
+
/** Scopes amplos definidos pelo protocolo (§4.4). */
|
|
23
|
+
export declare const POPPY_READ = "poppy:read";
|
|
24
|
+
export declare const POPPY_WRITE = "poppy:write";
|
|
25
|
+
/** Descrições default dos scopes do protocolo — o que a tela de consentimento mostra. */
|
|
26
|
+
export declare const POPPY_SCOPE_DESCRIPTIONS: Record<string, string>;
|
|
27
|
+
export type PoppySignInType = 'direct' | 'device' | 'mediated';
|
|
28
|
+
/** Um campo de credencial do Mediated Sign-In (§4.7). */
|
|
29
|
+
export interface PoppyMediatedField {
|
|
30
|
+
/** Chave que o agente manda em `credentials`. */
|
|
31
|
+
name: string;
|
|
32
|
+
/** O que pedir ao usuário. */
|
|
33
|
+
label: string;
|
|
34
|
+
/** Valor que o agente precisa proteger (senha, PIN). */
|
|
35
|
+
secret: boolean;
|
|
36
|
+
}
|
|
37
|
+
/** Entrada do callback que verifica as credenciais do Mediated Sign-In. */
|
|
38
|
+
export interface PoppyMediatedVerifyInput {
|
|
39
|
+
credentials: Record<string, string>;
|
|
40
|
+
/** Scopes pedidos (já validados contra os do tipo mediated). */
|
|
41
|
+
scopes: string[];
|
|
42
|
+
/** `client_id` do personal agent. */
|
|
43
|
+
clientId: string;
|
|
44
|
+
/** User ID que o agente deu ao usuário (opaco). */
|
|
45
|
+
userId: string;
|
|
46
|
+
sessionId: string;
|
|
47
|
+
ctx: HttpContext;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Resultado do callback de credenciais. Nunca devolva as credenciais.
|
|
51
|
+
*
|
|
52
|
+
* - `complete`: credenciais certas, nada mais a fazer — a Session entra na conta.
|
|
53
|
+
* - `code_required`: o app MANDOU um código de uso único (SMS, e-mail, WhatsApp…)
|
|
54
|
+
* e o agente precisa submetê-lo. Ou devolve o `code` que mandou (a lib guarda
|
|
55
|
+
* só o hash e confere), ou deixa sem `code` e implementa `verifyCode` (ex.: um
|
|
56
|
+
* serviço de OTP que confere do lado dele). `state` é opaco e volta no
|
|
57
|
+
* `verifyCode`.
|
|
58
|
+
* - `failed`: senha errada, conta bloqueada…
|
|
59
|
+
*
|
|
60
|
+
* **Sem enumeração de contas.** Num fluxo só de identificador (e-mail/telefone + código), `failed`
|
|
61
|
+
* para um identificador desconhecido conta ao agente quem tem conta. Devolva então `code_required`
|
|
62
|
+
* com `accountId: null` (e o mesmo `sentTo` que uma conta real teria) e NÃO mande código: a lib
|
|
63
|
+
* responde igual a uma conta real e recusa qualquer código como um código errado (mesmas
|
|
64
|
+
* tentativas, mesmo `failed` no fim). Faça o mesmo trabalho nos dois caminhos (ex.: enfileire o
|
|
65
|
+
* e-mail em vez de mandar na hora) para o tempo de resposta não denunciar.
|
|
66
|
+
*/
|
|
67
|
+
export type PoppyMediatedResult = {
|
|
68
|
+
status: 'complete';
|
|
69
|
+
accountId: string;
|
|
70
|
+
} | {
|
|
71
|
+
status: 'code_required';
|
|
72
|
+
/** `null` = identificador sem conta elegível: nenhum código vale (ver acima). */
|
|
73
|
+
accountId: string | null;
|
|
74
|
+
/** Texto para o usuário: "Text to the phone number ending in 71". */
|
|
75
|
+
sentTo: string;
|
|
76
|
+
code?: string;
|
|
77
|
+
state?: unknown;
|
|
78
|
+
} | {
|
|
79
|
+
status: 'failed';
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* @experimental Poppy (Personal Agent Protocol) Draft 0.1 — pode mudar de forma incompatível enquanto a spec for draft.
|
|
83
|
+
*/
|
|
84
|
+
export interface PoppyMediatedConfigInput {
|
|
85
|
+
/** Campos pedidos (§4.7). Ex.: `[{ name: 'email', … }, { name: 'password', secret: true, … }]`. */
|
|
86
|
+
fields: PoppyMediatedField[];
|
|
87
|
+
/** Scopes que o Mediated Sign-In pode conceder. Default: `['poppy:read']`. */
|
|
88
|
+
scopes?: string[];
|
|
89
|
+
/** Confere as credenciais (e, se for o caso, manda o código). */
|
|
90
|
+
verify: (input: PoppyMediatedVerifyInput) => Promise<PoppyMediatedResult>;
|
|
91
|
+
/**
|
|
92
|
+
* Confere um código quando `verify` devolveu `code_required` SEM `code`.
|
|
93
|
+
* `true` = código certo.
|
|
94
|
+
*/
|
|
95
|
+
verifyCode?: (input: {
|
|
96
|
+
code: string;
|
|
97
|
+
accountId: string;
|
|
98
|
+
state: unknown;
|
|
99
|
+
clientId: string;
|
|
100
|
+
userId: string;
|
|
101
|
+
}) => Promise<boolean>;
|
|
102
|
+
/** Validade do sign-in pendente de código, em segundos. Default: 600. */
|
|
103
|
+
codeTtl?: number;
|
|
104
|
+
/** Tentativas de código antes de `failed`. Default: 5. */
|
|
105
|
+
maxCodeAttempts?: number;
|
|
106
|
+
}
|
|
107
|
+
/** Uma entrada de `agent.protocols` (§3.1, §7.1). */
|
|
108
|
+
export interface PoppyAgentProtocolEntry {
|
|
109
|
+
/** `'poppy'` para as conversas deste protocolo (§7). */
|
|
110
|
+
type: string;
|
|
111
|
+
/** URL HTTPS do endpoint de conversas. */
|
|
112
|
+
endpoint: string;
|
|
113
|
+
/** Exige tokens emitidos para este `resource` (§4.3). */
|
|
114
|
+
resource?: string;
|
|
115
|
+
}
|
|
116
|
+
/** Uma entrada de `apis` (§3.1, §6). */
|
|
117
|
+
export interface PoppyApiEntry {
|
|
118
|
+
type: 'openapi' | 'mcp' | (string & {});
|
|
119
|
+
url: string;
|
|
120
|
+
description: string;
|
|
121
|
+
/** Exige tokens emitidos para este `resource` (§4.3). MCP sempre exige o próprio `url`. */
|
|
122
|
+
resource?: string;
|
|
123
|
+
}
|
|
124
|
+
/** Hook de login do navegador do agente (§5) para apps com sessão de login própria. */
|
|
125
|
+
export type PoppyBrowserLogin = (ctx: HttpContext, input: {
|
|
126
|
+
sessionId: string;
|
|
127
|
+
clientId: string;
|
|
128
|
+
userId: string;
|
|
129
|
+
/** Conta da Session quando ela está logada; `null` = Session deslogada. */
|
|
130
|
+
accountId: string | null;
|
|
131
|
+
scopes: string[];
|
|
132
|
+
}) => Promise<void>;
|
|
133
|
+
/**
|
|
134
|
+
* Onde o protocolo guarda os `jti` já vistos (asserções, client assertions,
|
|
135
|
+
* provas DPoP, asserções de navegador) até expirarem. `claim` devolve `true` na
|
|
136
|
+
* primeira vez que vê a chave e `false` num replay.
|
|
137
|
+
*
|
|
138
|
+
* @experimental Poppy (Personal Agent Protocol) Draft 0.1 — acompanha a spec em desenvolvimento e PODE MUDAR
|
|
139
|
+
* de forma incompatível fora de majors enquanto ela for draft.
|
|
140
|
+
*/
|
|
141
|
+
export interface PoppyReplayStore {
|
|
142
|
+
claim(key: string, expiresAt: Date): Promise<boolean>;
|
|
143
|
+
}
|
|
144
|
+
/** Limitador de taxa: `null` = passa; número = segundos até tentar de novo (429). */
|
|
145
|
+
export type PoppyLimiter = (input: {
|
|
146
|
+
action: 'session' | 'mediated';
|
|
147
|
+
key: string;
|
|
148
|
+
}) => Promise<number | null>;
|
|
149
|
+
export interface PoppyClientsConfigInput {
|
|
150
|
+
/**
|
|
151
|
+
* `true` = só agentes registrados (`registered`) iniciam Sessions; os demais
|
|
152
|
+
* levam `invalid_client` (§4.1). Default: false — qualquer `client_id` com um
|
|
153
|
+
* documento de metadata válido.
|
|
154
|
+
*/
|
|
155
|
+
requireRegistration?: boolean;
|
|
156
|
+
/** `client_id`s registrados fora de banda (lista ou função, para quem guarda no banco). */
|
|
157
|
+
registered?: string[] | ((clientId: string) => boolean | Promise<boolean>);
|
|
158
|
+
/** `client_id`s bloqueados (revogados por mau uso). */
|
|
159
|
+
blocked?: string[] | ((clientId: string) => boolean | Promise<boolean>);
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* @experimental Poppy (Personal Agent Protocol) Draft 0.1 — pode mudar de forma incompatível enquanto a spec for draft.
|
|
163
|
+
*/
|
|
164
|
+
export interface PoppyConfigInput {
|
|
165
|
+
/** Nome exibido e domínio (`organization` do poppy.json). */
|
|
166
|
+
organization: {
|
|
167
|
+
name: string;
|
|
168
|
+
/** Domínio da empresa (sem esquema). O poppy.json só é servido nele (ignorando `www.`). */
|
|
169
|
+
domain: string;
|
|
170
|
+
/** Outros domínios da mesma empresa (um por país…) — também vão para `poppy_domains`. */
|
|
171
|
+
aliases?: string[];
|
|
172
|
+
};
|
|
173
|
+
/** Prefixo das rotas e path do issuer. Default: `/poppy`. */
|
|
174
|
+
prefix?: string;
|
|
175
|
+
/**
|
|
176
|
+
* Scopes próprios do app (`custom_scopes`), `id → descrição para o usuário`.
|
|
177
|
+
* Default: os de `personalAgents.delegation.scopes`, quando houver — um
|
|
178
|
+
* catálogo só para os dois protocolos. Não podem começar com `poppy:`.
|
|
179
|
+
*/
|
|
180
|
+
scopes?: Record<string, string>;
|
|
181
|
+
/** Oferece os scopes amplos `poppy:read`/`poppy:write`. Default: true. */
|
|
182
|
+
broadScopes?: boolean;
|
|
183
|
+
/** Descrições próprias para `poppy:read`/`poppy:write` na tela de consentimento. */
|
|
184
|
+
broadScopeDescriptions?: Partial<Record<'poppy:read' | 'poppy:write', string>>;
|
|
185
|
+
/** Tipos de sign-in. `false` desliga; um array restringe os scopes do tipo. */
|
|
186
|
+
signIn?: {
|
|
187
|
+
/** Default: todos os scopes. */
|
|
188
|
+
direct?: boolean | {
|
|
189
|
+
scopes?: string[];
|
|
190
|
+
};
|
|
191
|
+
/** Default: todos os scopes. */
|
|
192
|
+
device?: boolean | {
|
|
193
|
+
scopes?: string[];
|
|
194
|
+
};
|
|
195
|
+
/** Desligado por default: o app precisa conferir as credenciais. */
|
|
196
|
+
mediated?: PoppyMediatedConfigInput;
|
|
197
|
+
};
|
|
198
|
+
/** Company Agent (§7) — o endpoint de conversas é implementado pelo `@adonis-agora/agent`. */
|
|
199
|
+
agent?: {
|
|
200
|
+
protocols: PoppyAgentProtocolEntry[];
|
|
201
|
+
};
|
|
202
|
+
/** APIs do app que aceitam Session Tokens (§6). */
|
|
203
|
+
apis?: PoppyApiEntry[];
|
|
204
|
+
/**
|
|
205
|
+
* Site (§5). Default: liga o `browser_session_endpoint`. `false` desliga. `login`
|
|
206
|
+
* substitui o login default (sessão de conta do authkit + `adonisAuth.guard`).
|
|
207
|
+
*/
|
|
208
|
+
web?: false | {
|
|
209
|
+
browserSession?: boolean;
|
|
210
|
+
login?: PoppyBrowserLogin;
|
|
211
|
+
};
|
|
212
|
+
/** Extensões suportadas (§3.3), publicadas como estão. */
|
|
213
|
+
extensions?: Record<string, {
|
|
214
|
+
version: string;
|
|
215
|
+
} & Record<string, unknown>>;
|
|
216
|
+
/** Registro/allowlist/blocklist de agentes. */
|
|
217
|
+
clients?: PoppyClientsConfigInput;
|
|
218
|
+
/**
|
|
219
|
+
* SÓ DESENVOLVIMENTO/TESTE — NÃO conforme à spec. Aceita `client_id` (e o
|
|
220
|
+
* `jwks_uri`/`redirect_uris` do agente) em `http://` e busca o documento sem a
|
|
221
|
+
* proteção de SSRF (loopback e rede privada liberados), para testar com um
|
|
222
|
+
* agente local (`poppy-agent --insecure-dev`). Lança no boot com
|
|
223
|
+
* `NODE_ENV=production`. Default: false.
|
|
224
|
+
*/
|
|
225
|
+
allowInsecureClientIds?: boolean;
|
|
226
|
+
/** Vida do Session Token, em segundos. Default: 3600 (a spec pede horas, não dias). */
|
|
227
|
+
sessionTokenTtl?: number;
|
|
228
|
+
/** Vida do Account Token (`refresh_token_expires_in`), em segundos. Default: 30 dias. */
|
|
229
|
+
accountTokenTtl?: number;
|
|
230
|
+
/** Uma Session sem uso por este tempo termina (`invalid_session`). Default: 30 dias. */
|
|
231
|
+
sessionTtl?: number;
|
|
232
|
+
/** Vida do código de autorização (Direct), em segundos. Default: 60. */
|
|
233
|
+
authorizationCodeTtl?: number;
|
|
234
|
+
/** Validade do `device_code`, em segundos. Default: 600. */
|
|
235
|
+
deviceCodeTtl?: number;
|
|
236
|
+
/** Intervalo mínimo de polling do device flow, em segundos. Default: 5. */
|
|
237
|
+
pollInterval?: number;
|
|
238
|
+
/** Vida máxima das asserções do agente (`exp - iat`), em segundos. Default: 300. */
|
|
239
|
+
assertionMaxLifetime?: number;
|
|
240
|
+
/** DPoP (§4.3). */
|
|
241
|
+
dpop?: {
|
|
242
|
+
/** Exige `nonce` emitido pelo servidor nas provas (RFC 9449 §8). Default: false. */
|
|
243
|
+
requireNonce?: boolean;
|
|
244
|
+
/** Janela de aceitação do `iat` da prova, em segundos. Default: 60. */
|
|
245
|
+
maxAge?: number;
|
|
246
|
+
/** Algoritmos aceitos na prova. Default: `['ES256']`. */
|
|
247
|
+
algorithms?: string[];
|
|
248
|
+
};
|
|
249
|
+
/**
|
|
250
|
+
* Onde guardar os `jti` vistos. Default: `'database'` (tabela lib-owned
|
|
251
|
+
* `auth_poppy_jtis` — vale entre instâncias). `'memory'` só serve com uma
|
|
252
|
+
* instância. `{ redis: 'main' }` usa uma conexão do `@adonisjs/redis`.
|
|
253
|
+
*/
|
|
254
|
+
replay?: 'database' | 'memory' | {
|
|
255
|
+
redis: string;
|
|
256
|
+
} | PoppyReplayStore;
|
|
257
|
+
/**
|
|
258
|
+
* Limites por `client_id` (in-memory, por processo) ou um limitador próprio.
|
|
259
|
+
* `false` desliga. Default: 120 Sessions novas/min por agente; 10 tentativas
|
|
260
|
+
* de Mediated Sign-In/hora por (agente, usuário).
|
|
261
|
+
*/
|
|
262
|
+
rateLimit?: false | {
|
|
263
|
+
sessionsPerMinute?: number;
|
|
264
|
+
mediatedPerHour?: number;
|
|
265
|
+
} | PoppyLimiter;
|
|
266
|
+
/**
|
|
267
|
+
* Mapeia o principal verificado para o `actor` do app (ex.: o Actor do Agora
|
|
268
|
+
* com roles `scope:<id>`). Quando definido, `verifyPoppyRequest` e o slot
|
|
269
|
+
* global devolvem `actor`.
|
|
270
|
+
*/
|
|
271
|
+
toActor?: (principal: PoppyPrincipalBase) => unknown | Promise<unknown>;
|
|
272
|
+
}
|
|
273
|
+
/** O que um Session Token válido autoriza — sem o `actor`. *
|
|
274
|
+
/** O que um Session Token válido autoriza — sem o `actor`. * @experimental Poppy (Personal Agent Protocol) Draft 0.1 — acompanha a spec em desenvolvimento e PODE MUDAR
|
|
275
|
+
/** O que um Session Token válido autoriza — sem o `actor`. * de forma incompatível fora de majors enquanto ela for draft.
|
|
276
|
+
/** O que um Session Token válido autoriza — sem o `actor`. */
|
|
277
|
+
export interface PoppyPrincipalBase {
|
|
278
|
+
/** User ID que o personal agent deu ao usuário NESTE app (opaco, estável). */
|
|
279
|
+
userId: string;
|
|
280
|
+
/** Conta deste app quando a Session está logada; `null` deslogada. */
|
|
281
|
+
accountId: string | null;
|
|
282
|
+
/** `client_id` do personal agent (URL do documento de metadata). */
|
|
283
|
+
clientId: string;
|
|
284
|
+
/** Scopes do token (`poppy:read`, `poppy:write`, próprios). Vazio deslogado. */
|
|
285
|
+
scopes: string[];
|
|
286
|
+
sessionId: string;
|
|
287
|
+
signedIn: boolean;
|
|
288
|
+
/** `resource` para o qual o token foi emitido (RFC 8707), ou `null`. */
|
|
289
|
+
resource: string | null;
|
|
290
|
+
tokenType: 'DPoP' | 'Bearer';
|
|
291
|
+
}
|
|
292
|
+
export interface ResolvedPoppyMediatedConfig {
|
|
293
|
+
fields: PoppyMediatedField[];
|
|
294
|
+
scopes: string[];
|
|
295
|
+
verify: PoppyMediatedConfigInput['verify'];
|
|
296
|
+
verifyCode?: PoppyMediatedConfigInput['verifyCode'];
|
|
297
|
+
codeTtl: number;
|
|
298
|
+
maxCodeAttempts: number;
|
|
299
|
+
}
|
|
300
|
+
export interface ResolvedPoppyConfig {
|
|
301
|
+
organization: {
|
|
302
|
+
name: string;
|
|
303
|
+
domain: string;
|
|
304
|
+
aliases: string[];
|
|
305
|
+
};
|
|
306
|
+
prefix: string;
|
|
307
|
+
/** Todos os scopes oferecidos, `id → descrição`. */
|
|
308
|
+
scopes: Record<string, string>;
|
|
309
|
+
/** Só os próprios do app (o `custom_scopes`). */
|
|
310
|
+
customScopes: Record<string, string>;
|
|
311
|
+
signIn: {
|
|
312
|
+
direct: {
|
|
313
|
+
scopes: string[];
|
|
314
|
+
} | null;
|
|
315
|
+
device: {
|
|
316
|
+
scopes: string[];
|
|
317
|
+
} | null;
|
|
318
|
+
mediated: ResolvedPoppyMediatedConfig | null;
|
|
319
|
+
};
|
|
320
|
+
agent: {
|
|
321
|
+
protocols: PoppyAgentProtocolEntry[];
|
|
322
|
+
} | null;
|
|
323
|
+
apis: PoppyApiEntry[];
|
|
324
|
+
web: {
|
|
325
|
+
browserSession: boolean;
|
|
326
|
+
login?: PoppyBrowserLogin;
|
|
327
|
+
} | null;
|
|
328
|
+
extensions: Record<string, {
|
|
329
|
+
version: string;
|
|
330
|
+
} & Record<string, unknown>>;
|
|
331
|
+
clients: {
|
|
332
|
+
requireRegistration: boolean;
|
|
333
|
+
isRegistered: (clientId: string) => Promise<boolean>;
|
|
334
|
+
isBlocked: (clientId: string) => Promise<boolean>;
|
|
335
|
+
};
|
|
336
|
+
/** Dev/teste: `client_id` http e busca sem guarda de SSRF (ver o input). */
|
|
337
|
+
allowInsecureClientIds: boolean;
|
|
338
|
+
sessionTokenTtl: number;
|
|
339
|
+
accountTokenTtl: number;
|
|
340
|
+
sessionTtl: number;
|
|
341
|
+
authorizationCodeTtl: number;
|
|
342
|
+
deviceCodeTtl: number;
|
|
343
|
+
pollInterval: number;
|
|
344
|
+
assertionMaxLifetime: number;
|
|
345
|
+
dpop: {
|
|
346
|
+
requireNonce: boolean;
|
|
347
|
+
maxAge: number;
|
|
348
|
+
algorithms: string[];
|
|
349
|
+
};
|
|
350
|
+
replay: 'database' | 'memory' | {
|
|
351
|
+
redis: string;
|
|
352
|
+
} | PoppyReplayStore;
|
|
353
|
+
rateLimit: false | {
|
|
354
|
+
sessionsPerMinute: number;
|
|
355
|
+
mediatedPerHour: number;
|
|
356
|
+
} | PoppyLimiter;
|
|
357
|
+
toActor?: PoppyConfigInput['toActor'];
|
|
358
|
+
}
|
|
359
|
+
/** `www.example.com` → `example.com`, minúsculo. */
|
|
360
|
+
export declare function stripWww(host: string): string;
|
|
361
|
+
export declare function normalizePoppyPrefix(prefix: string | undefined): string;
|
|
362
|
+
export declare function resolvePoppyConfig(input: PoppyConfigInput | undefined, fallbackScopes: Record<string, string> | undefined): ResolvedPoppyConfig | undefined;
|
|
@@ -0,0 +1,201 @@
|
|
|
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
|
+
};
|
|
9
|
+
const SCOPE_ID = /^[\x21\x23-\x5B\x5D-\x7E]+$/; // RFC 6749 §3.3 scope-token
|
|
10
|
+
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
|
+
function fail(message) {
|
|
12
|
+
throw new Error(`authkit: personalAgents.poppy.${message}`);
|
|
13
|
+
}
|
|
14
|
+
function positive(value, fallback, what) {
|
|
15
|
+
if (value === undefined)
|
|
16
|
+
return fallback;
|
|
17
|
+
if (!Number.isFinite(value) || value <= 0)
|
|
18
|
+
fail(`${what} precisa ser > 0.`);
|
|
19
|
+
return Math.floor(value);
|
|
20
|
+
}
|
|
21
|
+
function absoluteUrl(value, what) {
|
|
22
|
+
let url;
|
|
23
|
+
try {
|
|
24
|
+
url = new URL(value);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
fail(`${what} precisa ser uma URL absoluta (recebeu "${value}").`);
|
|
28
|
+
}
|
|
29
|
+
if (url.protocol !== 'https:' && url.protocol !== 'http:') {
|
|
30
|
+
fail(`${what} precisa ser http(s) (recebeu "${value}").`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
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
|
+
export function normalizePoppyPrefix(prefix) {
|
|
39
|
+
const trimmed = (prefix ?? '/poppy').trim().replace(/\/+$/, '');
|
|
40
|
+
if (!trimmed)
|
|
41
|
+
return '/poppy';
|
|
42
|
+
return trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
|
|
43
|
+
}
|
|
44
|
+
function membership(value) {
|
|
45
|
+
if (typeof value === 'function')
|
|
46
|
+
return async (id) => (await value(id)) === true;
|
|
47
|
+
const set = new Set(value ?? []);
|
|
48
|
+
return async (id) => set.has(id);
|
|
49
|
+
}
|
|
50
|
+
function typeScopes(input, all, what) {
|
|
51
|
+
if (input === false)
|
|
52
|
+
return null;
|
|
53
|
+
const scopes = typeof input === 'object' && input.scopes ? [...new Set(input.scopes)] : all;
|
|
54
|
+
for (const id of scopes) {
|
|
55
|
+
if (!all.includes(id))
|
|
56
|
+
fail(`signIn.${what}.scopes — scope desconhecido "${id}".`);
|
|
57
|
+
}
|
|
58
|
+
if (scopes.length === 0)
|
|
59
|
+
fail(`signIn.${what}.scopes precisa de pelo menos um scope.`);
|
|
60
|
+
return { scopes };
|
|
61
|
+
}
|
|
62
|
+
export function resolvePoppyConfig(input, fallbackScopes) {
|
|
63
|
+
if (!input)
|
|
64
|
+
return undefined;
|
|
65
|
+
const org = input.organization;
|
|
66
|
+
if (!org?.name?.trim())
|
|
67
|
+
fail('organization.name é obrigatório.');
|
|
68
|
+
const domains = [org.domain, ...(org.aliases ?? [])].map((d) => stripWww(d ?? ''));
|
|
69
|
+
for (const d of domains) {
|
|
70
|
+
if (!DOMAIN.test(d))
|
|
71
|
+
fail(`organization — domínio inválido "${d}" (só o host, sem esquema).`);
|
|
72
|
+
}
|
|
73
|
+
const customScopes = { ...(input.scopes ?? fallbackScopes ?? {}) };
|
|
74
|
+
for (const id of Object.keys(customScopes)) {
|
|
75
|
+
if (!SCOPE_ID.test(id))
|
|
76
|
+
fail(`scopes — id inválido "${id}" (sem espaços/aspas).`);
|
|
77
|
+
if (id.startsWith('poppy:')) {
|
|
78
|
+
fail(`scopes — "${id}": nomes com "poppy:" são reservados ao protocolo (§4.4).`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const broad = input.broadScopes !== false;
|
|
82
|
+
const scopes = {
|
|
83
|
+
...(broad
|
|
84
|
+
? {
|
|
85
|
+
[POPPY_READ]: input.broadScopeDescriptions?.[POPPY_READ] ?? POPPY_SCOPE_DESCRIPTIONS[POPPY_READ],
|
|
86
|
+
[POPPY_WRITE]: input.broadScopeDescriptions?.[POPPY_WRITE] ?? POPPY_SCOPE_DESCRIPTIONS[POPPY_WRITE],
|
|
87
|
+
}
|
|
88
|
+
: {}),
|
|
89
|
+
...customScopes,
|
|
90
|
+
};
|
|
91
|
+
const all = Object.keys(scopes);
|
|
92
|
+
const signInInput = input.signIn ?? {};
|
|
93
|
+
let mediated = null;
|
|
94
|
+
if (signInInput.mediated) {
|
|
95
|
+
const m = signInInput.mediated;
|
|
96
|
+
if (!Array.isArray(m.fields) || m.fields.length === 0) {
|
|
97
|
+
fail('signIn.mediated.fields precisa de pelo menos um campo.');
|
|
98
|
+
}
|
|
99
|
+
const names = new Set();
|
|
100
|
+
for (const f of m.fields) {
|
|
101
|
+
if (!f?.name || !f.label || typeof f.secret !== 'boolean') {
|
|
102
|
+
fail('signIn.mediated.fields — cada campo precisa de name, label e secret.');
|
|
103
|
+
}
|
|
104
|
+
if (names.has(f.name))
|
|
105
|
+
fail(`signIn.mediated.fields — campo duplicado "${f.name}".`);
|
|
106
|
+
names.add(f.name);
|
|
107
|
+
}
|
|
108
|
+
if (typeof m.verify !== 'function')
|
|
109
|
+
fail('signIn.mediated.verify é obrigatório.');
|
|
110
|
+
const mediatedScopes = typeScopes({ scopes: m.scopes ?? (broad ? [POPPY_READ] : all) }, all, 'mediated').scopes;
|
|
111
|
+
mediated = {
|
|
112
|
+
fields: m.fields.map((f) => ({ name: f.name, label: f.label, secret: f.secret })),
|
|
113
|
+
scopes: mediatedScopes,
|
|
114
|
+
verify: m.verify,
|
|
115
|
+
verifyCode: m.verifyCode,
|
|
116
|
+
codeTtl: positive(m.codeTtl, 600, 'signIn.mediated.codeTtl'),
|
|
117
|
+
maxCodeAttempts: positive(m.maxCodeAttempts, 5, 'signIn.mediated.maxCodeAttempts'),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
if (all.length === 0 && (signInInput.direct !== false || signInInput.device !== false)) {
|
|
121
|
+
fail('nenhum scope: com broadScopes: false, declare `scopes`.');
|
|
122
|
+
}
|
|
123
|
+
for (const entry of input.agent?.protocols ?? []) {
|
|
124
|
+
if (!entry.type)
|
|
125
|
+
fail('agent.protocols[].type é obrigatório.');
|
|
126
|
+
absoluteUrl(entry.endpoint, 'agent.protocols[].endpoint');
|
|
127
|
+
if (entry.resource)
|
|
128
|
+
absoluteUrl(entry.resource, 'agent.protocols[].resource');
|
|
129
|
+
}
|
|
130
|
+
for (const api of input.apis ?? []) {
|
|
131
|
+
if (!api.type)
|
|
132
|
+
fail('apis[].type é obrigatório.');
|
|
133
|
+
absoluteUrl(api.url, 'apis[].url');
|
|
134
|
+
if (api.resource)
|
|
135
|
+
absoluteUrl(api.resource, 'apis[].resource');
|
|
136
|
+
if (typeof api.description !== 'string')
|
|
137
|
+
fail('apis[].description é obrigatório.');
|
|
138
|
+
}
|
|
139
|
+
for (const name of Object.keys(input.extensions ?? {})) {
|
|
140
|
+
if (typeof input.extensions[name]?.version !== 'string') {
|
|
141
|
+
fail(`extensions.${name}.version é obrigatório (string).`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const dpopAlgs = input.dpop?.algorithms ?? ['ES256'];
|
|
145
|
+
if (dpopAlgs.length === 0 || dpopAlgs.some((a) => a === 'none' || a.startsWith('HS'))) {
|
|
146
|
+
fail('dpop.algorithms precisa de algoritmos assimétricos.');
|
|
147
|
+
}
|
|
148
|
+
const allowInsecureClientIds = input.allowInsecureClientIds === true;
|
|
149
|
+
if (allowInsecureClientIds && process.env.NODE_ENV === 'production') {
|
|
150
|
+
fail('allowInsecureClientIds é só para desenvolvimento/teste e não pode ser ligado com NODE_ENV=production.');
|
|
151
|
+
}
|
|
152
|
+
let rateLimit;
|
|
153
|
+
if (input.rateLimit === false)
|
|
154
|
+
rateLimit = false;
|
|
155
|
+
else if (typeof input.rateLimit === 'function')
|
|
156
|
+
rateLimit = input.rateLimit;
|
|
157
|
+
else {
|
|
158
|
+
rateLimit = {
|
|
159
|
+
sessionsPerMinute: positive(input.rateLimit?.sessionsPerMinute, 120, 'rateLimit.sessionsPerMinute'),
|
|
160
|
+
mediatedPerHour: positive(input.rateLimit?.mediatedPerHour, 10, 'rateLimit.mediatedPerHour'),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
organization: { name: org.name, domain: domains[0], aliases: domains.slice(1) },
|
|
165
|
+
prefix: normalizePoppyPrefix(input.prefix),
|
|
166
|
+
scopes,
|
|
167
|
+
customScopes,
|
|
168
|
+
signIn: {
|
|
169
|
+
direct: typeScopes(signInInput.direct, all, 'direct'),
|
|
170
|
+
device: typeScopes(signInInput.device, all, 'device'),
|
|
171
|
+
mediated,
|
|
172
|
+
},
|
|
173
|
+
agent: input.agent?.protocols?.length ? { protocols: [...input.agent.protocols] } : null,
|
|
174
|
+
apis: [...(input.apis ?? [])],
|
|
175
|
+
web: input.web === false
|
|
176
|
+
? null
|
|
177
|
+
: { browserSession: input.web?.browserSession !== false, login: input.web?.login },
|
|
178
|
+
extensions: { ...(input.extensions ?? {}) },
|
|
179
|
+
clients: {
|
|
180
|
+
requireRegistration: input.clients?.requireRegistration === true,
|
|
181
|
+
isRegistered: membership(input.clients?.registered),
|
|
182
|
+
isBlocked: membership(input.clients?.blocked),
|
|
183
|
+
},
|
|
184
|
+
allowInsecureClientIds,
|
|
185
|
+
sessionTokenTtl: positive(input.sessionTokenTtl, 3600, 'sessionTokenTtl'),
|
|
186
|
+
accountTokenTtl: positive(input.accountTokenTtl, 30 * 24 * 3600, 'accountTokenTtl'),
|
|
187
|
+
sessionTtl: positive(input.sessionTtl, 30 * 24 * 3600, 'sessionTtl'),
|
|
188
|
+
authorizationCodeTtl: positive(input.authorizationCodeTtl, 60, 'authorizationCodeTtl'),
|
|
189
|
+
deviceCodeTtl: positive(input.deviceCodeTtl, 600, 'deviceCodeTtl'),
|
|
190
|
+
pollInterval: positive(input.pollInterval, 5, 'pollInterval'),
|
|
191
|
+
assertionMaxLifetime: positive(input.assertionMaxLifetime, 300, 'assertionMaxLifetime'),
|
|
192
|
+
dpop: {
|
|
193
|
+
requireNonce: input.dpop?.requireNonce === true,
|
|
194
|
+
maxAge: positive(input.dpop?.maxAge, 60, 'dpop.maxAge'),
|
|
195
|
+
algorithms: [...dpopAlgs],
|
|
196
|
+
},
|
|
197
|
+
replay: input.replay ?? 'database',
|
|
198
|
+
rateLimit,
|
|
199
|
+
toActor: input.toActor,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { PoppyReplayStore } from './config.js';
|
|
2
|
+
export type DpopFailure = {
|
|
3
|
+
ok: false;
|
|
4
|
+
error: 'invalid_dpop_proof' | 'use_dpop_nonce';
|
|
5
|
+
description: string;
|
|
6
|
+
};
|
|
7
|
+
export type DpopResult = {
|
|
8
|
+
ok: true;
|
|
9
|
+
jkt: string;
|
|
10
|
+
jti: string;
|
|
11
|
+
} | DpopFailure;
|
|
12
|
+
export interface DpopNonceSource {
|
|
13
|
+
/** Nonce novo para o header `DPoP-Nonce`. */
|
|
14
|
+
issue(): string;
|
|
15
|
+
/** O nonce ainda vale? */
|
|
16
|
+
valid(nonce: string): boolean;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Nonces sem estado: `base64url(ts).base64url(hmac(ts))`, válidos por
|
|
20
|
+
* `ttlSeconds` (default 300). Qualquer instância com o mesmo segredo valida.
|
|
21
|
+
*
|
|
22
|
+
* @experimental Poppy (Personal Agent Protocol) Draft 0.1 — acompanha a spec em desenvolvimento e PODE MUDAR
|
|
23
|
+
* de forma incompatível fora de majors enquanto ela for draft.
|
|
24
|
+
*/
|
|
25
|
+
export declare function hmacNonceSource(secret: Buffer | string, options?: {
|
|
26
|
+
ttlSeconds?: number;
|
|
27
|
+
now?: () => number;
|
|
28
|
+
}): DpopNonceSource;
|
|
29
|
+
/** `ath`: base64url(SHA-256(access token)). */
|
|
30
|
+
export declare function accessTokenHash(token: string): string;
|
|
31
|
+
/**
|
|
32
|
+
* `htu` normalizado para comparação: esquema e host minúsculos, sem porta
|
|
33
|
+
* default, sem query nem fragmento. `null` se não for URL http(s).
|
|
34
|
+
*/
|
|
35
|
+
export declare function normalizeHtu(value: string): string | null;
|
|
36
|
+
export interface VerifyDpopInput {
|
|
37
|
+
/** Valor do(s) header(s) `DPoP`. Mais de um = inválido. */
|
|
38
|
+
proof: string | string[] | undefined | null;
|
|
39
|
+
method: string;
|
|
40
|
+
/** URL da request (a query é ignorada). */
|
|
41
|
+
url: string;
|
|
42
|
+
/** Access token apresentado junto (resource server) — exige `ath`. */
|
|
43
|
+
accessToken?: string;
|
|
44
|
+
/** Thumbprint (RFC 7638) a que o token está preso. */
|
|
45
|
+
expectedJkt?: string;
|
|
46
|
+
algorithms: string[];
|
|
47
|
+
/** Janela do `iat`, em segundos. */
|
|
48
|
+
maxAge: number;
|
|
49
|
+
/** Tolerância de relógio para `iat` no futuro, em segundos. Default: 5. */
|
|
50
|
+
clockSkew?: number;
|
|
51
|
+
replay: PoppyReplayStore;
|
|
52
|
+
nonce?: {
|
|
53
|
+
required: boolean;
|
|
54
|
+
source: DpopNonceSource;
|
|
55
|
+
};
|
|
56
|
+
now?: () => number;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* @experimental Poppy (Personal Agent Protocol) Draft 0.1 — pode mudar de forma incompatível enquanto a spec for draft.
|
|
60
|
+
*/
|
|
61
|
+
export declare function verifyDpopProof(input: VerifyDpopInput): Promise<DpopResult>;
|