Files
accounted/app/.well-known/oauth-authorization-server/__tests__/route.test.ts
T
e92365b86e feat(mcp): distribution polish for agent-first onboarding: CIMD, plugin start skill, bridge hint (#1814 PR 4) (#1866)
* feat(mcp): distribution polish for agent-first onboarding: CIMD, plugin start skill, bridge hint

Fourth PR of agent-first onboarding (#1814).

- The OAuth AS metadata advertises client_id_metadata_document_supported
  next to the existing `none` token auth, the pair Claude.ai, Claude Code
  and Codex look for to use CIMD instead of registering a DCR client per
  connection. authorize/token never keyed on client_id (the redirect-URI
  allowlist is the trust boundary), so nothing else changes; DCR stays
  for ChatGPT.
- The plugin's start skill no longer sends a user without an account to
  the website: the /mcp OAuth screen creates the account, and a
  NO_COMPANY_YET briefing failure routes to the onboarding skill and
  accounted_create_company. README updated to match.
- `npx accounted-mcp` without ACCOUNTED_API_KEY prints the OAuth
  alternative (Claude Code, Codex, Claude.ai connector) and that the
  account can be created on the sign-in screen; package README too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018wCdzRTatKiDByKB8hCNT6

* fix(oauth): do not advertise CIMD until redirect URIs are matched against the client document

CodeRabbit on #1866: advertising client_id_metadata_document_supported
makes Claude and Codex send URL client_ids and expects an exact
redirect_uri match against that document; the authorize endpoint only
checks the global allowlist and never fetches client metadata. The flag
is withheld until an SSRF-safe, cached CIMD fetch with exact redirect
matching exists. DCR stays the registration path (stateless, so free).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018wCdzRTatKiDByKB8hCNT6

---------

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 13:05:59 +02:00

35 lines
1.7 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { GET } from '../route'
describe('GET /.well-known/oauth-authorization-server', () => {
beforeEach(() => {
vi.stubEnv('NEXT_PUBLIC_APP_URL', 'https://app.example.test')
})
afterEach(() => {
vi.unstubAllEnvs()
})
it('does not advertise CIMD until redirect URIs are validated against the client document', async () => {
// Advertising client_id_metadata_document_supported makes Claude and
// Codex send URL client_ids and expects an exact redirect_uri match
// against that document; the authorize endpoint only checks the global
// allowlist today, so the flag must stay off (see the route comment).
const res = await GET(new Request('https://app.example.test/.well-known/oauth-authorization-server'))
expect(res.status).toBe(200)
const body = await res.json()
expect(body.client_id_metadata_document_supported).toBeUndefined()
expect(body.token_endpoint_auth_methods_supported).toContain('none')
// DCR (stateless register endpoint) remains the registration path.
expect(body.registration_endpoint).toBe('https://app.example.test/api/mcp-oauth/register')
expect(body.code_challenge_methods_supported).toEqual(['S256'])
expect(body.authorization_response_iss_parameter_supported).toBe(true)
})
it('does not enumerate write scopes in public discovery', async () => {
const res = await GET(new Request('https://app.example.test/.well-known/oauth-authorization-server'))
const body = await res.json()
expect(body.scopes_supported).toContain('mcp')
expect(body.scopes_supported.some((s: string) => s.endsWith(':write'))).toBe(false)
})
})