@adonis-agora/authkit-server 0.75.0 → 0.76.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/build/host/views/account/apps.edge +30 -0
- package/build/host/views/agents/consent.edge +60 -0
- package/build/host/views/agents/done.edge +24 -0
- package/build/host/views/consent.edge +9 -1
- package/build/host/views/partials/styles.edge +1 -1
- package/build/index.d.ts +9 -0
- package/build/index.js +4 -0
- package/build/providers/authkit_server_provider.js +75 -50
- package/build/src/agents/agent_identity.d.ts +33 -0
- package/build/src/agents/agent_identity.js +156 -0
- package/build/src/agents/config.d.ts +99 -0
- package/build/src/agents/config.js +101 -0
- package/build/src/agents/delegation_service.d.ts +154 -0
- package/build/src/agents/delegation_service.js +394 -0
- package/build/src/agents/delegation_store.d.ts +93 -0
- package/build/src/agents/delegation_store.js +222 -0
- package/build/src/agents/middleware.d.ts +73 -0
- package/build/src/agents/middleware.js +113 -0
- package/build/src/agents/protocol.d.ts +62 -0
- package/build/src/agents/protocol.js +51 -0
- package/build/src/agents/runtime.d.ts +36 -0
- package/build/src/agents/runtime.js +65 -0
- package/build/src/agents/signer.d.ts +34 -0
- package/build/src/agents/signer.js +70 -0
- package/build/src/audit/audit_sink.d.ts +1 -1
- package/build/src/audit/audit_sink.js +5 -0
- package/build/src/controllers/authorization_server_metadata_controller.d.ts +10 -0
- package/build/src/controllers/authorization_server_metadata_controller.js +20 -0
- package/build/src/define_config.d.ts +26 -3
- package/build/src/define_config.js +34 -1
- package/build/src/host/account_api/account_api_controller.d.ts +12 -0
- package/build/src/host/account_api/account_api_controller.js +40 -0
- package/build/src/host/admin_api/dto.d.ts +1 -1
- package/build/src/host/admin_sessions_service.d.ts +2 -0
- package/build/src/host/admin_sessions_service.js +18 -0
- package/build/src/host/auth_host_config.d.ts +4 -0
- package/build/src/host/client_names.d.ts +11 -0
- package/build/src/host/client_names.js +12 -0
- package/build/src/host/controllers/account_apps_controller.d.ts +2 -0
- package/build/src/host/controllers/account_apps_controller.js +39 -3
- package/build/src/host/controllers/account_orgs_controller.js +2 -1
- package/build/src/host/controllers/account_session_controller.js +2 -1
- package/build/src/host/controllers/agent_consent_controller.d.ts +17 -0
- package/build/src/host/controllers/agent_consent_controller.js +138 -0
- package/build/src/host/controllers/agent_oauth_controller.d.ts +23 -0
- package/build/src/host/controllers/agent_oauth_controller.js +110 -0
- package/build/src/host/controllers/interaction_controller.js +28 -0
- package/build/src/host/csrf.d.ts +8 -18
- package/build/src/host/csrf.js +28 -2
- package/build/src/host/i18n.d.ts +54 -0
- package/build/src/host/i18n.js +54 -0
- package/build/src/host/redirect_exact.d.ts +12 -0
- package/build/src/host/redirect_exact.js +17 -0
- package/build/src/host/register_auth_host.js +48 -7
- package/build/src/host/request_url.d.ts +10 -0
- package/build/src/host/request_url.js +17 -0
- package/build/src/host/sudo/methods/magic_link.js +2 -1
- package/build/src/host/sudo/runtime.js +3 -2
- package/build/src/host/sudo_mode.js +4 -4
- package/build/src/mcp/mcp_oauth.d.ts +85 -0
- package/build/src/mcp/mcp_oauth.js +154 -0
- package/build/src/provider/build_provider.js +15 -1
- package/build/src/provider/oidc_service.d.ts +8 -0
- package/build/src/provider/oidc_service.js +10 -0
- package/build/src/schema/ensure.js +92 -0
- package/build/stubs/ui/react/pages/consent.tsx +17 -1
- package/package.json +1 -1
- package/stubs/ui/react/pages/consent.tsx +17 -1
|
@@ -9,6 +9,8 @@ import { normalizeAdminApiPrefix, normalizeAdminPrefix, setAdminApiPrefix, setAd
|
|
|
9
9
|
import { getAuthHostConfig, markAuthHostAutoMounted, wasAuthHostAutoMounted, } from './auth_host_config.js';
|
|
10
10
|
import { ensureConsoleSession } from './idp_session_bridge.js';
|
|
11
11
|
import { createAuthThrottles } from './rate_limit.js';
|
|
12
|
+
import { redirectExact } from './redirect_exact.js';
|
|
13
|
+
import { requestPathWithQuery } from './request_url.js';
|
|
12
14
|
import { resolveRuntimeSettings } from './runtime_settings.js';
|
|
13
15
|
import { resolveEffectiveSessionPolicy } from './runtime_toggles.js';
|
|
14
16
|
import { magicLink as sudoMagicLink } from './sudo/methods/magic_link.js';
|
|
@@ -90,9 +92,7 @@ function buildLoginRedirect(ctx, extra) {
|
|
|
90
92
|
// host que desmontou a tela de login (`account: { login: false }`) aponta para
|
|
91
93
|
// a própria rota de login dele (ex.: `/login`). Ver `account_login_url.ts`.
|
|
92
94
|
const loginUrl = getAccountLoginUrl();
|
|
93
|
-
const
|
|
94
|
-
const qs = ctx.request?.parsedUrl?.search ?? '';
|
|
95
|
-
const dest = qs ? `${url}${qs}` : url;
|
|
95
|
+
const dest = requestPathWithQuery(ctx.request);
|
|
96
96
|
// Só inclui return_to quando há um caminho real (não vazio, não é o próprio login).
|
|
97
97
|
if (dest && dest !== '/' && !dest.startsWith(loginUrl)) {
|
|
98
98
|
const encoded = encodeURIComponent(dest);
|
|
@@ -116,12 +116,12 @@ function buildLoginRedirect(ctx, extra) {
|
|
|
116
116
|
const accountGuard = async (ctx, next) => {
|
|
117
117
|
// Sessão do console — ou, com `accountSession.acceptIdpSession`, a do IdP (SSO).
|
|
118
118
|
if (!(await ensureConsoleSession(ctx))) {
|
|
119
|
-
return ctx.response
|
|
119
|
+
return redirectExact(ctx.response, buildLoginRedirect(ctx));
|
|
120
120
|
}
|
|
121
121
|
// Idle timeout: encerra e redireciona com query param de motivo.
|
|
122
122
|
const idleExpired = await checkAndRefreshIdle(ctx);
|
|
123
123
|
if (idleExpired) {
|
|
124
|
-
return ctx.response
|
|
124
|
+
return redirectExact(ctx.response, buildLoginRedirect(ctx, 'reason=idle'));
|
|
125
125
|
}
|
|
126
126
|
return next();
|
|
127
127
|
};
|
|
@@ -150,12 +150,12 @@ export const adminGuard = async (ctx, next) => {
|
|
|
150
150
|
const accountId = ctx.session?.get(ACCOUNT_SESSION_KEY);
|
|
151
151
|
if (!accountId) {
|
|
152
152
|
// `/account/login` é sempre o login da conta — NÃO muda com o prefixo admin.
|
|
153
|
-
return ctx.response
|
|
153
|
+
return redirectExact(ctx.response, buildLoginRedirect(ctx));
|
|
154
154
|
}
|
|
155
155
|
// Idle timeout: também protege o console admin.
|
|
156
156
|
const idleExpired = await checkAndRefreshIdle(ctx);
|
|
157
157
|
if (idleExpired) {
|
|
158
|
-
return ctx.response
|
|
158
|
+
return redirectExact(ctx.response, buildLoginRedirect(ctx, 'reason=idle'));
|
|
159
159
|
}
|
|
160
160
|
const allowed = cfg.admin.roles;
|
|
161
161
|
const account = await cfg.accountStore.findById(accountId);
|
|
@@ -173,6 +173,7 @@ export const adminGuard = async (ctx, next) => {
|
|
|
173
173
|
};
|
|
174
174
|
const C = {
|
|
175
175
|
oidc: () => import('../controllers/oidc_callback_controller.js'),
|
|
176
|
+
authorizationServerMetadata: () => import('../controllers/authorization_server_metadata_controller.js'),
|
|
176
177
|
interaction: () => import('./controllers/interaction_controller.js'),
|
|
177
178
|
registration: () => import('./controllers/registration_controller.js'),
|
|
178
179
|
social: () => import('./controllers/social_controller.js'),
|
|
@@ -184,6 +185,8 @@ const C = {
|
|
|
184
185
|
accountMfa: () => import('./controllers/account_mfa_controller.js'),
|
|
185
186
|
accountOrgs: () => import('./controllers/account_orgs_controller.js'),
|
|
186
187
|
accountConfirm: () => import('./controllers/account_confirm_controller.js'),
|
|
188
|
+
agentOAuth: () => import('./controllers/agent_oauth_controller.js'),
|
|
189
|
+
agentConsent: () => import('./controllers/agent_consent_controller.js'),
|
|
187
190
|
webauthnAsset: () => import('./controllers/webauthn_asset_controller.js'),
|
|
188
191
|
logoutAsset: () => import('./controllers/logout_asset_controller.js'),
|
|
189
192
|
passkeyAutofillAsset: () => import('./controllers/passkey_autofill_asset_controller.js'),
|
|
@@ -421,6 +424,15 @@ export function registerAuthHost(router, opts = {}) {
|
|
|
421
424
|
.get('/authkit/assets/webauthn_confirm.js', [C.webauthnConfirmAsset])
|
|
422
425
|
.as('authkit.assets.webauthnConfirm');
|
|
423
426
|
router.get('/authkit/assets/submit_lock.js', [C.submitLockAsset]).as('authkit.assets.submitLock');
|
|
427
|
+
// Metadata do servidor de autorização no caminho da RFC 8414 §3.1 (issuer com path: os
|
|
428
|
+
// clientes MCP procuram `/.well-known/oauth-authorization-server/oidc` antes do OIDC Discovery).
|
|
429
|
+
// O issuer termina no mountPath, então o mount É o path do issuer.
|
|
430
|
+
const issuerPath = mount.replace(/\/+$/, '');
|
|
431
|
+
if (issuerPath !== '') {
|
|
432
|
+
router
|
|
433
|
+
.get(`/.well-known/oauth-authorization-server${issuerPath}`, [C.authorizationServerMetadata])
|
|
434
|
+
.as('authkit.oauth_authorization_server');
|
|
435
|
+
}
|
|
424
436
|
// Provider OIDC (wildcard + root) — o que registerOidcRoutes fazia.
|
|
425
437
|
router.any(`${mount}/*`, [C.oidc]).as('authkit.oidc.wildcard');
|
|
426
438
|
router.any(mount, [C.oidc]).as('authkit.oidc.root');
|
|
@@ -463,6 +475,27 @@ export function registerAuthHost(router, opts = {}) {
|
|
|
463
475
|
}
|
|
464
476
|
// PAT introspection (server-to-server).
|
|
465
477
|
withIntrospection(router.post('/authkit/pat/introspect', [C.patIntrospection, 'handle']));
|
|
478
|
+
// Personal agents (PACT §5): authorization server de delegação. Os endpoints
|
|
479
|
+
// OAuth são server-to-server (o agente se autentica pelo JWT dele, sem
|
|
480
|
+
// sessão); a tela de consentimento exige a sessão de conta — sem ela o
|
|
481
|
+
// `accountGuard` manda para o login com `return_to`, e o usuário volta com o
|
|
482
|
+
// `user_code`. Montado só quando `personalAgents` está no config.
|
|
483
|
+
const agentsPrefix = hostCfg?.personalAgents?.prefix;
|
|
484
|
+
if (agentsPrefix) {
|
|
485
|
+
const oauth = `${agentsPrefix}/oauth`;
|
|
486
|
+
router.get(`${oauth}/.well-known/oauth-authorization-server`, [C.agentOAuth, 'metadata']);
|
|
487
|
+
router.get(`${oauth}/jwks.json`, [C.agentOAuth, 'jwks']);
|
|
488
|
+
router.post(`${oauth}/device_authorization`, [C.agentOAuth, 'deviceAuthorization']);
|
|
489
|
+
router.post(`${oauth}/token`, [C.agentOAuth, 'token']);
|
|
490
|
+
// Throttle de código (bucket do OTP, por IP): o `user_code` tem 8 letras e
|
|
491
|
+
// a RFC 8628 §5.1 conta com rate limit para não ser adivinhável.
|
|
492
|
+
router
|
|
493
|
+
.group(() => {
|
|
494
|
+
withOtpLogin(router.get(`${agentsPrefix}/consent`, [C.agentConsent, 'show']));
|
|
495
|
+
withOtpLogin(router.post(`${agentsPrefix}/consent`, [C.agentConsent, 'decide']));
|
|
496
|
+
})
|
|
497
|
+
.use([accountGuard]);
|
|
498
|
+
}
|
|
466
499
|
// Paths do console de conta (configuráveis/localizáveis via `accountRoutes`).
|
|
467
500
|
// As TELAS vêm de `accountPath(key)` (prefixo + segmento configurável); os
|
|
468
501
|
// action-subpaths concatenados (`/password`, `/enroll`, ...) são FIXOS —
|
|
@@ -531,6 +564,9 @@ export function registerAuthHost(router, opts = {}) {
|
|
|
531
564
|
if (mountApps) {
|
|
532
565
|
router.get(appsPath, [C.accountApps, 'index']);
|
|
533
566
|
router.post(`${appsPath}/:clientId/revoke`, [C.accountApps, 'revoke']);
|
|
567
|
+
if (agentsPrefix) {
|
|
568
|
+
router.post(`${appsPath}/agents/:grantId/revoke`, [C.accountApps, 'revokeAgent']);
|
|
569
|
+
}
|
|
534
570
|
}
|
|
535
571
|
// MFA — TOTP + passkeys (tela `mfa`).
|
|
536
572
|
if (mountMfa) {
|
|
@@ -642,6 +678,11 @@ export function registerAuthHost(router, opts = {}) {
|
|
|
642
678
|
// Apps (grants).
|
|
643
679
|
router.get(`${apiBase}/apps`, [C.accountApi, 'listApps']);
|
|
644
680
|
router.delete(`${apiBase}/apps/:clientId`, [C.accountApi, 'revokeApp']);
|
|
681
|
+
// Personal agents com delegação (PACT).
|
|
682
|
+
if (agentsPrefix) {
|
|
683
|
+
router.get(`${apiBase}/agents`, [C.accountApi, 'listAgents']);
|
|
684
|
+
router.delete(`${apiBase}/agents/:id`, [C.accountApi, 'revokeAgent']);
|
|
685
|
+
}
|
|
645
686
|
// MFA + passkeys.
|
|
646
687
|
router.get(`${apiBase}/mfa`, [C.accountApi, 'mfaStatus']);
|
|
647
688
|
// Login methods preference (self-service, por usuário).
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path + query string da request atual, para montar um `return_to`.
|
|
3
|
+
*
|
|
4
|
+
* `request.url(true)` é a API do AdonisJS que inclui a query. O código antigo lia
|
|
5
|
+
* `request.parsedUrl.search`, que o AdonisJS 7 não tem mais (o `parsedUrl` virou
|
|
6
|
+
* `{ pathname, query }`) — todo redirect para o login perdia a query em silêncio,
|
|
7
|
+
* e o usuário voltava para a página sem os parâmetros (um `user_code`, uma
|
|
8
|
+
* paginação). O `search` fica como fallback para hosts/dublês que ainda o expõem.
|
|
9
|
+
*/
|
|
10
|
+
export declare function requestPathWithQuery(request: any): string;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path + query string da request atual, para montar um `return_to`.
|
|
3
|
+
*
|
|
4
|
+
* `request.url(true)` é a API do AdonisJS que inclui a query. O código antigo lia
|
|
5
|
+
* `request.parsedUrl.search`, que o AdonisJS 7 não tem mais (o `parsedUrl` virou
|
|
6
|
+
* `{ pathname, query }`) — todo redirect para o login perdia a query em silêncio,
|
|
7
|
+
* e o usuário voltava para a página sem os parâmetros (um `user_code`, uma
|
|
8
|
+
* paginação). O `search` fica como fallback para hosts/dublês que ainda o expõem.
|
|
9
|
+
*/
|
|
10
|
+
export function requestPathWithQuery(request) {
|
|
11
|
+
const withQuery = request?.url?.(true) ?? '';
|
|
12
|
+
if (withQuery.includes('?'))
|
|
13
|
+
return withQuery;
|
|
14
|
+
const parsed = request?.parsedUrl;
|
|
15
|
+
const legacy = parsed?.search ?? (parsed?.query ? `?${parsed.query}` : '');
|
|
16
|
+
return `${withQuery}${legacy}`;
|
|
17
|
+
}
|
|
@@ -2,6 +2,7 @@ import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
|
|
|
2
2
|
import { accountPath } from '../../account_paths.js';
|
|
3
3
|
import { sendSudoLinkEmail } from '../../default_mailer.js';
|
|
4
4
|
import { translate } from '../../i18n.js';
|
|
5
|
+
import { redirectExact } from '../../redirect_exact.js';
|
|
5
6
|
import { isSudoMethodEnabled } from '../runtime.js';
|
|
6
7
|
/** Token de sudo pendente, guardado na sessão que o pediu. */
|
|
7
8
|
export const SUDO_LINK_SESSION_KEY = 'authkit_sudo_link';
|
|
@@ -170,7 +171,7 @@ export function magicLink() {
|
|
|
170
171
|
// `translate(...)` em `confirmError`, e a tela leria dois formatos
|
|
171
172
|
// diferentes se este aqui mandasse a chave.
|
|
172
173
|
ctx.session.flash('confirmNotice', translate(c.cfg.messages, 'account.confirm.magic_link_sent'));
|
|
173
|
-
return ctx.response
|
|
174
|
+
return redirectExact(ctx.response, `${accountPath('confirm')}${qs}`);
|
|
174
175
|
});
|
|
175
176
|
router.get(`${accountPath('confirm')}/magic-link/:token`, async (ctx) => {
|
|
176
177
|
const c = await h.contextFrom(ctx);
|
|
@@ -3,6 +3,7 @@ import { accountPath } from '../account_paths.js';
|
|
|
3
3
|
import { ACCOUNT_SESSION_KEY } from '../account_session_key.js';
|
|
4
4
|
import { validateReturnTo } from '../controllers/account_session_controller.js';
|
|
5
5
|
import { translate } from '../i18n.js';
|
|
6
|
+
import { redirectExact } from '../redirect_exact.js';
|
|
6
7
|
import { markSudo } from '../sudo_mode.js';
|
|
7
8
|
/** Último método usado com sucesso — só ordena a tela, não restringe nada. */
|
|
8
9
|
export const LAST_METHOD_SESSION_KEY = 'authkit_sudo_last_method';
|
|
@@ -362,7 +363,7 @@ export async function completeSudo(c, methodId) {
|
|
|
362
363
|
ip: c.ctx.request.ip?.() ?? null,
|
|
363
364
|
metadata: { method: methodId },
|
|
364
365
|
});
|
|
365
|
-
return c.ctx.response
|
|
366
|
+
return redirectExact(c.ctx.response, c.returnTo ?? accountHome(c.cfg));
|
|
366
367
|
}
|
|
367
368
|
/**
|
|
368
369
|
* Falha de confirmação: flash + volta pro /account/confirm preservando o
|
|
@@ -372,7 +373,7 @@ export async function completeSudo(c, methodId) {
|
|
|
372
373
|
export async function fail(c, messageKey) {
|
|
373
374
|
c.ctx.session.flash('confirmError', translate(c.cfg.messages, messageKey));
|
|
374
375
|
const qs = c.returnTo ? `?return_to=${encodeURIComponent(c.returnTo)}` : '';
|
|
375
|
-
return c.ctx.response
|
|
376
|
+
return redirectExact(c.ctx.response, `${accountPath('confirm')}${qs}`);
|
|
376
377
|
}
|
|
377
378
|
/**
|
|
378
379
|
* Filtra os métodos disponíveis para esta conta e promove o último usado.
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { accountPath } from './account_paths.js';
|
|
17
17
|
import { ACCOUNT_SESSION_KEY } from './account_session_key.js';
|
|
18
|
+
import { redirectExact } from './redirect_exact.js';
|
|
19
|
+
import { requestPathWithQuery } from './request_url.js';
|
|
18
20
|
import { SETTING_KEYS } from './runtime_toggles.js';
|
|
19
21
|
export const SUDO_MODE_DEFAULTS = {
|
|
20
22
|
enabled: true,
|
|
@@ -235,11 +237,9 @@ export async function requireSudo(ctx, settings) {
|
|
|
235
237
|
if (await isSudoSatisfied(ctx, settings))
|
|
236
238
|
return true;
|
|
237
239
|
// Fora da graça: redireciona para confirmação.
|
|
238
|
-
const
|
|
239
|
-
const qs = ctx.request.parsedUrl?.search ?? '';
|
|
240
|
-
const dest = qs ? `${rawUrl}${qs}` : rawUrl;
|
|
240
|
+
const dest = requestPathWithQuery(ctx.request);
|
|
241
241
|
const returnTo = dest && dest !== '/' && !dest.startsWith(accountPath('confirm'))
|
|
242
242
|
? `?return_to=${encodeURIComponent(dest)}`
|
|
243
243
|
: '';
|
|
244
|
-
return ctx.response
|
|
244
|
+
return redirectExact(ctx.response, `${accountPath('confirm')}${returnTo}`);
|
|
245
245
|
}
|
|
@@ -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
|
|
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;
|
|
@@ -216,6 +225,7 @@ export class OidcService {
|
|
|
216
225
|
this.#callback = callback;
|
|
217
226
|
this.#interactions = interactions;
|
|
218
227
|
this.#publicJwks = toPublicJwks(jwks);
|
|
228
|
+
this.#signingJwks = jwks;
|
|
219
229
|
}
|
|
220
230
|
/**
|
|
221
231
|
* 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
|