1import * as Schema from "effect/Schema";
2import * as HttpApiSchema from "effect/http-api/HttpApiSchema";
3
4import {
5 AuthSessionId,
6 ForwardCompatibleArray,
7 ClientSurface,
8 TrimmedNonEmptyString,
9} from "./baseSchemas.ts";
10
11/**
12 * Declares the server's overall authentication posture.
13 *
14 * This is a high-level policy label that tells clients how the environment is
15 * expected to be accessed, not a transport detail and not an exhaustive list
16 * of every accepted credential.
17 *
18 * Typical usage:
19 * - rendered in auth/pairing UI so the user understands what kind of
20 * environment they are connecting to
21 * - used by clients to decide whether silent desktop bootstrap is expected or
22 * whether an explicit pairing flow should be shown
23 *
24 * Meanings:
25 * - `desktop-managed-local`: local desktop-managed environment with narrow
26 * trusted bootstrap, intended to avoid login prompts on the same machine
27 * - `loopback-browser`: standalone local server intended for browser pairing on
28 * the same machine
29 * - `remote-reachable`: environment intended to be reached from other devices
30 * or networks, where explicit pairing/auth is expected
31 * - `unsafe-no-auth`: intentionally unauthenticated mode; this is an explicit
32 * unsafe escape hatch, not a normal deployment mode
33 */
34export const ServerAuthPolicy = Schema.Literals([
35 "desktop-managed-local",
36 "loopback-browser",
37 "remote-reachable",
38 "unsafe-no-auth",
39]);
40export type ServerAuthPolicy = typeof ServerAuthPolicy.Type;
41
42/**
43 * A credential type that can be exchanged for a real authenticated session.
44 *
45 * Bootstrap methods are for establishing trust at the start of a connection or
46 * pairing flow. They are not the long-lived credential used for ordinary
47 * authenticated HTTP / WebSocket traffic after pairing succeeds.
48 *
49 * Current methods:
50 * - `desktop-bootstrap`: a trusted local desktop handoff, used so the desktop
51 * shell can pair the renderer without a login screen
52 * - `one-time-token`: a short-lived pairing token, suitable for manual pairing
53 * flows such as `/pair?token=...`
54 */
55export const ServerAuthBootstrapMethod = Schema.Literals(["desktop-bootstrap", "one-time-token"]);
56export type ServerAuthBootstrapMethod = typeof ServerAuthBootstrapMethod.Type;
57
58/**
59 * A credential type accepted for steady-state authenticated requests after a
60 * client has already paired.
61 *
62 * These methods are used by the server-wide auth layer for privileged HTTP and
63 * WebSocket access. They are distinct from bootstrap methods so clients can
64 * reason clearly about "pair first, then use session auth".
65 *
66 * Current methods:
67 * - `browser-session-cookie`: cookie-backed browser session, used by the web
68 * app after bootstrap/pairing
69 * - `bearer-access-token`: scoped token suitable for non-cookie or
70 * non-browser clients
71 * - `dpop-access-token`: scoped proof-of-possession token used by managed
72 * relay connections
73 */
74export const ServerAuthSessionMethod = Schema.Literals([
75 "browser-session-cookie",
76 "bearer-access-token",
77 "dpop-access-token",
78]);
79export type ServerAuthSessionMethod = typeof ServerAuthSessionMethod.Type;
80
81export const AuthOrchestrationReadScope = "orchestration:read" as const;
82export const AuthOrchestrationOperateScope = "orchestration:operate" as const;
83export const AuthEnvironmentConnectScope = "environment:connect" as const;
84export const AuthArchiveReadScope = "archive:read" as const;
85export const AuthArchiveWriteScope = "archive:write" as const;
86export const AuthSettingsWriteScope = "settings:write" as const;
87export const AuthProvidersManageScope = "providers:manage" as const;
88export const AuthEnvironmentMaintainScope = "environment:maintain" as const;
89export const AuthPreviewOperateScope = "preview:operate" as const;
90export const AuthDiagnosticsReadScope = "diagnostics:read" as const;
91export const AuthTerminalReadScope = "terminal:read" as const;
92export const AuthTerminalOperateScope = "terminal:operate" as const;
93export const AuthSourceControlWriteScope = "source-control:write" as const;
94export const AuthFilesystemReadScope = "filesystem:read" as const;
95export const AuthFilesystemWriteScope = "filesystem:write" as const;
96/** Retained for decoding existing credentials; grants no current RPC access. */
97const AuthReviewWriteScope = "review:write" as const;
98export const AuthAccessReadScope = "access:read" as const;
99export const AuthAccessWriteScope = "access:write" as const;
100export const AuthRelayReadScope = "relay:read" as const;
101export const AuthRelayWriteScope = "relay:write" as const;
102export const AuthEnvironmentScope = Schema.Literals([
103 AuthEnvironmentConnectScope,
104 AuthOrchestrationReadScope,
105 AuthOrchestrationOperateScope,
106 AuthArchiveReadScope,
107 AuthArchiveWriteScope,
108 AuthSettingsWriteScope,
109 AuthProvidersManageScope,
110 AuthEnvironmentMaintainScope,
111 AuthPreviewOperateScope,
112 AuthDiagnosticsReadScope,
113 AuthTerminalReadScope,
114 AuthTerminalOperateScope,
115 AuthFilesystemReadScope,
116 AuthFilesystemWriteScope,
117 AuthReviewWriteScope,
118 AuthSourceControlWriteScope,
119 AuthAccessReadScope,
120 AuthAccessWriteScope,
121 AuthRelayReadScope,
122 AuthRelayWriteScope,
123]);
124export type AuthEnvironmentScope = typeof AuthEnvironmentScope.Type;
125export const AuthEnvironmentScopes = Schema.Array(AuthEnvironmentScope);
126export type AuthEnvironmentScopes = typeof AuthEnvironmentScopes.Type;
127
128export const AuthGrantScope = Schema.Literals(
129 AuthEnvironmentScope.literals.filter((scope) => scope !== AuthReviewWriteScope),
130);
131export type AuthGrantScope = typeof AuthGrantScope.Type;
132export const AuthGrantScopes = Schema.Array(AuthGrantScope);
133export type AuthGrantScopes = typeof AuthGrantScopes.Type;
134
135// Frozen wire vocabulary for clients released before granular permissions.
136const legacyScopes = new Set<AuthEnvironmentScope>([
137 AuthOrchestrationReadScope,
138 AuthOrchestrationOperateScope,
139 AuthTerminalOperateScope,
140 AuthReviewWriteScope,
141 AuthAccessReadScope,
142 AuthAccessWriteScope,
143 AuthRelayReadScope,
144 AuthRelayWriteScope,
145]);
146
147/** Format public auth metadata without changing the server's authorization grant. */
148export function authScopeResponse(scopes: ReadonlyArray<AuthEnvironmentScope>) {
149 return { scopes: scopes.filter((scope) => legacyScopes.has(scope)), permissions: scopes };
150}
151
152const authScopeResponseFields = {
153 scopes: AuthEnvironmentScopes,
154 permissions: Schema.optionalKey(ForwardCompatibleArray(AuthEnvironmentScope)),
155};
156
157// Only clients talking to an old server use these parent checks. Servers never
158// expand stored grants, and an explicitly empty permissions array grants nothing.
159const legacyParents: Partial<Record<AuthEnvironmentScope, AuthEnvironmentScope>> = {
160 [AuthArchiveReadScope]: AuthOrchestrationReadScope,
161 [AuthArchiveWriteScope]: AuthOrchestrationOperateScope,
162 [AuthFilesystemReadScope]: AuthOrchestrationReadScope,
163 [AuthDiagnosticsReadScope]: AuthOrchestrationReadScope,
164 [AuthSettingsWriteScope]: AuthOrchestrationOperateScope,
165 [AuthProvidersManageScope]: AuthOrchestrationOperateScope,
166 [AuthEnvironmentMaintainScope]: AuthOrchestrationOperateScope,
167 [AuthPreviewOperateScope]: AuthOrchestrationOperateScope,
168 [AuthSourceControlWriteScope]: AuthOrchestrationOperateScope,
169 [AuthFilesystemWriteScope]: AuthOrchestrationOperateScope,
170 [AuthTerminalReadScope]: AuthTerminalOperateScope,
171};
172
173/** Keep permission denials decodable by clients with the original scope enum. */
174export function authScopeRequiredResponse(requiredPermission: AuthEnvironmentScope) {
175 return {
176 requiredScope: legacyParents[requiredPermission] ?? requiredPermission,
177 requiredPermission,
178 };
179}
180
181export interface SessionGrantInput {
182 readonly authenticated: boolean;
183 readonly scopes?: ReadonlyArray<AuthEnvironmentScope> | undefined;
184 readonly permissions?: ReadonlyArray<AuthEnvironmentScope> | undefined;
185 readonly auth?: { readonly serverUpdateScope?: string | undefined } | undefined;
186}
187
188export function sessionGrantsScope(
189 session: SessionGrantInput,
190 scope: AuthEnvironmentScope,
191): boolean {
192 if (!session.authenticated) return false;
193 if (session.permissions !== undefined) return session.permissions.includes(scope);
194 if (session.scopes?.includes(scope)) return true;
195 // Also recognize servers from the first granular-scope release.
196 if (session.auth?.serverUpdateScope !== undefined) return false;
197 const parent = legacyParents[scope];
198 return parent !== undefined && session.scopes?.includes(parent) === true;
199}
200
201/** Old-only grants lost the child permissions formerly implied by their broad scopes. */
202export function sessionHasLegacyPermissions(session: SessionGrantInput): boolean {
203 const permissions = session.permissions;
204 return (
205 session.authenticated &&
206 permissions !== undefined &&
207 permissions.every((scope) => legacyScopes.has(scope)) &&
208 Object.values(legacyParents).some((parent) => permissions.includes(parent))
209 );
210}
211
212export const AuthStandardClientScopes = [
213 AuthOrchestrationReadScope,
214 AuthOrchestrationOperateScope,
215 AuthArchiveReadScope,
216 AuthArchiveWriteScope,
217 AuthSettingsWriteScope,
218 AuthProvidersManageScope,
219 AuthEnvironmentMaintainScope,
220 AuthPreviewOperateScope,
221 AuthDiagnosticsReadScope,
222 AuthTerminalReadScope,
223 AuthTerminalOperateScope,
224 AuthSourceControlWriteScope,
225 AuthFilesystemReadScope,
226 AuthFilesystemWriteScope,
227 AuthRelayReadScope,
228] as const;
229export const AuthAdministrativeScopes = [
230 AuthEnvironmentConnectScope,
231 ...AuthStandardClientScopes,
232 AuthAccessReadScope,
233 AuthAccessWriteScope,
234 AuthRelayWriteScope,
235] as const;
236
237export const AuthTokenExchangeGrantType =
238 "urn:ietf:params:oauth:grant-type:token-exchange" as const;
239export const AuthAccessTokenType = "urn:ietf:params:oauth:token-type:access_token" as const;
240export const AuthEnvironmentBootstrapTokenType =
241 "urn:t3:params:oauth:token-type:environment-bootstrap" as const;
242
243/**
244 * Server-advertised auth capabilities for a specific execution environment.
245 *
246 * Clients should treat this as the authoritative description of how that
247 * environment expects to be paired and how authenticated requests should be
248 * made afterward.
249 *
250 * Field meanings:
251 * - `policy`: high-level auth posture for the environment
252 * - `bootstrapMethods`: pairing/bootstrap methods the server is currently
253 * willing to accept
254 * - `sessionMethods`: authenticated request/session methods the server supports
255 * once pairing is complete
256 * - `sessionCookieName`: cookie name clients should expect when
257 * `browser-session-cookie` is in use
258 *
259 * This descriptor is intentionally capability-oriented. It lets clients choose
260 * the right UX without embedding server-specific auth logic or assuming a
261 * single access method.
262 */
263export const ServerAuthDescriptor = Schema.Struct({
264 policy: ServerAuthPolicy,
265 bootstrapMethods: Schema.Array(ServerAuthBootstrapMethod),
266 sessionMethods: Schema.Array(ServerAuthSessionMethod),
267 sessionCookieName: TrimmedNonEmptyString,
268 /** Older servers omit this and authorize self-updates with orchestration:operate. */
269 serverUpdateScope: Schema.optionalKey(Schema.Literal(AuthEnvironmentMaintainScope)),
270});
271export type ServerAuthDescriptor = typeof ServerAuthDescriptor.Type;
272
273export const AuthBrowserSessionRequest = Schema.Struct({
274 credential: TrimmedNonEmptyString,
275});
276export type AuthBrowserSessionRequest = typeof AuthBrowserSessionRequest.Type;
277
278export const AuthBrowserSessionResult = Schema.Struct({
279 authenticated: Schema.Literal(true),
280 ...authScopeResponseFields,
281 sessionMethod: ServerAuthSessionMethod,
282 expiresAt: Schema.DateTimeUtc,
283});
284export type AuthBrowserSessionResult = typeof AuthBrowserSessionResult.Type;
285
286export const AuthClientMetadataDeviceType = Schema.Literals([
287 "desktop",
288 "mobile",
289 "tablet",
290 "bot",
291 "unknown",
292]);
293export type AuthClientMetadataDeviceType = typeof AuthClientMetadataDeviceType.Type;
294
295export const AuthClientPresentationMetadata = Schema.Struct({
296 label: Schema.optionalKey(TrimmedNonEmptyString),
297 deviceType: Schema.optionalKey(AuthClientMetadataDeviceType),
298 os: Schema.optionalKey(TrimmedNonEmptyString),
299 surface: Schema.optionalKey(ClientSurface),
300 appVersion: Schema.optionalKey(TrimmedNonEmptyString),
301});
302export type AuthClientPresentationMetadata = typeof AuthClientPresentationMetadata.Type;
303
304export const AuthTokenExchangeRequest = Schema.Struct({
305 grant_type: Schema.Literal(AuthTokenExchangeGrantType),
306 subject_token: TrimmedNonEmptyString,
307 subject_token_type: Schema.Literal(AuthEnvironmentBootstrapTokenType),
308 requested_token_type: Schema.Literal(AuthAccessTokenType),
309 scope: Schema.optionalKey(TrimmedNonEmptyString),
310 client_label: Schema.optionalKey(TrimmedNonEmptyString),
311 client_device_type: Schema.optionalKey(AuthClientMetadataDeviceType),
312 client_os: Schema.optionalKey(TrimmedNonEmptyString),
313}).pipe(HttpApiSchema.asFormUrlEncoded());
314export type AuthTokenExchangeRequest = typeof AuthTokenExchangeRequest.Type;
315
316export const AuthAccessTokenResult = Schema.Struct({
317 access_token: TrimmedNonEmptyString,
318 issued_token_type: Schema.Literal(AuthAccessTokenType),
319 token_type: Schema.Literals(["Bearer", "DPoP"]),
320 expires_in: Schema.Number,
321 scope: TrimmedNonEmptyString,
322});
323export type AuthAccessTokenResult = typeof AuthAccessTokenResult.Type;
324
325export const AuthWebSocketTicketResult = Schema.Struct({
326 ticket: TrimmedNonEmptyString,
327 expiresAt: Schema.DateTimeUtc,
328});
329export type AuthWebSocketTicketResult = typeof AuthWebSocketTicketResult.Type;
330
331export const AuthPairingCredentialResult = Schema.Struct({
332 id: TrimmedNonEmptyString,
333 credential: TrimmedNonEmptyString,
334 label: Schema.optionalKey(TrimmedNonEmptyString),
335 expiresAt: Schema.DateTimeUtc,
336});
337export type AuthPairingCredentialResult = typeof AuthPairingCredentialResult.Type;
338
339// Read models contain metadata only. Credentials are returned by creation alone.
340export const AuthPairingLink = Schema.Struct({
341 id: TrimmedNonEmptyString,
342 ...authScopeResponseFields,
343 subject: TrimmedNonEmptyString,
344 label: Schema.optionalKey(TrimmedNonEmptyString),
345 createdAt: Schema.DateTimeUtc,
346 expiresAt: Schema.DateTimeUtc,
347});
348export type AuthPairingLink = typeof AuthPairingLink.Type;
349
350export const AuthClientMetadata = Schema.Struct({
351 label: Schema.optionalKey(TrimmedNonEmptyString),
352 ipAddress: Schema.optionalKey(TrimmedNonEmptyString),
353 userAgent: Schema.optionalKey(TrimmedNonEmptyString),
354 deviceType: AuthClientMetadataDeviceType,
355 os: Schema.optionalKey(TrimmedNonEmptyString),
356 browser: Schema.optionalKey(TrimmedNonEmptyString),
357});
358export type AuthClientMetadata = typeof AuthClientMetadata.Type;
359
360export const AuthClientSession = Schema.Struct({
361 sessionId: AuthSessionId,
362 subject: TrimmedNonEmptyString,
363 ...authScopeResponseFields,
364 method: ServerAuthSessionMethod,
365 client: AuthClientMetadata,
366 issuedAt: Schema.DateTimeUtc,
367 expiresAt: Schema.DateTimeUtc,
368 lastConnectedAt: Schema.NullOr(Schema.DateTimeUtc),
369 connected: Schema.Boolean,
370 current: Schema.Boolean,
371});
372export type AuthClientSession = typeof AuthClientSession.Type;
373
374export const AuthAccessSnapshot = Schema.Struct({
375 pairingLinks: Schema.Array(AuthPairingLink),
376 clientSessions: Schema.Array(AuthClientSession),
377});
378export type AuthAccessSnapshot = typeof AuthAccessSnapshot.Type;
379
380export const AuthAccessStreamSnapshotEvent = Schema.Struct({
381 version: Schema.Literal(1),
382 revision: Schema.Number,
383 type: Schema.Literal("snapshot"),
384 payload: AuthAccessSnapshot,
385});
386export type AuthAccessStreamSnapshotEvent = typeof AuthAccessStreamSnapshotEvent.Type;
387
388export const AuthAccessStreamPairingLinkUpsertedEvent = Schema.Struct({
389 version: Schema.Literal(1),
390 revision: Schema.Number,
391 type: Schema.Literal("pairingLinkUpserted"),
392 payload: AuthPairingLink,
393});
394export type AuthAccessStreamPairingLinkUpsertedEvent =
395 typeof AuthAccessStreamPairingLinkUpsertedEvent.Type;
396
397export const AuthAccessStreamPairingLinkRemovedEvent = Schema.Struct({
398 version: Schema.Literal(1),
399 revision: Schema.Number,
400 type: Schema.Literal("pairingLinkRemoved"),
401 payload: Schema.Struct({
402 id: TrimmedNonEmptyString,
403 }),
404});
405export type AuthAccessStreamPairingLinkRemovedEvent =
406 typeof AuthAccessStreamPairingLinkRemovedEvent.Type;
407
408export class AuthAccessStreamError extends Schema.TaggedError<AuthAccessStreamError>()(
409 "AuthAccessStreamError",
410 {
411 message: Schema.String,
412 },
413) {}
414
415export class EnvironmentAuthorizationError extends Schema.TaggedError<EnvironmentAuthorizationError>()(
416 "EnvironmentAuthorizationError",
417 {
418 message: Schema.String,
419 requiredScope: AuthEnvironmentScope,
420 requiredPermission: Schema.optionalKey(Schema.String),
421 },
422) {}
423
424export const AuthAccessStreamClientUpsertedEvent = Schema.Struct({
425 version: Schema.Literal(1),
426 revision: Schema.Number,
427 type: Schema.Literal("clientUpserted"),
428 payload: AuthClientSession,
429});
430export type AuthAccessStreamClientUpsertedEvent = typeof AuthAccessStreamClientUpsertedEvent.Type;
431
432export const AuthAccessStreamClientRemovedEvent = Schema.Struct({
433 version: Schema.Literal(1),
434 revision: Schema.Number,
435 type: Schema.Literal("clientRemoved"),
436 payload: Schema.Struct({
437 sessionId: AuthSessionId,
438 }),
439});
440export type AuthAccessStreamClientRemovedEvent = typeof AuthAccessStreamClientRemovedEvent.Type;
441
442export const AuthAccessStreamEvent = Schema.Union([
443 AuthAccessStreamSnapshotEvent,
444 AuthAccessStreamPairingLinkUpsertedEvent,
445 AuthAccessStreamPairingLinkRemovedEvent,
446 AuthAccessStreamClientUpsertedEvent,
447 AuthAccessStreamClientRemovedEvent,
448]);
449export type AuthAccessStreamEvent = typeof AuthAccessStreamEvent.Type;
450
451export const AuthRevokePairingLinkInput = Schema.Struct({
452 id: TrimmedNonEmptyString,
453});
454export type AuthRevokePairingLinkInput = typeof AuthRevokePairingLinkInput.Type;
455
456export const AuthRevokeClientSessionInput = Schema.Struct({
457 sessionId: AuthSessionId,
458});
459export type AuthRevokeClientSessionInput = typeof AuthRevokeClientSessionInput.Type;
460
461export const AuthCreatePairingCredentialInput = Schema.Struct({
462 label: Schema.optionalKey(TrimmedNonEmptyString),
463 scopes: Schema.optionalKey(AuthGrantScopes),
464});
465export type AuthCreatePairingCredentialInput = typeof AuthCreatePairingCredentialInput.Type;
466
467export const AuthCreateDelegatedSessionInput = Schema.Struct({
468 label: Schema.optionalKey(TrimmedNonEmptyString),
469 scopes: AuthGrantScopes,
470});
471export type AuthCreateDelegatedSessionInput = typeof AuthCreateDelegatedSessionInput.Type;
472
473export const AuthSessionState = Schema.Struct({
474 authenticated: Schema.Boolean,
475 auth: ServerAuthDescriptor,
476 scopes: Schema.optionalKey(AuthEnvironmentScopes),
477 permissions: authScopeResponseFields.permissions,
478 sessionMethod: Schema.optionalKey(ServerAuthSessionMethod),
479 expiresAt: Schema.optionalKey(Schema.DateTimeUtc),
480});
481export type AuthSessionState = typeof AuthSessionState.Type;
482
483/**
484 * What an agent signed in through MCP OAuth may do, least to most: only read,
485 * or act on threads that never run above the given runtime mode.
486 */
487export const AuthMcpClientAccess = Schema.Literals([
488 "read-only",
489 "approval-required",
490 "auto-accept-edits",
491 "auto",
492 "full-access",
493]);
494export type AuthMcpClientAccess = typeof AuthMcpClientAccess.Type;
495
496/** RFC 9728 metadata for an environment's `/mcp` resource. */
497export const AuthMcpProtectedResourceMetadata = Schema.Struct({
498 resource: Schema.String,
499 authorization_servers: Schema.Array(Schema.String),
500 scopes_supported: Schema.Array(AuthEnvironmentScope),
501 bearer_methods_supported: Schema.Array(Schema.Literal("header")),
502 resource_name: Schema.String,
503});
504export type AuthMcpProtectedResourceMetadata = typeof AuthMcpProtectedResourceMetadata.Type;
505
506/** RFC 8414 metadata for the authorization server MCP clients sign in through. */
507export const AuthMcpAuthorizationServerMetadata = Schema.Struct({
508 issuer: Schema.String,
509 authorization_endpoint: Schema.String,
510 token_endpoint: Schema.String,
511 registration_endpoint: Schema.String,
512 response_types_supported: Schema.Array(Schema.Literal("code")),
513 grant_types_supported: Schema.Array(Schema.Literal("authorization_code")),
514 code_challenge_methods_supported: Schema.Array(Schema.Literal("S256")),
515 token_endpoint_auth_methods_supported: Schema.Array(Schema.Literal("none")),
516 scopes_supported: Schema.Array(AuthEnvironmentScope),
517 authorization_response_iss_parameter_supported: Schema.Boolean,
518});
519export type AuthMcpAuthorizationServerMetadata = typeof AuthMcpAuthorizationServerMetadata.Type;
520
521/** RFC 7591 client metadata. Fields the server does not use are dropped. */
522export const AuthMcpClientRegistration = Schema.Struct({
523 client_name: Schema.optionalKey(Schema.String),
524 redirect_uris: Schema.optionalKey(Schema.Array(Schema.String)),
525 token_endpoint_auth_method: Schema.optionalKey(Schema.String),
526});
527export type AuthMcpClientRegistration = typeof AuthMcpClientRegistration.Type;
528
529export const AuthMcpRegisteredClient = Schema.Struct({
530 client_id: Schema.String,
531 client_name: Schema.String,
532 redirect_uris: Schema.Array(Schema.String),
533 grant_types: Schema.Array(Schema.Literal("authorization_code")),
534 response_types: Schema.Array(Schema.Literal("code")),
535 token_endpoint_auth_method: Schema.Literal("none"),
536}).pipe(HttpApiSchema.status(201));
537export type AuthMcpRegisteredClient = typeof AuthMcpRegisteredClient.Type;
538
539/** RFC 7591 §3.2.2 registration error. */
540export class AuthMcpRegistrationError extends Schema.Error<AuthMcpRegistrationError>(
541 "AuthMcpRegistrationError",
542)(
543 {
544 error: Schema.Literals(["invalid_client_metadata", "invalid_redirect_uri"]),
545 error_description: Schema.String,
546 },
547 { httpApiStatus: 400 },
548) {
549 override get message(): string {
550 return this.error_description;
551 }
552}
553
554/**
555 * An agent's authorization request, as the approval page received it in its
556 * URL. Every field is checked by the server, so all are optional here.
557 */
558export const AuthMcpAuthorizationRequest = Schema.Struct({
559 response_type: Schema.optionalKey(Schema.String),
560 client_id: Schema.optionalKey(Schema.String),
561 redirect_uri: Schema.optionalKey(Schema.String),
562 code_challenge: Schema.optionalKey(Schema.String),
563 code_challenge_method: Schema.optionalKey(Schema.String),
564 state: Schema.optionalKey(Schema.String),
565 resource: Schema.optionalKey(Schema.String),
566});
567export type AuthMcpAuthorizationRequest = typeof AuthMcpAuthorizationRequest.Type;
568
569/**
570 * What the approval page needs to show for an MCP OAuth sign-in. The server
571 * has already validated the request; nothing here is trusted by the client
572 * except for display.
573 */
574export const AuthMcpApprovalDetails = Schema.Struct({
575 /** Self-declared by the client, so shown as such. */
576 clientName: Schema.String,
577 /**
578 * Where the code goes: a loopback address on the browser's machine (a CLI
579 * agent), or an https host (a hosted agent).
580 */
581 redirectHost: Schema.String,
582 environmentHost: Schema.String,
583 /** Present when this browser's session may approve without a pairing code. */
584 csrfToken: Schema.optionalKey(Schema.String),
585 /**
586 * What that session may approve in one click: only access whose scopes it
587 * holds. Anything else still needs a pairing code. Absent with `csrfToken`.
588 */
589 oneClickAccess: Schema.optionalKey(Schema.Array(AuthMcpClientAccess)),
590});
591export type AuthMcpApprovalDetails = typeof AuthMcpApprovalDetails.Type;
592
593/**
594 * Where the approval page sends the browser next: back to the agent with a
595 * code, a denial, or a protocol error the agent should receive.
596 */
597export const AuthMcpApprovalRedirect = Schema.Struct({
598 redirectTo: Schema.String,
599});
600export type AuthMcpApprovalRedirect = typeof AuthMcpApprovalRedirect.Type;
601
602export const AuthMcpApprovalDecision = Schema.Union([
603 Schema.TaggedStruct("deny", {}),
604 Schema.TaggedStruct("pairing-code", {
605 access: AuthMcpClientAccess,
606 code: TrimmedNonEmptyString,
607 }),
608 /** One click, for a browser session that may approve (see `csrfToken`). */
609 Schema.TaggedStruct("browser-session", {
610 access: AuthMcpClientAccess,
611 csrfToken: Schema.String,
612 }),
613]);
614export type AuthMcpApprovalDecision = typeof AuthMcpApprovalDecision.Type;
615
616export const AuthMcpApprovalDecisionRequest = Schema.Struct({
617 authorization: AuthMcpAuthorizationRequest,
618 decision: AuthMcpApprovalDecision,
619});
620export type AuthMcpApprovalDecisionRequest = typeof AuthMcpApprovalDecisionRequest.Type;
621
622/** A problem the approval page shows the user without redirecting anywhere. */
623export class AuthMcpApprovalError extends Schema.TaggedError<AuthMcpApprovalError>()(
624 "AuthMcpApprovalError",
625 { message: Schema.String },
626 { httpApiStatus: 400 },
627) {}
628
629/** RFC 6749 §4.1.3 token request. Every field is checked by the server. */
630export const AuthMcpTokenRequest = Schema.Struct({
631 grant_type: Schema.optionalKey(Schema.String),
632 code: Schema.optionalKey(Schema.String),
633 redirect_uri: Schema.optionalKey(Schema.String),
634 client_id: Schema.optionalKey(Schema.String),
635 code_verifier: Schema.optionalKey(Schema.String),
636 resource: Schema.optionalKey(Schema.String),
637}).pipe(HttpApiSchema.asFormUrlEncoded());
638export type AuthMcpTokenRequest = typeof AuthMcpTokenRequest.Type;
639
640export const AuthMcpTokenResult = Schema.Struct({
641 access_token: Schema.String,
642 token_type: Schema.Literal("Bearer"),
643 expires_in: Schema.Number,
644 scope: Schema.String,
645});
646export type AuthMcpTokenResult = typeof AuthMcpTokenResult.Type;
647
648/** RFC 6749 §5.2 token error. */
649export class AuthMcpTokenError extends Schema.Error<AuthMcpTokenError>("AuthMcpTokenError")(
650 {
651 error: Schema.Literals([
652 "invalid_request",
653 "invalid_client",
654 "invalid_grant",
655 "unsupported_grant_type",
656 ]),
657 error_description: Schema.String,
658 },
659 { httpApiStatus: 400 },
660) {
661 override get message(): string {
662 return this.error_description;
663 }
664}