| 1 | import { |
| 2 | AuthAccessTokenType, |
| 3 | type AuthClientPresentationMetadata, |
| 4 | AuthEnvironmentBootstrapTokenType, |
| 5 | AuthTokenExchangeGrantType, |
| 6 | type AuthEnvironmentScope, |
| 7 | } from "@t3tools/contracts"; |
| 8 | import { encodeOAuthScope } from "@t3tools/shared/oauthScope"; |
| 9 | import * as Effect from "effect/Effect"; |
| 10 | import { environmentEndpointUrl, environmentWebSocketUrl } from "../environment/endpoint.ts"; |
| 11 | import { |
| 12 | executeEnvironmentHttpRequest, |
| 13 | makeEnvironmentHttpApiGroupClient, |
| 14 | type RemoteEnvironmentRequestError, |
| 15 | } from "../rpc/http.ts"; |
| 16 | |
| 17 | export { |
| 18 | RemoteEnvironmentAuthInvalidJsonError, |
| 19 | RemoteEnvironmentAuthTimeoutError, |
| 20 | RemoteEnvironmentAuthUndeclaredStatusError, |
| 21 | } from "../rpc/http.ts"; |
| 22 | export type RemoteEnvironmentAuthError = RemoteEnvironmentRequestError; |
| 23 | |
| 24 | const DEFAULT_REMOTE_REQUEST_TIMEOUT_MS = 10_000; |
| 25 | |
| 26 | const clientMetadataTokenExchangeFields = ( |
| 27 | clientMetadata: AuthClientPresentationMetadata | undefined, |
| 28 | ) => { |
| 29 | const displayOs = clientMetadata?.os; |
| 30 | return { |
| 31 | ...(clientMetadata?.label ? { client_label: clientMetadata.label } : {}), |
| 32 | ...(clientMetadata?.deviceType ? { client_device_type: clientMetadata.deviceType } : {}), |
| 33 | ...(displayOs && displayOs !== "unknown" && displayOs !== "other" |
| 34 | ? { client_os: displayOs } |
| 35 | : {}), |
| 36 | }; |
| 37 | }; |
| 38 | |
| 39 | // The server reads these off the /ws upgrade URL next to wsTicket. Optional on |
| 40 | // both ends: old servers ignore unknown params, old clients never send them. |
| 41 | export const appendClientConnectionParams = ( |
| 42 | url: URL, |
| 43 | clientMetadata: AuthClientPresentationMetadata | undefined, |
| 44 | ): void => { |
| 45 | if (clientMetadata?.surface) { |
| 46 | url.searchParams.set("clientSurface", clientMetadata.surface); |
| 47 | } |
| 48 | if (clientMetadata?.appVersion) { |
| 49 | url.searchParams.set("clientAppVersion", clientMetadata.appVersion); |
| 50 | } |
| 51 | }; |
| 52 | |
| 53 | export const exchangeRemoteDpopAccessToken = Effect.fn( |
| 54 | "clientRuntime.authorization.exchangeRemoteDpopAccessToken", |
| 55 | )(function* (input: { |
| 56 | readonly httpBaseUrl: string; |
| 57 | readonly credential: string; |
| 58 | readonly scopes?: ReadonlyArray<AuthEnvironmentScope>; |
| 59 | readonly clientMetadata?: AuthClientPresentationMetadata; |
| 60 | readonly dpopProof: string; |
| 61 | readonly timeoutMs?: number; |
| 62 | }) { |
| 63 | const client = yield* makeEnvironmentHttpApiGroupClient(input.httpBaseUrl, "auth"); |
| 64 | const response = yield* executeEnvironmentHttpRequest( |
| 65 | environmentEndpointUrl(input.httpBaseUrl, "/oauth/token"), |
| 66 | input.timeoutMs ?? DEFAULT_REMOTE_REQUEST_TIMEOUT_MS, |
| 67 | client.token({ |
| 68 | headers: { dpop: input.dpopProof }, |
| 69 | payload: { |
| 70 | grant_type: AuthTokenExchangeGrantType, |
| 71 | subject_token: input.credential, |
| 72 | subject_token_type: AuthEnvironmentBootstrapTokenType, |
| 73 | requested_token_type: AuthAccessTokenType, |
| 74 | ...(input.scopes ? { scope: encodeOAuthScope(input.scopes) } : {}), |
| 75 | ...clientMetadataTokenExchangeFields(input.clientMetadata), |
| 76 | }, |
| 77 | }), |
| 78 | ); |
| 79 | return response; |
| 80 | }); |
| 81 | |
| 82 | export const bootstrapRemoteBearerSession = Effect.fn( |
| 83 | "clientRuntime.authorization.bootstrapRemoteBearerSession", |
| 84 | )(function* (input: { |
| 85 | readonly httpBaseUrl: string; |
| 86 | readonly credential: string; |
| 87 | readonly scopes?: ReadonlyArray<AuthEnvironmentScope>; |
| 88 | readonly clientMetadata?: AuthClientPresentationMetadata; |
| 89 | readonly timeoutMs?: number; |
| 90 | }) { |
| 91 | const client = yield* makeEnvironmentHttpApiGroupClient(input.httpBaseUrl, "auth"); |
| 92 | return yield* executeEnvironmentHttpRequest( |
| 93 | environmentEndpointUrl(input.httpBaseUrl, "/oauth/token"), |
| 94 | input.timeoutMs ?? DEFAULT_REMOTE_REQUEST_TIMEOUT_MS, |
| 95 | client.token({ |
| 96 | headers: {}, |
| 97 | payload: { |
| 98 | grant_type: AuthTokenExchangeGrantType, |
| 99 | subject_token: input.credential, |
| 100 | subject_token_type: AuthEnvironmentBootstrapTokenType, |
| 101 | requested_token_type: AuthAccessTokenType, |
| 102 | ...(input.scopes ? { scope: encodeOAuthScope(input.scopes) } : {}), |
| 103 | ...clientMetadataTokenExchangeFields(input.clientMetadata), |
| 104 | }, |
| 105 | }), |
| 106 | ); |
| 107 | }); |
| 108 | |
| 109 | export const fetchRemoteSessionState = Effect.fn( |
| 110 | "clientRuntime.authorization.fetchRemoteSessionState", |
| 111 | )(function* (input: { |
| 112 | readonly httpBaseUrl: string; |
| 113 | readonly bearerToken: string; |
| 114 | readonly timeoutMs?: number; |
| 115 | }) { |
| 116 | const client = yield* makeEnvironmentHttpApiGroupClient(input.httpBaseUrl, "auth"); |
| 117 | return yield* executeEnvironmentHttpRequest( |
| 118 | environmentEndpointUrl(input.httpBaseUrl, "/api/auth/session"), |
| 119 | input.timeoutMs ?? DEFAULT_REMOTE_REQUEST_TIMEOUT_MS, |
| 120 | client.session({ |
| 121 | headers: { |
| 122 | authorization: `Bearer ${input.bearerToken}`, |
| 123 | }, |
| 124 | }), |
| 125 | ); |
| 126 | }); |
| 127 | |
| 128 | export const issueRemoteWebSocketTicket = Effect.fn( |
| 129 | "clientRuntime.authorization.issueRemoteWebSocketTicket", |
| 130 | )(function* (input: { |
| 131 | readonly httpBaseUrl: string; |
| 132 | readonly bearerToken: string; |
| 133 | readonly timeoutMs?: number; |
| 134 | }) { |
| 135 | const client = yield* makeEnvironmentHttpApiGroupClient(input.httpBaseUrl, "auth"); |
| 136 | return yield* executeEnvironmentHttpRequest( |
| 137 | environmentEndpointUrl(input.httpBaseUrl, "/api/auth/websocket-ticket"), |
| 138 | input.timeoutMs ?? DEFAULT_REMOTE_REQUEST_TIMEOUT_MS, |
| 139 | client.webSocketTicket({ |
| 140 | headers: { |
| 141 | authorization: `Bearer ${input.bearerToken}`, |
| 142 | }, |
| 143 | }), |
| 144 | ); |
| 145 | }); |
| 146 | |
| 147 | export const issueRemoteDpopWebSocketTicket = Effect.fn( |
| 148 | "clientRuntime.authorization.issueRemoteDpopWebSocketTicket", |
| 149 | )(function* (input: { |
| 150 | readonly httpBaseUrl: string; |
| 151 | readonly accessToken: string; |
| 152 | readonly dpopProof: string; |
| 153 | readonly timeoutMs?: number; |
| 154 | }) { |
| 155 | const client = yield* makeEnvironmentHttpApiGroupClient(input.httpBaseUrl, "auth"); |
| 156 | return yield* executeEnvironmentHttpRequest( |
| 157 | environmentEndpointUrl(input.httpBaseUrl, "/api/auth/websocket-ticket"), |
| 158 | input.timeoutMs ?? DEFAULT_REMOTE_REQUEST_TIMEOUT_MS, |
| 159 | client.webSocketTicket({ |
| 160 | headers: { |
| 161 | authorization: `DPoP ${input.accessToken}`, |
| 162 | dpop: input.dpopProof, |
| 163 | }, |
| 164 | }), |
| 165 | ); |
| 166 | }); |
| 167 | |
| 168 | export const resolveRemoteWebSocketConnectionUrl = Effect.fn( |
| 169 | "clientRuntime.authorization.resolveRemoteWebSocketConnectionUrl", |
| 170 | )(function* (input: { |
| 171 | readonly wsBaseUrl: string; |
| 172 | readonly httpBaseUrl: string; |
| 173 | readonly bearerToken: string; |
| 174 | readonly clientMetadata?: AuthClientPresentationMetadata; |
| 175 | readonly timeoutMs?: number; |
| 176 | }) { |
| 177 | const issued = yield* issueRemoteWebSocketTicket({ |
| 178 | httpBaseUrl: input.httpBaseUrl, |
| 179 | bearerToken: input.bearerToken, |
| 180 | ...(input.timeoutMs ? { timeoutMs: input.timeoutMs } : {}), |
| 181 | }); |
| 182 | |
| 183 | const url = new URL(environmentWebSocketUrl(input.wsBaseUrl)); |
| 184 | url.searchParams.set("wsTicket", issued.ticket); |
| 185 | appendClientConnectionParams(url, input.clientMetadata); |
| 186 | return url.toString(); |
| 187 | }); |
| 188 | |
| 189 | export const resolveRemoteDpopWebSocketConnectionUrl = Effect.fn( |
| 190 | "clientRuntime.authorization.resolveRemoteDpopWebSocketConnectionUrl", |
| 191 | )(function* (input: { |
| 192 | readonly wsBaseUrl: string; |
| 193 | readonly httpBaseUrl: string; |
| 194 | readonly accessToken: string; |
| 195 | readonly dpopProof: string; |
| 196 | readonly clientMetadata?: AuthClientPresentationMetadata; |
| 197 | readonly timeoutMs?: number; |
| 198 | }) { |
| 199 | const issued = yield* issueRemoteDpopWebSocketTicket({ |
| 200 | httpBaseUrl: input.httpBaseUrl, |
| 201 | accessToken: input.accessToken, |
| 202 | dpopProof: input.dpopProof, |
| 203 | ...(input.timeoutMs ? { timeoutMs: input.timeoutMs } : {}), |
| 204 | }); |
| 205 | const url = new URL(environmentWebSocketUrl(input.wsBaseUrl)); |
| 206 | url.searchParams.set("wsTicket", issued.ticket); |
| 207 | appendClientConnectionParams(url, input.clientMetadata); |
| 208 | return url.toString(); |
| 209 | }); |