1import { HostedAuthAccount, HostedAuthDevice } from "@t3tools/contracts";
2import * as DateTime from "effect/DateTime";
3import * as Schema from "effect/Schema";
4
5const decodeAccount = Schema.decodeUnknownSync(Schema.toCodecJson(HostedAuthAccount));
6const decodeDevices = Schema.decodeUnknownSync(Schema.toCodecJson(Schema.Array(HostedAuthDevice)));
7
8let account: { server: string; value: HostedAuthAccount } | null = null;
9let browserRequest: { server: string; value: Promise<HostedAuthAccount> } | null = null;
10
11export async function hostedAuthHeaders(
12 server: string,
13 rejectedCsrf?: string,
14): Promise<Record<string, string>> {
15 const desktop = window.desktopBridge;
16 if (desktop?.getHostedAccessToken) {
17 return { authorization: `Bearer ${await desktop.getHostedAccessToken(server)}` };
18 }
19 if (new URL(server).origin !== window.location.origin) {
20 throw new Error("Open this server in your browser to sign in.");
21 }
22 if (
23 rejectedCsrf !== undefined &&
24 account?.server === server &&
25 account.value.csrfToken === rejectedCsrf
26 ) {
27 account = null;
28 }
29 if (
30 account?.server === server &&
31 DateTime.toEpochMillis(account.value.expiresAt) > Date.now() + 60_000
32 ) {
33 return { "x-csrf-token": account.value.csrfToken };
34 }
35 if (browserRequest?.server !== server) {
36 const previous = account?.server === server ? account.value : null;
37 const pending = (async () => {
38 let response = await fetch(
39 `${server}/api/auth/${previous === null ? "account" : "session/refresh"}`,
40 {
41 ...(previous === null
42 ? {}
43 : { method: "POST", headers: { "x-csrf-token": previous.csrfToken } }),
44 credentials: "same-origin",
45 redirect: "error",
46 },
47 );
48 if (response.status === 401 && previous !== null) {
49 const current = await fetch(`${server}/api/auth/account`, {
50 credentials: "same-origin",
51 redirect: "error",
52 });
53 if (!current.ok) throw new Error("Sign in to your server again to reconnect.");
54 const refreshed = decodeAccount(await current.json());
55 response = await fetch(`${server}/api/auth/session/refresh`, {
56 method: "POST",
57 headers: { "x-csrf-token": refreshed.csrfToken },
58 credentials: "same-origin",
59 redirect: "error",
60 });
61 }
62 if (!response.ok) throw new Error("Sign in to your server again to reconnect.");
63 return decodeAccount(await response.json());
64 })();
65 browserRequest = { server, value: pending };
66 }
67 const current = browserRequest;
68 try {
69 const value = await current.value;
70 if (browserRequest === current) account = { server, value };
71 return { "x-csrf-token": value.csrfToken };
72 } finally {
73 if (browserRequest === current) browserRequest = null;
74 }
75}
76
77export async function hostedAccountRequest(server: string, path: string, method = "GET") {
78 const request = (headers: Record<string, string>) =>
79 fetch(`${server}${path}`, {
80 method,
81 headers,
82 credentials: "same-origin",
83 redirect: "error",
84 });
85 const headers = await hostedAuthHeaders(server);
86 let response = await request(headers);
87 if (response.status === 401 && headers["x-csrf-token"]) {
88 response = await request(await hostedAuthHeaders(server, headers["x-csrf-token"]));
89 }
90 if (!response.ok) throw new Error("Couldn't update this server connection. Try again.");
91 return response;
92}
93
94export async function hostedDevices(server: string) {
95 const response = await hostedAccountRequest(server, "/api/auth/devices");
96 return decodeDevices(await response.json());
97}
98
99export async function beginHostedSignIn(server: string) {
100 if (window.desktopBridge?.beginHostedAuth) {
101 await window.desktopBridge.beginHostedAuth(server);
102 return;
103 }
104 const url = new URL(`${server}/api/auth/oidc/start`);
105 url.searchParams.set("returnTo", "/");
106 window.location.assign(url.href);
107}
108
109export function forgetHostedWebSession() {
110 account = null;
111 browserRequest = null;
112}