import { AuthAccessTokenType, AuthAccessWriteScope, AuthAdministrativeScopes, AuthStandardClientScopes, type AuthAccessTokenResult, type AuthBrowserSessionResult, type AuthClientMetadata, type AuthClientSession, type AuthCreatePairingCredentialInput, type AuthEnvironmentScope, type AuthPairingLink, type AuthPairingCredentialResult, type AuthSessionId, type AuthSessionState, type ServerAuthDescriptor, type ServerAuthSessionMethod, type AuthWebSocketTicketResult, DpopFailureReason, type DpopFailureReason as DpopFailureReasonType, } from "@t3tools/contracts"; import { encodeOAuthScope } from "@t3tools/shared/oauthScope"; import * as Context from "effect/Context"; import * as Crypto from "effect/Crypto"; import * as DateTime from "effect/DateTime"; import * as Duration from "effect/Duration"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as Schema from "effect/Schema"; import * as HttpServerRequest from "effect/unstable/http/HttpServerRequest"; import * as ServerEnvironment from "../environment/ServerEnvironment.ts"; import * as EnvironmentAuthPolicy from "./EnvironmentAuthPolicy.ts"; import * as PairingGrantStore from "./PairingGrantStore.ts"; import * as ServerSecretStore from "./ServerSecretStore.ts"; import * as SessionStore from "./SessionStore.ts"; import { verifyRequestDpopProof } from "./dpop.ts"; import { layerConfig as SqlitePersistenceLayer } from "../persistence/Layers/Sqlite.ts"; const DEFAULT_SESSION_SUBJECT = "cli-issued-session"; export const INTERNAL_ADMINISTRATIVE_BOOTSTRAP_SUBJECT = "administrative-bootstrap"; export interface IssuedPairingLink { readonly id: string; readonly credential: string; readonly scopes: ReadonlyArray; readonly subject: string; readonly label?: string; readonly createdAt: DateTime.Utc; readonly expiresAt: DateTime.Utc; } export interface IssuedBearerSession { readonly sessionId: AuthSessionId; readonly token: string; readonly method: "bearer-access-token"; readonly scopes: ReadonlyArray; readonly subject: string; readonly client: AuthClientMetadata; readonly expiresAt: DateTime.Utc; } export interface AuthenticatedSession { readonly sessionId: AuthSessionId; readonly subject: string; readonly method: ServerAuthSessionMethod; readonly scopes: ReadonlyArray; readonly proofKeyThumbprint?: string; readonly expiresAt?: DateTime.DateTime; } const serverAuthInternalErrorContext = { cause: Schema.Defect(), }; export class ServerAuthBootstrapCredentialValidationError extends Schema.TaggedError()( "ServerAuthBootstrapCredentialValidationError", { ...serverAuthInternalErrorContext, }, ) { override get message(): string { return "Failed to validate bootstrap credential."; } } export class ServerAuthSessionCredentialValidationError extends Schema.TaggedError()( "ServerAuthSessionCredentialValidationError", { ...serverAuthInternalErrorContext, }, ) { override get message(): string { return "Failed to validate session credential."; } } export class ServerAuthAuthenticatedSessionIssueError extends Schema.TaggedError()( "ServerAuthAuthenticatedSessionIssueError", { ...serverAuthInternalErrorContext, }, ) { override get message(): string { return "Failed to issue authenticated session."; } } export class ServerAuthAuthenticatedAccessTokenIssueError extends Schema.TaggedError()( "ServerAuthAuthenticatedAccessTokenIssueError", { ...serverAuthInternalErrorContext, }, ) { override get message(): string { return "Failed to issue authenticated access token."; } } export class ServerAuthPairingLinkCreationError extends Schema.TaggedError()( "ServerAuthPairingLinkCreationError", { ...serverAuthInternalErrorContext, }, ) { override get message(): string { return "Failed to create pairing link."; } } export class ServerAuthPairingLinksListError extends Schema.TaggedError()( "ServerAuthPairingLinksListError", { ...serverAuthInternalErrorContext, }, ) { override get message(): string { return "Failed to list pairing links."; } } export class ServerAuthPairingLinkRevocationError extends Schema.TaggedError()( "ServerAuthPairingLinkRevocationError", { ...serverAuthInternalErrorContext, }, ) { override get message(): string { return "Failed to revoke pairing link."; } } export class ServerAuthSessionTokenIssueError extends Schema.TaggedError()( "ServerAuthSessionTokenIssueError", { ...serverAuthInternalErrorContext, }, ) { override get message(): string { return "Failed to issue session token."; } } export class ServerAuthSessionsListError extends Schema.TaggedError()( "ServerAuthSessionsListError", { ...serverAuthInternalErrorContext, }, ) { override get message(): string { return "Failed to list sessions."; } } export class ServerAuthSessionRevocationError extends Schema.TaggedError()( "ServerAuthSessionRevocationError", { ...serverAuthInternalErrorContext, }, ) { override get message(): string { return "Failed to revoke session."; } } export class ServerAuthOtherSessionsRevocationError extends Schema.TaggedError()( "ServerAuthOtherSessionsRevocationError", { ...serverAuthInternalErrorContext, }, ) { override get message(): string { return "Failed to revoke other sessions."; } } export class ServerAuthWebSocketTokenIssueError extends Schema.TaggedError()( "ServerAuthWebSocketTokenIssueError", { ...serverAuthInternalErrorContext, }, ) { override get message(): string { return "Failed to issue websocket token."; } } export class ServerAuthDpopReplayStateRecordError extends Schema.TaggedError()( "ServerAuthDpopReplayStateRecordError", { ...serverAuthInternalErrorContext, }, ) { override get message(): string { return "Failed to record DPoP proof replay state."; } } export class ServerAuthDpopReplayKeyCalculationError extends Schema.TaggedError()( "ServerAuthDpopReplayKeyCalculationError", { ...serverAuthInternalErrorContext, }, ) { override get message(): string { return "Failed to calculate DPoP replay key."; } } export class ServerAuthLinkedCloudAccountVerificationError extends Schema.TaggedError()( "ServerAuthLinkedCloudAccountVerificationError", { ...serverAuthInternalErrorContext, }, ) { override get message(): string { return "Could not verify the linked cloud account."; } } export class ServerAuthLinkedCloudAccountReadError extends Schema.TaggedError()( "ServerAuthLinkedCloudAccountReadError", { ...serverAuthInternalErrorContext, }, ) { override get message(): string { return "Could not read the linked cloud account."; } } export class ServerAuthLinkedCloudAccountMissingError extends Schema.TaggedError()( "ServerAuthLinkedCloudAccountMissingError", {}, ) { override get message(): string { return "Cloud linked user is not installed for this environment."; } } export class ServerAuthCloudLinkJwtSigningError extends Schema.TaggedError()( "ServerAuthCloudLinkJwtSigningError", { ...serverAuthInternalErrorContext, }, ) { override get message(): string { return "Failed to sign cloud link JWT."; } } export class ServerAuthCloudMintPublicKeyMissingError extends Schema.TaggedError()( "ServerAuthCloudMintPublicKeyMissingError", {}, ) { override get message(): string { return "Cloud mint public key is not installed for this environment."; } } export class ServerAuthCloudRelayIssuerMissingError extends Schema.TaggedError()( "ServerAuthCloudRelayIssuerMissingError", {}, ) { override get message(): string { return "Cloud relay issuer is not installed for this environment."; } } export class ServerAuthCloudHealthJwtSigningError extends Schema.TaggedError()( "ServerAuthCloudHealthJwtSigningError", { ...serverAuthInternalErrorContext, }, ) { override get message(): string { return "Failed to sign cloud health JWT."; } } export class ServerAuthCloudMintJwtSigningError extends Schema.TaggedError()( "ServerAuthCloudMintJwtSigningError", { ...serverAuthInternalErrorContext, }, ) { override get message(): string { return "Failed to sign cloud mint JWT."; } } export const ServerAuthInternalError = Schema.Union([ ServerAuthBootstrapCredentialValidationError, ServerAuthSessionCredentialValidationError, ServerAuthAuthenticatedSessionIssueError, ServerAuthAuthenticatedAccessTokenIssueError, ServerAuthPairingLinkCreationError, ServerAuthPairingLinksListError, ServerAuthPairingLinkRevocationError, ServerAuthSessionTokenIssueError, ServerAuthSessionsListError, ServerAuthSessionRevocationError, ServerAuthOtherSessionsRevocationError, ServerAuthWebSocketTokenIssueError, ServerAuthDpopReplayStateRecordError, ServerAuthDpopReplayKeyCalculationError, ServerAuthLinkedCloudAccountVerificationError, ServerAuthLinkedCloudAccountReadError, ServerAuthLinkedCloudAccountMissingError, ServerAuthCloudLinkJwtSigningError, ServerAuthCloudMintPublicKeyMissingError, ServerAuthCloudRelayIssuerMissingError, ServerAuthCloudHealthJwtSigningError, ServerAuthCloudMintJwtSigningError, ]); export type ServerAuthInternalError = typeof ServerAuthInternalError.Type; export const isServerAuthInternalError = Schema.is(ServerAuthInternalError); export class ServerAuthMissingCredentialError extends Schema.TaggedError()( "ServerAuthMissingCredentialError", {}, ) { override get message(): string { return "Server authentication credential is missing."; } } export class ServerAuthInvalidCredentialError extends Schema.TaggedError()( "ServerAuthInvalidCredentialError", { diagnostic: Schema.optional(Schema.String), dpopFailureReason: Schema.optionalKey(DpopFailureReason), cause: Schema.optional(Schema.Defect()), }, ) { override get message(): string { return "Server authentication credential is invalid."; } } export const ServerAuthCredentialError = Schema.Union([ ServerAuthMissingCredentialError, ServerAuthInvalidCredentialError, ]); export type ServerAuthCredentialError = typeof ServerAuthCredentialError.Type; export const isServerAuthCredentialError = Schema.is(ServerAuthCredentialError); export const serverAuthCredentialReason = ( error: ServerAuthCredentialError, ): "missing_credential" | "invalid_credential" => error._tag === "ServerAuthMissingCredentialError" ? "missing_credential" : "invalid_credential"; export const serverAuthDpopFailureReason = ( error: ServerAuthCredentialError, ): DpopFailureReasonType | undefined => error._tag === "ServerAuthInvalidCredentialError" ? error.dpopFailureReason : undefined; export class ServerAuthInvalidScopeError extends Schema.TaggedError()( "ServerAuthInvalidScopeError", {}, ) { override get message(): string { return "The requested authentication scope is invalid."; } } export class ServerAuthScopeNotGrantedError extends Schema.TaggedError()( "ServerAuthScopeNotGrantedError", {}, ) { override get message(): string { return "The requested authentication scope was not granted."; } } export const ServerAuthInvalidRequestError = Schema.Union([ ServerAuthInvalidScopeError, ServerAuthScopeNotGrantedError, ]); export type ServerAuthInvalidRequestError = typeof ServerAuthInvalidRequestError.Type; export const isServerAuthInvalidRequestError = Schema.is(ServerAuthInvalidRequestError); export const serverAuthInvalidRequestReason = ( error: ServerAuthInvalidRequestError, ): "invalid_scope" | "scope_not_granted" => error._tag === "ServerAuthInvalidScopeError" ? "invalid_scope" : "scope_not_granted"; export class ServerAuthForbiddenOperationError extends Schema.TaggedError()( "ServerAuthForbiddenOperationError", {}, ) { override get message(): string { return "The current authentication session cannot revoke itself."; } } export class EnvironmentAuth extends Context.Service< EnvironmentAuth, { readonly getDescriptor: () => Effect.Effect; readonly getSessionState: ( request: HttpServerRequest.HttpServerRequest, ) => Effect.Effect; readonly createBrowserSession: ( credential: string, requestMetadata: AuthClientMetadata, ) => Effect.Effect< { readonly response: AuthBrowserSessionResult; readonly sessionToken: string; }, ServerAuthInvalidCredentialError | ServerAuthInternalError >; readonly exchangeBootstrapCredentialForAccessToken: ( credential: string, requestedScopes: ReadonlyArray | undefined, requestMetadata: AuthClientMetadata, input?: { readonly proofKeyThumbprint?: string; }, ) => Effect.Effect< AuthAccessTokenResult, ServerAuthInvalidCredentialError | ServerAuthInvalidRequestError | ServerAuthInternalError >; readonly createPairingLink: (input?: { readonly ttl?: Duration.Duration; readonly label?: string; readonly scopes?: ReadonlyArray; readonly subject?: string; readonly proofKeyThumbprint?: string; readonly purpose?: "startup"; }) => Effect.Effect; readonly issuePairingCredential: ( input?: AuthCreatePairingCredentialInput, ) => Effect.Effect; readonly issueStartupPairingCredential: () => Effect.Effect< AuthPairingCredentialResult, ServerAuthInternalError >; readonly listPairingLinks: (input?: { readonly excludeSubjects?: ReadonlyArray; }) => Effect.Effect, ServerAuthInternalError>; readonly revokePairingLink: (id: string) => Effect.Effect; readonly issueSession: (input?: { readonly ttl?: Duration.Duration; readonly subject?: string; readonly scopes?: ReadonlyArray; readonly label?: string; }) => Effect.Effect; readonly listSessions: () => Effect.Effect< ReadonlyArray, ServerAuthInternalError >; readonly revokeSession: ( sessionId: AuthSessionId, ) => Effect.Effect; readonly revokeOtherSessionsExcept: ( sessionId: AuthSessionId, ) => Effect.Effect; readonly listClientSessions: ( currentSessionId: AuthSessionId, ) => Effect.Effect, ServerAuthInternalError>; readonly revokeClientSession: ( currentSessionId: AuthSessionId, targetSessionId: AuthSessionId, ) => Effect.Effect; readonly revokeOtherClientSessions: ( currentSessionId: AuthSessionId, ) => Effect.Effect; readonly authenticateHttpRequest: ( request: HttpServerRequest.HttpServerRequest, ) => Effect.Effect; readonly authenticateWebSocketUpgrade: ( request: HttpServerRequest.HttpServerRequest, ) => Effect.Effect; readonly issueWebSocketTicket: ( session: Pick, ) => Effect.Effect; readonly issueStartupPairingUrl: ( baseUrl: string, ) => Effect.Effect; } >()("t3/auth/EnvironmentAuth") {} type BootstrapExchangeResult = { readonly response: AuthBrowserSessionResult; readonly sessionToken: string; }; const AUTHORIZATION_PREFIX = "Bearer "; const DPOP_AUTHORIZATION_PREFIX = "DPoP "; const WEBSOCKET_TICKET_QUERY_PARAM = "wsTicket"; const bySessionPriority = (left: AuthClientSession, right: AuthClientSession) => { const leftCanManage = left.scopes.includes(AuthAccessWriteScope); const rightCanManage = right.scopes.includes(AuthAccessWriteScope); if (leftCanManage !== rightCanManage) { return leftCanManage ? -1 : 1; } if (left.connected !== right.connected) { return left.connected ? -1 : 1; } return right.issuedAt.epochMilliseconds - left.issuedAt.epochMilliseconds; }; export function toBootstrapExchangeError( cause: PairingGrantStore.BootstrapCredentialError, ): ServerAuthInvalidCredentialError | ServerAuthInternalError { if (PairingGrantStore.isBootstrapCredentialInternalError(cause)) { return new ServerAuthBootstrapCredentialValidationError({ cause }); } return new ServerAuthInvalidCredentialError({ cause, }); } const mapSessionVerificationErrors = ( effect: Effect.Effect, ): Effect.Effect => effect.pipe( Effect.mapError((cause) => SessionStore.isSessionCredentialInvalidError(cause) ? new ServerAuthInvalidCredentialError({ cause }) : new ServerAuthSessionCredentialValidationError({ cause }), ), ); function parseBearerToken(request: HttpServerRequest.HttpServerRequest): string | null { const header = request.headers["authorization"]; if (typeof header !== "string" || !header.startsWith(AUTHORIZATION_PREFIX)) { return null; } const token = header.slice(AUTHORIZATION_PREFIX.length).trim(); return token.length > 0 ? token : null; } function parseDpopToken(request: HttpServerRequest.HttpServerRequest): string | null { const header = request.headers["authorization"]; if (typeof header !== "string" || !header.startsWith(DPOP_AUTHORIZATION_PREFIX)) { return null; } const token = header.slice(DPOP_AUTHORIZATION_PREFIX.length).trim(); return token.length > 0 ? token : null; } export function selectRequestCredential( request: HttpServerRequest.HttpServerRequest, cookieName: string, legacyCookieName: string | undefined, ) { const cookieToken = request.cookies[cookieName]; if (cookieToken !== undefined) { return { token: cookieToken, source: "cookie" } as const; } const bearerToken = parseBearerToken(request); if (bearerToken !== null) { return { token: bearerToken, source: "bearer" } as const; } const dpopToken = parseDpopToken(request); if (dpopToken !== null) { return { token: dpopToken, source: "dpop" } as const; } const legacyToken = legacyCookieName ? request.cookies[legacyCookieName] : undefined; if (legacyToken !== undefined) { return { token: legacyToken, source: "legacy-cookie" } as const; } return undefined; } /** @public Service construction is part of the canonical Effect module API. */ export const make = Effect.gen(function* () { const policy = yield* EnvironmentAuthPolicy.EnvironmentAuthPolicy; const bootstrapCredentials = yield* PairingGrantStore.PairingGrantStore; const sessions = yield* SessionStore.SessionStore; const secretStore = yield* ServerSecretStore.ServerSecretStore; const crypto = yield* Crypto.Crypto; const descriptor = yield* policy.getDescriptor(); const authenticateToken = ( token: string, ): Effect.Effect< AuthenticatedSession, ServerAuthInvalidCredentialError | ServerAuthInternalError > => sessions.verify(token).pipe( Effect.tapError((cause) => SessionStore.isSessionCredentialInvalidError(cause) ? Effect.logWarning("Rejected authenticated session credential.").pipe( Effect.annotateLogs({ reason: cause.message, }), ) : Effect.void, ), Effect.map((session) => ({ sessionId: session.sessionId, subject: session.subject, method: session.method, scopes: session.scopes, ...(session.proofKeyThumbprint ? { proofKeyThumbprint: session.proofKeyThumbprint } : {}), ...(session.expiresAt ? { expiresAt: session.expiresAt } : {}), })), mapSessionVerificationErrors, ); const authenticateRequest = ( request: HttpServerRequest.HttpServerRequest, ): Effect.Effect => { const credential = selectRequestCredential( request, sessions.cookieName, sessions.legacyCookieName, ); if (!credential?.token) { return Effect.fail(new ServerAuthMissingCredentialError({})); } const dpopToken = parseDpopToken(request); return authenticateToken(credential.token).pipe( Effect.flatMap((session) => { if (session.proofKeyThumbprint) { if (!dpopToken || dpopToken !== credential.token) { return Effect.fail( new ServerAuthInvalidCredentialError({ diagnostic: "DPoP-bound access token requires DPoP authorization.", dpopFailureReason: "invalid_proof", }), ); } return verifyRequestDpopProof({ request, expectedThumbprint: session.proofKeyThumbprint, expectedAccessToken: dpopToken, }).pipe( Effect.provideService(ServerSecretStore.ServerSecretStore, secretStore), Effect.provideService(Crypto.Crypto, crypto), Effect.as(session), ); } if (dpopToken) { return Effect.fail( new ServerAuthInvalidCredentialError({ diagnostic: "DPoP authorization requires a proof-bound access token.", dpopFailureReason: "invalid_proof", }), ); } return Effect.succeed(session); }), ); }; const getSessionState: EnvironmentAuth["Service"]["getSessionState"] = (request) => authenticateRequest(request).pipe( Effect.map( (session) => ({ authenticated: true, auth: descriptor, scopes: session.scopes, sessionMethod: session.method, ...(session.expiresAt ? { expiresAt: DateTime.toUtc(session.expiresAt) } : {}), }) satisfies AuthSessionState, ), Effect.catchIf(isServerAuthCredentialError, () => Effect.succeed({ authenticated: false, auth: descriptor, } satisfies AuthSessionState), ), Effect.withSpan("EnvironmentAuth.getSessionState"), ); const createBrowserSession: EnvironmentAuth["Service"]["createBrowserSession"] = ( credential, requestMetadata, ) => bootstrapCredentials.consume(credential).pipe( Effect.mapError(toBootstrapExchangeError), Effect.flatMap((grant) => sessions .issue({ method: "browser-session-cookie", subject: grant.subject, scopes: grant.scopes, client: { ...requestMetadata, ...(grant.label ? { label: grant.label } : {}), }, }) .pipe( Effect.mapError((cause) => new ServerAuthAuthenticatedSessionIssueError({ cause })), ), ), Effect.map( (session) => ({ response: { authenticated: true, scopes: session.scopes, sessionMethod: session.method, expiresAt: DateTime.toUtc(session.expiresAt), } satisfies AuthBrowserSessionResult, sessionToken: session.token, }) satisfies BootstrapExchangeResult, ), Effect.withSpan("EnvironmentAuth.createBrowserSession"), ); const exchangeBootstrapCredentialForAccessToken: EnvironmentAuth["Service"]["exchangeBootstrapCredentialForAccessToken"] = (credential, requestedScopes, requestMetadata, input) => bootstrapCredentials.consume(credential, input).pipe( Effect.mapError(toBootstrapExchangeError), Effect.flatMap((grant) => Effect.gen(function* () { const grantedScopes = requestedScopes ?? grant.scopes; if (!grantedScopes.every((scope) => grant.scopes.includes(scope))) { return yield* new ServerAuthScopeNotGrantedError({}); } return yield* sessions .issue({ method: input?.proofKeyThumbprint ? "dpop-access-token" : "bearer-access-token", subject: grant.subject, scopes: grantedScopes, ...(input?.proofKeyThumbprint ? { proofKeyThumbprint: input.proofKeyThumbprint, ttl: Duration.hours(1), } : {}), // Desktop restarts forget the previous bearer token. Replace // its session, including stale entries left by older versions. replaceActiveForSubjectAndMethod: grant.method === "desktop-bootstrap", client: { ...requestMetadata, ...(grant.label ? { label: grant.label } : {}), }, }) .pipe( Effect.mapError( (cause) => new ServerAuthAuthenticatedAccessTokenIssueError({ cause }), ), ); }), ), Effect.flatMap((session) => DateTime.now.pipe( Effect.map( (now) => ({ access_token: session.token, issued_token_type: AuthAccessTokenType, token_type: input?.proofKeyThumbprint ? "DPoP" : "Bearer", expires_in: Math.max( 0, Math.floor( (session.expiresAt.epochMilliseconds - now.epochMilliseconds) / 1000, ), ), scope: encodeOAuthScope(session.scopes), }) satisfies AuthAccessTokenResult, ), ), ), Effect.withSpan("EnvironmentAuth.exchangeBootstrapCredentialForAccessToken"), ); const issuePairingCredentialForSubject = (input: { readonly scopes: ReadonlyArray; readonly subject: string; readonly label?: string; readonly purpose?: "startup"; }) => createPairingLink({ scopes: input.scopes, subject: input.subject, ...(input.label ? { label: input.label } : {}), ...(input.purpose ? { purpose: input.purpose } : {}), }).pipe( Effect.map( (issued) => ({ id: issued.id, credential: issued.credential, ...(issued.label ? { label: issued.label } : {}), expiresAt: issued.expiresAt, }) satisfies AuthPairingCredentialResult, ), ); const createPairingLink: EnvironmentAuth["Service"]["createPairingLink"] = Effect.fn( "EnvironmentAuth.createPairingLink", )( function* (input) { const createdAt = yield* DateTime.now; const issued = yield* bootstrapCredentials.issueOneTimeToken({ scopes: input?.scopes ?? AuthStandardClientScopes, subject: input?.subject ?? "one-time-token", ...(input?.ttl ? { ttl: input.ttl } : {}), ...(input?.label ? { label: input.label } : {}), ...(input?.proofKeyThumbprint ? { proofKeyThumbprint: input.proofKeyThumbprint } : {}), ...(input?.purpose ? { purpose: input.purpose } : {}), }); return { id: issued.id, credential: issued.credential, scopes: input?.scopes ?? AuthStandardClientScopes, subject: input?.subject ?? "one-time-token", ...(issued.label ? { label: issued.label } : {}), createdAt: DateTime.toUtc(createdAt), expiresAt: DateTime.toUtc(issued.expiresAt), } satisfies IssuedPairingLink; }, Effect.mapError((cause) => new ServerAuthPairingLinkCreationError({ cause })), ); const listPairingLinks: EnvironmentAuth["Service"]["listPairingLinks"] = (input) => bootstrapCredentials.listActive().pipe( Effect.map((pairingLinks) => { const excludedSubjects = input?.excludeSubjects ?? [ INTERNAL_ADMINISTRATIVE_BOOTSTRAP_SUBJECT, ]; return pairingLinks .filter((pairingLink) => !excludedSubjects.includes(pairingLink.subject)) .toSorted( (left, right) => right.createdAt.epochMilliseconds - left.createdAt.epochMilliseconds, ); }), Effect.mapError((cause) => new ServerAuthPairingLinksListError({ cause })), Effect.withSpan("EnvironmentAuth.listPairingLinks"), ); const revokePairingLink: EnvironmentAuth["Service"]["revokePairingLink"] = (id) => bootstrapCredentials.revoke(id).pipe( Effect.mapError((cause) => new ServerAuthPairingLinkRevocationError({ cause })), Effect.withSpan("EnvironmentAuth.revokePairingLink"), ); const issueSession: EnvironmentAuth["Service"]["issueSession"] = (input) => sessions .issue({ subject: input?.subject ?? DEFAULT_SESSION_SUBJECT, method: "bearer-access-token", scopes: input?.scopes ?? AuthAdministrativeScopes, client: { ...(input?.label ? { label: input.label } : {}), deviceType: "bot", }, ...(input?.ttl ? { ttl: input.ttl } : {}), }) .pipe( Effect.map( (issued) => ({ sessionId: issued.sessionId, token: issued.token, method: "bearer-access-token", scopes: issued.scopes, subject: input?.subject ?? DEFAULT_SESSION_SUBJECT, client: issued.client, expiresAt: DateTime.toUtc(issued.expiresAt), }) satisfies IssuedBearerSession, ), Effect.mapError((cause) => new ServerAuthSessionTokenIssueError({ cause })), Effect.withSpan("EnvironmentAuth.issueSession"), ); const listSessions: EnvironmentAuth["Service"]["listSessions"] = () => sessions.listActive().pipe( Effect.map((activeSessions) => activeSessions.toSorted(bySessionPriority)), Effect.mapError((cause) => new ServerAuthSessionsListError({ cause })), Effect.withSpan("EnvironmentAuth.listSessions"), ); const revokeSession: EnvironmentAuth["Service"]["revokeSession"] = (sessionId) => sessions.revoke(sessionId).pipe( Effect.mapError((cause) => new ServerAuthSessionRevocationError({ cause })), Effect.withSpan("EnvironmentAuth.revokeSession"), ); const revokeOtherSessionsExcept: EnvironmentAuth["Service"]["revokeOtherSessionsExcept"] = ( sessionId, ) => sessions.revokeAllExcept(sessionId).pipe( Effect.mapError((cause) => new ServerAuthOtherSessionsRevocationError({ cause })), Effect.withSpan("EnvironmentAuth.revokeOtherSessionsExcept"), ); const issuePairingCredential: EnvironmentAuth["Service"]["issuePairingCredential"] = (input) => issuePairingCredentialForSubject({ scopes: input?.scopes ?? AuthStandardClientScopes, subject: "one-time-token", ...(input?.label ? { label: input.label } : {}), }).pipe(Effect.withSpan("EnvironmentAuth.issuePairingCredential")); const issueStartupPairingCredential: EnvironmentAuth["Service"]["issueStartupPairingCredential"] = () => issuePairingCredentialForSubject({ scopes: AuthAdministrativeScopes, subject: INTERNAL_ADMINISTRATIVE_BOOTSTRAP_SUBJECT, purpose: "startup", }).pipe(Effect.withSpan("EnvironmentAuth.issueStartupPairingCredential")); const listClientSessions: EnvironmentAuth["Service"]["listClientSessions"] = (currentSessionId) => listSessions().pipe( Effect.map((clientSessions) => clientSessions.map((clientSession): AuthClientSession => ({ ...clientSession, current: clientSession.sessionId === currentSessionId, })), ), Effect.withSpan("EnvironmentAuth.listClientSessions"), ); const revokeClientSession: EnvironmentAuth["Service"]["revokeClientSession"] = Effect.fn( "EnvironmentAuth.revokeClientSession", )(function* (currentSessionId, targetSessionId) { if (currentSessionId === targetSessionId) { return yield* new ServerAuthForbiddenOperationError({}); } return yield* revokeSession(targetSessionId); }); const revokeOtherClientSessions: EnvironmentAuth["Service"]["revokeOtherClientSessions"] = ( currentSessionId, ) => revokeOtherSessionsExcept(currentSessionId).pipe( Effect.withSpan("EnvironmentAuth.revokeOtherClientSessions"), ); const issueStartupPairingUrl: EnvironmentAuth["Service"]["issueStartupPairingUrl"] = (baseUrl) => issueStartupPairingCredential().pipe( Effect.map((issued) => { const url = new URL(baseUrl); url.pathname = "/pair"; url.searchParams.delete("token"); url.hash = new URLSearchParams([["token", issued.credential]]).toString(); return url.toString(); }), Effect.withSpan("EnvironmentAuth.issueStartupPairingUrl"), ); const issueWebSocketTicket: EnvironmentAuth["Service"]["issueWebSocketTicket"] = (session) => sessions.issueWebSocketToken(session.sessionId).pipe( Effect.mapError((cause) => new ServerAuthWebSocketTokenIssueError({ cause })), Effect.map( (issued) => ({ ticket: issued.token, expiresAt: DateTime.toUtc(issued.expiresAt), }) satisfies AuthWebSocketTicketResult, ), Effect.withSpan("EnvironmentAuth.issueWebSocketTicket"), ); const authenticateHttpRequest: EnvironmentAuth["Service"]["authenticateHttpRequest"] = ( request, ) => authenticateRequest(request).pipe(Effect.withSpan("EnvironmentAuth.authenticateHttpRequest")); const authenticateWebSocketUpgrade: EnvironmentAuth["Service"]["authenticateWebSocketUpgrade"] = Effect.fn("EnvironmentAuth.authenticateWebSocketUpgrade")(function* (request) { const requestUrl = HttpServerRequest.toURL(request); if (Option.isSome(requestUrl)) { const websocketTicket = requestUrl.value.searchParams.get(WEBSOCKET_TICKET_QUERY_PARAM); if (websocketTicket && websocketTicket.trim().length > 0) { return yield* sessions.verifyWebSocketToken(websocketTicket).pipe( Effect.map((session) => ({ sessionId: session.sessionId, subject: session.subject, method: session.method, scopes: session.scopes, ...(session.expiresAt ? { expiresAt: session.expiresAt } : {}), })), mapSessionVerificationErrors, ); } } return yield* authenticateRequest(request); }); return EnvironmentAuth.of({ getDescriptor: () => Effect.succeed(descriptor).pipe(Effect.withSpan("EnvironmentAuth.getDescriptor")), getSessionState, createBrowserSession, exchangeBootstrapCredentialForAccessToken, createPairingLink, issuePairingCredential, issueStartupPairingCredential, listPairingLinks, revokePairingLink, issueSession, listSessions, revokeSession, revokeOtherSessionsExcept, listClientSessions, revokeClientSession, revokeOtherClientSessions, authenticateHttpRequest, authenticateWebSocketUpgrade, issueWebSocketTicket, issueStartupPairingUrl, }); }); export const layer = Layer.effect(EnvironmentAuth, make).pipe( Layer.provideMerge(PairingGrantStore.layer), Layer.provideMerge(SessionStore.layer), Layer.provideMerge(EnvironmentAuthPolicy.layer), ); const storageLayer = Layer.mergeAll(ServerSecretStore.layer, SqlitePersistenceLayer); export const runtimeLayer = layer.pipe( Layer.provideMerge(storageLayer), Layer.provideMerge(ServerEnvironment.identityLayer), );