@adonis-agora/authkit-server 0.75.0 → 0.77.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/README.md +148 -0
  2. package/build/host/views/account/apps.edge +30 -0
  3. package/build/host/views/agents/consent.edge +60 -0
  4. package/build/host/views/agents/done.edge +24 -0
  5. package/build/host/views/consent.edge +9 -1
  6. package/build/host/views/mfa-challenge.edge +42 -3
  7. package/build/host/views/partials/styles.edge +1 -1
  8. package/build/index.d.ts +18 -1
  9. package/build/index.js +9 -1
  10. package/build/providers/authkit_server_provider.js +75 -50
  11. package/build/src/accounts/account_store.d.ts +2 -0
  12. package/build/src/accounts/lucid_account_store.js +2 -0
  13. package/build/src/adapters/adapter_contract.d.ts +2 -0
  14. package/build/src/adapters/database_adapter.d.ts +1 -0
  15. package/build/src/adapters/database_adapter.js +26 -0
  16. package/build/src/adapters/redis_adapter.d.ts +1 -0
  17. package/build/src/adapters/redis_adapter.js +30 -0
  18. package/build/src/agents/agent_identity.d.ts +33 -0
  19. package/build/src/agents/agent_identity.js +156 -0
  20. package/build/src/agents/config.d.ts +99 -0
  21. package/build/src/agents/config.js +101 -0
  22. package/build/src/agents/delegation_service.d.ts +154 -0
  23. package/build/src/agents/delegation_service.js +394 -0
  24. package/build/src/agents/delegation_store.d.ts +93 -0
  25. package/build/src/agents/delegation_store.js +222 -0
  26. package/build/src/agents/middleware.d.ts +73 -0
  27. package/build/src/agents/middleware.js +113 -0
  28. package/build/src/agents/protocol.d.ts +62 -0
  29. package/build/src/agents/protocol.js +51 -0
  30. package/build/src/agents/runtime.d.ts +36 -0
  31. package/build/src/agents/runtime.js +65 -0
  32. package/build/src/agents/signer.d.ts +34 -0
  33. package/build/src/agents/signer.js +70 -0
  34. package/build/src/audit/audit_sink.d.ts +1 -1
  35. package/build/src/audit/audit_sink.js +5 -0
  36. package/build/src/controllers/authorization_server_metadata_controller.d.ts +10 -0
  37. package/build/src/controllers/authorization_server_metadata_controller.js +20 -0
  38. package/build/src/define_config.d.ts +50 -3
  39. package/build/src/define_config.js +42 -1
  40. package/build/src/host/account_api/account_api_controller.d.ts +12 -0
  41. package/build/src/host/account_api/account_api_controller.js +40 -0
  42. package/build/src/host/admin_api/dto.d.ts +1 -1
  43. package/build/src/host/admin_sessions_service.d.ts +2 -0
  44. package/build/src/host/admin_sessions_service.js +18 -0
  45. package/build/src/host/auth_host_config.d.ts +4 -0
  46. package/build/src/host/client_names.d.ts +11 -0
  47. package/build/src/host/client_names.js +12 -0
  48. package/build/src/host/controllers/account_apps_controller.d.ts +2 -0
  49. package/build/src/host/controllers/account_apps_controller.js +39 -3
  50. package/build/src/host/controllers/account_orgs_controller.js +2 -1
  51. package/build/src/host/controllers/account_security_controller.js +8 -4
  52. package/build/src/host/controllers/account_session_controller.js +2 -1
  53. package/build/src/host/controllers/agent_consent_controller.d.ts +17 -0
  54. package/build/src/host/controllers/agent_consent_controller.js +138 -0
  55. package/build/src/host/controllers/agent_oauth_controller.d.ts +23 -0
  56. package/build/src/host/controllers/agent_oauth_controller.js +110 -0
  57. package/build/src/host/controllers/interaction_controller.d.ts +13 -1
  58. package/build/src/host/controllers/interaction_controller.js +380 -24
  59. package/build/src/host/csrf.d.ts +8 -18
  60. package/build/src/host/csrf.js +28 -2
  61. package/build/src/host/custom_login.d.ts +36 -0
  62. package/build/src/host/custom_login.js +76 -0
  63. package/build/src/host/custom_login_completion.d.ts +4 -0
  64. package/build/src/host/custom_login_completion.js +33 -0
  65. package/build/src/host/custom_mfa.d.ts +77 -0
  66. package/build/src/host/custom_mfa.js +268 -0
  67. package/build/src/host/i18n.d.ts +58 -0
  68. package/build/src/host/i18n.js +64 -6
  69. package/build/src/host/idp_session_bridge.d.ts +5 -3
  70. package/build/src/host/idp_session_bridge.js +5 -3
  71. package/build/src/host/oidc_rp_guard.d.ts +5 -1
  72. package/build/src/host/oidc_rp_guard.js +15 -2
  73. package/build/src/host/persistent_rp_session.d.ts +8 -0
  74. package/build/src/host/persistent_rp_session.js +74 -0
  75. package/build/src/host/redirect_exact.d.ts +12 -0
  76. package/build/src/host/redirect_exact.js +17 -0
  77. package/build/src/host/register_auth_host.js +60 -7
  78. package/build/src/host/request_url.d.ts +10 -0
  79. package/build/src/host/request_url.js +17 -0
  80. package/build/src/host/sudo/methods/magic_link.js +2 -1
  81. package/build/src/host/sudo/runtime.js +3 -2
  82. package/build/src/host/sudo_mode.js +4 -4
  83. package/build/src/host/whatsapp_code_sender.d.ts +20 -0
  84. package/build/src/host/whatsapp_code_sender.js +9 -0
  85. package/build/src/host/whatsapp_senders/http.d.ts +5 -0
  86. package/build/src/host/whatsapp_senders/http.js +33 -0
  87. package/build/src/host/whatsapp_senders/meta_whatsapp_code_sender.d.ts +20 -0
  88. package/build/src/host/whatsapp_senders/meta_whatsapp_code_sender.js +51 -0
  89. package/build/src/host/whatsapp_senders/whatsmiau_code_sender.d.ts +14 -0
  90. package/build/src/host/whatsapp_senders/whatsmiau_code_sender.js +36 -0
  91. package/build/src/mcp/mcp_oauth.d.ts +85 -0
  92. package/build/src/mcp/mcp_oauth.js +154 -0
  93. package/build/src/provider/build_provider.js +15 -1
  94. package/build/src/provider/oidc_service.d.ts +8 -0
  95. package/build/src/provider/oidc_service.js +16 -2
  96. package/build/src/schema/ensure.js +92 -0
  97. package/build/stubs/ui/react/pages/consent.tsx +17 -1
  98. package/build/stubs/ui/react/pages/mfa-challenge.tsx +207 -37
  99. package/package.json +2 -1
  100. package/stubs/ui/react/pages/consent.tsx +17 -1
  101. package/stubs/ui/react/pages/mfa-challenge.tsx +207 -37
