import { type ClientConnectionMethod, EnvironmentId, type ExecutionEnvironmentDescriptor,} from "@t3tools/contracts";import { RelayEnvironmentConnectScope } from "@t3tools/contracts/relay";import { withRelayClientTracing } from "@t3tools/shared/relayTracing";import { exchangeRemoteDpopAccessToken, type RemoteEnvironmentAuthError, resolveRemoteDpopWebSocketConnectionUrl, resolveRemoteWebSocketConnectionUrl,} from "./remote.ts";import { environmentMismatchError, mapManagedRelayError, mapRemoteDpopEnvironmentError, mapRemoteEnvironmentError,} from "../connection/errors.ts";import { ConnectionBlockedError, ConnectionTransientError, type ConnectionAttemptError,} from "../connection/model.ts";import { fetchRemoteEnvironmentDescriptor } from "../environment/descriptor.ts";import { environmentEndpointUrl } from "../environment/endpoint.ts";import * as ClientCapabilities from "../platform/capabilities.ts";import * as ManagedRelay from "../relay/managedRelay.ts";import * as TokenStore from "./tokenStore.ts";import * as Clock from "effect/Clock";import * as Context from "effect/Context";import * as Effect from "effect/Effect";import * as Fiber from "effect/Fiber";import * as Layer from "effect/Layer";import * as Option from "effect/Option";import * as Ref from "effect/Ref";import * as Result from "effect/Result";import * as Scope from "effect/Scope";import * as Semaphore from "effect/Semaphore";import * as HttpClient from "effect/unstable/http/HttpClient"; import { DPOP_ACCESS_TOKEN_REFRESH_SKEW_MS, type PreparedHttpAuthorization,} from "../connection/model.ts"; export interface AuthorizedRemoteEnvironment { readonly environmentId: EnvironmentId; readonly label: string; readonly httpBaseUrl: string; readonly socketUrl: string; readonly httpAuthorization: PreparedHttpAuthorization;} export interface AuthorizedRemoteHttpEnvironment { readonly environmentId: EnvironmentId; readonly label: string; readonly httpBaseUrl: string; readonly httpAuthorization: Extract<PreparedHttpAuthorization, { readonly _tag: "Dpop" }>;} export class RemoteEnvironmentAuthorization extends Context.Service< RemoteEnvironmentAuthorization, { readonly authorizeBearer: (input: { readonly expectedEnvironmentId: EnvironmentId; readonly httpBaseUrl: string; readonly wsBaseUrl: string; readonly bearerToken: string; readonly connectionMethod: ClientConnectionMethod; }) => Effect.Effect<AuthorizedRemoteEnvironment, ConnectionAttemptError>; readonly authorizeDpop: (input: { readonly expectedEnvironmentId: EnvironmentId; }) => Effect.Effect<AuthorizedRemoteEnvironment, ConnectionAttemptError>; readonly authorizeDpopHttp: (input: { readonly expectedEnvironmentId: EnvironmentId; readonly rejectedAccessToken?: string; }) => Effect.Effect<AuthorizedRemoteHttpEnvironment, ConnectionAttemptError>; }>()("@t3tools/client-runtime/authorization/service/RemoteEnvironmentAuthorization") {} const CACHED_ENDPOINT_SOCKET_TIMEOUT_MS = 3_000;const BEARER_DESCRIPTOR_CACHE_TTL_MS = 10_000;const DPOP_AUTHORIZATION_TIMEOUT_MS = 30_000; function mapDpopSocketError(error: RemoteEnvironmentAuthError | ConnectionAttemptError) { return error._tag === "ConnectionTransientError" || error._tag === "ConnectionBlockedError" ? error : mapRemoteDpopEnvironmentError(error);} const fetchDescriptor = Effect.fn("clientRuntime.connection.remote.fetchDescriptor")(function* ( httpBaseUrl: string, connectionMethod: ClientConnectionMethod,) { return yield* fetchRemoteEnvironmentDescriptor({ httpBaseUrl }).pipe( Effect.mapError((error) => mapRemoteEnvironmentError(error, connectionMethod)), );}); export const make = Effect.gen(function* () { const serviceScope = yield* Scope.Scope; const signer = yield* ManagedRelay.ManagedRelayDpopSigner; const relay = yield* ManagedRelay.ManagedRelayClient; const cloudSession = yield* ClientCapabilities.CloudSession; const deviceIdentity = yield* ClientCapabilities.RelayDeviceIdentity; const presentation = yield* ClientCapabilities.ClientPresentation; const tokenStore = yield* TokenStore.RemoteDpopAccessTokenStore; const httpClient = yield* HttpClient.HttpClient; const tokenLock = yield* Semaphore.make(1); const tokenOwners = new Map< EnvironmentId, { readonly accessToken: string; readonly identity: ClientCapabilities.CloudSessionIdentity } >(); const pendingTokens = new Map< EnvironmentId, { readonly id: object; readonly identity: ClientCapabilities.CloudSessionIdentity; readonly thumbprint: string; readonly token: Effect.Effect<TokenStore.RemoteDpopAccessToken, ConnectionAttemptError>; } >(); const bearerDescriptors = yield* Ref.make< ReadonlyMap< EnvironmentId, { readonly httpBaseUrl: string; readonly descriptor: ExecutionEnvironmentDescriptor; readonly validatedAtEpochMs: number; } > >(new Map()); const authorizeBearer = Effect.fn("clientRuntime.connection.remote.authorizeBearer")( function* (input: { readonly expectedEnvironmentId: Parameters< RemoteEnvironmentAuthorization["Service"]["authorizeBearer"] >[0]["expectedEnvironmentId"]; readonly httpBaseUrl: string; readonly wsBaseUrl: string; readonly bearerToken: string; readonly connectionMethod: ClientConnectionMethod; }) { const now = yield* Clock.currentTimeMillis; const cachedDescriptor = (yield* Ref.get(bearerDescriptors)).get(input.expectedEnvironmentId); const canReuseDescriptor = cachedDescriptor?.httpBaseUrl === input.httpBaseUrl && cachedDescriptor.validatedAtEpochMs + BEARER_DESCRIPTOR_CACHE_TTL_MS > now; const descriptor = canReuseDescriptor ? cachedDescriptor.descriptor : yield* fetchDescriptor(input.httpBaseUrl, input.connectionMethod).pipe( Effect.provideService(HttpClient.HttpClient, httpClient), ); if (descriptor.environmentId !== input.expectedEnvironmentId) { return yield* environmentMismatchError({ expected: input.expectedEnvironmentId, actual: descriptor.environmentId, }); } if (!canReuseDescriptor) { yield* Ref.update(bearerDescriptors, (current) => { const next = new Map(current); next.set(input.expectedEnvironmentId, { httpBaseUrl: input.httpBaseUrl, descriptor, validatedAtEpochMs: now, }); return next; }); } const socketUrl = yield* resolveRemoteWebSocketConnectionUrl({ wsBaseUrl: input.wsBaseUrl, httpBaseUrl: input.httpBaseUrl, bearerToken: input.bearerToken, clientMetadata: presentation.metadata, connectionMethod: input.connectionMethod, }).pipe( Effect.mapError(mapRemoteEnvironmentError), Effect.provideService(HttpClient.HttpClient, httpClient), ); return { environmentId: descriptor.environmentId, label: descriptor.label, httpBaseUrl: input.httpBaseUrl, socketUrl, httpAuthorization: { _tag: "Bearer" as const, token: input.bearerToken, }, }; }, ); const createDpopSocketUrl = Effect.fn("clientRuntime.connection.remote.createDpopSocketUrl")( function* (token: TokenStore.RemoteDpopAccessToken, timeoutMs?: number) { const ticketProof = yield* signer .createProof({ method: "POST", url: environmentEndpointUrl(token.endpoint.httpBaseUrl, "/api/auth/websocket-ticket"), accessToken: token.accessToken, }) .pipe( Effect.mapError( () => new ConnectionBlockedError({ reason: "configuration", detail: "Could not create the websocket authorization proof.", }), ), ); return yield* resolveRemoteDpopWebSocketConnectionUrl({ wsBaseUrl: token.endpoint.wsBaseUrl, httpBaseUrl: token.endpoint.httpBaseUrl, accessToken: token.accessToken, dpopProof: ticketProof, clientMetadata: presentation.metadata, connectionMethod: "relay", ...(timeoutMs === undefined ? {} : { timeoutMs }), }).pipe(Effect.provideService(HttpClient.HttpClient, httpClient)); }, ); const sessionChanged = () => new ConnectionBlockedError({ reason: "authentication", detail: "Your cloud sign-in changed. Sign in again to authorize the environment.", }); const assertSession = Effect.fnUntraced(function* ( identity: ClientCapabilities.CloudSessionIdentity, ) { const current = yield* cloudSession.identity; if (Option.isNone(current) || current.value !== identity) { return yield* sessionChanged(); } }); // Called under tokenLock so a late rejection cannot remove a newer credential. const removeRejectedToken = Effect.fnUntraced(function* ( environmentId: EnvironmentId, accessToken: string, ) { const cached = yield* tokenStore.get(environmentId); if (Option.isSome(cached) && cached.value.accessToken === accessToken) { yield* tokenStore.remove(environmentId); tokenOwners.delete(environmentId); } }); const obtainBootstrap = Effect.fn("relay.connection.bootstrap.obtain")(function* ( environmentId: EnvironmentId, identity: ClientCapabilities.CloudSessionIdentity, ) { yield* assertSession(identity); const clerkToken = yield* cloudSession.clerkToken.pipe( Effect.withSpan("relay.connection.cloudSessionToken.resolve"), ); const deviceId = yield* deviceIdentity.deviceId.pipe( Effect.withSpan("relay.connection.deviceIdentity.resolve"), ); yield* assertSession(identity); const connected = yield* relay .connectEnvironment({ clerkToken, scopes: [RelayEnvironmentConnectScope], environmentId, ...(Option.isSome(deviceId) ? { deviceId: deviceId.value } : {}), }) .pipe(Effect.mapError(mapManagedRelayError)); if (connected.environmentId !== environmentId) { return yield* environmentMismatchError({ expected: environmentId, actual: connected.environmentId, }); } yield* assertSession(identity); return connected; }); const exchangeDpopToken = Effect.fn("clientRuntime.connection.remote.exchangeDpopToken")( function* ( environmentId: EnvironmentId, thumbprint: string, identity: ClientCapabilities.CloudSessionIdentity, ) { const bootstrap = yield* obtainBootstrap(environmentId, identity); const descriptor = yield* fetchDescriptor(bootstrap.endpoint.httpBaseUrl, "relay").pipe( Effect.provideService(HttpClient.HttpClient, httpClient), Effect.withSpan("environment.authorization.descriptor"), ); if (descriptor.environmentId !== environmentId) { return yield* environmentMismatchError({ expected: environmentId, actual: descriptor.environmentId, }); } const bootstrapProof = yield* signer .createProof({ method: "POST", url: environmentEndpointUrl(bootstrap.endpoint.httpBaseUrl, "/oauth/token"), }) .pipe( Effect.mapError( () => new ConnectionBlockedError({ reason: "configuration", detail: "Could not create the environment authorization proof.", }), ), ); yield* assertSession(identity); const access = yield* exchangeRemoteDpopAccessToken({ httpBaseUrl: bootstrap.endpoint.httpBaseUrl, credential: bootstrap.credential, dpopProof: bootstrapProof, scopes: presentation.scopes, clientMetadata: presentation.metadata, }).pipe( Effect.mapError(mapRemoteDpopEnvironmentError), Effect.provideService(HttpClient.HttpClient, httpClient), Effect.withSpan("environment.authorization.accessToken.exchange"), ); const issuedAt = yield* Clock.currentTimeMillis; return new TokenStore.RemoteDpopAccessToken({ environmentId: descriptor.environmentId, accountId: identity.accountId, label: descriptor.label, endpoint: bootstrap.endpoint, accessToken: access.access_token, expiresAtEpochMs: issuedAt + access.expires_in * 1_000, dpopThumbprint: thumbprint, }); }, ); const getDpopToken = Effect.fn("clientRuntime.connection.remote.getDpopToken")(function* ( input: Parameters<RemoteEnvironmentAuthorization["Service"]["authorizeDpopHttp"]>[0], ) { const session = yield* cloudSession.identity; if (Option.isNone(session)) { return yield* sessionChanged(); } const identity = session.value; const thumbprint = yield* signer.thumbprint.pipe( Effect.mapError( () => new ConnectionBlockedError({ reason: "configuration", detail: "Could not load the environment authorization key.", }), ), Effect.withSpan("environment.authorization.dpopKey.resolve"), ); const selected = yield* tokenLock.withPermits(1)( Effect.gen(function* () { yield* assertSession(identity); const now = yield* Clock.currentTimeMillis; const cached = yield* tokenStore .get(input.expectedEnvironmentId) .pipe(Effect.withSpan("environment.authorization.accessToken.cache")); const owner = tokenOwners.get(input.expectedEnvironmentId); if ( Option.isSome(cached) && cached.value.environmentId === input.expectedEnvironmentId && cached.value.accountId === identity.accountId && cached.value.dpopThumbprint === thumbprint && cached.value.expiresAtEpochMs > now + DPOP_ACCESS_TOKEN_REFRESH_SKEW_MS && cached.value.accessToken !== input.rejectedAccessToken && (owner?.accessToken !== cached.value.accessToken || owner.identity === identity) ) { tokenOwners.set(input.expectedEnvironmentId, { accessToken: cached.value.accessToken, identity, }); return { token: Effect.succeed(cached.value), fromCache: true }; } if (input.rejectedAccessToken !== undefined) { yield* removeRejectedToken(input.expectedEnvironmentId, input.rejectedAccessToken); } const pending = pendingTokens.get(input.expectedEnvironmentId); if (pending?.identity === identity && pending.thumbprint === thumbprint) { return { token: pending.token, fromCache: false }; } const id = {}; const fiber = yield* exchangeDpopToken( input.expectedEnvironmentId, thumbprint, identity, ).pipe( Effect.flatMap((token) => tokenLock.withPermits(1)( Effect.gen(function* () { yield* assertSession(identity); yield* tokenStore .put(token) .pipe(Effect.withSpan("environment.authorization.accessToken.persist")); const current = yield* cloudSession.identity; if (Option.isNone(current) || current.value !== identity) { yield* removeRejectedToken(input.expectedEnvironmentId, token.accessToken); return yield* sessionChanged(); } tokenOwners.set(input.expectedEnvironmentId, { accessToken: token.accessToken, identity, }); if (pendingTokens.get(input.expectedEnvironmentId)?.id === id) { pendingTokens.delete(input.expectedEnvironmentId); } return token; }), ), ), Effect.timeoutOrElse({ duration: DPOP_AUTHORIZATION_TIMEOUT_MS, orElse: () => Effect.fail( new ConnectionTransientError({ reason: "timeout", detail: "Timed out renewing the environment credential.", }), ), }), Effect.ensuring( tokenLock.withPermits(1)( Effect.sync(() => { if (pendingTokens.get(input.expectedEnvironmentId)?.id === id) { pendingTokens.delete(input.expectedEnvironmentId); } }), ), ), // One caller's timeout must not cancel renewal for other HTTP requests. Effect.forkIn(serviceScope), ); const token = Fiber.join(fiber); pendingTokens.set(input.expectedEnvironmentId, { id, identity, thumbprint, token }); return { token, fromCache: false }; }), ); yield* Effect.annotateCurrentSpan({ "connection.remote_token_cache": selected.fromCache ? "hit" : "miss", }); const token = yield* selected.token; yield* assertSession(identity); return { token, fromCache: selected.fromCache, identity }; }); const httpAuthorization = ( token: TokenStore.RemoteDpopAccessToken, ): AuthorizedRemoteHttpEnvironment => ({ environmentId: token.environmentId, label: token.label, httpBaseUrl: token.endpoint.httpBaseUrl, httpAuthorization: { _tag: "Dpop", accessToken: token.accessToken, expiresAtEpochMs: token.expiresAtEpochMs, }, }); const authorizeDpop = Effect.fn("clientRuntime.connection.remote.authorizeDpop")(function* ( input: Parameters<RemoteEnvironmentAuthorization["Service"]["authorizeDpop"]>[0], ) { let selected = yield* getDpopToken(input); if (selected.fromCache) { const cachedSocket = yield* createDpopSocketUrl( selected.token, CACHED_ENDPOINT_SOCKET_TIMEOUT_MS, ).pipe(Effect.result); if (Result.isSuccess(cachedSocket)) { yield* assertSession(selected.identity); return { ...httpAuthorization(selected.token), socketUrl: cachedSocket.success }; } if (cachedSocket.failure._tag === "ConnectionBlockedError") { return yield* mapDpopSocketError(cachedSocket.failure); } selected = yield* getDpopToken({ ...input, rejectedAccessToken: selected.token.accessToken, }); } const socket = yield* createDpopSocketUrl(selected.token).pipe(Effect.result); if (Result.isFailure(socket)) { yield* tokenLock.withPermits(1)( removeRejectedToken(input.expectedEnvironmentId, selected.token.accessToken), ); return yield* mapDpopSocketError(socket.failure); } yield* assertSession(selected.identity); return { ...httpAuthorization(selected.token), socketUrl: socket.success }; }); return RemoteEnvironmentAuthorization.of({ authorizeBearer, authorizeDpop: (input) => authorizeDpop(input).pipe(Effect.withSpan("environment.authorization")), authorizeDpopHttp: (input) => getDpopToken(input).pipe(Lines 1–500 of 508. Load more
import { type ClientConnectionMethod, EnvironmentId, type ExecutionEnvironmentDescriptor, } from "@t3tools/contracts"; import { RelayEnvironmentConnectScope } from "@t3tools/contracts/relay"; import { withRelayClientTracing } from "@t3tools/shared/relayTracing"; import { exchangeRemoteDpopAccessToken, type RemoteEnvironmentAuthError, resolveRemoteDpopWebSocketConnectionUrl, resolveRemoteWebSocketConnectionUrl, } from "./remote.ts"; import { environmentMismatchError, mapManagedRelayError, mapRemoteDpopEnvironmentError, mapRemoteEnvironmentError, } from "../connection/errors.ts"; import { ConnectionBlockedError, ConnectionTransientError, type ConnectionAttemptError, } from "../connection/model.ts"; import { fetchRemoteEnvironmentDescriptor } from "../environment/descriptor.ts"; import { environmentEndpointUrl } from "../environment/endpoint.ts"; import * as ClientCapabilities from "../platform/capabilities.ts"; import * as ManagedRelay from "../relay/managedRelay.ts"; import * as TokenStore from "./tokenStore.ts"; import * as Clock from "effect/Clock"; import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as Fiber from "effect/Fiber"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as Ref from "effect/Ref"; import * as Result from "effect/Result"; import * as Scope from "effect/Scope"; import * as Semaphore from "effect/Semaphore"; import * as HttpClient from "effect/unstable/http/HttpClient"; import { DPOP_ACCESS_TOKEN_REFRESH_SKEW_MS, type PreparedHttpAuthorization, } from "../connection/model.ts"; export interface AuthorizedRemoteEnvironment { readonly environmentId: EnvironmentId; readonly label: string; readonly httpBaseUrl: string; readonly socketUrl: string; readonly httpAuthorization: PreparedHttpAuthorization; } export interface AuthorizedRemoteHttpEnvironment { readonly environmentId: EnvironmentId; readonly label: string; readonly httpBaseUrl: string; readonly httpAuthorization: Extract<PreparedHttpAuthorization, { readonly _tag: "Dpop" }>; } export class RemoteEnvironmentAuthorization extends Context.Service< RemoteEnvironmentAuthorization, { readonly authorizeBearer: (input: { readonly expectedEnvironmentId: EnvironmentId; readonly httpBaseUrl: string; readonly wsBaseUrl: string; readonly bearerToken: string; readonly connectionMethod: ClientConnectionMethod; }) => Effect.Effect<AuthorizedRemoteEnvironment, ConnectionAttemptError>; readonly authorizeDpop: (input: { readonly expectedEnvironmentId: EnvironmentId; }) => Effect.Effect<AuthorizedRemoteEnvironment, ConnectionAttemptError>; readonly authorizeDpopHttp: (input: { readonly expectedEnvironmentId: EnvironmentId; readonly rejectedAccessToken?: string; }) => Effect.Effect<AuthorizedRemoteHttpEnvironment, ConnectionAttemptError>; } >()("@t3tools/client-runtime/authorization/service/RemoteEnvironmentAuthorization") {} const CACHED_ENDPOINT_SOCKET_TIMEOUT_MS = 3_000; const BEARER_DESCRIPTOR_CACHE_TTL_MS = 10_000; const DPOP_AUTHORIZATION_TIMEOUT_MS = 30_000; function mapDpopSocketError(error: RemoteEnvironmentAuthError | ConnectionAttemptError) { return error._tag === "ConnectionTransientError" || error._tag === "ConnectionBlockedError" ? error : mapRemoteDpopEnvironmentError(error); } const fetchDescriptor = Effect.fn("clientRuntime.connection.remote.fetchDescriptor")(function* ( httpBaseUrl: string, connectionMethod: ClientConnectionMethod, ) { return yield* fetchRemoteEnvironmentDescriptor({ httpBaseUrl }).pipe( Effect.mapError((error) => mapRemoteEnvironmentError(error, connectionMethod)), ); }); export const make = Effect.gen(function* () { const serviceScope = yield* Scope.Scope; const signer = yield* ManagedRelay.ManagedRelayDpopSigner; const relay = yield* ManagedRelay.ManagedRelayClient; const cloudSession = yield* ClientCapabilities.CloudSession; const deviceIdentity = yield* ClientCapabilities.RelayDeviceIdentity; const presentation = yield* ClientCapabilities.ClientPresentation; const tokenStore = yield* TokenStore.RemoteDpopAccessTokenStore; const httpClient = yield* HttpClient.HttpClient; const tokenLock = yield* Semaphore.make(1); const tokenOwners = new Map< EnvironmentId, { readonly accessToken: string; readonly identity: ClientCapabilities.CloudSessionIdentity } >(); const pendingTokens = new Map< EnvironmentId, { readonly id: object; readonly identity: ClientCapabilities.CloudSessionIdentity; readonly thumbprint: string; readonly token: Effect.Effect<TokenStore.RemoteDpopAccessToken, ConnectionAttemptError>; } >(); const bearerDescriptors = yield* Ref.make< ReadonlyMap< EnvironmentId, { readonly httpBaseUrl: string; readonly descriptor: ExecutionEnvironmentDescriptor; readonly validatedAtEpochMs: number; } > >(new Map()); const authorizeBearer = Effect.fn("clientRuntime.connection.remote.authorizeBearer")( function* (input: { readonly expectedEnvironmentId: Parameters< RemoteEnvironmentAuthorization["Service"]["authorizeBearer"] >[0]["expectedEnvironmentId"]; readonly httpBaseUrl: string; readonly wsBaseUrl: string; readonly bearerToken: string; readonly connectionMethod: ClientConnectionMethod; }) { const now = yield* Clock.currentTimeMillis; const cachedDescriptor = (yield* Ref.get(bearerDescriptors)).get(input.expectedEnvironmentId); const canReuseDescriptor = cachedDescriptor?.httpBaseUrl === input.httpBaseUrl && cachedDescriptor.validatedAtEpochMs + BEARER_DESCRIPTOR_CACHE_TTL_MS > now; const descriptor = canReuseDescriptor ? cachedDescriptor.descriptor : yield* fetchDescriptor(input.httpBaseUrl, input.connectionMethod).pipe( Effect.provideService(HttpClient.HttpClient, httpClient), ); if (descriptor.environmentId !== input.expectedEnvironmentId) { return yield* environmentMismatchError({ expected: input.expectedEnvironmentId, actual: descriptor.environmentId, }); } if (!canReuseDescriptor) { yield* Ref.update(bearerDescriptors, (current) => { const next = new Map(current); next.set(input.expectedEnvironmentId, { httpBaseUrl: input.httpBaseUrl, descriptor, validatedAtEpochMs: now, }); return next; }); } const socketUrl = yield* resolveRemoteWebSocketConnectionUrl({ wsBaseUrl: input.wsBaseUrl, httpBaseUrl: input.httpBaseUrl, bearerToken: input.bearerToken, clientMetadata: presentation.metadata, connectionMethod: input.connectionMethod, }).pipe( Effect.mapError(mapRemoteEnvironmentError), Effect.provideService(HttpClient.HttpClient, httpClient), ); return { environmentId: descriptor.environmentId, label: descriptor.label, httpBaseUrl: input.httpBaseUrl, socketUrl, httpAuthorization: { _tag: "Bearer" as const, token: input.bearerToken, }, }; }, ); const createDpopSocketUrl = Effect.fn("clientRuntime.connection.remote.createDpopSocketUrl")( function* (token: TokenStore.RemoteDpopAccessToken, timeoutMs?: number) { const ticketProof = yield* signer .createProof({ method: "POST", url: environmentEndpointUrl(token.endpoint.httpBaseUrl, "/api/auth/websocket-ticket"), accessToken: token.accessToken, }) .pipe( Effect.mapError( () => new ConnectionBlockedError({ reason: "configuration", detail: "Could not create the websocket authorization proof.", }), ), ); return yield* resolveRemoteDpopWebSocketConnectionUrl({ wsBaseUrl: token.endpoint.wsBaseUrl, httpBaseUrl: token.endpoint.httpBaseUrl, accessToken: token.accessToken, dpopProof: ticketProof, clientMetadata: presentation.metadata, connectionMethod: "relay", ...(timeoutMs === undefined ? {} : { timeoutMs }), }).pipe(Effect.provideService(HttpClient.HttpClient, httpClient)); }, ); const sessionChanged = () => new ConnectionBlockedError({ reason: "authentication", detail: "Your cloud sign-in changed. Sign in again to authorize the environment.", }); const assertSession = Effect.fnUntraced(function* ( identity: ClientCapabilities.CloudSessionIdentity, ) { const current = yield* cloudSession.identity; if (Option.isNone(current) || current.value !== identity) { return yield* sessionChanged(); } }); // Called under tokenLock so a late rejection cannot remove a newer credential. const removeRejectedToken = Effect.fnUntraced(function* ( environmentId: EnvironmentId, accessToken: string, ) { const cached = yield* tokenStore.get(environmentId); if (Option.isSome(cached) && cached.value.accessToken === accessToken) { yield* tokenStore.remove(environmentId); tokenOwners.delete(environmentId); } }); const obtainBootstrap = Effect.fn("relay.connection.bootstrap.obtain")(function* ( environmentId: EnvironmentId, identity: ClientCapabilities.CloudSessionIdentity, ) { yield* assertSession(identity); const clerkToken = yield* cloudSession.clerkToken.pipe( Effect.withSpan("relay.connection.cloudSessionToken.resolve"), ); const deviceId = yield* deviceIdentity.deviceId.pipe( Effect.withSpan("relay.connection.deviceIdentity.resolve"), ); yield* assertSession(identity); const connected = yield* relay .connectEnvironment({ clerkToken, scopes: [RelayEnvironmentConnectScope], environmentId, ...(Option.isSome(deviceId) ? { deviceId: deviceId.value } : {}), }) .pipe(Effect.mapError(mapManagedRelayError)); if (connected.environmentId !== environmentId) { return yield* environmentMismatchError({ expected: environmentId, actual: connected.environmentId, }); } yield* assertSession(identity); return connected; }); const exchangeDpopToken = Effect.fn("clientRuntime.connection.remote.exchangeDpopToken")( function* ( environmentId: EnvironmentId, thumbprint: string, identity: ClientCapabilities.CloudSessionIdentity, ) { const bootstrap = yield* obtainBootstrap(environmentId, identity); const descriptor = yield* fetchDescriptor(bootstrap.endpoint.httpBaseUrl, "relay").pipe( Effect.provideService(HttpClient.HttpClient, httpClient), Effect.withSpan("environment.authorization.descriptor"), ); if (descriptor.environmentId !== environmentId) { return yield* environmentMismatchError({ expected: environmentId, actual: descriptor.environmentId, }); } const bootstrapProof = yield* signer .createProof({ method: "POST", url: environmentEndpointUrl(bootstrap.endpoint.httpBaseUrl, "/oauth/token"), }) .pipe( Effect.mapError( () => new ConnectionBlockedError({ reason: "configuration", detail: "Could not create the environment authorization proof.", }), ), ); yield* assertSession(identity); const access = yield* exchangeRemoteDpopAccessToken({ httpBaseUrl: bootstrap.endpoint.httpBaseUrl, credential: bootstrap.credential, dpopProof: bootstrapProof, scopes: presentation.scopes, clientMetadata: presentation.metadata, }).pipe( Effect.mapError(mapRemoteDpopEnvironmentError), Effect.provideService(HttpClient.HttpClient, httpClient), Effect.withSpan("environment.authorization.accessToken.exchange"), ); const issuedAt = yield* Clock.currentTimeMillis; return new TokenStore.RemoteDpopAccessToken({ environmentId: descriptor.environmentId, accountId: identity.accountId, label: descriptor.label, endpoint: bootstrap.endpoint, accessToken: access.access_token, expiresAtEpochMs: issuedAt + access.expires_in * 1_000, dpopThumbprint: thumbprint, }); }, ); const getDpopToken = Effect.fn("clientRuntime.connection.remote.getDpopToken")(function* ( input: Parameters<RemoteEnvironmentAuthorization["Service"]["authorizeDpopHttp"]>[0], ) { const session = yield* cloudSession.identity; if (Option.isNone(session)) { return yield* sessionChanged(); } const identity = session.value; const thumbprint = yield* signer.thumbprint.pipe( Effect.mapError( () => new ConnectionBlockedError({ reason: "configuration", detail: "Could not load the environment authorization key.", }), ), Effect.withSpan("environment.authorization.dpopKey.resolve"), ); const selected = yield* tokenLock.withPermits(1)( Effect.gen(function* () { yield* assertSession(identity); const now = yield* Clock.currentTimeMillis; const cached = yield* tokenStore .get(input.expectedEnvironmentId) .pipe(Effect.withSpan("environment.authorization.accessToken.cache")); const owner = tokenOwners.get(input.expectedEnvironmentId); if ( Option.isSome(cached) && cached.value.environmentId === input.expectedEnvironmentId && cached.value.accountId === identity.accountId && cached.value.dpopThumbprint === thumbprint && cached.value.expiresAtEpochMs > now + DPOP_ACCESS_TOKEN_REFRESH_SKEW_MS && cached.value.accessToken !== input.rejectedAccessToken && (owner?.accessToken !== cached.value.accessToken || owner.identity === identity) ) { tokenOwners.set(input.expectedEnvironmentId, { accessToken: cached.value.accessToken, identity, }); return { token: Effect.succeed(cached.value), fromCache: true }; } if (input.rejectedAccessToken !== undefined) { yield* removeRejectedToken(input.expectedEnvironmentId, input.rejectedAccessToken); } const pending = pendingTokens.get(input.expectedEnvironmentId); if (pending?.identity === identity && pending.thumbprint === thumbprint) { return { token: pending.token, fromCache: false }; } const id = {}; const fiber = yield* exchangeDpopToken( input.expectedEnvironmentId, thumbprint, identity, ).pipe( Effect.flatMap((token) => tokenLock.withPermits(1)( Effect.gen(function* () { yield* assertSession(identity); yield* tokenStore .put(token) .pipe(Effect.withSpan("environment.authorization.accessToken.persist")); const current = yield* cloudSession.identity; if (Option.isNone(current) || current.value !== identity) { yield* removeRejectedToken(input.expectedEnvironmentId, token.accessToken); return yield* sessionChanged(); } tokenOwners.set(input.expectedEnvironmentId, { accessToken: token.accessToken, identity, }); if (pendingTokens.get(input.expectedEnvironmentId)?.id === id) { pendingTokens.delete(input.expectedEnvironmentId); } return token; }), ), ), Effect.timeoutOrElse({ duration: DPOP_AUTHORIZATION_TIMEOUT_MS, orElse: () => Effect.fail( new ConnectionTransientError({ reason: "timeout", detail: "Timed out renewing the environment credential.", }), ), }), Effect.ensuring( tokenLock.withPermits(1)( Effect.sync(() => { if (pendingTokens.get(input.expectedEnvironmentId)?.id === id) { pendingTokens.delete(input.expectedEnvironmentId); } }), ), ), // One caller's timeout must not cancel renewal for other HTTP requests. Effect.forkIn(serviceScope), ); const token = Fiber.join(fiber); pendingTokens.set(input.expectedEnvironmentId, { id, identity, thumbprint, token }); return { token, fromCache: false }; }), ); yield* Effect.annotateCurrentSpan({ "connection.remote_token_cache": selected.fromCache ? "hit" : "miss", }); const token = yield* selected.token; yield* assertSession(identity); return { token, fromCache: selected.fromCache, identity }; }); const httpAuthorization = ( token: TokenStore.RemoteDpopAccessToken, ): AuthorizedRemoteHttpEnvironment => ({ environmentId: token.environmentId, label: token.label, httpBaseUrl: token.endpoint.httpBaseUrl, httpAuthorization: { _tag: "Dpop", accessToken: token.accessToken, expiresAtEpochMs: token.expiresAtEpochMs, }, }); const authorizeDpop = Effect.fn("clientRuntime.connection.remote.authorizeDpop")(function* ( input: Parameters<RemoteEnvironmentAuthorization["Service"]["authorizeDpop"]>[0], ) { let selected = yield* getDpopToken(input); if (selected.fromCache) { const cachedSocket = yield* createDpopSocketUrl( selected.token, CACHED_ENDPOINT_SOCKET_TIMEOUT_MS, ).pipe(Effect.result); if (Result.isSuccess(cachedSocket)) { yield* assertSession(selected.identity); return { ...httpAuthorization(selected.token), socketUrl: cachedSocket.success }; } if (cachedSocket.failure._tag === "ConnectionBlockedError") { return yield* mapDpopSocketError(cachedSocket.failure); } selected = yield* getDpopToken({ ...input, rejectedAccessToken: selected.token.accessToken, }); } const socket = yield* createDpopSocketUrl(selected.token).pipe(Effect.result); if (Result.isFailure(socket)) { yield* tokenLock.withPermits(1)( removeRejectedToken(input.expectedEnvironmentId, selected.token.accessToken), ); return yield* mapDpopSocketError(socket.failure); } yield* assertSession(selected.identity); return { ...httpAuthorization(selected.token), socketUrl: socket.success }; }); return RemoteEnvironmentAuthorization.of({ authorizeBearer, authorizeDpop: (input) => authorizeDpop(input).pipe(Effect.withSpan("environment.authorization")), authorizeDpopHttp: (input) => getDpopToken(input).pipe(