@adonis-agora/authkit-server 1.0.0-rc.1 → 1.0.0-rc.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/README.md +4 -5
  3. package/build/commands/agora_doctor.d.ts +22 -0
  4. package/build/commands/agora_doctor.js +109 -0
  5. package/build/commands/clients_create.d.ts +4 -0
  6. package/build/commands/clients_create.js +97 -0
  7. package/build/commands/commands.json +78 -0
  8. package/build/commands/doctor.js +8 -74
  9. package/build/host/views/account/apps.edge +1 -5
  10. package/build/host/views/account/extension.edge +77 -0
  11. package/build/host/views/account/mfa.edge +2 -0
  12. package/build/host/views/account/partials/extension_actions.edge +14 -0
  13. package/build/host/views/account/partials/extension_block.edge +197 -0
  14. package/build/host/views/account/partials/extension_value.edge +8 -0
  15. package/build/host/views/account/partials/nav.edge +14 -0
  16. package/build/host/views/account/security.edge +1 -5
  17. package/build/host/views/account/tokens.edge +2 -2
  18. package/build/host/views/agents/consent.edge +11 -2
  19. package/build/host/views/agents/done.edge +1 -1
  20. package/build/host/views/partials/styles.edge +1 -1
  21. package/build/index.d.ts +12 -12
  22. package/build/index.js +7 -9
  23. package/build/providers/authkit_server_provider.js +28 -8
  24. package/build/src/account/extension.d.ts +10 -0
  25. package/build/src/account/extension.js +9 -0
  26. package/build/src/account/http.d.ts +77 -0
  27. package/build/src/account/http.js +381 -0
  28. package/build/src/account/mount.d.ts +40 -0
  29. package/build/src/account/mount.js +171 -0
  30. package/build/src/account/registry.d.ts +41 -0
  31. package/build/src/account/registry.js +81 -0
  32. package/build/src/account/remote.d.ts +72 -0
  33. package/build/src/account/remote.js +406 -0
  34. package/build/src/account/runtime.d.ts +289 -0
  35. package/build/src/account/runtime.js +641 -0
  36. package/build/src/accounts/account_store.d.ts +6 -0
  37. package/build/src/accounts/lucid_store/organizations.js +6 -0
  38. package/build/src/agents/config.d.ts +9 -13
  39. package/build/src/agents/config.js +17 -11
  40. package/build/src/agents/delegation_service.js +3 -3
  41. package/build/src/agents/delegation_store.d.ts +3 -2
  42. package/build/src/agents/delegation_store.js +6 -5
  43. package/build/src/agents/extension.d.ts +261 -0
  44. package/build/src/agents/extension.js +40 -0
  45. package/build/src/agents/middleware.js +1 -1
  46. package/build/src/agents/protocol.d.ts +4 -4
  47. package/build/src/agents/registry_store.d.ts +3 -2
  48. package/build/src/agents/registry_store.js +1 -1
  49. package/build/src/agents/runtime.d.ts +26 -7
  50. package/build/src/agents/runtime.js +110 -75
  51. package/build/src/audit/audit_sink.d.ts +1 -1
  52. package/build/src/audit/audit_sink.js +10 -1
  53. package/build/src/console_switcher.d.ts +132 -0
  54. package/build/src/console_switcher.js +206 -0
  55. package/build/src/define_config.d.ts +34 -12
  56. package/build/src/define_config.js +15 -7
  57. package/build/src/dev_tools/contract.d.ts +72 -0
  58. package/build/src/dev_tools/contract.js +10 -0
  59. package/build/src/dev_tools/gate.d.ts +17 -0
  60. package/build/src/dev_tools/gate.js +69 -0
  61. package/build/src/dev_tools/register.d.ts +11 -0
  62. package/build/src/dev_tools/register.js +25 -0
  63. package/build/src/dev_tools/tools.d.ts +24 -0
  64. package/build/src/dev_tools/tools.js +384 -0
  65. package/build/src/doctor/agora_checks.d.ts +15 -0
  66. package/build/src/doctor/agora_checks.js +150 -0
  67. package/build/src/doctor/agora_doctor.d.ts +115 -0
  68. package/build/src/doctor/agora_doctor.js +692 -0
  69. package/build/src/doctor/doctor_input.d.ts +27 -0
  70. package/build/src/doctor/doctor_input.js +86 -0
  71. package/build/src/doctor/index.d.ts +5 -0
  72. package/build/src/doctor/index.js +4 -0
  73. package/build/src/host/access_token_verifier.js +7 -0
  74. package/build/src/host/account_api/account_api_controller.js +8 -6
  75. package/build/src/host/account_session_sign_in.d.ts +12 -0
  76. package/build/src/host/account_session_sign_in.js +19 -0
  77. package/build/src/host/admin_api/admin_users_service.d.ts +5 -2
  78. package/build/src/host/admin_api/api_clients_controller.d.ts +37 -0
  79. package/build/src/host/admin_api/api_clients_controller.js +31 -1
  80. package/build/src/host/admin_api/dto.d.ts +3 -1
  81. package/build/src/host/admin_api/dto.js +2 -0
  82. package/build/src/host/admin_authorize.d.ts +8 -0
  83. package/build/src/host/admin_authorize.js +25 -0
  84. package/build/src/host/admin_clients_service.d.ts +20 -0
  85. package/build/src/host/admin_clients_service.js +55 -0
  86. package/build/src/host/admin_console/admin_console_entry.d.ts +14 -0
  87. package/build/src/host/admin_console/admin_console_entry.js +80 -0
  88. package/build/src/host/admin_console/admin_shell_controller.js +8 -0
  89. package/build/src/host/admin_console/console_clients_controller.d.ts +41 -0
  90. package/build/src/host/admin_console/console_clients_controller.js +34 -1
  91. package/build/src/host/admin_console/console_personal_agents_controller.d.ts +1 -6
  92. package/build/src/host/admin_console/console_personal_agents_controller.js +50 -30
  93. package/build/src/host/admin_console/console_switcher_controller.d.ts +11 -0
  94. package/build/src/host/admin_console/console_switcher_controller.js +23 -0
  95. package/build/src/host/admin_sessions_service.js +8 -5
  96. package/build/src/host/admin_validators.d.ts +29 -0
  97. package/build/src/host/admin_validators.js +30 -0
  98. package/build/src/host/auth_host_config.d.ts +9 -2
  99. package/build/src/host/controllers/account_apps_controller.js +15 -11
  100. package/build/src/host/csrf.d.ts +3 -3
  101. package/build/src/host/csrf.js +3 -13
  102. package/build/src/host/default_mailer.d.ts +11 -6
  103. package/build/src/host/default_mailer.js +16 -12
  104. package/build/src/host/i18n.d.ts +41 -14
  105. package/build/src/host/i18n.js +55 -13
  106. package/build/src/host/rate_limit.d.ts +8 -0
  107. package/build/src/host/rate_limit.js +9 -0
  108. package/build/src/host/register_auth_host.d.ts +14 -0
  109. package/build/src/host/register_auth_host.js +167 -106
  110. package/build/src/host/renderers/edge_renderer.js +5 -0
  111. package/build/src/host/renderers/inertia_renderer.js +7 -1
  112. package/build/src/host/screen_props.d.ts +39 -8
  113. package/build/src/host/ui-dist/assets/index-Dq3dJVBv.js +67 -0
  114. package/build/src/host/ui-dist/assets/{index-DTSmD4RU.css → index-WNIyCEPA.css} +1 -1
  115. package/build/src/host/ui-dist/index.html +2 -2
  116. package/build/src/provider/build_provider.js +37 -6
  117. package/build/src/provider/oidc_service.d.ts +1 -1
  118. package/build/src/provider/oidc_service.js +1 -1
  119. package/build/src/schema/ensure.d.ts +14 -0
  120. package/build/src/schema/ensure.js +43 -150
  121. package/build/src/service_tenants/client_metadata.d.ts +39 -0
  122. package/build/src/service_tenants/client_metadata.js +80 -0
  123. package/build/src/service_tenants/config.d.ts +44 -0
  124. package/build/src/service_tenants/config.js +44 -0
  125. package/build/src/service_tenants/provider_hooks.d.ts +52 -0
  126. package/build/src/service_tenants/provider_hooks.js +124 -0
  127. package/build/src/service_tenants/tenant_verifier.d.ts +73 -0
  128. package/build/src/service_tenants/tenant_verifier.js +104 -0
  129. package/build/src/tenancy/tenant_labels.d.ts +30 -0
  130. package/build/src/tenancy/tenant_labels.js +64 -0
  131. package/build/types.d.ts +3 -1
  132. package/package.json +9 -6
  133. package/build/src/agents/poppy/client_registry.d.ts +0 -50
  134. package/build/src/agents/poppy/client_registry.js +0 -260
  135. package/build/src/agents/poppy/config.d.ts +0 -396
  136. package/build/src/agents/poppy/config.js +0 -228
  137. package/build/src/agents/poppy/dpop.d.ts +0 -61
  138. package/build/src/agents/poppy/dpop.js +0 -158
  139. package/build/src/agents/poppy/email_code_sign_in.d.ts +0 -27
  140. package/build/src/agents/poppy/email_code_sign_in.js +0 -140
  141. package/build/src/agents/poppy/errors.d.ts +0 -25
  142. package/build/src/agents/poppy/errors.js +0 -25
  143. package/build/src/agents/poppy/replay.d.ts +0 -25
  144. package/build/src/agents/poppy/replay.js +0 -106
  145. package/build/src/agents/poppy/resource.d.ts +0 -100
  146. package/build/src/agents/poppy/resource.js +0 -129
  147. package/build/src/agents/poppy/safe_fetch.d.ts +0 -48
  148. package/build/src/agents/poppy/safe_fetch.js +0 -262
  149. package/build/src/agents/poppy/service.d.ts +0 -293
  150. package/build/src/agents/poppy/service.js +0 -1458
  151. package/build/src/agents/poppy/session_jwt.d.ts +0 -30
  152. package/build/src/agents/poppy/session_jwt.js +0 -133
  153. package/build/src/agents/poppy/store.d.ts +0 -106
  154. package/build/src/agents/poppy/store.js +0 -261
  155. package/build/src/agents/poppy/web_session.d.ts +0 -39
  156. package/build/src/agents/poppy/web_session.js +0 -122
  157. package/build/src/host/controllers/poppy_consent_controller.d.ts +0 -19
  158. package/build/src/host/controllers/poppy_consent_controller.js +0 -199
  159. package/build/src/host/controllers/poppy_controller.d.ts +0 -33
  160. package/build/src/host/controllers/poppy_controller.js +0 -219
  161. package/build/src/host/poppy_session_key.d.ts +0 -9
  162. package/build/src/host/poppy_session_key.js +0 -9
  163. package/build/src/host/ui-dist/assets/index-D0EIeyKK.js +0 -67