@@ -0,0 +1,20 @@
1
+ import type { HttpContext } from '@adonisjs/core/http';
2
+ /** Provider-neutral delivery input. Only the host generates and verifies the OTP. */
3
+ export interface WhatsappCodeInput {
4
+ phone: string;
5
+ code: string;
6
+ locale: string;
7
+ expiresInSeconds: number;
8
+ /** Optional host-localized copy for transports that send free-form messages. */
9
+ text?: string;
10
+ }
11
+ /** Implement this interface to support any WhatsApp provider. Reject on failed delivery. */
12
+ export interface WhatsappCodeSender {
13
+ sendCode(input: WhatsappCodeInput): Promise<void>;
14
+ }
15
+ /** A configured instance, or an injectable class resolved by the request container. */
16
+ export type WhatsappCodeSenderConstructor = new (...args: never[]) => WhatsappCodeSender;
17
+ export type WhatsappCodeSenderBinding = WhatsappCodeSender | WhatsappCodeSenderConstructor;
18
+ export declare function resolveWhatsappCodeSender(resolver: Pick<HttpContext['containerResolver'], 'make'>, binding: WhatsappCodeSenderBinding): Promise<WhatsappCodeSender>;
19
+ export { MetaWhatsappCodeSender, type MetaWhatsappCodeSenderOptions, } from './whatsapp_senders/meta_whatsapp_code_sender.js';
20
+ export { WhatsmiauCodeSender, type WhatsmiauCodeSenderOptions, } from './whatsapp_senders/whatsmiau_code_sender.js';
@@ -0,0 +1,9 @@
1
+ export async function resolveWhatsappCodeSender(resolver, binding) {
2
+ const sender = typeof binding === 'function' ? await resolver.make(binding) : binding;
3
+ if (!sender || typeof sender.sendCode !== 'function') {
4
+ throw new Error('Invalid WhatsApp code sender: expected sendCode(input)');
5
+ }
6
+ return sender;
7
+ }
8
+ export { MetaWhatsappCodeSender, } from './whatsapp_senders/meta_whatsapp_code_sender.js';
9
+ export { WhatsmiauCodeSender, } from './whatsapp_senders/whatsmiau_code_sender.js';
@@ -0,0 +1,5 @@
1
+ /** Never include configuration values in validation errors. */
2
+ export declare function requireString(value: string, field: string): void;
3
+ export declare function recipientPhone(phone: string): string;
4
+ /** Response bodies and fetch causes can contain credentials and OTPs: discard both. */
5
+ export declare function postJson(provider: string, url: URL, headers: Record<string, string>, body: unknown): Promise<void>;
@@ -0,0 +1,33 @@
1
+ /** Never include configuration values in validation errors. */
2
+ export function requireString(value, field) {
3
+ if (typeof value !== 'string' || !value.trim() || /[\r\n]/.test(value)) {
4
+ throw new Error(`Invalid WhatsApp sender ${field}`);
5
+ }
6
+ }
7
+ export function recipientPhone(phone) {
8
+ if (typeof phone !== 'string' || !/^\+?[1-9]\d{6,14}$/.test(phone)) {
9
+ throw new Error('Invalid WhatsApp recipient phone');
10
+ }
11
+ return phone.replace(/^\+/, '');
12
+ }
13
+ /** Response bodies and fetch causes can contain credentials and OTPs: discard both. */
14
+ export async function postJson(provider, url, headers, body) {
15
+ let response;
16
+ try {
17
+ response = await fetch(url, {
18
+ method: 'POST',
19
+ headers: { 'Content-Type': 'application/json', ...headers },
20
+ body: JSON.stringify(body),
21
+ signal: AbortSignal.timeout(15_000),
22
+ redirect: 'error',
23
+ });
24
+ }
25
+ catch {
26
+ throw new Error(`WhatsApp code delivery failed (${provider})`);
27
+ }
28
+ // Do not parse or log the body, even when a provider echoes the submitted code.
29
+ void response.body?.cancel().catch(() => { });
30
+ if (!response.ok) {
31
+ throw new Error(`WhatsApp code delivery failed (${provider}, HTTP ${response.status})`);
32
+ }
33
+ }
@@ -0,0 +1,20 @@
1
+ import type { WhatsappCodeInput, WhatsappCodeSender } from '../whatsapp_code_sender.js';
2
+ export interface MetaWhatsappCodeSenderOptions {
3
+ accessToken: string;
4
+ phoneNumberId: string;
5
+ /** Explicit supported Graph API version, for example v23.0. */
6
+ apiVersion: string;
7
+ /** An approved authentication template with an OTP URL button. */
8
+ templateName: string;
9
+ /** Overrides the host locale when the template uses a different language. */
10
+ languageCode?: string;
11
+ }
12
+ /** Meta Cloud API authentication template transport (copy-code / OTP URL button). */
13
+ export declare class MetaWhatsappCodeSender implements WhatsappCodeSender {
14
+ private readonly accessToken;
15
+ private readonly endpoint;
16
+ private readonly templateName;
17
+ private readonly languageCode?;
18
+ constructor(options: MetaWhatsappCodeSenderOptions);
19
+ sendCode(input: WhatsappCodeInput): Promise<void>;
20
+ }
@@ -0,0 +1,51 @@
1
+ import { postJson, recipientPhone, requireString } from './http.js';
2
+ const LANGUAGE_CODES = { 'pt-BR': 'pt_BR', en: 'en_US', es: 'es' };
3
+ /** Meta Cloud API authentication template transport (copy-code / OTP URL button). */
4
+ export class MetaWhatsappCodeSender {
5
+ accessToken;
6
+ endpoint;
7
+ templateName;
8
+ languageCode;
9
+ constructor(options) {
10
+ requireString(options.accessToken, 'accessToken');
11
+ if (typeof options.apiVersion !== 'string' || !/^v\d+\.\d+$/.test(options.apiVersion)) {
12
+ throw new Error('Invalid WhatsApp sender apiVersion');
13
+ }
14
+ if (typeof options.phoneNumberId !== 'string' || !/^\d+$/.test(options.phoneNumberId)) {
15
+ throw new Error('Invalid WhatsApp sender phoneNumberId');
16
+ }
17
+ if (typeof options.templateName !== 'string' || !/^[a-z0-9_]+$/.test(options.templateName)) {
18
+ throw new Error('Invalid WhatsApp sender templateName');
19
+ }
20
+ if (options.languageCode !== undefined) {
21
+ requireString(options.languageCode, 'languageCode');
22
+ }
23
+ this.endpoint = new URL(`https://graph.facebook.com/${options.apiVersion}/${options.phoneNumberId}/messages`);
24
+ this.accessToken = options.accessToken;
25
+ this.templateName = options.templateName;
26
+ this.languageCode = options.languageCode;
27
+ }
28
+ async sendCode(input) {
29
+ await postJson('Meta', this.endpoint, { Authorization: `Bearer ${this.accessToken}` }, {
30
+ messaging_product: 'whatsapp',
31
+ recipient_type: 'individual',
32
+ to: recipientPhone(input.phone),
33
+ type: 'template',
34
+ template: {
35
+ name: this.templateName,
36
+ language: {
37
+ code: this.languageCode ?? LANGUAGE_CODES[input.locale] ?? input.locale.replace(/-/g, '_'),
38
+ },
39
+ components: [
40
+ { type: 'body', parameters: [{ type: 'text', text: input.code }] },
41
+ {
42
+ type: 'button',
43
+ sub_type: 'url',
44
+ index: '0',
45
+ parameters: [{ type: 'text', text: input.code }],
46
+ },
47
+ ],
48
+ },
49
+ });
50
+ }
51
+ }
@@ -0,0 +1,14 @@
1
+ import type { WhatsappCodeInput, WhatsappCodeSender } from '../whatsapp_code_sender.js';
2
+ export interface WhatsmiauCodeSenderOptions {
3
+ apiKey: string;
4
+ instanceName: string;
5
+ /** Includes the API version path. Defaults to Whatsmiau Cloud's /v2 endpoint. */
6
+ baseUrl?: string;
7
+ }
8
+ /** Whatsmiau Cloud v2 / Evolution-compatible free-form text transport. */
9
+ export declare class WhatsmiauCodeSender implements WhatsappCodeSender {
10
+ private readonly apiKey;
11
+ private readonly endpoint;
12
+ constructor(options: WhatsmiauCodeSenderOptions);
13
+ sendCode(input: WhatsappCodeInput): Promise<void>;
14
+ }
@@ -0,0 +1,36 @@
1
+ import { postJson, recipientPhone, requireString } from './http.js';
2
+ /** Whatsmiau Cloud v2 / Evolution-compatible free-form text transport. */
3
+ export class WhatsmiauCodeSender {
4
+ apiKey;
5
+ endpoint;
6
+ constructor(options) {
7
+ requireString(options.apiKey, 'apiKey');
8
+ if (typeof options.instanceName !== 'string' ||
9
+ !/^[A-Za-z0-9_-]+$/.test(options.instanceName)) {
10
+ throw new Error('Invalid WhatsApp sender instanceName');
11
+ }
12
+ let baseUrl;
13
+ try {
14
+ baseUrl = new URL(options.baseUrl ?? 'https://api.whatsmiau.dev/v2');
15
+ }
16
+ catch {
17
+ throw new Error('Invalid WhatsApp sender baseUrl');
18
+ }
19
+ if (!['http:', 'https:'].includes(baseUrl.protocol) ||
20
+ baseUrl.username ||
21
+ baseUrl.password ||
22
+ baseUrl.search ||
23
+ baseUrl.hash) {
24
+ throw new Error('Invalid WhatsApp sender baseUrl');
25
+ }
26
+ baseUrl.pathname = `${baseUrl.pathname.replace(/\/+$/, '')}/message/sendText/${options.instanceName}`;
27
+ this.endpoint = baseUrl;
28
+ this.apiKey = options.apiKey;
29
+ }
30
+ async sendCode(input) {
31
+ await postJson('Whatsmiau', this.endpoint, { apikey: this.apiKey }, {
32
+ number: recipientPhone(input.phone),
33
+ text: input.text ?? input.code,
34
+ });
35
+ }
36
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Login OAuth de clientes MCP (Claude Code, Claude, ChatGPT, VS Code, Cursor) — a autorização da
3
+ * spec do MCP: OAuth 2.1 com PKCE, registro dinâmico (RFC 7591), metadata do servidor de
4
+ * autorização (RFC 8414) e o token amarrado ao servidor MCP pelo `resource` (RFC 8707).
5
+ *
6
+ * `mcp: true` na config liga tudo de uma vez:
7
+ * - o registro dinâmico ABERTO, restrito aos redirects dos clientes MCP conhecidos
8
+ * ({@link MCP_CLIENT_REDIRECTS}) — loopback para os de linha de comando;
9
+ * - o refresh token desses clientes: o registro pede `offline_access` e o authorize ganha
10
+ * `prompt=consent` (OIDC Core §11), então a pessoa consente uma vez e o cliente não precisa
11
+ * logar de novo a cada hora;
12
+ * - os `resource` dos servidores MCP: os de `mcp.resources` e os REGISTRADOS em runtime
13
+ * ({@link registerOAuthResource}) — é por aí que o MCP do `@adonis-agora/agent` se anuncia
14
+ * sem o app listar a URL dele aqui.
15
+ *
16
+ * Os clientes registrados pelo `/reg` se distinguem pelo `client_id_issued_at`, que só o registro
17
+ * dinâmico grava: clients estáticos ou criados pelo console/CLI não mudam de comportamento.
18
+ */
19
+ import type { RedirectUriPolicy, ResolvedRedirectUriPolicy } from '../provider/registration_policy.js';
20
+ /** Callbacks dos clientes MCP conhecidos. Loopback (Claude Code, CLIs) já entra pela política. */
21
+ export declare const MCP_CLIENT_REDIRECTS: Required<RedirectUriPolicy>;
22
+ /** Escopos de um token de servidor MCP: identidade + refresh. */
23
+ export declare const MCP_RESOURCE_SCOPES: string[];
24
+ export interface McpOAuthConfigInput {
25
+ /**
26
+ * Redirects aceitos ALÉM dos clientes MCP conhecidos — ex.: o callback de um cliente próprio.
27
+ * Somados a {@link MCP_CLIENT_REDIRECTS}.
28
+ */
29
+ redirectUris?: RedirectUriPolicy;
30
+ /**
31
+ * URLs dos servidores MCP para os quais este IdP emite tokens, além dos que se registram em
32
+ * runtime ({@link registerOAuthResource}). Ex.: `['https://app.example.com/mcp']`.
33
+ */
34
+ resources?: string[];
35
+ }
36
+ export interface ResolvedMcpOAuthConfig {
37
+ enabled: boolean;
38
+ redirectUriPolicy: ResolvedRedirectUriPolicy;
39
+ resources: string[];
40
+ }
41
+ export declare function resolveMcpOAuth(input?: boolean | McpOAuthConfigInput): ResolvedMcpOAuthConfig;
42
+ /**
43
+ * Um servidor protegido (RFC 9728) que aceita tokens deste IdP. `url` é o `resource` exato; sem
44
+ * ela, `path` casa com qualquer `resource` na origem do issuer (quem registra no boot nem sempre
45
+ * sabe a URL pública).
46
+ */
47
+ export interface OAuthResourceRegistration {
48
+ url?: string;
49
+ path?: string;
50
+ /** Escopos do token para este resource. Default: {@link MCP_RESOURCE_SCOPES}. */
51
+ scopes?: string[];
52
+ }
53
+ export declare function registerOAuthResource(resource: OAuthResourceRegistration): void;
54
+ export declare function registeredOAuthResources(): readonly OAuthResourceRegistration[];
55
+ /**
56
+ * O resource MCP que `indicator` nomeia, ou `null`. Casa com `mcp.resources`, com uma URL
57
+ * registrada, ou com um `path` registrado na origem do issuer. Barra final tolerada.
58
+ */
59
+ export declare function findMcpResource(indicator: string, issuer: string, config: ResolvedMcpOAuthConfig): {
60
+ audience: string;
61
+ scopes: string[];
62
+ } | null;
63
+ /**
64
+ * Registro de um cliente MCP: quem pede `refresh_token` ganha `openid offline_access` no escopo
65
+ * registrado, senão o provider recusaria pedi-los no authorize.
66
+ */
67
+ export declare function mcpClientRegistration(metadata: Record<string, unknown>): Record<string, unknown>;
68
+ /**
69
+ * `scope`/`prompt` do authorize de um cliente MCP para que saia um refresh token: `offline_access`
70
+ * no escopo e `consent` no prompt (sem ele o provider descarta o `offline_access`). `null` quando
71
+ * já estão lá. `prompt=none` pede "sem interação", o contrário de consentir: sai.
72
+ */
73
+ export declare function withOfflineAccess(params: Record<string, unknown>): {
74
+ scope: string;
75
+ prompt: string;
76
+ } | null;
77
+ /**
78
+ * Middleware do provider (Koa) que aplica {@link withOfflineAccess} ao `GET /auth` de um client
79
+ * registrado dinamicamente que tem o grant `refresh_token`.
80
+ */
81
+ export declare function mcpAuthorizeMiddleware(provider: {
82
+ Client: {
83
+ find(id: string): Promise<any>;
84
+ };
85
+ }): (ctx: any, next: () => Promise<void>) => Promise<void>;
@@ -0,0 +1,154 @@
1
+ import { resolveRedirectUriPolicy } from '../provider/registration_policy.js';
2
+ /** Callbacks dos clientes MCP conhecidos. Loopback (Claude Code, CLIs) já entra pela política. */
3
+ export const MCP_CLIENT_REDIRECTS = {
4
+ loopback: true,
5
+ exact: [
6
+ 'https://claude.ai/api/mcp/auth_callback',
7
+ 'https://claude.com/api/mcp/auth_callback',
8
+ 'https://chatgpt.com/connector_platform_oauth_redirect',
9
+ 'https://vscode.dev/redirect',
10
+ 'https://insiders.vscode.dev/redirect',
11
+ ],
12
+ appSchemes: ['cursor', 'vscode', 'vscode-insiders'],
13
+ anyHttps: false,
14
+ };
15
+ /** Escopos de um token de servidor MCP: identidade + refresh. */
16
+ export const MCP_RESOURCE_SCOPES = ['openid', 'profile', 'email', 'offline_access'];
17
+ export function resolveMcpOAuth(input) {
18
+ const options = typeof input === 'object' ? input : {};
19
+ const extra = options.redirectUris ?? {};
20
+ return {
21
+ enabled: input === true || typeof input === 'object',
22
+ redirectUriPolicy: resolveRedirectUriPolicy({
23
+ loopback: extra.loopback ?? MCP_CLIENT_REDIRECTS.loopback,
24
+ exact: [...new Set([...MCP_CLIENT_REDIRECTS.exact, ...(extra.exact ?? [])])],
25
+ appSchemes: [...new Set([...MCP_CLIENT_REDIRECTS.appSchemes, ...(extra.appSchemes ?? [])])],
26
+ anyHttps: extra.anyHttps ?? MCP_CLIENT_REDIRECTS.anyHttps,
27
+ }),
28
+ resources: [...(options.resources ?? [])],
29
+ };
30
+ }
31
+ /**
32
+ * Registro em runtime — um slot global, para que outra lib (o MCP do `@adonis-agora/agent`) se
33
+ * registre sem importar esta: o contrato é o símbolo, não o módulo.
34
+ */
35
+ const REGISTRY = Symbol.for('@adonis-agora/oauth:resources');
36
+ function registry() {
37
+ const slot = globalThis;
38
+ if (!Array.isArray(slot[REGISTRY]))
39
+ slot[REGISTRY] = [];
40
+ return slot[REGISTRY];
41
+ }
42
+ export function registerOAuthResource(resource) {
43
+ if (!resource.url && !resource.path) {
44
+ throw new Error('authkit: registerOAuthResource precisa de `url` ou `path`.');
45
+ }
46
+ registry().push({ ...resource });
47
+ }
48
+ export function registeredOAuthResources() {
49
+ return registry();
50
+ }
51
+ const trim = (value) => value.replace(/\/+$/, '');
52
+ function normalizePath(path) {
53
+ return `/${path.replace(/^\/+|\/+$/g, '')}`;
54
+ }
55
+ /**
56
+ * O resource MCP que `indicator` nomeia, ou `null`. Casa com `mcp.resources`, com uma URL
57
+ * registrada, ou com um `path` registrado na origem do issuer. Barra final tolerada.
58
+ */
59
+ export function findMcpResource(indicator, issuer, config) {
60
+ if (!config.enabled)
61
+ return null;
62
+ let url;
63
+ try {
64
+ url = new URL(indicator);
65
+ }
66
+ catch {
67
+ return null;
68
+ }
69
+ const wanted = trim(url.href);
70
+ for (const declared of config.resources) {
71
+ if (trim(declared) === wanted)
72
+ return { audience: trim(declared), scopes: MCP_RESOURCE_SCOPES };
73
+ }
74
+ const issuerOrigin = new URL(issuer).origin;
75
+ for (const resource of registeredOAuthResources()) {
76
+ const scopes = resource.scopes ?? MCP_RESOURCE_SCOPES;
77
+ if (resource.url && trim(resource.url) === wanted)
78
+ return { audience: trim(resource.url), scopes };
79
+ if (resource.path &&
80
+ url.origin === issuerOrigin &&
81
+ trim(url.pathname) === normalizePath(resource.path) &&
82
+ !url.search) {
83
+ return { audience: wanted, scopes };
84
+ }
85
+ }
86
+ return null;
87
+ }
88
+ /**
89
+ * Registro de um cliente MCP: quem pede `refresh_token` ganha `openid offline_access` no escopo
90
+ * registrado, senão o provider recusaria pedi-los no authorize.
91
+ */
92
+ export function mcpClientRegistration(metadata) {
93
+ const grants = Array.isArray(metadata.grant_types)
94
+ ? metadata.grant_types
95
+ : ['authorization_code'];
96
+ if (!grants.includes('refresh_token') || typeof metadata.scope !== 'string')
97
+ return metadata;
98
+ const scopes = metadata.scope.split(' ').filter(Boolean);
99
+ for (const needed of ['openid', 'offline_access']) {
100
+ if (!scopes.includes(needed))
101
+ scopes.push(needed);
102
+ }
103
+ return { ...metadata, scope: scopes.join(' ') };
104
+ }
105
+ /**
106
+ * `scope`/`prompt` do authorize de um cliente MCP para que saia um refresh token: `offline_access`
107
+ * no escopo e `consent` no prompt (sem ele o provider descarta o `offline_access`). `null` quando
108
+ * já estão lá. `prompt=none` pede "sem interação", o contrário de consentir: sai.
109
+ */
110
+ export function withOfflineAccess(params) {
111
+ const scopes = String(params.scope ?? '')
112
+ .split(' ')
113
+ .filter(Boolean);
114
+ const prompts = String(params.prompt ?? '')
115
+ .split(' ')
116
+ .filter(Boolean);
117
+ const needsScope = !scopes.includes('offline_access');
118
+ const needsPrompt = !prompts.includes('consent');
119
+ if (!needsScope && !needsPrompt)
120
+ return null;
121
+ if (!scopes.includes('openid'))
122
+ scopes.unshift('openid');
123
+ if (needsScope)
124
+ scopes.push('offline_access');
125
+ const prompt = needsPrompt ? [...prompts.filter((p) => p !== 'none'), 'consent'] : prompts;
126
+ return { scope: scopes.join(' '), prompt: prompt.join(' ') };
127
+ }
128
+ /**
129
+ * Middleware do provider (Koa) que aplica {@link withOfflineAccess} ao `GET /auth` de um client
130
+ * registrado dinamicamente que tem o grant `refresh_token`.
131
+ */
132
+ export function mcpAuthorizeMiddleware(provider) {
133
+ return async (ctx, next) => {
134
+ if (ctx.method !== 'GET' || ctx.path !== '/auth')
135
+ return next();
136
+ const query = ctx.query;
137
+ const clientId = typeof query.client_id === 'string' ? query.client_id : '';
138
+ if (!clientId || (query.response_type !== undefined && query.response_type !== 'code')) {
139
+ return next();
140
+ }
141
+ const client = await provider.Client.find(clientId).catch(() => undefined);
142
+ const metadata = client?.metadata?.() ?? {};
143
+ const grants = metadata.grant_types;
144
+ if (metadata.client_id_issued_at === undefined ||
145
+ !Array.isArray(grants) ||
146
+ !grants.includes('refresh_token')) {
147
+ return next();
148
+ }
149
+ const widened = withOfflineAccess(query);
150
+ if (widened)
151
+ ctx.query = { ...query, ...widened };
152
+ return next();
153
+ };
154
+ }
@@ -2,6 +2,7 @@ import * as oidc from 'oidc-provider';
2
2
  import { pickModelAdapterClass } from '../adapters/factory.js';
3
3
  import { normalizeActiveOrg, readActiveOrgFromKoaCtx } from '../host/active_org_cookie.js';
4
4
  import { assertClientMetadata } from '../host/client_metadata.js';
5
+ import { findMcpResource, mcpAuthorizeMiddleware } from '../mcp/mcp_oauth.js';
5
6
  import { createDeviceSources } from './device_sources.js';
6
7
  import { createLogoutSources } from './logout_sources.js';
7
8
  import { registrationPolicyMiddleware } from './registration_policy.js';
@@ -77,7 +78,8 @@ export function buildProvider(config, options, sessionTtlHolder, tokenTtlHolder)
77
78
  const key = declaredResources.find((k) => k.replace(/\/+$/, '') === trimmed);
78
79
  return key ? { key, rc: at.resources[key] } : null;
79
80
  };
