Skip to content
Source
Sign inSign up

auth.ts

616 lines23.4 KB

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 };
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 };
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;
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;
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 );
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()) };
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 }
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 }