1import {
2 AuthAccessTokenType,
3 type AuthClientPresentationMetadata,
4 AuthEnvironmentBootstrapTokenType,
5 AuthTokenExchangeGrantType,
6 type AuthEnvironmentScope,
7} from "@t3tools/contracts";
8import { encodeOAuthScope } from "@t3tools/shared/oauthScope";
9import * as Effect from "effect/Effect";
10import { environmentEndpointUrl, environmentWebSocketUrl } from "../environment/endpoint.ts";
11import {
12 executeEnvironmentHttpRequest,
13 makeEnvironmentHttpApiGroupClient,
14 type RemoteEnvironmentRequestError,
15} from "../rpc/http.ts";
16
17export {
18 RemoteEnvironmentAuthInvalidJsonError,
19 RemoteEnvironmentAuthTimeoutError,
20 RemoteEnvironmentAuthUndeclaredStatusError,
21} from "../rpc/http.ts";
22export type RemoteEnvironmentAuthError = RemoteEnvironmentRequestError;
23
24const DEFAULT_REMOTE_REQUEST_TIMEOUT_MS = 10_000;
25
26const 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.
41export 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
53export 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
82export 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
109export 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
128export 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
147export 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
168export 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
189export 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});