80
- const resourceIndicatorFeatures = at.anyJwt || declaredResources.length > 0
81
+ const mcp = config.mcp;
82
+ const resourceIndicatorFeatures = at.anyJwt || declaredResources.length > 0 || mcp.enabled
81
83
  ? {
82
84
  resourceIndicators: {
83
85
  enabled: true,
@@ -94,6 +96,15 @@ export function buildProvider(config, options, sessionTtlHolder, tokenTtlHolder)
94
96
  getResourceServerInfo: (_ctx, resourceIndicator, _client) => {
95
97
  const found = findResource(resourceIndicator);
96
98
  const isDefault = at.anyJwt && resourceIndicator === at.audience;
99
+ // Servidores MCP (`mcp`): os declarados e os registrados em runtime.
100
+ const mcpResource = found || isDefault ? null : findMcpResource(resourceIndicator, config.issuer, mcp);
101
+ if (mcpResource) {
102
+ return {
103
+ scope: mcpResource.scopes.join(' '),
104
+ audience: mcpResource.audience,
105
+ accessTokenFormat: 'opaque',
106
+ };
107
+ }
97
108
  if (!found && !isDefault) {
98
109
  throw new oidc.errors.InvalidTarget(`resource indicator not allowed: ${resourceIndicator}`);
99
110
  }
@@ -341,6 +352,9 @@ export function buildProvider(config, options, sessionTtlHolder, tokenTtlHolder)
341
352
  validate: dynReg.validateRegistration,
342
353
  }));
