| 1 | import * as Schema from "effect/Schema"; |
| 2 | import * as HttpApiSchema from "effect/http-api/HttpApiSchema"; |
| 3 | |
| 4 | import { |
| 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 | */ |
| 34 | export const ServerAuthPolicy = Schema.Literals([ |
| 35 | "desktop-managed-local", |
| 36 | "loopback-browser", |
| 37 | "remote-reachable", |
| 38 | "unsafe-no-auth", |
| 39 | ]); |
| 40 | export 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 | */ |
| 55 | export const ServerAuthBootstrapMethod = Schema.Literals(["desktop-bootstrap", "one-time-token"]); |
| 56 | export 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 | */ |
| 74 | export const ServerAuthSessionMethod = Schema.Literals([ |
| 75 | "browser-session-cookie", |
| 76 | "bearer-access-token", |
| 77 | "dpop-access-token", |
| 78 | ]); |
| 79 | export type ServerAuthSessionMethod = typeof ServerAuthSessionMethod.Type; |
| 80 | |
| 81 | export const AuthOrchestrationReadScope = "orchestration:read" as const; |
| 82 | export const AuthOrchestrationOperateScope = "orchestration:operate" as const; |
| 83 | export const AuthEnvironmentConnectScope = "environment:connect" as const; |
| 84 | export const AuthArchiveReadScope = "archive:read" as const; |
| 85 | export const AuthArchiveWriteScope = "archive:write" as const; |
| 86 | export const AuthSettingsWriteScope = "settings:write" as const; |
| 87 | export const AuthProvidersManageScope = "providers:manage" as const; |
| 88 | export const AuthEnvironmentMaintainScope = "environment:maintain" as const; |
| 89 | export const AuthPreviewOperateScope = "preview:operate" as const; |
| 90 | export const AuthDiagnosticsReadScope = "diagnostics:read" as const; |
| 91 | export const AuthTerminalReadScope = "terminal:read" as const; |
| 92 | export const AuthTerminalOperateScope = "terminal:operate" as const; |
| 93 | export const AuthSourceControlWriteScope = "source-control:write" as const; |
| 94 | export const AuthFilesystemReadScope = "filesystem:read" as const; |
| 95 | export const AuthFilesystemWriteScope = "filesystem:write" as const; |
| 96 | /** Retained for decoding existing credentials; grants no current RPC access. */ |
| 97 | const AuthReviewWriteScope = "review:write" as const; |
| 98 | export const AuthAccessReadScope = "access:read" as const; |
| 99 | export const AuthAccessWriteScope = "access:write" as const; |
| 100 | export const AuthRelayReadScope = "relay:read" as const; |
| 101 | export const AuthRelayWriteScope = "relay:write" as const; |
| 102 | export 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 | ]); |
| 124 | export type AuthEnvironmentScope = typeof AuthEnvironmentScope.Type; |
| 125 | export const AuthEnvironmentScopes = Schema.Array(AuthEnvironmentScope); |
| 126 | export type AuthEnvironmentScopes = typeof AuthEnvironmentScopes.Type; |
| 127 | |
| 128 | export const AuthGrantScope = Schema.Literals( |
| 129 | AuthEnvironmentScope.literals.filter((scope) => scope !== AuthReviewWriteScope), |
| 130 | ); |
| 131 | export type AuthGrantScope = typeof AuthGrantScope.Type; |
| 132 | export const AuthGrantScopes = Schema.Array(AuthGrantScope); |
| 133 | export type AuthGrantScopes = typeof AuthGrantScopes.Type; |
| 134 | |
| 135 | // Frozen wire vocabulary for clients released before granular permissions. |
| 136 | const 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. */ |
| 148 | export function authScopeResponse(scopes: ReadonlyArray<AuthEnvironmentScope>) { |
| 149 | return { scopes: scopes.filter((scope) => legacyScopes.has(scope)), permissions: scopes }; |
| 150 | } |
| 151 | |
| 152 | const 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. |
| 159 | const 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. */ |
| 174 | export function authScopeRequiredResponse(requiredPermission: AuthEnvironmentScope) { |
| 175 | return { |
| 176 | requiredScope: legacyParents[requiredPermission] ?? requiredPermission, |
| 177 | requiredPermission, |
| 178 | }; |
| 179 | } |
| 180 | |
| 181 | export 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 | |
| 188 | export 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. */ |
| 202 | export 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 | |
| 212 | export 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; |
| 229 | export const AuthAdministrativeScopes = [ |
| 230 | AuthEnvironmentConnectScope, |
| 231 | ...AuthStandardClientScopes, |
| 232 | AuthAccessReadScope, |
| 233 | AuthAccessWriteScope, |
| 234 | AuthRelayWriteScope, |
| 235 | ] as const; |
| 236 | |
| 237 | export const AuthTokenExchangeGrantType = |
| 238 | "urn:ietf:params:oauth:grant-type:token-exchange" as const; |
| 239 | export const AuthAccessTokenType = "urn:ietf:params:oauth:token-type:access_token" as const; |
| 240 | export 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 | */ |
| 263 | export 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 | }); |
| 271 | export type ServerAuthDescriptor = typeof ServerAuthDescriptor.Type; |
| 272 | |
| 273 | export const AuthBrowserSessionRequest = Schema.Struct({ |
| 274 | credential: TrimmedNonEmptyString, |
| 275 | }); |
| 276 | export type AuthBrowserSessionRequest = typeof AuthBrowserSessionRequest.Type; |
| 277 | |
| 278 | export const AuthBrowserSessionResult = Schema.Struct({ |
| 279 | authenticated: Schema.Literal(true), |
| 280 | ...authScopeResponseFields, |
| 281 | sessionMethod: ServerAuthSessionMethod, |
| 282 | expiresAt: Schema.DateTimeUtc, |
| 283 | }); |
| 284 | export type AuthBrowserSessionResult = typeof AuthBrowserSessionResult.Type; |
| 285 | |
| 286 | export const AuthClientMetadataDeviceType = Schema.Literals([ |
| 287 | "desktop", |
| 288 | "mobile", |
| 289 | "tablet", |
| 290 | "bot", |
| 291 | "unknown", |
| 292 | ]); |
| 293 | export type AuthClientMetadataDeviceType = typeof AuthClientMetadataDeviceType.Type; |
| 294 | |
| 295 | export 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 | }); |
| 302 | export type AuthClientPresentationMetadata = typeof AuthClientPresentationMetadata.Type; |
| 303 | |
| 304 | export 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()); |
| 314 | export type AuthTokenExchangeRequest = typeof AuthTokenExchangeRequest.Type; |
| 315 | |
| 316 | export 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 | }); |
| 323 | export type AuthAccessTokenResult = typeof AuthAccessTokenResult.Type; |
| 324 | |
| 325 | export const AuthWebSocketTicketResult = Schema.Struct({ |
| 326 | ticket: TrimmedNonEmptyString, |
| 327 | expiresAt: Schema.DateTimeUtc, |
| 328 | }); |
| 329 | export type AuthWebSocketTicketResult = typeof AuthWebSocketTicketResult.Type; |
| 330 | |
| 331 | export const AuthPairingCredentialResult = Schema.Struct({ |
| 332 | id: TrimmedNonEmptyString, |
| 333 | credential: TrimmedNonEmptyString, |
| 334 | label: Schema.optionalKey(TrimmedNonEmptyString), |
| 335 | expiresAt: Schema.DateTimeUtc, |
| 336 | }); |
| 337 | export type AuthPairingCredentialResult = typeof AuthPairingCredentialResult.Type; |
| 338 | |
| 339 | // Read models contain metadata only. Credentials are returned by creation alone. |
| 340 | export 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 | }); |
| 348 | export type AuthPairingLink = typeof AuthPairingLink.Type; |
| 349 | |
| 350 | export 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 | }); |
| 358 | export type AuthClientMetadata = typeof AuthClientMetadata.Type; |
| 359 | |
| 360 | export 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 | }); |
| 372 | export type AuthClientSession = typeof AuthClientSession.Type; |
| 373 | |
| 374 | export const AuthAccessSnapshot = Schema.Struct({ |
| 375 | pairingLinks: Schema.Array(AuthPairingLink), |
| 376 | clientSessions: Schema.Array(AuthClientSession), |
| 377 | }); |
| 378 | export type AuthAccessSnapshot = typeof AuthAccessSnapshot.Type; |
| 379 | |
| 380 | export const AuthAccessStreamSnapshotEvent = Schema.Struct({ |
| 381 | version: Schema.Literal(1), |
| 382 | revision: Schema.Number, |
| 383 | type: Schema.Literal("snapshot"), |
| 384 | payload: AuthAccessSnapshot, |
| 385 | }); |
| 386 | export type AuthAccessStreamSnapshotEvent = typeof AuthAccessStreamSnapshotEvent.Type; |
| 387 | |
| 388 | export const AuthAccessStreamPairingLinkUpsertedEvent = Schema.Struct({ |
| 389 | version: Schema.Literal(1), |
| 390 | revision: Schema.Number, |
| 391 | type: Schema.Literal("pairingLinkUpserted"), |
| 392 | payload: AuthPairingLink, |
| 393 | }); |
| 394 | export type AuthAccessStreamPairingLinkUpsertedEvent = |
| 395 | typeof AuthAccessStreamPairingLinkUpsertedEvent.Type; |
| 396 | |
| 397 | export 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 | }); |
| 405 | export type AuthAccessStreamPairingLinkRemovedEvent = |
| 406 | typeof AuthAccessStreamPairingLinkRemovedEvent.Type; |
| 407 | |
| 408 | export class AuthAccessStreamError extends Schema.TaggedError<AuthAccessStreamError>()( |
| 409 | "AuthAccessStreamError", |
| 410 | { |
| 411 | message: Schema.String, |
| 412 | }, |
| 413 | ) {} |
| 414 | |
| 415 | export 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 | |
| 424 | export const AuthAccessStreamClientUpsertedEvent = Schema.Struct({ |
| 425 | version: Schema.Literal(1), |
| 426 | revision: Schema.Number, |
| 427 | type: Schema.Literal("clientUpserted"), |
| 428 | payload: AuthClientSession, |
| 429 | }); |
| 430 | export type AuthAccessStreamClientUpsertedEvent = typeof AuthAccessStreamClientUpsertedEvent.Type; |
| 431 | |
| 432 | export 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 | }); |
| 440 | export type AuthAccessStreamClientRemovedEvent = typeof AuthAccessStreamClientRemovedEvent.Type; |
| 441 | |
| 442 | export const AuthAccessStreamEvent = Schema.Union([ |
| 443 | AuthAccessStreamSnapshotEvent, |
| 444 | AuthAccessStreamPairingLinkUpsertedEvent, |
| 445 | AuthAccessStreamPairingLinkRemovedEvent, |
| 446 | AuthAccessStreamClientUpsertedEvent, |
| 447 | AuthAccessStreamClientRemovedEvent, |
| 448 | ]); |
| 449 | export type AuthAccessStreamEvent = typeof AuthAccessStreamEvent.Type; |
| 450 | |
| 451 | export const AuthRevokePairingLinkInput = Schema.Struct({ |
| 452 | id: TrimmedNonEmptyString, |
| 453 | }); |
| 454 | export type AuthRevokePairingLinkInput = typeof AuthRevokePairingLinkInput.Type; |
| 455 | |
| 456 | export const AuthRevokeClientSessionInput = Schema.Struct({ |
| 457 | sessionId: AuthSessionId, |
| 458 | }); |
| 459 | export type AuthRevokeClientSessionInput = typeof AuthRevokeClientSessionInput.Type; |
| 460 | |
| 461 | export const AuthCreatePairingCredentialInput = Schema.Struct({ |
| 462 | label: Schema.optionalKey(TrimmedNonEmptyString), |
| 463 | scopes: Schema.optionalKey(AuthGrantScopes), |
| 464 | }); |
| 465 | export type AuthCreatePairingCredentialInput = typeof AuthCreatePairingCredentialInput.Type; |
| 466 | |
| 467 | export const AuthCreateDelegatedSessionInput = Schema.Struct({ |
| 468 | label: Schema.optionalKey(TrimmedNonEmptyString), |
| 469 | scopes: AuthGrantScopes, |
| 470 | }); |
| 471 | export type AuthCreateDelegatedSessionInput = typeof AuthCreateDelegatedSessionInput.Type; |
| 472 | |
| 473 | export 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 | }); |
| 481 | export 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 | */ |
| 487 | export const AuthMcpClientAccess = Schema.Literals([ |
| 488 | "read-only", |
| 489 | "approval-required", |
| 490 | "auto-accept-edits", |
| 491 | "auto", |
| 492 | "full-access", |
| 493 | ]); |
| 494 | export type AuthMcpClientAccess = typeof AuthMcpClientAccess.Type; |
| 495 | |
| 496 | /** RFC 9728 metadata for an environment's `/mcp` resource. */ |
| 497 | export 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 | }); |
| 504 | export type AuthMcpProtectedResourceMetadata = typeof AuthMcpProtectedResourceMetadata.Type; |
| 505 | |
| 506 | /** RFC 8414 metadata for the authorization server MCP clients sign in through. */ |
| 507 | export 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 | }); |
| 519 | export type AuthMcpAuthorizationServerMetadata = typeof AuthMcpAuthorizationServerMetadata.Type; |
| 520 | |
| 521 | /** RFC 7591 client metadata. Fields the server does not use are dropped. */ |
| 522 | export 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 | }); |
| 527 | export type AuthMcpClientRegistration = typeof AuthMcpClientRegistration.Type; |
| 528 | |
| 529 | export 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)); |
| 537 | export type AuthMcpRegisteredClient = typeof AuthMcpRegisteredClient.Type; |
| 538 | |
| 539 | /** RFC 7591 §3.2.2 registration error. */ |
| 540 | export 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 | */ |
| 558 | export 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 | }); |
| 567 | export 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 | */ |
| 574 | export 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 | }); |
| 591 | export 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 | */ |
| 597 | export const AuthMcpApprovalRedirect = Schema.Struct({ |
| 598 | redirectTo: Schema.String, |
| 599 | }); |
| 600 | export type AuthMcpApprovalRedirect = typeof AuthMcpApprovalRedirect.Type; |
| 601 | |
| 602 | export 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 | ]); |
| 614 | export type AuthMcpApprovalDecision = typeof AuthMcpApprovalDecision.Type; |
| 615 | |
| 616 | export const AuthMcpApprovalDecisionRequest = Schema.Struct({ |
| 617 | authorization: AuthMcpAuthorizationRequest, |
| 618 | decision: AuthMcpApprovalDecision, |
| 619 | }); |
| 620 | export type AuthMcpApprovalDecisionRequest = typeof AuthMcpApprovalDecisionRequest.Type; |
| 621 | |
| 622 | /** A problem the approval page shows the user without redirecting anywhere. */ |
| 623 | export 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. */ |
| 630 | export 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()); |
| 638 | export type AuthMcpTokenRequest = typeof AuthMcpTokenRequest.Type; |
| 639 | |
| 640 | export 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 | }); |
| 646 | export type AuthMcpTokenResult = typeof AuthMcpTokenResult.Type; |
| 647 | |
| 648 | /** RFC 6749 §5.2 token error. */ |
| 649 | export 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 | } |