1import * as Schema from "effect/Schema";
2import * as HttpApiSchema from "effect/http-api/HttpApiSchema";
3
4import {
5 AuthSessionId,
6 ForwardCompatibleArray,
7 ClientSurface,
8 ClientWebDeployment,
9 TrimmedNonEmptyString,
10} from "./baseSchemas.ts";
11import { RuntimeMode } from "./providerPolicy.ts";
12
13export const ServerAuthPolicy = Schema.Literals([
14 "desktop-managed-local",
15 "loopback-browser",
16 "remote-reachable",
17 "unsafe-no-auth",
18]);
19export type ServerAuthPolicy = typeof ServerAuthPolicy.Type;
20
21export const ServerAuthBootstrapMethod = Schema.Literals(["desktop-bootstrap", "one-time-token"]);
22export type ServerAuthBootstrapMethod = typeof ServerAuthBootstrapMethod.Type;
23
24export const ServerAuthSessionMethod = Schema.Literals([
25 "browser-session-cookie",
26 "bearer-access-token",
27 "dpop-access-token",
28]);
29export type ServerAuthSessionMethod = typeof ServerAuthSessionMethod.Type;
30
31export const AuthOrchestrationReadScope = "orchestration:read" as const;
32export const AuthOrchestrationOperateScope = "orchestration:operate" as const;
33export const AuthSettingsWriteScope = "settings:write" as const;
34export const AuthProvidersManageScope = "providers:manage" as const;
35export const AuthEnvironmentMaintainScope = "environment:maintain" as const;
36export const AuthPreviewOperateScope = "preview:operate" as const;
37export const AuthDiagnosticsReadScope = "diagnostics:read" as const;
38export const AuthTerminalReadScope = "terminal:read" as const;
39export const AuthTerminalOperateScope = "terminal:operate" as const;
40export const AuthSourceControlWriteScope = "source-control:write" as const;
41export const AuthFilesystemReadScope = "filesystem:read" as const;
42export const AuthFilesystemWriteScope = "filesystem:write" as const;
43/** Retained for decoding existing credentials; grants no current RPC access. */
44const AuthReviewWriteScope = "review:write" as const;
45export const AuthAccessReadScope = "access:read" as const;
46export const AuthAccessWriteScope = "access:write" as const;
47export const AuthRelayReadScope = "relay:read" as const;
48export const AuthRelayWriteScope = "relay:write" as const;
49export const AuthEnvironmentScope = Schema.Literals([
50 AuthOrchestrationReadScope,
51 AuthOrchestrationOperateScope,
52 AuthSettingsWriteScope,
53 AuthProvidersManageScope,
54 AuthEnvironmentMaintainScope,
55 AuthPreviewOperateScope,
56 AuthDiagnosticsReadScope,
57 AuthTerminalReadScope,
58 AuthTerminalOperateScope,
59 AuthFilesystemReadScope,
60 AuthFilesystemWriteScope,
61 AuthReviewWriteScope,
62 AuthSourceControlWriteScope,
63 AuthAccessReadScope,
64 AuthAccessWriteScope,
65 AuthRelayReadScope,
66 AuthRelayWriteScope,
67]);
68export type AuthEnvironmentScope = typeof AuthEnvironmentScope.Type;
69export const AuthEnvironmentScopes = Schema.Array(AuthEnvironmentScope);
70export type AuthEnvironmentScopes = typeof AuthEnvironmentScopes.Type;
71
72export const AuthGrantScope = Schema.Literals(
73 AuthEnvironmentScope.literals.filter((scope) => scope !== AuthReviewWriteScope),
74);
75export type AuthGrantScope = typeof AuthGrantScope.Type;
76export const AuthGrantScopes = Schema.Array(AuthGrantScope);
77export type AuthGrantScopes = typeof AuthGrantScopes.Type;
78
79const legacyScopes = new Set<AuthEnvironmentScope>([
80 AuthOrchestrationReadScope,
81 AuthOrchestrationOperateScope,
82 AuthTerminalOperateScope,
83 AuthReviewWriteScope,
84 AuthAccessReadScope,
85 AuthAccessWriteScope,
86 AuthRelayReadScope,
87 AuthRelayWriteScope,
88]);
89
90export function authScopeResponse(scopes: ReadonlyArray<AuthEnvironmentScope>) {
91 return { scopes: scopes.filter((scope) => legacyScopes.has(scope)), permissions: scopes };
92}
93
94const authScopeResponseFields = {
95 scopes: AuthEnvironmentScopes,
96 permissions: Schema.optionalKey(ForwardCompatibleArray(AuthEnvironmentScope)),
97};
98
99// Client-only fallback for old servers. Servers never expand grants; empty permissions grant nothing.
100const legacyParents: Partial<Record<AuthEnvironmentScope, AuthEnvironmentScope>> = {
101 [AuthFilesystemReadScope]: AuthOrchestrationReadScope,
102 [AuthDiagnosticsReadScope]: AuthOrchestrationReadScope,
103 [AuthSettingsWriteScope]: AuthOrchestrationOperateScope,
104 [AuthProvidersManageScope]: AuthOrchestrationOperateScope,
105 [AuthEnvironmentMaintainScope]: AuthOrchestrationOperateScope,
106 [AuthPreviewOperateScope]: AuthOrchestrationOperateScope,
107 [AuthSourceControlWriteScope]: AuthOrchestrationOperateScope,
108 [AuthFilesystemWriteScope]: AuthOrchestrationOperateScope,
109 [AuthTerminalReadScope]: AuthTerminalOperateScope,
110};
111
112/** Keep permission denials decodable by clients with the original scope enum. */
113export function authScopeRequiredResponse(requiredPermission: AuthEnvironmentScope) {
114 return {
115 requiredScope: legacyParents[requiredPermission] ?? requiredPermission,
116 requiredPermission,
117 };
118}
119
120export interface SessionGrantInput {
121 readonly authenticated: boolean;
122 readonly scopes?: ReadonlyArray<AuthEnvironmentScope> | undefined;
123 readonly permissions?: ReadonlyArray<AuthEnvironmentScope> | undefined;
124 readonly auth?: { readonly serverUpdateScope?: string | undefined } | undefined;
125}
126
127export function sessionGrantsScope(
128 session: SessionGrantInput,
129 scope: AuthEnvironmentScope,
130): boolean {
131 if (!session.authenticated) return false;
132 if (session.permissions !== undefined) return session.permissions.includes(scope);
133 if (session.scopes?.includes(scope)) return true;
134 if (session.auth?.serverUpdateScope !== undefined) return false;
135 const parent = legacyParents[scope];
136 return parent !== undefined && session.scopes?.includes(parent) === true;
137}
138
139export function sessionHasLegacyPermissions(session: SessionGrantInput): boolean {
140 const permissions = session.permissions;
141 return (
142 session.authenticated &&
143 permissions !== undefined &&
144 permissions.every((scope) => legacyScopes.has(scope)) &&
145 Object.values(legacyParents).some((parent) => permissions.includes(parent))
146 );
147}
148
149// Shell and full-access agent scopes already confer access and relay authority.
150export const AuthStandardClientScopes = [
151 AuthOrchestrationReadScope,
152 AuthOrchestrationOperateScope,
153 AuthSettingsWriteScope,
154 AuthProvidersManageScope,
155 AuthEnvironmentMaintainScope,
156 AuthPreviewOperateScope,
157 AuthDiagnosticsReadScope,
158 AuthTerminalReadScope,
159 AuthTerminalOperateScope,
160 AuthSourceControlWriteScope,
161 AuthFilesystemReadScope,
162 AuthFilesystemWriteScope,
163 AuthRelayReadScope,
164 AuthAccessReadScope,
165 AuthAccessWriteScope,
166 AuthRelayWriteScope,
167] as const;
168
169export const AuthTokenExchangeGrantType =
170 "urn:ietf:params:oauth:grant-type:token-exchange" as const;
171export const AuthAccessTokenType = "urn:ietf:params:oauth:token-type:access_token" as const;
172export const AuthEnvironmentBootstrapTokenType =
173 "urn:t3:params:oauth:token-type:environment-bootstrap" as const;
174
175export const ServerAuthDescriptor = Schema.Struct({
176 policy: ServerAuthPolicy,
177 bootstrapMethods: Schema.Array(ServerAuthBootstrapMethod),
178 sessionMethods: Schema.Array(ServerAuthSessionMethod),
179 sessionCookieName: TrimmedNonEmptyString,
180 /** Older servers omit this and authorize self-updates with orchestration:operate. */
181 serverUpdateScope: Schema.optionalKey(Schema.Literal(AuthEnvironmentMaintainScope)),
182});
183export type ServerAuthDescriptor = typeof ServerAuthDescriptor.Type;
184
185export const AuthBrowserSessionRequest = Schema.Struct({
186 credential: TrimmedNonEmptyString,
187});
188export type AuthBrowserSessionRequest = typeof AuthBrowserSessionRequest.Type;
189
190export const AuthBrowserSessionResult = Schema.Struct({
191 authenticated: Schema.Literal(true),
192 ...authScopeResponseFields,
193 sessionMethod: ServerAuthSessionMethod,
194 expiresAt: Schema.DateTimeUtc,
195});
196export type AuthBrowserSessionResult = typeof AuthBrowserSessionResult.Type;
197
198export const AuthClientMetadataDeviceType = Schema.Literals([
199 "desktop",
200 "mobile",
201 "tablet",
202 "bot",
203 "unknown",
204]);
205export type AuthClientMetadataDeviceType = typeof AuthClientMetadataDeviceType.Type;
206
207export const AuthClientPresentationMetadata = Schema.Struct({
208 label: Schema.optionalKey(TrimmedNonEmptyString),
209 deviceType: Schema.optionalKey(AuthClientMetadataDeviceType),
210 os: Schema.optionalKey(TrimmedNonEmptyString),
211 osMajorVersion: Schema.optionalKey(Schema.Int),
212 deviceModel: Schema.optionalKey(TrimmedNonEmptyString),
213 surface: Schema.optionalKey(ClientSurface),
214 webDeployment: Schema.optionalKey(ClientWebDeployment),
215 browser: Schema.optionalKey(TrimmedNonEmptyString),
216 appVersion: Schema.optionalKey(TrimmedNonEmptyString),
217});
218export type AuthClientPresentationMetadata = typeof AuthClientPresentationMetadata.Type;
219
220export const AuthTokenExchangeRequest = Schema.Struct({
221 grant_type: Schema.Literal(AuthTokenExchangeGrantType),
222 subject_token: TrimmedNonEmptyString,
223 subject_token_type: Schema.Literal(AuthEnvironmentBootstrapTokenType),
224 requested_token_type: Schema.Literal(AuthAccessTokenType),
225 scope: Schema.optionalKey(TrimmedNonEmptyString),
226 client_label: Schema.optionalKey(TrimmedNonEmptyString),
227 client_device_type: Schema.optionalKey(AuthClientMetadataDeviceType),
228 client_os: Schema.optionalKey(TrimmedNonEmptyString),
229}).pipe(HttpApiSchema.asFormUrlEncoded());
230export type AuthTokenExchangeRequest = typeof AuthTokenExchangeRequest.Type;
231
232export const AuthAccessTokenResult = Schema.Struct({
233 access_token: TrimmedNonEmptyString,
234 issued_token_type: Schema.Literal(AuthAccessTokenType),
235 token_type: Schema.Literals(["Bearer", "DPoP"]),
236 expires_in: Schema.Number,
237 scope: TrimmedNonEmptyString,
238});
239export type AuthAccessTokenResult = typeof AuthAccessTokenResult.Type;
240
241export const AuthWebSocketTicketResult = Schema.Struct({
242 ticket: Schema.RedactedFromValue(TrimmedNonEmptyString),
243 expiresAt: Schema.DateTimeUtc,
244});
245export type AuthWebSocketTicketResult = typeof AuthWebSocketTicketResult.Type;
246
247/**
248 * Media routes share a ticket across requests; RPC uses a separate ticket.
249 * Use a path segment so the DPoP mint proof covers it; `htu` excludes query parameters,
250 * which could be stripped to change the ticket's use.
251 * Older clients mint at the bare path and receive tickets valid for either use.
252 */
253export const AuthWebSocketTicketUse = Schema.Literals(["rpc", "media"]);
254export type AuthWebSocketTicketUse = typeof AuthWebSocketTicketUse.Type;
255
256export const AuthWebSocketTicketParams = Schema.Struct({ use: AuthWebSocketTicketUse });
257
258export const AuthMcpClientAccess = Schema.Literals([
259 "read-only",
260 "approval-required",
261 "auto-accept-edits",
262 "auto",
263 "full-access",
264]);
265export type AuthMcpClientAccess = typeof AuthMcpClientAccess.Type;
266
267/** The scopes an agent session gets for an access level, and a pairing code must carry. */
268export const mcpClientScopes = (access: AuthMcpClientAccess): ReadonlyArray<AuthGrantScope> =>
269 access === "read-only"
270 ? [AuthOrchestrationReadScope]
271 : access === "full-access"
272 ? [
273 AuthOrchestrationReadScope,
274 AuthOrchestrationOperateScope,
275 AuthSettingsWriteScope,
276 AuthSourceControlWriteScope,
277 ]
278 : [AuthOrchestrationReadScope, AuthOrchestrationOperateScope];
279
280/** The random part of a one-time pairing code: this many characters from this alphabet. */
281export const PAIRING_CODE_ALPHABET = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ";
282export const PAIRING_CODE_LENGTH = 12;
283
284const MCP_APPROVAL_CODE_PREFIXES = {
285 "read-only": "READ",
286 "approval-required": "SUPERVISED",
287 "auto-accept-edits": "EDITS",
288 auto: "AUTO",
289 "full-access": "FULL",
290} as const satisfies Record<AuthMcpClientAccess, string>;
291
292const MCP_APPROVAL_CODE_PATTERN = new RegExp(
293 `^(${Object.values(MCP_APPROVAL_CODE_PREFIXES).join("|")})-([${PAIRING_CODE_ALPHABET}]{${PAIRING_CODE_LENGTH}})$`,
294 "i",
295);
296
297/** A pairing code that approves one agent sign-in at `access`, such as `AUTO-Q8RZ3NWD5KTC`. */
298export const mcpApprovalCode = (access: AuthMcpClientAccess, token: string): string =>
299 `${MCP_APPROVAL_CODE_PREFIXES[access]}-${token}`;
300
301/** The level a code approves and the code as issued, or undefined for any other code. */
302export function parseMcpApprovalCode(
303 code: string,
304): { readonly access: AuthMcpClientAccess; readonly code: string } | undefined {
305 const match = MCP_APPROVAL_CODE_PATTERN.exec(code.trim());
306 if (match === null) return undefined;
307 const prefix = match[1]!.toUpperCase();
308 const access = AuthMcpClientAccess.literals.find(
309 (level) => MCP_APPROVAL_CODE_PREFIXES[level] === prefix,
310 );
311 return access && { access, code: mcpApprovalCode(access, match[2]!.toUpperCase()) };
312}
313
314export const AuthPairingCredentialResult = Schema.Struct({
315 id: TrimmedNonEmptyString,
316 credential: TrimmedNonEmptyString,
317 label: Schema.optionalKey(TrimmedNonEmptyString),
318 expiresAt: Schema.DateTimeUtc,
319 /** Set when the code approves one agent sign-in at this level. Older servers omit it. */
320 access: Schema.optionalKey(AuthMcpClientAccess),
321});
322export type AuthPairingCredentialResult = typeof AuthPairingCredentialResult.Type;
323
324export const AuthPairingLink = Schema.Struct({
325 id: TrimmedNonEmptyString,
326 ...authScopeResponseFields,
327 subject: TrimmedNonEmptyString,
328 label: Schema.optionalKey(TrimmedNonEmptyString),
329 createdAt: Schema.DateTimeUtc,
330 expiresAt: Schema.DateTimeUtc,
331});
332export type AuthPairingLink = typeof AuthPairingLink.Type;
333
334export const AuthClientMetadata = Schema.Struct({
335 label: Schema.optionalKey(TrimmedNonEmptyString),
336 ipAddress: Schema.optionalKey(TrimmedNonEmptyString),
337 userAgent: Schema.optionalKey(TrimmedNonEmptyString),
338 deviceType: AuthClientMetadataDeviceType,
339 os: Schema.optionalKey(TrimmedNonEmptyString),
340 browser: Schema.optionalKey(TrimmedNonEmptyString),
341});
342export type AuthClientMetadata = typeof AuthClientMetadata.Type;
343
344export const AuthClientSession = Schema.Struct({
345 sessionId: AuthSessionId,
346 subject: TrimmedNonEmptyString,
347 ...authScopeResponseFields,
348 method: ServerAuthSessionMethod,
349 client: AuthClientMetadata,
350 issuedAt: Schema.DateTimeUtc,
351 expiresAt: Schema.DateTimeUtc,
352 lastConnectedAt: Schema.NullOr(Schema.DateTimeUtc),
353 connected: Schema.Boolean,
354 current: Schema.Boolean,
355});
356export type AuthClientSession = typeof AuthClientSession.Type;
357
358export const AuthAccessSnapshot = Schema.Struct({
359 pairingLinks: Schema.Array(AuthPairingLink),
360 clientSessions: Schema.Array(AuthClientSession),
361});
362export type AuthAccessSnapshot = typeof AuthAccessSnapshot.Type;
363
364export const AuthAccessStreamSnapshotEvent = Schema.Struct({
365 version: Schema.Literal(1),
366 revision: Schema.Number,
367 type: Schema.Literal("snapshot"),
368 payload: AuthAccessSnapshot,
369});
370export type AuthAccessStreamSnapshotEvent = typeof AuthAccessStreamSnapshotEvent.Type;
371
372export const AuthAccessStreamPairingLinkUpsertedEvent = Schema.Struct({
373 version: Schema.Literal(1),
374 revision: Schema.Number,
375 type: Schema.Literal("pairingLinkUpserted"),
376 payload: AuthPairingLink,
377});
378export type AuthAccessStreamPairingLinkUpsertedEvent =
379 typeof AuthAccessStreamPairingLinkUpsertedEvent.Type;
380
381export const AuthAccessStreamPairingLinkRemovedEvent = Schema.Struct({
382 version: Schema.Literal(1),
383 revision: Schema.Number,
384 type: Schema.Literal("pairingLinkRemoved"),
385 payload: Schema.Struct({
386 id: TrimmedNonEmptyString,
387 }),
388});
389export type AuthAccessStreamPairingLinkRemovedEvent =
390 typeof AuthAccessStreamPairingLinkRemovedEvent.Type;
391
392export class AuthAccessStreamError extends Schema.TaggedError<AuthAccessStreamError>()(
393 "AuthAccessStreamError",
394 {
395 message: Schema.String,
396 },
397) {}
398
399export class EnvironmentAuthorizationError extends Schema.TaggedError<EnvironmentAuthorizationError>()(
400 "EnvironmentAuthorizationError",
401 {
402 message: Schema.String,
403 requiredScope: AuthEnvironmentScope,
404 requiredPermission: Schema.optionalKey(Schema.String),
405 },
406) {}
407
408export const AuthAccessStreamClientUpsertedEvent = Schema.Struct({
409 version: Schema.Literal(1),
410 revision: Schema.Number,
411 type: Schema.Literal("clientUpserted"),
412 payload: AuthClientSession,
413});
414export type AuthAccessStreamClientUpsertedEvent = typeof AuthAccessStreamClientUpsertedEvent.Type;
415
416export const AuthAccessStreamClientRemovedEvent = Schema.Struct({
417 version: Schema.Literal(1),
418 revision: Schema.Number,
419 type: Schema.Literal("clientRemoved"),
420 payload: Schema.Struct({
421 sessionId: AuthSessionId,
422 }),
423});
424export type AuthAccessStreamClientRemovedEvent = typeof AuthAccessStreamClientRemovedEvent.Type;
425
426export const AuthAccessStreamEvent = Schema.Union([
427 AuthAccessStreamSnapshotEvent,
428 AuthAccessStreamPairingLinkUpsertedEvent,
429 AuthAccessStreamPairingLinkRemovedEvent,
430 AuthAccessStreamClientUpsertedEvent,
431 AuthAccessStreamClientRemovedEvent,
432]);
433export type AuthAccessStreamEvent = typeof AuthAccessStreamEvent.Type;
434
435export const AuthRevokePairingLinkInput = Schema.Struct({
436 id: TrimmedNonEmptyString,
437});
438export type AuthRevokePairingLinkInput = typeof AuthRevokePairingLinkInput.Type;
439
440export const AuthRevokeClientSessionInput = Schema.Struct({
441 sessionId: AuthSessionId,
442});
443export type AuthRevokeClientSessionInput = typeof AuthRevokeClientSessionInput.Type;
444
445export const AuthCreatePairingCredentialInput = Schema.Struct({
446 label: Schema.optionalKey(TrimmedNonEmptyString),
447 scopes: Schema.optionalKey(AuthGrantScopes),
448 /** Binds the code to one agent sign-in at this level. The server derives the scopes from it. */
449 access: Schema.optionalKey(AuthMcpClientAccess),
450});
451export type AuthCreatePairingCredentialInput = typeof AuthCreatePairingCredentialInput.Type;
452
453export const AuthSessionState = Schema.Struct({
454 authenticated: Schema.Boolean,
455 auth: ServerAuthDescriptor,
456 scopes: Schema.optionalKey(AuthEnvironmentScopes),
457 permissions: authScopeResponseFields.permissions,
458 sessionMethod: Schema.optionalKey(ServerAuthSessionMethod),
459 expiresAt: Schema.optionalKey(Schema.DateTimeUtc),
460});
461export type AuthSessionState = typeof AuthSessionState.Type;
462
463export const AuthMcpProtectedResourceMetadata = Schema.Struct({
464 resource: Schema.String,
465 authorization_servers: Schema.Array(Schema.String),
466 scopes_supported: Schema.Array(AuthEnvironmentScope),
467 bearer_methods_supported: Schema.Array(Schema.Literal("header")),
468 resource_name: Schema.String,
469});
470export type AuthMcpProtectedResourceMetadata = typeof AuthMcpProtectedResourceMetadata.Type;
471
472export const AuthMcpAuthorizationServerMetadata = Schema.Struct({
473 issuer: Schema.String,
474 authorization_endpoint: Schema.String,
475 token_endpoint: Schema.String,
476 registration_endpoint: Schema.String,
477 response_types_supported: Schema.Array(Schema.Literal("code")),
478 grant_types_supported: Schema.Array(Schema.Literal("authorization_code")),
479 code_challenge_methods_supported: Schema.Array(Schema.Literal("S256")),
480 token_endpoint_auth_methods_supported: Schema.Array(Schema.Literal("none")),
481 scopes_supported: Schema.Array(AuthEnvironmentScope),
482 authorization_response_iss_parameter_supported: Schema.Boolean,
483});
484export type AuthMcpAuthorizationServerMetadata = typeof AuthMcpAuthorizationServerMetadata.Type;
485
486/**
487 * The `software_id` another T3 Code environment registers with when it links
488 * to this one. Its session is then a peer link: the work it starts here stays
489 * within that link. Claiming it only ever adds limits.
490 */
491export const MCP_PEER_LINK_SOFTWARE_ID = "t3code-peer-link";
492
493export const AuthMcpClientRegistration = Schema.Struct({
494 client_name: Schema.optionalKey(Schema.String),
495 software_id: Schema.optionalKey(Schema.String),
496 redirect_uris: Schema.optionalKey(Schema.Array(Schema.String)),
497 token_endpoint_auth_method: Schema.optionalKey(Schema.String),
498});
499export type AuthMcpClientRegistration = typeof AuthMcpClientRegistration.Type;
500
501export const AuthMcpRegisteredClient = Schema.Struct({
502 client_id: Schema.String,
503 client_name: Schema.String,
504 redirect_uris: Schema.Array(Schema.String),
505 grant_types: Schema.Array(Schema.Literal("authorization_code")),
506 response_types: Schema.Array(Schema.Literal("code")),
507 token_endpoint_auth_method: Schema.Literal("none"),
508}).pipe(HttpApiSchema.status(201));
509export type AuthMcpRegisteredClient = typeof AuthMcpRegisteredClient.Type;
510
511export class AuthMcpRegistrationError extends Schema.Error<AuthMcpRegistrationError>(
512 "AuthMcpRegistrationError",
513)(
514 {
515 error: Schema.Literals(["invalid_client_metadata", "invalid_redirect_uri"]),
516 error_description: Schema.String,
517 },
518 { httpApiStatus: 400 },
519) {
520 override get message(): string {
521 return this.error_description;
522 }
523}
524
525/** Fields are optional here because the server validates them. */
526export const AuthMcpAuthorizationRequest = Schema.Struct({
527 response_type: Schema.optionalKey(Schema.String),
528 client_id: Schema.optionalKey(Schema.String),
529 redirect_uri: Schema.optionalKey(Schema.String),
530 code_challenge: Schema.optionalKey(Schema.String),
531 code_challenge_method: Schema.optionalKey(Schema.String),
532 state: Schema.optionalKey(Schema.RedactedFromValue(Schema.String)),
533 resource: Schema.optionalKey(Schema.String),
534});
535export type AuthMcpAuthorizationRequest = typeof AuthMcpAuthorizationRequest.Type;
536
537/** Server-validated request details, trusted by the client only for display. */
538export const AuthMcpApprovalDetails = Schema.Struct({
539 /** Self-declared by the client, so shown as such. */
540 clientName: Schema.String,
541 redirectHost: Schema.String,
542 environmentHost: Schema.String,
543 csrfToken: Schema.optionalKey(Schema.String),
544 /** Only scopes held by this session qualify; others need a pairing code. Absent with `csrfToken`. */
545 oneClickAccess: Schema.optionalKey(Schema.Array(AuthMcpClientAccess)),
546 /** A managed policy's runtime-mode ceiling; the page loads without the server's config. */
547 runtimeModeCeiling: Schema.optionalKey(
548 Schema.Struct({ organization: Schema.String, maxRuntimeMode: RuntimeMode }),
549 ),
550});
551export type AuthMcpApprovalDetails = typeof AuthMcpApprovalDetails.Type;
552
553export const AuthMcpApprovalRedirect = Schema.Struct({
554 redirectTo: Schema.String,
555});
556export type AuthMcpApprovalRedirect = typeof AuthMcpApprovalRedirect.Type;
557
558export const AuthMcpApprovalDecision = Schema.Union([
559 Schema.TaggedStruct("deny", {}),
560 Schema.TaggedStruct("pairing-code", {
561 access: AuthMcpClientAccess,
562 code: TrimmedNonEmptyString,
563 }),
564 Schema.TaggedStruct("browser-session", {
565 access: AuthMcpClientAccess,
566 csrfToken: Schema.String,
567 }),
568]);
569export type AuthMcpApprovalDecision = typeof AuthMcpApprovalDecision.Type;
570
571export const AuthMcpApprovalDecisionRequest = Schema.Struct({
572 authorization: AuthMcpAuthorizationRequest,
573 decision: AuthMcpApprovalDecision,
574});
575export type AuthMcpApprovalDecisionRequest = typeof AuthMcpApprovalDecisionRequest.Type;
576
577export class AuthMcpApprovalError extends Schema.TaggedError<AuthMcpApprovalError>()(
578 "AuthMcpApprovalError",
579 { message: Schema.String },
580 { httpApiStatus: 400 },
581) {}
582
583export const AuthMcpTokenRequest = Schema.Struct({
584 grant_type: Schema.optionalKey(Schema.String),
585 code: Schema.optionalKey(Schema.RedactedFromValue(Schema.String)),
586 redirect_uri: Schema.optionalKey(Schema.String),
587 client_id: Schema.optionalKey(Schema.String),
588 code_verifier: Schema.optionalKey(Schema.RedactedFromValue(Schema.String)),
589 resource: Schema.optionalKey(Schema.String),
590}).pipe(HttpApiSchema.asFormUrlEncoded());
591export type AuthMcpTokenRequest = typeof AuthMcpTokenRequest.Type;
592
593export const AuthMcpTokenResult = Schema.Struct({
594 access_token: Schema.String,
595 token_type: Schema.Literal("Bearer"),
596 expires_in: Schema.Number,
597 scope: Schema.String,
598});
599export type AuthMcpTokenResult = typeof AuthMcpTokenResult.Type;
600
601export class AuthMcpTokenError extends Schema.Error<AuthMcpTokenError>("AuthMcpTokenError")(
602 {
603 error: Schema.Literals([
604 "invalid_request",
605 "invalid_client",
606 "invalid_grant",
607 "unsupported_grant_type",
608 ]),
609 error_description: Schema.String,
610 },
611 { httpApiStatus: 400 },
612) {
613 override get message(): string {
614 return this.error_description;
615 }
616}