343
354
  }
355
+ // Refresh token dos clientes MCP registrados dinamicamente (ver mcp/mcp_oauth.ts).
356
+ if (mcp.enabled)
357
+ provider.use(mcpAuthorizeMiddleware(provider));
344
358
  provider.proxy = true;
345
359
  return provider;
346
360
  }
@@ -17,6 +17,14 @@ export declare class OidcService {
17
17
  get publicJwks(): {
18
18
  keys: Record<string, any>[];
19
19
  };
20
+ /**
21
+ * @internal JWKS PRIVADO em uso (mesmas chaves do `publicJwks`). Só para quem
22
+ * assina in-process com o keystore do IdP — tokens de delegação e recibos de
23
+ * personal agents. Troca junto com o provider numa rotação.
24
+ */
25
+ get signingJwks(): {
26
+ keys: Record<string, any>[];
27
+ };
20
28
  /** Pathname do issuer sem barra final (ex.: `/oidc`). Vazio quando montado na raiz. */
21
29
  readonly mountPath: string;
22
30
  readonly recorder: MetricsRecorder;
@@ -14,6 +14,7 @@ export class OidcService {
14
14
  #callback;
15
15
  #interactions;
16
16
  #publicJwks;
17
+ #signingJwks;
17
18
  #appKey;
18
19
  get provider() {
19
20
  return this.#provider;
@@ -32,6 +33,14 @@ export class OidcService {
32
33
  get publicJwks() {
33
34
  return this.#publicJwks;
34
35
  }
36
+ /**
37
+ * @internal JWKS PRIVADO em uso (mesmas chaves do `publicJwks`). Só para quem
38
+ * assina in-process com o keystore do IdP — tokens de delegação e recibos de
39
+ * personal agents. Troca junto com o provider numa rotação.
40
+ */
41
+ get signingJwks() {
42
+ return this.#signingJwks;
43
+ }
35
44
  /** Pathname do issuer sem barra final (ex.: `/oidc`). Vazio quando montado na raiz. */
36
45
  mountPath;
37
46
  recorder;
@@ -128,8 +137,12 @@ export class OidcService {
128
137
  claims: async (_use, _scope) => {
129
138
  const base = {
130
139
  sub: user.id,
131
- email: user.email,
132
- email_verified: true,
140
+ ...(user.email
141
+ ? {
142
+ email: user.email,
143
+ email_verified: (await config.accountStore.isEmailVerified?.(user.id)) ?? false,
144
+ }
145
+ : {}),
133
146
  name: user.name,
134
147
  picture: user.avatarUrl,
135
148
  };
@@ -216,6 +229,7 @@ export class OidcService {
216
229
  this.#callback = callback;
217
230
  this.#interactions = interactions;
218
231
  this.#publicJwks = toPublicJwks(jwks);
232
+ this.#signingJwks = jwks;
219
233
  }
220
234
  /**
221
235
  * Recarrega as chaves de assinatura AO VIVO: relê o keystore do cofre e reconstrói
@@ -203,6 +203,98 @@ const TABLES = [
203
203
  updated_at: (t) => t.timestamp('updated_at', { useTz: true }).nullable(),
204
204
  },
205
205
  },
206
+ /*
207
+ * As três tabelas de personal agents usam `dateTime(…, precision 3)` e não
208
+ * `timestamp`: no MySQL o TIMESTAMP sem fração arredonda o `last_polled_at`
209
+ * (um agente que respeita o `interval` levaria `slow_down`) e, com
210
+ * `explicit_defaults_for_timestamp=OFF`, o primeiro TIMESTAMP NOT NULL da
211
+ * tabela ganha `ON UPDATE CURRENT_TIMESTAMP` — marcar um refresh como usado
212
+ * reescreveria a validade dele. DATETIME não tem nenhum dos dois; no Postgres
213
+ * vira o mesmo `timestamptz`.
214
+ */
215
+ {
216
+ name: 'auth_agent_device_codes',
217
+ /**
218
+ * Pedidos de delegação de personal agents (device flow, RFC 8628). O
219
+ * `device_code` só existe como hash; o `user_code` é o que o usuário vê.
220
+ * Linhas expiradas são apagadas no próximo pedido.
221
+ */
222
+ create: (t) => {
223
+ t.string('id').primary();
224
+ t.string('device_code_hash', 64).notNullable().unique();
225
+ t.string('user_code', 16).notNullable().unique();
226
+ t.string('client_id', 2048).notNullable();
227
+ t.string('agent_sub').notNullable();
228
+ t.text('requested_scope').notNullable();
229
+ t.string('status', 16).notNullable();
230
+ t.string('account_id').nullable();
231
+ t.string('grant_id').nullable();
232
+ t.integer('interval_seconds').notNullable();
233
+ t.dateTime('last_polled_at', { useTz: true, precision: 3 }).nullable();
234
+ t.dateTime('expires_at', { useTz: true, precision: 3 }).notNullable().index();
235
+ t.dateTime('created_at', { useTz: true, precision: 3 }).notNullable();
236
+ },
237
+ columns: {
238
+ device_code_hash: (t) => t.string('device_code_hash', 64),
239
+ user_code: (t) => t.string('user_code', 16),
240
+ client_id: (t) => t.string('client_id', 2048),
241
+ agent_sub: (t) => t.string('agent_sub'),
242
+ requested_scope: (t) => t.text('requested_scope'),
243
+ status: (t) => t.string('status', 16),
244
+ account_id: (t) => t.string('account_id').nullable(),
245
+ grant_id: (t) => t.string('grant_id').nullable(),
246
+ interval_seconds: (t) => t.integer('interval_seconds'),
247
+ last_polled_at: (t) => t.dateTime('last_polled_at', { useTz: true, precision: 3 }).nullable(),
248
+ expires_at: (t) => t.dateTime('expires_at', { useTz: true, precision: 3 }).nullable(),
249
+ created_at: (t) => t.dateTime('created_at', { useTz: true, precision: 3 }).nullable(),
250
+ },
251
+ },
252
+ {
253
+ name: 'auth_agent_grants',
254
+ /**
255
+ * O que um usuário autorizou um personal agent a fazer na conta dele: um
256
+ * grant por (conta, agente, usuário do agente); aprovar mais scopes soma.
257
+ * `revoked_at` preenchido = revogado (os tokens param na próxima request).
258
+ */
259
+ create: (t) => {
260
+ t.string('id').primary();
261
+ t.string('account_id').notNullable().index();
262
+ t.string('client_id', 2048).notNullable();
263
+ t.string('agent_sub').notNullable();
264
+ t.text('scope').notNullable();
265
+ t.dateTime('expires_at', { useTz: true, precision: 3 }).notNullable();
266
+ t.dateTime('revoked_at', { useTz: true, precision: 3 }).nullable();
267
+ t.dateTime('created_at', { useTz: true, precision: 3 }).notNullable();
268
+ t.dateTime('updated_at', { useTz: true, precision: 3 }).notNullable();
269
+ },
270
+ columns: {
271
+ account_id: (t) => t.string('account_id'),
272
+ client_id: (t) => t.string('client_id', 2048),
273
+ agent_sub: (t) => t.string('agent_sub'),
274
+ scope: (t) => t.text('scope'),
275
+ expires_at: (t) => t.dateTime('expires_at', { useTz: true, precision: 3 }).nullable(),
276
+ revoked_at: (t) => t.dateTime('revoked_at', { useTz: true, precision: 3 }).nullable(),
277
+ created_at: (t) => t.dateTime('created_at', { useTz: true, precision: 3 }).nullable(),
278
+ updated_at: (t) => t.dateTime('updated_at', { useTz: true, precision: 3 }).nullable(),
279
+ },
280
+ },
281
+ {
282
+ name: 'auth_agent_refresh_tokens',
283
+ /** Refresh tokens dos grants de personal agents — só o hash, uso único. */
284
+ create: (t) => {
285
+ t.string('token_hash', 64).primary();
286
+ t.string('grant_id').notNullable().index();
287
+ t.dateTime('expires_at', { useTz: true, precision: 3 }).notNullable();
288
+ t.dateTime('used_at', { useTz: true, precision: 3 }).nullable();
289
+ t.dateTime('created_at', { useTz: true, precision: 3 }).notNullable();
290
+ },
291
+ columns: {
292
+ grant_id: (t) => t.string('grant_id').index(),
293
+ expires_at: (t) => t.dateTime('expires_at', { useTz: true, precision: 3 }).nullable(),
294
+ used_at: (t) => t.dateTime('used_at', { useTz: true, precision: 3 }).nullable(),
295
+ created_at: (t) => t.dateTime('created_at', { useTz: true, precision: 3 }).nullable(),
296
+ },
297
+ },
206
298
  ];
207
299
  /**
208
300
  * Probe searchPath-aware: `schema.hasTable` no Postgres ignora o