@adonis-agora/authkit-server 1.0.0-rc.1 → 1.0.0-rc.11
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/CHANGELOG.md +107 -0
- package/README.md +4 -5
- package/build/commands/agora_doctor.d.ts +22 -0
- package/build/commands/agora_doctor.js +109 -0
- package/build/commands/clients_create.d.ts +4 -0
- package/build/commands/clients_create.js +97 -0
- package/build/commands/commands.json +78 -0
- package/build/commands/doctor.js +8 -74
- package/build/host/views/account/apps.edge +1 -5
- package/build/host/views/account/extension.edge +77 -0
- package/build/host/views/account/mfa.edge +2 -0
- package/build/host/views/account/partials/extension_actions.edge +14 -0
- package/build/host/views/account/partials/extension_block.edge +197 -0
- package/build/host/views/account/partials/extension_value.edge +8 -0
- package/build/host/views/account/partials/nav.edge +14 -0
- package/build/host/views/account/security.edge +1 -5
- package/build/host/views/account/tokens.edge +2 -2
- package/build/host/views/agents/consent.edge +11 -2
- package/build/host/views/agents/done.edge +1 -1
- package/build/host/views/partials/styles.edge +1 -1
- package/build/index.d.ts +12 -12
- package/build/index.js +7 -9
- package/build/providers/authkit_server_provider.js +29 -8
- package/build/src/account/extension.d.ts +10 -0
- package/build/src/account/extension.js +9 -0
- package/build/src/account/http.d.ts +77 -0
- package/build/src/account/http.js +381 -0
- package/build/src/account/mount.d.ts +40 -0
- package/build/src/account/mount.js +171 -0
- package/build/src/account/registry.d.ts +41 -0
- package/build/src/account/registry.js +81 -0
- package/build/src/account/remote.d.ts +72 -0
- package/build/src/account/remote.js +406 -0
- package/build/src/account/runtime.d.ts +289 -0
- package/build/src/account/runtime.js +641 -0
- package/build/src/accounts/account_store.d.ts +13 -7
- package/build/src/accounts/lucid_account_store.d.ts +3 -3
- package/build/src/accounts/lucid_account_store.js +42 -17
- package/build/src/accounts/lucid_store/mfa.js +3 -3
- package/build/src/accounts/lucid_store/organization_models.d.ts +14 -3
- package/build/src/accounts/lucid_store/organization_models.js +31 -3
- package/build/src/accounts/lucid_store/organizations.d.ts +2 -2
- package/build/src/accounts/lucid_store/organizations.js +40 -31
- package/build/src/accounts/lucid_store/shared.d.ts +11 -3
- package/build/src/accounts/lucid_store/shared.js +7 -5
- package/build/src/accounts/lucid_store/webauthn.js +1 -1
- package/build/src/adapters/factory.d.ts +13 -2
- package/build/src/adapters/factory.js +2 -2
- package/build/src/agents/config.d.ts +9 -13
- package/build/src/agents/config.js +17 -11
- package/build/src/agents/delegation_service.js +3 -3
- package/build/src/agents/delegation_store.d.ts +3 -2
- package/build/src/agents/delegation_store.js +6 -5
- package/build/src/agents/extension.d.ts +261 -0
- package/build/src/agents/extension.js +40 -0
- package/build/src/agents/middleware.js +1 -1
- package/build/src/agents/protocol.d.ts +4 -4
- package/build/src/agents/registry_store.d.ts +3 -2
- package/build/src/agents/registry_store.js +1 -1
- package/build/src/agents/runtime.d.ts +26 -7
- package/build/src/agents/runtime.js +110 -75
- package/build/src/audit/audit_sink.d.ts +1 -1
- package/build/src/audit/audit_sink.js +10 -1
- package/build/src/commands/expire_scan_command.js +1 -1
- package/build/src/commands/settings_commands.js +2 -2
- package/build/src/console_switcher.d.ts +132 -0
- package/build/src/console_switcher.js +206 -0
- package/build/src/define_config.d.ts +38 -13
- package/build/src/define_config.js +32 -10
- package/build/src/dev_tools/contract.d.ts +72 -0
- package/build/src/dev_tools/contract.js +10 -0
- package/build/src/dev_tools/gate.d.ts +17 -0
- package/build/src/dev_tools/gate.js +69 -0
- package/build/src/dev_tools/register.d.ts +11 -0
- package/build/src/dev_tools/register.js +25 -0
- package/build/src/dev_tools/tools.d.ts +24 -0
- package/build/src/dev_tools/tools.js +384 -0
- package/build/src/doctor/agora_checks.d.ts +15 -0
- package/build/src/doctor/agora_checks.js +150 -0
- package/build/src/doctor/agora_doctor.d.ts +115 -0
- package/build/src/doctor/agora_doctor.js +692 -0
- package/build/src/doctor/checks.js +2 -2
- package/build/src/doctor/doctor_input.d.ts +27 -0
- package/build/src/doctor/doctor_input.js +86 -0
- package/build/src/doctor/index.d.ts +5 -0
- package/build/src/doctor/index.js +4 -0
- package/build/src/host/access_token_verifier.js +7 -0
- package/build/src/host/account_api/account_api_controller.js +8 -6
- package/build/src/host/account_session_sign_in.d.ts +12 -0
- package/build/src/host/account_session_sign_in.js +19 -0
- package/build/src/host/admin_api/admin_users_service.d.ts +5 -2
- package/build/src/host/admin_api/api_clients_controller.d.ts +37 -0
- package/build/src/host/admin_api/api_clients_controller.js +31 -1
- package/build/src/host/admin_api/dto.d.ts +3 -1
- package/build/src/host/admin_api/dto.js +2 -0
- package/build/src/host/admin_authorize.d.ts +8 -0
- package/build/src/host/admin_authorize.js +25 -0
- package/build/src/host/admin_clients_service.d.ts +20 -0
- package/build/src/host/admin_clients_service.js +55 -0
- package/build/src/host/admin_console/admin_console_entry.d.ts +14 -0
- package/build/src/host/admin_console/admin_console_entry.js +80 -0
- package/build/src/host/admin_console/admin_shell_controller.js +8 -0
- package/build/src/host/admin_console/console_clients_controller.d.ts +41 -0
- package/build/src/host/admin_console/console_clients_controller.js +34 -1
- package/build/src/host/admin_console/console_personal_agents_controller.d.ts +1 -6
- package/build/src/host/admin_console/console_personal_agents_controller.js +50 -30
- package/build/src/host/admin_console/console_switcher_controller.d.ts +11 -0
- package/build/src/host/admin_console/console_switcher_controller.js +23 -0
- package/build/src/host/admin_sessions_service.js +8 -5
- package/build/src/host/admin_validators.d.ts +29 -0
- package/build/src/host/admin_validators.js +30 -0
- package/build/src/host/auth_host_config.d.ts +9 -2
- package/build/src/host/controllers/account_apps_controller.js +15 -11
- package/build/src/host/csrf.d.ts +3 -3
- package/build/src/host/csrf.js +3 -13
- package/build/src/host/default_mailer.d.ts +11 -6
- package/build/src/host/default_mailer.js +16 -12
- package/build/src/host/i18n.d.ts +41 -14
- package/build/src/host/i18n.js +55 -13
- package/build/src/host/rate_limit.d.ts +8 -0
- package/build/src/host/rate_limit.js +9 -0
- package/build/src/host/register_auth_host.d.ts +14 -0
- package/build/src/host/register_auth_host.js +167 -106
- package/build/src/host/renderers/edge_renderer.js +5 -0
- package/build/src/host/renderers/inertia_renderer.js +7 -1
- package/build/src/host/runtime_settings.d.ts +6 -6
- package/build/src/host/runtime_settings.js +4 -3
- package/build/src/host/screen_props.d.ts +39 -8
- package/build/src/host/ui-dist/assets/index-Dq3dJVBv.js +67 -0
- package/build/src/host/ui-dist/assets/{index-DTSmD4RU.css → index-WNIyCEPA.css} +1 -1
- package/build/src/host/ui-dist/index.html +2 -2
- package/build/src/provider/build_provider.js +37 -6
- package/build/src/provider/oidc_service.d.ts +1 -1
- package/build/src/provider/oidc_service.js +1 -1
- package/build/src/schema/ensure.d.ts +24 -1
- package/build/src/schema/ensure.js +44 -154
- package/build/src/schema/tables_location.d.ts +27 -0
- package/build/src/schema/tables_location.js +22 -0
- package/build/src/service_tenants/client_metadata.d.ts +39 -0
- package/build/src/service_tenants/client_metadata.js +80 -0
- package/build/src/service_tenants/config.d.ts +44 -0
- package/build/src/service_tenants/config.js +44 -0
- package/build/src/service_tenants/provider_hooks.d.ts +52 -0
- package/build/src/service_tenants/provider_hooks.js +124 -0
- package/build/src/service_tenants/tenant_verifier.d.ts +73 -0
- package/build/src/service_tenants/tenant_verifier.js +104 -0
- package/build/src/tenancy/tenant_labels.d.ts +30 -0
- package/build/src/tenancy/tenant_labels.js +64 -0
- package/build/types.d.ts +3 -1
- package/package.json +9 -6
- package/build/src/agents/poppy/client_registry.d.ts +0 -50
- package/build/src/agents/poppy/client_registry.js +0 -260
- package/build/src/agents/poppy/config.d.ts +0 -396
- package/build/src/agents/poppy/config.js +0 -228
- package/build/src/agents/poppy/dpop.d.ts +0 -61
- package/build/src/agents/poppy/dpop.js +0 -158
- package/build/src/agents/poppy/email_code_sign_in.d.ts +0 -27
- package/build/src/agents/poppy/email_code_sign_in.js +0 -140
- package/build/src/agents/poppy/errors.d.ts +0 -25
- package/build/src/agents/poppy/errors.js +0 -25
- package/build/src/agents/poppy/replay.d.ts +0 -25
- package/build/src/agents/poppy/replay.js +0 -106
- package/build/src/agents/poppy/resource.d.ts +0 -100
- package/build/src/agents/poppy/resource.js +0 -129
- package/build/src/agents/poppy/safe_fetch.d.ts +0 -48
- package/build/src/agents/poppy/safe_fetch.js +0 -262
- package/build/src/agents/poppy/service.d.ts +0 -293
- package/build/src/agents/poppy/service.js +0 -1458
- package/build/src/agents/poppy/session_jwt.d.ts +0 -30
- package/build/src/agents/poppy/session_jwt.js +0 -133
- package/build/src/agents/poppy/store.d.ts +0 -106
- package/build/src/agents/poppy/store.js +0 -261
- package/build/src/agents/poppy/web_session.d.ts +0 -39
- package/build/src/agents/poppy/web_session.js +0 -122
- package/build/src/host/controllers/poppy_consent_controller.d.ts +0 -19
- package/build/src/host/controllers/poppy_consent_controller.js +0 -199
- package/build/src/host/controllers/poppy_controller.d.ts +0 -33
- package/build/src/host/controllers/poppy_controller.js +0 -219
- package/build/src/host/poppy_session_key.d.ts +0 -9
- package/build/src/host/poppy_session_key.js +0 -9
- package/build/src/host/ui-dist/assets/index-D0EIeyKK.js +0 -67
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
import { getBootedApp } from '../../../services/booted_app.js';
|
|
2
|
-
import { buildPersonalAgentsRuntime, personalAgentsFor } from '../runtime.js';
|
|
3
|
-
/** Símbolo do slot global de autenticação Poppy. */
|
|
4
|
-
export const POPPY_AUTHENTICATE_SLOT = Symbol.for('@adonis-agora/poppy:authenticate');
|
|
5
|
-
/** URL pública da request: atrás de proxy TLS, o esquema segue o do issuer. */
|
|
6
|
-
export function publicRequestUrl(ctx, poppy) {
|
|
7
|
-
const url = new URL(ctx.request.completeUrl(false));
|
|
8
|
-
if (new URL(poppy.urls.origin).protocol === 'https:' && url.protocol === 'http:') {
|
|
9
|
-
url.protocol = 'https:';
|
|
10
|
-
}
|
|
11
|
-
return url.toString();
|
|
12
|
-
}
|
|
13
|
-
function requestFromCtx(ctx, poppy) {
|
|
14
|
-
return {
|
|
15
|
-
method: ctx.request.method(),
|
|
16
|
-
url: publicRequestUrl(ctx, poppy),
|
|
17
|
-
headers: ctx.request.headers(),
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
const NOT_ENABLED = {
|
|
21
|
-
ok: false,
|
|
22
|
-
status: 400,
|
|
23
|
-
error: 'invalid_session',
|
|
24
|
-
description: 'Poppy is not enabled',
|
|
25
|
-
wwwAuthenticate: null,
|
|
26
|
-
headers: {},
|
|
27
|
-
};
|
|
28
|
-
async function verifyWith(poppy, input, options) {
|
|
29
|
-
const result = await poppy.verifyAccess(input, options);
|
|
30
|
-
if (!result.ok) {
|
|
31
|
-
const { ok: _ok, ...rest } = result;
|
|
32
|
-
return { ok: false, ...rest };
|
|
33
|
-
}
|
|
34
|
-
const principal = { ...result.principal };
|
|
35
|
-
if (poppy.cfg.toActor)
|
|
36
|
-
principal.actor = await poppy.cfg.toActor(result.principal);
|
|
37
|
-
return { ok: true, principal };
|
|
38
|
-
}
|
|
39
|
-
async function poppyFromBootedApp() {
|
|
40
|
-
const app = getBootedApp();
|
|
41
|
-
const service = await app.container.make('authkit.server');
|
|
42
|
-
const runtime = await buildPersonalAgentsRuntime(service, () => app.container.make('lucid.db'), { make: (binding) => app.container.make(binding) });
|
|
43
|
-
return runtime?.poppy ?? null;
|
|
44
|
-
}
|
|
45
|
-
/**
|
|
46
|
-
* Verifica o header `Poppy-Session` de uma request e, se houver, o access token. Aceita o
|
|
47
|
-
* `HttpContext` ou `{ method, url, headers }`.
|
|
48
|
-
*
|
|
49
|
-
* @experimental Poppy (Personal Agent Protocol) Draft 0.3 — acompanha a spec em desenvolvimento e PODE MUDAR
|
|
50
|
-
* de forma incompatível fora de majors enquanto ela for draft.
|
|
51
|
-
*/
|
|
52
|
-
export async function verifyPoppyRequest(input, options = {}) {
|
|
53
|
-
if ('request' in input && typeof input.request?.method === 'function') {
|
|
54
|
-
const ctx = input;
|
|
55
|
-
const poppy = (await personalAgentsFor(ctx))?.poppy;
|
|
56
|
-
if (!poppy)
|
|
57
|
-
return NOT_ENABLED;
|
|
58
|
-
return verifyWith(poppy, requestFromCtx(ctx, poppy), options);
|
|
59
|
-
}
|
|
60
|
-
const poppy = await poppyFromBootedApp();
|
|
61
|
-
if (!poppy)
|
|
62
|
-
return NOT_ENABLED;
|
|
63
|
-
return verifyWith(poppy, input, options);
|
|
64
|
-
}
|
|
65
|
-
/** Escreve a resposta de erro de um {@link PoppyAuthResult} recusado. */
|
|
66
|
-
export function writePoppyAuthError(ctx, result) {
|
|
67
|
-
if (result.wwwAuthenticate)
|
|
68
|
-
ctx.response.header('WWW-Authenticate', result.wwwAuthenticate);
|
|
69
|
-
for (const [k, v] of Object.entries(result.headers))
|
|
70
|
-
ctx.response.header(k, v);
|
|
71
|
-
ctx.response.header('Cache-Control', 'no-store');
|
|
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 });
|
|
78
|
-
}
|
|
79
|
-
/** Guardado fora do `HttpContext` (sem augmentation — ver `augmentation_isolation.spec.ts`). */
|
|
80
|
-
const principals = new WeakMap();
|
|
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
|
-
*/
|
|
87
|
-
export function poppyOf(ctx) {
|
|
88
|
-
return principals.get(ctx) ?? null;
|
|
89
|
-
}
|
|
90
|
-
/**
|
|
91
|
-
* Middleware das APIs/rotas que usam a Session Poppy.
|
|
92
|
-
*
|
|
93
|
-
* ```ts
|
|
94
|
-
* router.post('/orders/:id/exchanges', [Orders, 'exchange'])
|
|
95
|
-
* .use(poppyAuth({ scopes: ['orders:write'] }))
|
|
96
|
-
* ```
|
|
97
|
-
*
|
|
98
|
-
* Isente essas rotas do CSRF (são chamadas por agentes, sem navegador).
|
|
99
|
-
*
|
|
100
|
-
* @experimental Poppy (Personal Agent Protocol) Draft 0.3 — acompanha a spec em desenvolvimento e PODE MUDAR
|
|
101
|
-
* de forma incompatível fora de majors enquanto ela for draft.
|
|
102
|
-
*/
|
|
103
|
-
export function poppyAuth(options = {}) {
|
|
104
|
-
return async (ctx, next) => {
|
|
105
|
-
const result = await verifyPoppyRequest(ctx, options);
|
|
106
|
-
if (!result.ok)
|
|
107
|
-
return writePoppyAuthError(ctx, result);
|
|
108
|
-
principals.set(ctx, result.principal);
|
|
109
|
-
return next();
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
/**
|
|
113
|
-
* Publica o slot global `Symbol.for('@adonis-agora/poppy:authenticate')` (e
|
|
114
|
-
* `Symbol.for('@adonis-agora/poppy:issuer')`). Chamado no boot do provider
|
|
115
|
-
* quando `personalAgents.poppy` está no config.
|
|
116
|
-
*
|
|
117
|
-
* @experimental Poppy (Personal Agent Protocol) Draft 0.3 — acompanha a spec em desenvolvimento e PODE MUDAR
|
|
118
|
-
* de forma incompatível fora de majors enquanto ela for draft.
|
|
119
|
-
*/
|
|
120
|
-
export function installPoppySlots(issuer) {
|
|
121
|
-
const slot = globalThis;
|
|
122
|
-
const authenticate = (request, options) => verifyPoppyRequest({
|
|
123
|
-
method: request.method,
|
|
124
|
-
url: request.url,
|
|
125
|
-
headers: request.headers ?? {},
|
|
126
|
-
}, options);
|
|
127
|
-
slot[POPPY_AUTHENTICATE_SLOT] = authenticate;
|
|
128
|
-
slot[Symbol.for('@adonis-agora/poppy:issuer')] = issuer;
|
|
129
|
-
}
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `fetch` de documentos JSON de terceiros à prova de SSRF.
|
|
3
|
-
*
|
|
4
|
-
* O `client_id` de um agente Poppy é uma URL que QUALQUER um escolhe — buscá-la
|
|
5
|
-
* sem cuidado faria deste app um proxy para a rede interna. Regras:
|
|
6
|
-
* - só `https:`; sem credenciais na URL; porta qualquer;
|
|
7
|
-
* - o IP é conferido NA CONEXÃO (o `lookup` do socket), não num DNS separado —
|
|
8
|
-
* sem janela para DNS rebinding; loopback, privados, link-local, CGNAT,
|
|
9
|
-
* multicast, reservados e os equivalentes IPv6 são recusados;
|
|
10
|
-
* - IP literal na URL passa pela mesma regra;
|
|
11
|
-
* - sem redirects (o `client_id` tem de ser exatamente a URL buscada);
|
|
12
|
-
* - teto de tempo e de bytes.
|
|
13
|
-
*/
|
|
14
|
-
import { type LookupAddress } from 'node:dns';
|
|
15
|
-
export declare class SafeFetchError extends Error {
|
|
16
|
-
constructor(message: string);
|
|
17
|
-
}
|
|
18
|
-
export interface FetchedJson {
|
|
19
|
-
status: number;
|
|
20
|
-
body: unknown;
|
|
21
|
-
/** `max-age` do `Cache-Control`, em segundos, quando há. */
|
|
22
|
-
maxAge: number | null;
|
|
23
|
-
}
|
|
24
|
-
/** Busca um JSON (GET). Lança {@link SafeFetchError} em qualquer recusa. */
|
|
25
|
-
export type JsonFetcher = (url: string) => Promise<FetchedJson>;
|
|
26
|
-
export type LookupFn = (hostname: string, options: {
|
|
27
|
-
all: true;
|
|
28
|
-
}, callback: (err: NodeJS.ErrnoException | null, addresses: LookupAddress[]) => void) => void;
|
|
29
|
-
export interface SafeFetchOptions {
|
|
30
|
-
timeoutMs?: number;
|
|
31
|
-
maxBytes?: number;
|
|
32
|
-
/** Resolução de nomes — injetável nos testes. Default: `dns.lookup`. */
|
|
33
|
-
lookup?: LookupFn;
|
|
34
|
-
}
|
|
35
|
-
/** `true` quando o IP não pode ser alvo de uma busca de saída. */
|
|
36
|
-
export declare function isBlockedAddress(ip: string): boolean;
|
|
37
|
-
/**
|
|
38
|
-
* Busca `url` e devolve o JSON. Só HTTPS, sem redirect, IP público conferido na
|
|
39
|
-
* conexão, `timeoutMs` (default 5 s) e `maxBytes` (default 64 KiB).
|
|
40
|
-
*/
|
|
41
|
-
export declare function safeFetchJson(url: string, options?: SafeFetchOptions): Promise<FetchedJson>;
|
|
42
|
-
/**
|
|
43
|
-
* SÓ DEV/TESTE (`personalAgents.poppy.allowInsecureClientIds`): busca um JSON por
|
|
44
|
-
* `http:` ou `https:` SEM a guarda de SSRF — loopback e rede privada liberados,
|
|
45
|
-
* para um agente local (`http://localhost:7777/agent.json`). Mantém sem redirect,
|
|
46
|
-
* teto de tempo e de bytes. Nunca use em produção (a config recusa).
|
|
47
|
-
*/
|
|
48
|
-
export declare function insecureDevFetchJson(url: string, options?: Omit<SafeFetchOptions, 'lookup'>): Promise<FetchedJson>;
|
|
@@ -1,262 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `fetch` de documentos JSON de terceiros à prova de SSRF.
|
|
3
|
-
*
|
|
4
|
-
* O `client_id` de um agente Poppy é uma URL que QUALQUER um escolhe — buscá-la
|
|
5
|
-
* sem cuidado faria deste app um proxy para a rede interna. Regras:
|
|
6
|
-
* - só `https:`; sem credenciais na URL; porta qualquer;
|
|
7
|
-
* - o IP é conferido NA CONEXÃO (o `lookup` do socket), não num DNS separado —
|
|
8
|
-
* sem janela para DNS rebinding; loopback, privados, link-local, CGNAT,
|
|
9
|
-
* multicast, reservados e os equivalentes IPv6 são recusados;
|
|
10
|
-
* - IP literal na URL passa pela mesma regra;
|
|
11
|
-
* - sem redirects (o `client_id` tem de ser exatamente a URL buscada);
|
|
12
|
-
* - teto de tempo e de bytes.
|
|
13
|
-
*/
|
|
14
|
-
import { lookup as dnsLookup } from 'node:dns';
|
|
15
|
-
import { request as httpsRequest } from 'node:https';
|
|
16
|
-
import { isIP } from 'node:net';
|
|
17
|
-
export class SafeFetchError extends Error {
|
|
18
|
-
constructor(message) {
|
|
19
|
-
super(message);
|
|
20
|
-
this.name = 'SafeFetchError';
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
function ipv4ToInt(ip) {
|
|
24
|
-
return ip.split('.').reduce((acc, part) => (acc << 8) + Number(part), 0) >>> 0;
|
|
25
|
-
}
|
|
26
|
-
function inV4(ip, cidr) {
|
|
27
|
-
const [base, bits] = cidr.split('/');
|
|
28
|
-
const mask = Number(bits) === 0 ? 0 : (~0 << (32 - Number(bits))) >>> 0;
|
|
29
|
-
return (ip & mask) === (ipv4ToInt(base) & mask);
|
|
30
|
-
}
|
|
31
|
-
const BLOCKED_V4 = [
|
|
32
|
-
'0.0.0.0/8',
|
|
33
|
-
'10.0.0.0/8',
|
|
34
|
-
'100.64.0.0/10',
|
|
35
|
-
'127.0.0.0/8',
|
|
36
|
-
'169.254.0.0/16',
|
|
37
|
-
'172.16.0.0/12',
|
|
38
|
-
'192.0.0.0/24',
|
|
39
|
-
'192.0.2.0/24',
|
|
40
|
-
'192.88.99.0/24',
|
|
41
|
-
'192.168.0.0/16',
|
|
42
|
-
'198.18.0.0/15',
|
|
43
|
-
'198.51.100.0/24',
|
|
44
|
-
'203.0.113.0/24',
|
|
45
|
-
'224.0.0.0/4',
|
|
46
|
-
'240.0.0.0/4',
|
|
47
|
-
];
|
|
48
|
-
/** Expande um IPv6 em 8 grupos de 16 bits. */
|
|
49
|
-
function ipv6Groups(ip) {
|
|
50
|
-
let addr = ip.toLowerCase().split('%')[0];
|
|
51
|
-
// Sufixo IPv4 (::ffff:1.2.3.4).
|
|
52
|
-
const v4 = /(\d+\.\d+\.\d+\.\d+)$/.exec(addr);
|
|
53
|
-
if (v4) {
|
|
54
|
-
const n = ipv4ToInt(v4[1]);
|
|
55
|
-
addr = `${addr.slice(0, -v4[1].length)}${(n >>> 16).toString(16)}:${(n & 0xffff).toString(16)}`;
|
|
56
|
-
}
|
|
57
|
-
const [head, tail] = addr.split('::');
|
|
58
|
-
const h = head ? head.split(':') : [];
|
|
59
|
-
const t = tail !== undefined ? (tail ? tail.split(':') : []) : [];
|
|
60
|
-
const fill = addr.includes('::') ? 8 - h.length - t.length : 0;
|
|
61
|
-
const groups = [...h, ...Array(fill).fill('0'), ...t].map((g) => Number.parseInt(g || '0', 16));
|
|
62
|
-
return groups.length === 8 && groups.every((g) => g >= 0 && g <= 0xffff) ? groups : null;
|
|
63
|
-
}
|
|
64
|
-
/** `true` quando o IP não pode ser alvo de uma busca de saída. */
|
|
65
|
-
export function isBlockedAddress(ip) {
|
|
66
|
-
const family = isIP(ip);
|
|
67
|
-
if (family === 4) {
|
|
68
|
-
const n = ipv4ToInt(ip);
|
|
69
|
-
return BLOCKED_V4.some((cidr) => inV4(n, cidr));
|
|
70
|
-
}
|
|
71
|
-
if (family !== 6)
|
|
72
|
-
return true;
|
|
73
|
-
const g = ipv6Groups(ip);
|
|
74
|
-
if (!g)
|
|
75
|
-
return true;
|
|
76
|
-
if (g.every((x) => x === 0))
|
|
77
|
-
return true; // ::
|
|
78
|
-
if (g.slice(0, 7).every((x) => x === 0) && g[7] === 1)
|
|
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
|
-
}
|
|
86
|
-
// IPv4-mapped (::ffff:a.b.c.d) e IPv4-compatible: decide pelo IPv4.
|
|
87
|
-
if (g.slice(0, 5).every((x) => x === 0) && (g[5] === 0xffff || g[5] === 0)) {
|
|
88
|
-
const v4 = `${g[6] >> 8}.${g[6] & 255}.${g[7] >> 8}.${g[7] & 255}`;
|
|
89
|
-
return isBlockedAddress(v4);
|
|
90
|
-
}
|
|
91
|
-
if ((g[0] & 0xfe00) === 0xfc00)
|
|
92
|
-
return true; // fc00::/7 (ULA)
|
|
93
|
-
if ((g[0] & 0xffc0) === 0xfe80)
|
|
94
|
-
return true; // fe80::/10 (link-local)
|
|
95
|
-
if ((g[0] & 0xffc0) === 0xfec0)
|
|
96
|
-
return true; // fec0::/10 (site-local)
|
|
97
|
-
if ((g[0] & 0xff00) === 0xff00)
|
|
98
|
-
return true; // ff00::/8 (multicast)
|
|
99
|
-
if (g[0] === 0x2001 && g[1] === 0x0db8)
|
|
100
|
-
return true; // 2001:db8::/32 (doc)
|
|
101
|
-
if (g[0] === 0x64 && g[1] === 0xff9b)
|
|
102
|
-
return true; // 64:ff9b::/96 (NAT64)
|
|
103
|
-
if (g[0] === 0x2002) {
|
|
104
|
-
// 6to4: o IPv4 embutido.
|
|
105
|
-
return isBlockedAddress(`${g[1] >> 8}.${g[1] & 255}.${g[2] >> 8}.${g[2] & 255}`);
|
|
106
|
-
}
|
|
107
|
-
return false;
|
|
108
|
-
}
|
|
109
|
-
function parseMaxAge(header) {
|
|
110
|
-
const value = Array.isArray(header) ? header.join(',') : header;
|
|
111
|
-
if (!value)
|
|
112
|
-
return null;
|
|
113
|
-
if (/\bno-store\b|\bno-cache\b/i.test(value))
|
|
114
|
-
return 0;
|
|
115
|
-
const match = /\bmax-age=(\d+)/i.exec(value);
|
|
116
|
-
return match ? Number(match[1]) : null;
|
|
117
|
-
}
|
|
118
|
-
/**
|
|
119
|
-
* Busca `url` e devolve o JSON. Só HTTPS, sem redirect, IP público conferido na
|
|
120
|
-
* conexão, `timeoutMs` (default 5 s) e `maxBytes` (default 64 KiB).
|
|
121
|
-
*/
|
|
122
|
-
export function safeFetchJson(url, options = {}) {
|
|
123
|
-
const timeoutMs = options.timeoutMs ?? 5000;
|
|
124
|
-
const maxBytes = options.maxBytes ?? 64 * 1024;
|
|
125
|
-
const resolve = options.lookup ?? dnsLookup;
|
|
126
|
-
let parsed;
|
|
127
|
-
try {
|
|
128
|
-
parsed = new URL(url);
|
|
129
|
-
}
|
|
130
|
-
catch {
|
|
131
|
-
return Promise.reject(new SafeFetchError('invalid URL'));
|
|
132
|
-
}
|
|
133
|
-
if (parsed.protocol !== 'https:')
|
|
134
|
-
return Promise.reject(new SafeFetchError('only https URLs'));
|
|
135
|
-
if (parsed.username || parsed.password) {
|
|
136
|
-
return Promise.reject(new SafeFetchError('credentials in URL'));
|
|
137
|
-
}
|
|
138
|
-
const host = parsed.hostname.replace(/^\[|\]$/g, '');
|
|
139
|
-
if (isIP(host) && isBlockedAddress(host)) {
|
|
140
|
-
return Promise.reject(new SafeFetchError('blocked address'));
|
|
141
|
-
}
|
|
142
|
-
// `lookup` do socket: resolve e recusa se QUALQUER endereço for interno.
|
|
143
|
-
const guardedLookup = (hostname, opts, callback) => {
|
|
144
|
-
resolve(hostname, { all: true }, (err, addresses) => {
|
|
145
|
-
if (err)
|
|
146
|
-
return callback(err);
|
|
147
|
-
const list = addresses ?? [];
|
|
148
|
-
if (list.length === 0)
|
|
149
|
-
return callback(new SafeFetchError('no address'));
|
|
150
|
-
if (list.some((a) => isBlockedAddress(a.address))) {
|
|
151
|
-
return callback(new SafeFetchError('blocked address'));
|
|
152
|
-
}
|
|
153
|
-
if (opts?.all)
|
|
154
|
-
return callback(null, list);
|
|
155
|
-
return callback(null, list[0].address, list[0].family);
|
|
156
|
-
});
|
|
157
|
-
};
|
|
158
|
-
return new Promise((done, reject) => {
|
|
159
|
-
let settled = false;
|
|
160
|
-
const finish = (fn) => {
|
|
161
|
-
if (settled)
|
|
162
|
-
return;
|
|
163
|
-
settled = true;
|
|
164
|
-
clearTimeout(timer);
|
|
165
|
-
fn();
|
|
166
|
-
};
|
|
167
|
-
const req = httpsRequest(parsed, {
|
|
168
|
-
method: 'GET',
|
|
169
|
-
headers: { accept: 'application/json', 'user-agent': 'authkit-poppy/0.3' },
|
|
170
|
-
lookup: guardedLookup,
|
|
171
|
-
// IP literal: o `lookup` não roda — já conferido acima.
|
|
172
|
-
}, (res) => {
|
|
173
|
-
const status = res.statusCode ?? 0;
|
|
174
|
-
if (status >= 300 && status < 400) {
|
|
175
|
-
res.resume();
|
|
176
|
-
return finish(() => reject(new SafeFetchError('redirects are not followed')));
|
|
177
|
-
}
|
|
178
|
-
const chunks = [];
|
|
179
|
-
let size = 0;
|
|
180
|
-
res.on('data', (chunk) => {
|
|
181
|
-
size += chunk.length;
|
|
182
|
-
if (size > maxBytes) {
|
|
183
|
-
req.destroy();
|
|
184
|
-
finish(() => reject(new SafeFetchError('response too large')));
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
chunks.push(chunk);
|
|
188
|
-
});
|
|
189
|
-
res.on('end', () => finish(() => {
|
|
190
|
-
if (status !== 200)
|
|
191
|
-
return reject(new SafeFetchError(`HTTP ${status}`));
|
|
192
|
-
try {
|
|
193
|
-
done({
|
|
194
|
-
status,
|
|
195
|
-
body: JSON.parse(Buffer.concat(chunks).toString('utf8')),
|
|
196
|
-
maxAge: parseMaxAge(res.headers['cache-control']),
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
catch {
|
|
200
|
-
reject(new SafeFetchError('invalid JSON'));
|
|
201
|
-
}
|
|
202
|
-
}));
|
|
203
|
-
res.on('error', (e) => finish(() => reject(new SafeFetchError(e.message))));
|
|
204
|
-
});
|
|
205
|
-
const timer = setTimeout(() => {
|
|
206
|
-
req.destroy();
|
|
207
|
-
finish(() => reject(new SafeFetchError('timeout')));
|
|
208
|
-
}, timeoutMs);
|
|
209
|
-
req.on('error', (e) => finish(() => reject(e instanceof SafeFetchError ? e : new SafeFetchError(e.message))));
|
|
210
|
-
req.end();
|
|
211
|
-
});
|
|
212
|
-
}
|
|
213
|
-
/**
|
|
214
|
-
* SÓ DEV/TESTE (`personalAgents.poppy.allowInsecureClientIds`): busca um JSON por
|
|
215
|
-
* `http:` ou `https:` SEM a guarda de SSRF — loopback e rede privada liberados,
|
|
216
|
-
* para um agente local (`http://localhost:7777/agent.json`). Mantém sem redirect,
|
|
217
|
-
* teto de tempo e de bytes. Nunca use em produção (a config recusa).
|
|
218
|
-
*/
|
|
219
|
-
export async function insecureDevFetchJson(url, options = {}) {
|
|
220
|
-
const timeoutMs = options.timeoutMs ?? 5000;
|
|
221
|
-
const maxBytes = options.maxBytes ?? 64 * 1024;
|
|
222
|
-
let parsed;
|
|
223
|
-
try {
|
|
224
|
-
parsed = new URL(url);
|
|
225
|
-
}
|
|
226
|
-
catch {
|
|
227
|
-
throw new SafeFetchError('invalid URL');
|
|
228
|
-
}
|
|
229
|
-
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
|
|
230
|
-
throw new SafeFetchError('only http(s) URLs');
|
|
231
|
-
}
|
|
232
|
-
if (parsed.username || parsed.password)
|
|
233
|
-
throw new SafeFetchError('credentials in URL');
|
|
234
|
-
let res;
|
|
235
|
-
try {
|
|
236
|
-
res = await fetch(parsed, {
|
|
237
|
-
redirect: 'manual',
|
|
238
|
-
headers: { accept: 'application/json', 'user-agent': 'authkit-poppy/0.3 (insecure-dev)' },
|
|
239
|
-
signal: AbortSignal.timeout(timeoutMs),
|
|
240
|
-
});
|
|
241
|
-
}
|
|
242
|
-
catch (error) {
|
|
243
|
-
throw new SafeFetchError(error.name === 'TimeoutError' ? 'timeout' : 'unreachable');
|
|
244
|
-
}
|
|
245
|
-
if (res.status >= 300 && res.status < 400)
|
|
246
|
-
throw new SafeFetchError('redirects are not followed');
|
|
247
|
-
const buf = Buffer.from(await res.arrayBuffer());
|
|
248
|
-
if (buf.length > maxBytes)
|
|
249
|
-
throw new SafeFetchError('response too large');
|
|
250
|
-
if (res.status !== 200)
|
|
251
|
-
throw new SafeFetchError(`HTTP ${res.status}`);
|
|
252
|
-
try {
|
|
253
|
-
return {
|
|
254
|
-
status: res.status,
|
|
255
|
-
body: JSON.parse(buf.toString('utf8')),
|
|
256
|
-
maxAge: parseMaxAge(res.headers.get('cache-control') ?? undefined),
|
|
257
|
-
};
|
|
258
|
-
}
|
|
259
|
-
catch {
|
|
260
|
-
throw new SafeFetchError('invalid JSON');
|
|
261
|
-
}
|
|
262
|
-
}
|