@@ -1,25 +0,0 @@
1
- import type { PoppyLimiter, PoppyReplayStore } from './config.js';
2
- export declare const JTI_TABLE = "auth_poppy_jtis";
3
- /** Chave curta e de tamanho fixo para um `jti` de um tipo/agente. */
4
- export declare function replayKey(kind: string, ...parts: string[]): string;
5
- /** Em memória, por processo. Só serve com uma instância. */
6
- export declare function memoryReplayStore(options?: {
7
- max?: number;
8
- now?: () => number;
9
- }): PoppyReplayStore;
10
- /**
11
- * Na tabela lib-owned `auth_poppy_jtis` (PK = chave): o INSERT de uma chave que
12
- * já existe falha — atômico entre instâncias. Linhas vencidas saem aos poucos.
13
- */
14
- export declare function databaseReplayStore(conn: () => any, options?: {
15
- now?: () => Date;
16
- }): PoppyReplayStore;
17
- /** Numa conexão Redis (`SET key 1 PX ttl NX`). */
18
- export declare function redisReplayStore(redis: {
19
- set: (...args: any[]) => Promise<unknown>;
20
- }): PoppyReplayStore;
21
- /** Janelas fixas em memória, por processo: `sessionsPerMinute` por agente, `mediatedPerHour` por (agente, usuário). */
22
- export declare function memoryLimiter(limits: {
23
- sessionsPerMinute: number;
24
- mediatedPerHour: number;
25
- }, now?: () => number): PoppyLimiter;
@@ -1,106 +0,0 @@
1
- /**
2
- * Proteção contra replay (`jti` de asserções, client assertions, provas DPoP e
3
- * asserções de navegador) e limites de taxa do Poppy.
4
- */
5
- import { createHash } from 'node:crypto';
6
- export const JTI_TABLE = 'auth_poppy_jtis';
7
- /** Chave curta e de tamanho fixo para um `jti` de um tipo/agente. */
8
- export function replayKey(kind, ...parts) {
9
- return `${kind}:${createHash('sha256').update(parts.join('\u0000')).digest('base64url')}`;
10
- }
11
- /** Em memória, por processo. Só serve com uma instância. */
12
- export function memoryReplayStore(options = {}) {
13
- const seen = new Map();
14
- const max = options.max ?? 100_000;
15
- const now = options.now ?? Date.now;
16
- let claims = 0;
17
- return {
18
- async claim(key, expiresAt) {
19
- const t = now();
20
- if (++claims % 1000 === 0 || seen.size >= max) {
21
- for (const [k, exp] of seen)
22
- if (exp <= t)
23
- seen.delete(k);
24
- }
25
- const existing = seen.get(key);
26
- if (existing !== undefined && existing > t)
27
- return false;
28
- if (seen.size >= max)
29
- seen.delete(seen.keys().next().value);
30
- seen.set(key, expiresAt.getTime());
31
- return true;
32
- },
33
- };
34
- }
35
- /**
36
- * Na tabela lib-owned `auth_poppy_jtis` (PK = chave): o INSERT de uma chave que
37
- * já existe falha — atômico entre instâncias. Linhas vencidas saem aos poucos.
38
- */
39
- export function databaseReplayStore(conn, options = {}) {
40
- const now = options.now ?? (() => new Date());
41
- let claims = 0;
42
- return {
43
- async claim(key, expiresAt) {
44
- const t = now();
45
- if (++claims % 200 === 1) {
46
- await conn().from(JTI_TABLE).where('expires_at', '<', t).delete();
47
- }
48
- try {
49
- await conn().table(JTI_TABLE).insert({ key, expires_at: expiresAt });
50
- return true;
51
- }
52
- catch (error) {
53
- // Chave já usada — a não ser que a linha vencida ainda não tenha saído.
54
- const row = await conn().from(JTI_TABLE).where('key', key).first();
55
- if (!row)
56
- throw error;
57
- const exp = row.expires_at instanceof Date
58
- ? row.expires_at
59
- : new Date(Number(row.expires_at) || row.expires_at);
60
- if (exp.getTime() > t.getTime())
61
- return false;
62
- const updated = await conn()
63
- .from(JTI_TABLE)
64
- .where('key', key)
65
- .where('expires_at', '<=', t)
66
- .update({ expires_at: expiresAt });
67
- return ((typeof updated === 'number' ? updated : Array.isArray(updated) ? updated.length : 0) > 0);
68
- }
69
- },
70
- };
71
- }
72
- /** Numa conexão Redis (`SET key 1 PX ttl NX`). */
73
- export function redisReplayStore(redis) {
74
- return {
75
- async claim(key, expiresAt) {
76
- const ttl = Math.max(1000, expiresAt.getTime() - Date.now());
77
- const result = await redis.set(`authkit:poppy:jti:${key}`, '1', 'PX', ttl, 'NX');
78
- return result === 'OK';
79
- },
80
- };
81
- }
82
- /** Janelas fixas em memória, por processo: `sessionsPerMinute` por agente, `mediatedPerHour` por (agente, usuário). */
83
- export function memoryLimiter(limits, now = Date.now) {
84
- const windows = new Map();
85
- return async ({ action, key }) => {
86
- const [limit, windowMs] = action === 'session'
87
- ? [limits.sessionsPerMinute, 60_000]
88
- : [limits.mediatedPerHour, 3_600_000];
89
- const t = now();
90
- const id = `${action}:${key}`;
91
- let w = windows.get(id);
92
- if (!w || t - w.start >= windowMs) {
93
- w = { start: t, count: 0 };
94
- windows.set(id, w);
95
- if (windows.size > 50_000) {
96
- for (const [k, v] of windows)
97
- if (t - v.start >= 3_600_000)
98
- windows.delete(k);
99
- }
100
- }
101
- if (w.count >= limit)
102
- return Math.ceil((w.start + windowMs - t) / 1000);
103
- w.count++;
104
- return null;
105
- };
106
- }
@@ -1,100 +0,0 @@
1
- /**
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
- *
6
- * Contrato do slot (o mesmo padrão do `Symbol.for('@adonis-agora/oauth:resources')`):
7
- *
8
- * ```ts
9
- * const authenticate = globalThis[Symbol.for('@adonis-agora/poppy:authenticate')]
10
- * const result = await authenticate({ method, url, headers }, { scopes?, signedIn?, resource? })
11
- * // { ok: true, principal } | { ok: false, status, error, scope?, wwwAuthenticate, headers }
12
- * ```
13
- */
14
- import type { HttpContext } from '@adonisjs/core/http';
15
- import type { PoppyPrincipalBase } from './config.js';
16
- import type { PoppyAuthError, PoppyService, PoppyVerifyOptions } from './service.js';
17
- /** Símbolo do slot global de autenticação Poppy. */
18
- export declare const POPPY_AUTHENTICATE_SLOT: unique symbol;
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
- */
25
- export interface PoppyPrincipal extends PoppyPrincipalBase {
26
- /** O que `personalAgents.poppy.toActor` devolveu, quando configurado. */
27
- actor?: unknown;
28
- }
29
- /**
30
- * @experimental Poppy (Personal Agent Protocol) Draft 0.3 — pode mudar de forma incompatível enquanto a spec for draft.
31
- */
32
- export type PoppyAuthResult = {
33
- ok: true;
34
- principal: PoppyPrincipal;
35
- } | {
36
- ok: false;
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`). */
41
- scope?: string;
42
- /** Valor pronto do header `WWW-Authenticate`; `null` em `invalid_session` e no 429. */
43
- wwwAuthenticate: string | null;
44
- /** Texto para logs/corpo — nunca contém o token. */
45
- description: string;
46
- /** Headers extras a devolver (`DPoP-Nonce`, `Retry-After`). */
47
- headers: Record<string, string>;
48
- };
49
- /** Request no formato do slot (sem `HttpContext`). */
50
- export interface PoppySlotRequest {
51
- method: string;
52
- /** URL pública completa da request (a query é ignorada). */
53
- url: string;
54
- headers: Record<string, string | string[] | undefined>;
55
- }
56
- export type PoppyAuthenticate = (request: PoppySlotRequest, options?: PoppyVerifyOptions) => Promise<PoppyAuthResult>;
57
- /** URL pública da request: atrás de proxy TLS, o esquema segue o do issuer. */
58
- export declare function publicRequestUrl(ctx: HttpContext, poppy: PoppyService): string;
59
- /**
60
- * Verifica o header `Poppy-Session` de uma request e, se houver, o access token. Aceita o
61
- * `HttpContext` ou `{ method, url, headers }`.
62
- *
63
- * @experimental Poppy (Personal Agent Protocol) Draft 0.3 — acompanha a spec em desenvolvimento e PODE MUDAR
64
- * de forma incompatível fora de majors enquanto ela for draft.
65
- */
66
- export declare function verifyPoppyRequest(input: HttpContext | PoppySlotRequest, options?: PoppyVerifyOptions): Promise<PoppyAuthResult>;
67
- /** Escreve a resposta de erro de um {@link PoppyAuthResult} recusado. */
68
- export declare function writePoppyAuthError(ctx: HttpContext, result: Extract<PoppyAuthResult, {
69
- ok: false;
70
- }>): void;
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
- */
77
- export declare function poppyOf(ctx: HttpContext): PoppyPrincipal | null;
78
- /**
79
- * Middleware das APIs/rotas que usam a Session Poppy.
80
- *
81
- * ```ts
82
- * router.post('/orders/:id/exchanges', [Orders, 'exchange'])
83
- * .use(poppyAuth({ scopes: ['orders:write'] }))
84
- * ```
85
- *
86
- * Isente essas rotas do CSRF (são chamadas por agentes, sem navegador).
87
- *
88
- * @experimental Poppy (Personal Agent Protocol) Draft 0.3 — acompanha a spec em desenvolvimento e PODE MUDAR
89
- * de forma incompatível fora de majors enquanto ela for draft.
90
- */
91
- export declare function poppyAuth(options?: PoppyVerifyOptions): (ctx: HttpContext, next: () => Promise<void>) => Promise<void>;
92
- /**
93
- * Publica o slot global `Symbol.for('@adonis-agora/poppy:authenticate')` (e
94
- * `Symbol.for('@adonis-agora/poppy:issuer')`). Chamado no boot do provider
95
- * quando `personalAgents.poppy` está no config.
96
- *
97
- * @experimental Poppy (Personal Agent Protocol) Draft 0.3 — acompanha a spec em desenvolvimento e PODE MUDAR
98
- * de forma incompatível fora de majors enquanto ela for draft.
99
- */
100
- export declare function installPoppySlots(issuer: string): void;
@@ -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
- }