@lastshotlabs/snapshot 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1598 -0
- package/dist/cli.js +4529 -0
- package/dist/index.cjs +1135 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +396 -0
- package/dist/index.d.ts +396 -0
- package/dist/index.js +1106 -0
- package/dist/index.js.map +1 -0
- package/dist/vite.cjs +1186 -0
- package/dist/vite.d.cts +18 -0
- package/dist/vite.d.ts +18 -0
- package/dist/vite.js +1174 -0
- package/package.json +67 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1106 @@
|
|
|
1
|
+
// src/create-snapshot.tsx
|
|
2
|
+
import { QueryClient } from "@tanstack/react-query";
|
|
3
|
+
import { atom as atom2, useAtom as useAtom2, useAtomValue as useAtomValue3 } from "jotai";
|
|
4
|
+
|
|
5
|
+
// src/api/error.ts
|
|
6
|
+
function extractMessage(body) {
|
|
7
|
+
if (body && typeof body === "object") {
|
|
8
|
+
const b = body;
|
|
9
|
+
if (typeof b.message === "string") return b.message;
|
|
10
|
+
if (typeof b.error === "string") return b.error;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
var ApiError = class _ApiError extends Error {
|
|
14
|
+
constructor(status, body, message) {
|
|
15
|
+
super(message ?? extractMessage(body) ?? `HTTP ${status}`);
|
|
16
|
+
this.status = status;
|
|
17
|
+
this.body = body;
|
|
18
|
+
this.name = "ApiError";
|
|
19
|
+
Object.setPrototypeOf(this, _ApiError.prototype);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// src/auth/csrf.ts
|
|
24
|
+
var CSRF_COOKIE_NAME = "csrf_token";
|
|
25
|
+
var MUTATING_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
26
|
+
function getCsrfToken() {
|
|
27
|
+
if (typeof document === "undefined") return null;
|
|
28
|
+
const match = document.cookie.split("; ").find((row) => row.startsWith(`${CSRF_COOKIE_NAME}=`));
|
|
29
|
+
return match ? decodeURIComponent(match.split("=")[1]) : null;
|
|
30
|
+
}
|
|
31
|
+
function isMutatingMethod(method) {
|
|
32
|
+
return MUTATING_METHODS.has(method.toUpperCase());
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// src/api/client.ts
|
|
36
|
+
var ApiClient = class {
|
|
37
|
+
baseUrl;
|
|
38
|
+
authMode;
|
|
39
|
+
bearerToken;
|
|
40
|
+
storage = null;
|
|
41
|
+
onUnauthenticated;
|
|
42
|
+
onForbidden;
|
|
43
|
+
onMfaSetupRequired;
|
|
44
|
+
constructor(config) {
|
|
45
|
+
this.baseUrl = config.apiUrl.replace(/\/$/, "");
|
|
46
|
+
this.authMode = config.auth ?? "cookie";
|
|
47
|
+
this.bearerToken = config.bearerToken;
|
|
48
|
+
this.onUnauthenticated = config.onUnauthenticated;
|
|
49
|
+
this.onForbidden = config.onForbidden;
|
|
50
|
+
this.onMfaSetupRequired = config.onMfaSetupRequired;
|
|
51
|
+
if (this.bearerToken && typeof window !== "undefined") {
|
|
52
|
+
console.warn(
|
|
53
|
+
"[snapshot] bearerToken is a static API credential. It should not be used in browser deployments. See the snapshot security docs for the recommended cookie-auth model."
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
setStorage(storage) {
|
|
58
|
+
this.storage = storage;
|
|
59
|
+
}
|
|
60
|
+
buildHeaders(method, overrides) {
|
|
61
|
+
const headers = {
|
|
62
|
+
"Content-Type": "application/json",
|
|
63
|
+
...overrides
|
|
64
|
+
};
|
|
65
|
+
if (this.bearerToken) {
|
|
66
|
+
headers["Authorization"] = `Bearer ${this.bearerToken}`;
|
|
67
|
+
}
|
|
68
|
+
if (this.authMode === "cookie") {
|
|
69
|
+
if (isMutatingMethod(method)) {
|
|
70
|
+
const csrf = getCsrfToken();
|
|
71
|
+
if (csrf) headers["x-csrf-token"] = csrf;
|
|
72
|
+
}
|
|
73
|
+
return headers;
|
|
74
|
+
}
|
|
75
|
+
const userToken = this.storage?.get();
|
|
76
|
+
if (userToken) {
|
|
77
|
+
headers["x-user-token"] = userToken;
|
|
78
|
+
}
|
|
79
|
+
if (typeof process !== "undefined" && process.env?.["NODE_ENV"] !== "production" && !this.bearerToken && !userToken) {
|
|
80
|
+
console.warn(
|
|
81
|
+
"[snapshot] No auth credentials attached to request. Set bearerToken in createSnapshot config or ensure a user token is stored."
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
return headers;
|
|
85
|
+
}
|
|
86
|
+
async rawFetch(method, path, body, options) {
|
|
87
|
+
const url = `${this.baseUrl}${path}`;
|
|
88
|
+
const headers = this.buildHeaders(method, options?.headers);
|
|
89
|
+
const init = { method, headers };
|
|
90
|
+
if (this.authMode === "cookie") init.credentials = "include";
|
|
91
|
+
if (body !== void 0) init.body = JSON.stringify(body);
|
|
92
|
+
if (options?.signal) init.signal = options.signal;
|
|
93
|
+
return fetch(url, init);
|
|
94
|
+
}
|
|
95
|
+
async handleResponse(response) {
|
|
96
|
+
if (response.status === 403) {
|
|
97
|
+
const body = await response.json().catch(() => null);
|
|
98
|
+
if (body && typeof body === "object" && "code" in body && body.code === "MFA_SETUP_REQUIRED") {
|
|
99
|
+
this.onMfaSetupRequired?.();
|
|
100
|
+
} else {
|
|
101
|
+
this.onForbidden?.();
|
|
102
|
+
}
|
|
103
|
+
throw new ApiError(403, body);
|
|
104
|
+
}
|
|
105
|
+
if (!response.ok) {
|
|
106
|
+
const body = await response.json().catch(() => null);
|
|
107
|
+
throw new ApiError(response.status, body);
|
|
108
|
+
}
|
|
109
|
+
const contentType = response.headers.get("content-type");
|
|
110
|
+
if (!contentType?.includes("application/json")) {
|
|
111
|
+
return void 0;
|
|
112
|
+
}
|
|
113
|
+
return response.json();
|
|
114
|
+
}
|
|
115
|
+
async request(method, path, body, options) {
|
|
116
|
+
const response = await this.rawFetch(method, path, body, options);
|
|
117
|
+
if (response.status === 401) {
|
|
118
|
+
const refreshToken = this.storage?.getRefreshToken();
|
|
119
|
+
if (refreshToken && path !== "/auth/refresh") {
|
|
120
|
+
try {
|
|
121
|
+
const refreshResponse = await fetch(`${this.baseUrl}/auth/refresh`, {
|
|
122
|
+
method: "POST",
|
|
123
|
+
headers: { "Content-Type": "application/json" },
|
|
124
|
+
body: JSON.stringify({ refreshToken }),
|
|
125
|
+
credentials: "include"
|
|
126
|
+
});
|
|
127
|
+
if (refreshResponse.ok) {
|
|
128
|
+
const refreshData = await refreshResponse.json();
|
|
129
|
+
this.storage?.set(refreshData.token);
|
|
130
|
+
if (refreshData.refreshToken) {
|
|
131
|
+
this.storage?.setRefreshToken(refreshData.refreshToken);
|
|
132
|
+
}
|
|
133
|
+
const retryResponse = await this.rawFetch(method, path, body, options);
|
|
134
|
+
return this.handleResponse(retryResponse);
|
|
135
|
+
}
|
|
136
|
+
} catch {
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
this.storage?.clear();
|
|
140
|
+
this.storage?.clearRefreshToken();
|
|
141
|
+
this.onUnauthenticated?.();
|
|
142
|
+
const errBody = await response.json().catch(() => null);
|
|
143
|
+
throw new ApiError(401, errBody);
|
|
144
|
+
}
|
|
145
|
+
return this.handleResponse(response);
|
|
146
|
+
}
|
|
147
|
+
get(path, options) {
|
|
148
|
+
return this.request("GET", path, void 0, options);
|
|
149
|
+
}
|
|
150
|
+
post(path, body, options) {
|
|
151
|
+
return this.request("POST", path, body, options);
|
|
152
|
+
}
|
|
153
|
+
put(path, body, options) {
|
|
154
|
+
return this.request("PUT", path, body, options);
|
|
155
|
+
}
|
|
156
|
+
patch(path, body, options) {
|
|
157
|
+
return this.request("PATCH", path, body, options);
|
|
158
|
+
}
|
|
159
|
+
delete(path, body, options) {
|
|
160
|
+
return this.request("DELETE", path, body, options);
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// src/auth/storage.ts
|
|
165
|
+
function createLocalStorageStorage(key) {
|
|
166
|
+
const refreshKey = `${key}-refresh`;
|
|
167
|
+
return {
|
|
168
|
+
get: () => localStorage.getItem(key),
|
|
169
|
+
set: (token) => localStorage.setItem(key, token),
|
|
170
|
+
clear: () => localStorage.removeItem(key),
|
|
171
|
+
getRefreshToken: () => localStorage.getItem(refreshKey),
|
|
172
|
+
setRefreshToken: (token) => localStorage.setItem(refreshKey, token),
|
|
173
|
+
clearRefreshToken: () => localStorage.removeItem(refreshKey)
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
function createSessionStorageStorage(key) {
|
|
177
|
+
const refreshKey = `${key}-refresh`;
|
|
178
|
+
return {
|
|
179
|
+
get: () => sessionStorage.getItem(key),
|
|
180
|
+
set: (token) => sessionStorage.setItem(key, token),
|
|
181
|
+
clear: () => sessionStorage.removeItem(key),
|
|
182
|
+
getRefreshToken: () => sessionStorage.getItem(refreshKey),
|
|
183
|
+
setRefreshToken: (token) => sessionStorage.setItem(refreshKey, token),
|
|
184
|
+
clearRefreshToken: () => sessionStorage.removeItem(refreshKey)
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function createMemoryStorage() {
|
|
188
|
+
let token = null;
|
|
189
|
+
let refreshValue = null;
|
|
190
|
+
return {
|
|
191
|
+
get: () => token,
|
|
192
|
+
set: (t) => {
|
|
193
|
+
token = t;
|
|
194
|
+
},
|
|
195
|
+
clear: () => {
|
|
196
|
+
token = null;
|
|
197
|
+
},
|
|
198
|
+
getRefreshToken: () => refreshValue,
|
|
199
|
+
setRefreshToken: (t) => {
|
|
200
|
+
refreshValue = t;
|
|
201
|
+
},
|
|
202
|
+
clearRefreshToken: () => {
|
|
203
|
+
refreshValue = null;
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
function createNoopStorage() {
|
|
208
|
+
return {
|
|
209
|
+
get: () => null,
|
|
210
|
+
set: () => {
|
|
211
|
+
},
|
|
212
|
+
clear: () => {
|
|
213
|
+
},
|
|
214
|
+
getRefreshToken: () => null,
|
|
215
|
+
setRefreshToken: () => {
|
|
216
|
+
},
|
|
217
|
+
clearRefreshToken: () => {
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
function createTokenStorage(config) {
|
|
222
|
+
if (config.auth === "cookie") return createNoopStorage();
|
|
223
|
+
const key = config.tokenKey ?? "x-user-token";
|
|
224
|
+
const type = config.tokenStorage ?? "sessionStorage";
|
|
225
|
+
switch (type) {
|
|
226
|
+
case "sessionStorage":
|
|
227
|
+
return createSessionStorageStorage(key);
|
|
228
|
+
case "memory":
|
|
229
|
+
return createMemoryStorage();
|
|
230
|
+
case "localStorage":
|
|
231
|
+
default:
|
|
232
|
+
return createLocalStorageStorage(key);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// src/auth/hooks.ts
|
|
237
|
+
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
238
|
+
import { useNavigate } from "@tanstack/react-router";
|
|
239
|
+
import { useSetAtom } from "jotai";
|
|
240
|
+
|
|
241
|
+
// src/types.ts
|
|
242
|
+
function isMfaChallenge(result) {
|
|
243
|
+
return "mfaToken" in result && !("id" in result);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// src/auth/hooks.ts
|
|
247
|
+
var AUTH_QUERY_KEY = ["auth", "me"];
|
|
248
|
+
function createAuthHooks({ api, storage, config, pendingMfaChallengeAtom, onLoginSuccess }) {
|
|
249
|
+
function useUser() {
|
|
250
|
+
const { data: user = null, isLoading, isError } = useQuery({
|
|
251
|
+
queryKey: AUTH_QUERY_KEY,
|
|
252
|
+
queryFn: async () => {
|
|
253
|
+
try {
|
|
254
|
+
return await api.get("/auth/me");
|
|
255
|
+
} catch {
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
staleTime: config.staleTime ?? 5 * 60 * 1e3,
|
|
260
|
+
retry: false
|
|
261
|
+
});
|
|
262
|
+
return { user, isLoading, isError };
|
|
263
|
+
}
|
|
264
|
+
function useLogin() {
|
|
265
|
+
const queryClient = useQueryClient();
|
|
266
|
+
const navigate = useNavigate();
|
|
267
|
+
const setMfaChallenge = useSetAtom(pendingMfaChallengeAtom);
|
|
268
|
+
return useMutation({
|
|
269
|
+
mutationFn: async ({ redirectTo: _, ...body }) => {
|
|
270
|
+
const res = await api.post("/auth/login", body);
|
|
271
|
+
if (res.mfaRequired) {
|
|
272
|
+
return { mfaToken: res.mfaToken, mfaMethods: res.mfaMethods ?? [] };
|
|
273
|
+
}
|
|
274
|
+
if (config.auth !== "cookie") {
|
|
275
|
+
if (res.token) {
|
|
276
|
+
storage.set(res.token);
|
|
277
|
+
}
|
|
278
|
+
if (res.refreshToken) {
|
|
279
|
+
storage.setRefreshToken(res.refreshToken);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return api.get("/auth/me");
|
|
283
|
+
},
|
|
284
|
+
onSuccess: (result, vars) => {
|
|
285
|
+
if (isMfaChallenge(result)) {
|
|
286
|
+
setMfaChallenge({ mfaToken: result.mfaToken, mfaMethods: result.mfaMethods });
|
|
287
|
+
if (config.mfaPath) navigate({ to: config.mfaPath });
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
queryClient.setQueryData(AUTH_QUERY_KEY, result);
|
|
291
|
+
onLoginSuccess?.();
|
|
292
|
+
const to = vars.redirectTo ?? config.homePath;
|
|
293
|
+
if (to) navigate({ to });
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
function useLogout() {
|
|
298
|
+
const queryClient = useQueryClient();
|
|
299
|
+
const navigate = useNavigate();
|
|
300
|
+
const setMfaChallenge = useSetAtom(pendingMfaChallengeAtom);
|
|
301
|
+
function cleanup(vars) {
|
|
302
|
+
storage.clear();
|
|
303
|
+
storage.clearRefreshToken();
|
|
304
|
+
queryClient.clear();
|
|
305
|
+
config.onUnauthenticated?.();
|
|
306
|
+
const to = vars?.redirectTo ?? config.loginPath;
|
|
307
|
+
if (to) navigate({ to });
|
|
308
|
+
}
|
|
309
|
+
return useMutation({
|
|
310
|
+
mutationFn: () => api.post("/auth/logout", {}),
|
|
311
|
+
onSuccess: (_data, vars) => {
|
|
312
|
+
setMfaChallenge(null);
|
|
313
|
+
cleanup(vars);
|
|
314
|
+
},
|
|
315
|
+
onError: (_err, vars) => cleanup(vars)
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
function useRegister() {
|
|
319
|
+
const queryClient = useQueryClient();
|
|
320
|
+
const navigate = useNavigate();
|
|
321
|
+
return useMutation({
|
|
322
|
+
mutationFn: async ({ redirectTo: _, ...body }) => {
|
|
323
|
+
const res = await api.post("/auth/register", body);
|
|
324
|
+
if (config.auth !== "cookie") {
|
|
325
|
+
if (res && typeof res["token"] === "string") {
|
|
326
|
+
storage.set(res["token"]);
|
|
327
|
+
}
|
|
328
|
+
if (typeof res["refreshToken"] === "string") {
|
|
329
|
+
storage.setRefreshToken(res["refreshToken"]);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return api.get("/auth/me");
|
|
333
|
+
},
|
|
334
|
+
onSuccess: (user, vars) => {
|
|
335
|
+
queryClient.setQueryData(AUTH_QUERY_KEY, user);
|
|
336
|
+
onLoginSuccess?.();
|
|
337
|
+
const to = vars.redirectTo ?? config.homePath;
|
|
338
|
+
if (to) navigate({ to });
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
function useForgotPassword() {
|
|
343
|
+
return useMutation({
|
|
344
|
+
mutationFn: (body) => api.post("/auth/forgot-password", body)
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
return { useUser, useLogin, useLogout, useRegister, useForgotPassword };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// src/auth/mfa-hooks.ts
|
|
351
|
+
import { useQuery as useQuery2, useMutation as useMutation2, useQueryClient as useQueryClient2 } from "@tanstack/react-query";
|
|
352
|
+
import { useNavigate as useNavigate2 } from "@tanstack/react-router";
|
|
353
|
+
import { useAtomValue, useSetAtom as useSetAtom2 } from "jotai";
|
|
354
|
+
var AUTH_QUERY_KEY2 = ["auth", "me"];
|
|
355
|
+
function createMfaHooks({ api, storage, config, pendingMfaChallengeAtom, onLoginSuccess }) {
|
|
356
|
+
function useMfaVerify() {
|
|
357
|
+
const queryClient = useQueryClient2();
|
|
358
|
+
const navigate = useNavigate2();
|
|
359
|
+
const pendingChallenge = useAtomValue(pendingMfaChallengeAtom);
|
|
360
|
+
const setMfaChallenge = useSetAtom2(pendingMfaChallengeAtom);
|
|
361
|
+
return useMutation2({
|
|
362
|
+
mutationFn: async (body) => {
|
|
363
|
+
if (!pendingChallenge) throw new Error("No pending MFA challenge");
|
|
364
|
+
const res = await api.post("/auth/mfa/verify", {
|
|
365
|
+
...body,
|
|
366
|
+
mfaToken: pendingChallenge.mfaToken
|
|
367
|
+
});
|
|
368
|
+
if (config.auth !== "cookie" && res.token) {
|
|
369
|
+
storage.set(res.token);
|
|
370
|
+
if (res.refreshToken) {
|
|
371
|
+
storage.setRefreshToken(res.refreshToken);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return api.get("/auth/me");
|
|
375
|
+
},
|
|
376
|
+
onSuccess: (user) => {
|
|
377
|
+
setMfaChallenge(null);
|
|
378
|
+
queryClient.setQueryData(AUTH_QUERY_KEY2, user);
|
|
379
|
+
onLoginSuccess?.();
|
|
380
|
+
if (config.homePath) navigate({ to: config.homePath });
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
function useMfaSetup() {
|
|
385
|
+
return useMutation2({
|
|
386
|
+
mutationFn: () => api.post("/auth/mfa/setup", {})
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
function useMfaVerifySetup() {
|
|
390
|
+
return useMutation2({
|
|
391
|
+
mutationFn: (body) => api.post("/auth/mfa/verify-setup", body)
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
function useMfaDisable() {
|
|
395
|
+
return useMutation2({
|
|
396
|
+
mutationFn: (body) => api.delete("/auth/mfa", body)
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
function useMfaRecoveryCodes() {
|
|
400
|
+
return useMutation2({
|
|
401
|
+
mutationFn: (body) => api.post("/auth/mfa/recovery-codes", body)
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
function useMfaEmailOtpEnable() {
|
|
405
|
+
return useMutation2({
|
|
406
|
+
mutationFn: () => api.post("/auth/mfa/email-otp/enable", {})
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
function useMfaEmailOtpVerifySetup() {
|
|
410
|
+
return useMutation2({
|
|
411
|
+
mutationFn: (body) => api.post("/auth/mfa/email-otp/verify-setup", body)
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
function useMfaEmailOtpDisable() {
|
|
415
|
+
return useMutation2({
|
|
416
|
+
mutationFn: (body) => api.delete("/auth/mfa/email-otp", body)
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
function useMfaResend() {
|
|
420
|
+
return useMutation2({
|
|
421
|
+
mutationFn: (body) => api.post("/auth/mfa/resend", body)
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
function useMfaMethods() {
|
|
425
|
+
const { data: methods = null, isLoading, isError } = useQuery2({
|
|
426
|
+
queryKey: ["auth", "mfa", "methods"],
|
|
427
|
+
queryFn: async () => {
|
|
428
|
+
try {
|
|
429
|
+
const res = await api.get("/auth/mfa/methods");
|
|
430
|
+
return res.methods;
|
|
431
|
+
} catch {
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
},
|
|
435
|
+
staleTime: config.staleTime ?? 5 * 60 * 1e3,
|
|
436
|
+
retry: false
|
|
437
|
+
});
|
|
438
|
+
return { methods, isLoading, isError };
|
|
439
|
+
}
|
|
440
|
+
return {
|
|
441
|
+
useMfaVerify,
|
|
442
|
+
useMfaSetup,
|
|
443
|
+
useMfaVerifySetup,
|
|
444
|
+
useMfaDisable,
|
|
445
|
+
useMfaRecoveryCodes,
|
|
446
|
+
useMfaEmailOtpEnable,
|
|
447
|
+
useMfaEmailOtpVerifySetup,
|
|
448
|
+
useMfaEmailOtpDisable,
|
|
449
|
+
useMfaResend,
|
|
450
|
+
useMfaMethods
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// src/auth/account-hooks.ts
|
|
455
|
+
import { useQuery as useQuery3, useMutation as useMutation3, useQueryClient as useQueryClient3 } from "@tanstack/react-query";
|
|
456
|
+
import { useNavigate as useNavigate3 } from "@tanstack/react-router";
|
|
457
|
+
function createAccountHooks({ api, storage, config, onUnauthenticated, queryClient: qc }) {
|
|
458
|
+
function useResetPassword() {
|
|
459
|
+
return useMutation3({
|
|
460
|
+
mutationFn: (body) => api.post("/auth/reset-password", body)
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
function useVerifyEmail() {
|
|
464
|
+
return useMutation3({
|
|
465
|
+
mutationFn: (body) => api.post("/auth/verify-email", body)
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
function useResendVerification() {
|
|
469
|
+
return useMutation3({
|
|
470
|
+
mutationFn: (body) => api.post("/auth/resend-verification", body)
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
function useSetPassword() {
|
|
474
|
+
return useMutation3({
|
|
475
|
+
mutationFn: (body) => api.post("/auth/set-password", body)
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
function useDeleteAccount() {
|
|
479
|
+
const navigate = useNavigate3();
|
|
480
|
+
return useMutation3({
|
|
481
|
+
mutationFn: (body) => api.delete("/auth/me", body ?? {}),
|
|
482
|
+
onSuccess: () => {
|
|
483
|
+
storage.clear();
|
|
484
|
+
qc.clear();
|
|
485
|
+
onUnauthenticated?.();
|
|
486
|
+
if (config.loginPath) navigate({ to: config.loginPath });
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
function useCancelDeletion() {
|
|
491
|
+
return useMutation3({
|
|
492
|
+
mutationFn: () => api.post("/auth/cancel-deletion", {})
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
function useRefreshToken() {
|
|
496
|
+
return useMutation3({
|
|
497
|
+
mutationFn: (body) => api.post("/auth/refresh", body ?? {}),
|
|
498
|
+
onSuccess: (data) => {
|
|
499
|
+
storage.set(data.token);
|
|
500
|
+
if (data.refreshToken) {
|
|
501
|
+
storage.setRefreshToken(data.refreshToken);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
function useSessions() {
|
|
507
|
+
const { data, isLoading, isError } = useQuery3({
|
|
508
|
+
queryKey: ["auth", "sessions"],
|
|
509
|
+
queryFn: () => api.get("/auth/sessions")
|
|
510
|
+
});
|
|
511
|
+
return { sessions: data?.sessions ?? [], isLoading, isError };
|
|
512
|
+
}
|
|
513
|
+
function useRevokeSession() {
|
|
514
|
+
const queryClient = useQueryClient3();
|
|
515
|
+
return useMutation3({
|
|
516
|
+
mutationFn: (sessionId) => api.delete(`/auth/sessions/${sessionId}`, {}),
|
|
517
|
+
onSuccess: () => {
|
|
518
|
+
queryClient.invalidateQueries({ queryKey: ["auth", "sessions"] });
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
return {
|
|
523
|
+
useResetPassword,
|
|
524
|
+
useVerifyEmail,
|
|
525
|
+
useResendVerification,
|
|
526
|
+
useSetPassword,
|
|
527
|
+
useDeleteAccount,
|
|
528
|
+
useCancelDeletion,
|
|
529
|
+
useRefreshToken,
|
|
530
|
+
useSessions,
|
|
531
|
+
useRevokeSession
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// src/auth/oauth-hooks.ts
|
|
536
|
+
import { useMutation as useMutation4, useQueryClient as useQueryClient4 } from "@tanstack/react-query";
|
|
537
|
+
import { useNavigate as useNavigate4 } from "@tanstack/react-router";
|
|
538
|
+
var AUTH_QUERY_KEY3 = ["auth", "me"];
|
|
539
|
+
function createOAuthHooks({ api, storage, config, onLoginSuccess }) {
|
|
540
|
+
function getOAuthUrl(provider) {
|
|
541
|
+
return `${config.apiUrl}/auth/${provider}`;
|
|
542
|
+
}
|
|
543
|
+
function getLinkUrl(provider) {
|
|
544
|
+
return `${config.apiUrl}/auth/${provider}/link`;
|
|
545
|
+
}
|
|
546
|
+
function useOAuthExchange() {
|
|
547
|
+
const queryClient = useQueryClient4();
|
|
548
|
+
const navigate = useNavigate4();
|
|
549
|
+
return useMutation4({
|
|
550
|
+
mutationFn: (body) => api.post("/auth/oauth/exchange", body),
|
|
551
|
+
onSuccess: async (data) => {
|
|
552
|
+
if (config.auth !== "cookie" && data.token) {
|
|
553
|
+
storage.set(data.token);
|
|
554
|
+
if (data.refreshToken) {
|
|
555
|
+
storage.setRefreshToken(data.refreshToken);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
const user = await api.get("/auth/me");
|
|
559
|
+
queryClient.setQueryData(AUTH_QUERY_KEY3, user);
|
|
560
|
+
onLoginSuccess?.();
|
|
561
|
+
if (config.homePath) navigate({ to: config.homePath });
|
|
562
|
+
}
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
function useOAuthUnlink() {
|
|
566
|
+
const queryClient = useQueryClient4();
|
|
567
|
+
return useMutation4({
|
|
568
|
+
mutationFn: (provider) => api.delete(`/auth/${provider}/link`, {}),
|
|
569
|
+
onSuccess: () => {
|
|
570
|
+
queryClient.invalidateQueries({ queryKey: AUTH_QUERY_KEY3 });
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
return {
|
|
575
|
+
getOAuthUrl,
|
|
576
|
+
getLinkUrl,
|
|
577
|
+
useOAuthExchange,
|
|
578
|
+
useOAuthUnlink
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// src/auth/webauthn-hooks.ts
|
|
583
|
+
import { useQuery as useQuery4, useMutation as useMutation5, useQueryClient as useQueryClient5 } from "@tanstack/react-query";
|
|
584
|
+
import { useNavigate as useNavigate5 } from "@tanstack/react-router";
|
|
585
|
+
import { useSetAtom as useSetAtom3 } from "jotai";
|
|
586
|
+
var WEBAUTHN_CREDENTIALS_KEY = ["auth", "webauthn", "credentials"];
|
|
587
|
+
function createWebAuthnHooks({ api, storage, config, pendingMfaChallengeAtom, onLoginSuccess }) {
|
|
588
|
+
function useWebAuthnRegisterOptions() {
|
|
589
|
+
return useMutation5({
|
|
590
|
+
mutationFn: () => api.post("/auth/mfa/webauthn/register-options", {})
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
function useWebAuthnRegister() {
|
|
594
|
+
const queryClient = useQueryClient5();
|
|
595
|
+
return useMutation5({
|
|
596
|
+
mutationFn: (body) => api.post("/auth/mfa/webauthn/register", body),
|
|
597
|
+
onSuccess: () => {
|
|
598
|
+
queryClient.invalidateQueries({ queryKey: WEBAUTHN_CREDENTIALS_KEY });
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
function useWebAuthnCredentials() {
|
|
603
|
+
const { data, isLoading, isError } = useQuery4({
|
|
604
|
+
queryKey: WEBAUTHN_CREDENTIALS_KEY,
|
|
605
|
+
queryFn: () => api.get("/auth/mfa/webauthn/credentials")
|
|
606
|
+
});
|
|
607
|
+
return { credentials: data?.credentials ?? [], isLoading, isError };
|
|
608
|
+
}
|
|
609
|
+
function useWebAuthnRemoveCredential() {
|
|
610
|
+
const queryClient = useQueryClient5();
|
|
611
|
+
return useMutation5({
|
|
612
|
+
mutationFn: (credentialId) => api.delete(`/auth/mfa/webauthn/credentials/${credentialId}`),
|
|
613
|
+
onSuccess: () => {
|
|
614
|
+
queryClient.invalidateQueries({ queryKey: WEBAUTHN_CREDENTIALS_KEY });
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
function useWebAuthnDisable() {
|
|
619
|
+
return useMutation5({
|
|
620
|
+
mutationFn: () => api.delete("/auth/mfa/webauthn", {})
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
function usePasskeyLoginOptions() {
|
|
624
|
+
return useMutation5({
|
|
625
|
+
mutationFn: (body) => api.post("/auth/passkey/login-options", body)
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
function usePasskeyLogin() {
|
|
629
|
+
const queryClient = useQueryClient5();
|
|
630
|
+
const navigate = useNavigate5();
|
|
631
|
+
const setMfaChallenge = useSetAtom3(pendingMfaChallengeAtom);
|
|
632
|
+
return useMutation5({
|
|
633
|
+
mutationFn: async (body) => {
|
|
634
|
+
const response = await api.post("/auth/passkey/login", body);
|
|
635
|
+
if (response.mfaRequired && response.mfaToken && response.mfaMethods) {
|
|
636
|
+
return { mfaToken: response.mfaToken, mfaMethods: response.mfaMethods };
|
|
637
|
+
}
|
|
638
|
+
if (config?.auth !== "cookie" && response.token && storage) {
|
|
639
|
+
storage.set(response.token);
|
|
640
|
+
if (response.refreshToken) {
|
|
641
|
+
storage.setRefreshToken(response.refreshToken);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
return { id: response.userId, email: response.userId };
|
|
645
|
+
},
|
|
646
|
+
onSuccess: (result) => {
|
|
647
|
+
if (isMfaChallenge(result)) {
|
|
648
|
+
setMfaChallenge({ mfaToken: result.mfaToken, mfaMethods: result.mfaMethods });
|
|
649
|
+
if (config?.mfaPath) navigate({ to: config.mfaPath });
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
queryClient.invalidateQueries({ queryKey: ["auth", "me"] });
|
|
653
|
+
onLoginSuccess?.();
|
|
654
|
+
if (config?.homePath) {
|
|
655
|
+
window.location.href = config.homePath;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
return {
|
|
661
|
+
useWebAuthnRegisterOptions,
|
|
662
|
+
useWebAuthnRegister,
|
|
663
|
+
useWebAuthnCredentials,
|
|
664
|
+
useWebAuthnRemoveCredential,
|
|
665
|
+
useWebAuthnDisable,
|
|
666
|
+
usePasskeyLoginOptions,
|
|
667
|
+
usePasskeyLogin
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// src/ws/manager.ts
|
|
672
|
+
var WebSocketManager = class {
|
|
673
|
+
ws = null;
|
|
674
|
+
rooms = /* @__PURE__ */ new Set();
|
|
675
|
+
listeners = /* @__PURE__ */ new Map();
|
|
676
|
+
reconnectAttempts = 0;
|
|
677
|
+
reconnectTimer = null;
|
|
678
|
+
destroyed = false;
|
|
679
|
+
url;
|
|
680
|
+
autoReconnect;
|
|
681
|
+
reconnectOnFocus;
|
|
682
|
+
maxReconnectAttempts;
|
|
683
|
+
reconnectBaseDelay;
|
|
684
|
+
reconnectMaxDelay;
|
|
685
|
+
onConnected;
|
|
686
|
+
onDisconnected;
|
|
687
|
+
onReconnecting;
|
|
688
|
+
onReconnectFailed;
|
|
689
|
+
constructor(config) {
|
|
690
|
+
this.url = config.url;
|
|
691
|
+
this.autoReconnect = config.autoReconnect ?? true;
|
|
692
|
+
this.reconnectOnFocus = config.reconnectOnFocus ?? true;
|
|
693
|
+
this.maxReconnectAttempts = config.maxReconnectAttempts ?? Infinity;
|
|
694
|
+
this.reconnectBaseDelay = config.reconnectBaseDelay ?? 1e3;
|
|
695
|
+
this.reconnectMaxDelay = config.reconnectMaxDelay ?? 3e4;
|
|
696
|
+
this.onConnected = config.onConnected;
|
|
697
|
+
this.onDisconnected = config.onDisconnected;
|
|
698
|
+
this.onReconnecting = config.onReconnecting;
|
|
699
|
+
this.onReconnectFailed = config.onReconnectFailed;
|
|
700
|
+
this.connect();
|
|
701
|
+
if (this.reconnectOnFocus) {
|
|
702
|
+
document.addEventListener("visibilitychange", this.handleVisibilityChange);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
get isConnected() {
|
|
706
|
+
return this.ws?.readyState === WebSocket.OPEN;
|
|
707
|
+
}
|
|
708
|
+
handleVisibilityChange = () => {
|
|
709
|
+
if (document.visibilityState === "visible" && !this.isConnected && !this.destroyed) {
|
|
710
|
+
this.reconnect();
|
|
711
|
+
}
|
|
712
|
+
};
|
|
713
|
+
connect() {
|
|
714
|
+
if (this.destroyed) return;
|
|
715
|
+
try {
|
|
716
|
+
this.ws = new WebSocket(this.url);
|
|
717
|
+
} catch {
|
|
718
|
+
this.scheduleReconnect();
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
this.ws.onopen = () => {
|
|
722
|
+
this.reconnectAttempts = 0;
|
|
723
|
+
this.clearReconnectTimer();
|
|
724
|
+
this.onConnected?.();
|
|
725
|
+
this.rooms.forEach((room) => this.sendMessage({ type: "subscribe", room }));
|
|
726
|
+
};
|
|
727
|
+
this.ws.onclose = () => {
|
|
728
|
+
this.onDisconnected?.();
|
|
729
|
+
if (this.autoReconnect && !this.destroyed) {
|
|
730
|
+
this.scheduleReconnect();
|
|
731
|
+
}
|
|
732
|
+
};
|
|
733
|
+
this.ws.onerror = () => {
|
|
734
|
+
};
|
|
735
|
+
this.ws.onmessage = (event) => {
|
|
736
|
+
try {
|
|
737
|
+
const message = JSON.parse(event.data);
|
|
738
|
+
const handlers = this.listeners.get(message["type"]);
|
|
739
|
+
handlers?.forEach((h) => h(message));
|
|
740
|
+
} catch {
|
|
741
|
+
}
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
sendMessage(message) {
|
|
745
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
746
|
+
this.ws.send(JSON.stringify(message));
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
scheduleReconnect() {
|
|
750
|
+
if (this.destroyed || this.reconnectTimer !== null) return;
|
|
751
|
+
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
752
|
+
this.onReconnectFailed?.();
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
this.reconnectAttempts++;
|
|
756
|
+
const delay = Math.min(
|
|
757
|
+
this.reconnectBaseDelay * Math.pow(2, this.reconnectAttempts - 1),
|
|
758
|
+
this.reconnectMaxDelay
|
|
759
|
+
);
|
|
760
|
+
this.onReconnecting?.(this.reconnectAttempts);
|
|
761
|
+
this.reconnectTimer = setTimeout(() => {
|
|
762
|
+
this.reconnectTimer = null;
|
|
763
|
+
this.connect();
|
|
764
|
+
}, delay);
|
|
765
|
+
}
|
|
766
|
+
clearReconnectTimer() {
|
|
767
|
+
if (this.reconnectTimer !== null) {
|
|
768
|
+
clearTimeout(this.reconnectTimer);
|
|
769
|
+
this.reconnectTimer = null;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
subscribe(room) {
|
|
773
|
+
this.rooms.add(room);
|
|
774
|
+
this.sendMessage({ type: "subscribe", room });
|
|
775
|
+
}
|
|
776
|
+
unsubscribe(room) {
|
|
777
|
+
this.rooms.delete(room);
|
|
778
|
+
this.sendMessage({ type: "unsubscribe", room });
|
|
779
|
+
}
|
|
780
|
+
getRooms() {
|
|
781
|
+
return Array.from(this.rooms);
|
|
782
|
+
}
|
|
783
|
+
send(type, payload) {
|
|
784
|
+
this.sendMessage({ type, payload });
|
|
785
|
+
}
|
|
786
|
+
on(event, handler) {
|
|
787
|
+
const key = event;
|
|
788
|
+
if (!this.listeners.has(key)) {
|
|
789
|
+
this.listeners.set(key, /* @__PURE__ */ new Set());
|
|
790
|
+
}
|
|
791
|
+
this.listeners.get(key).add(handler);
|
|
792
|
+
}
|
|
793
|
+
off(event, handler) {
|
|
794
|
+
const key = event;
|
|
795
|
+
this.listeners.get(key)?.delete(handler);
|
|
796
|
+
}
|
|
797
|
+
reconnect() {
|
|
798
|
+
this.clearReconnectTimer();
|
|
799
|
+
if (this.ws) {
|
|
800
|
+
this.ws.onclose = null;
|
|
801
|
+
this.ws.close();
|
|
802
|
+
this.ws = null;
|
|
803
|
+
}
|
|
804
|
+
this.reconnectAttempts = 0;
|
|
805
|
+
this.connect();
|
|
806
|
+
}
|
|
807
|
+
disconnect() {
|
|
808
|
+
this.destroyed = true;
|
|
809
|
+
this.clearReconnectTimer();
|
|
810
|
+
document.removeEventListener("visibilitychange", this.handleVisibilityChange);
|
|
811
|
+
if (this.ws) {
|
|
812
|
+
this.ws.onclose = null;
|
|
813
|
+
this.ws.close();
|
|
814
|
+
this.ws = null;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
};
|
|
818
|
+
|
|
819
|
+
// src/ws/atom.ts
|
|
820
|
+
import { atom } from "jotai";
|
|
821
|
+
var wsManagerAtom = atom(null);
|
|
822
|
+
|
|
823
|
+
// src/ws/hook.ts
|
|
824
|
+
import { useAtomValue as useAtomValue2 } from "jotai";
|
|
825
|
+
import { useEffect, useState } from "react";
|
|
826
|
+
function createWsHooks() {
|
|
827
|
+
function useWebSocketManager() {
|
|
828
|
+
return useAtomValue2(wsManagerAtom);
|
|
829
|
+
}
|
|
830
|
+
function useSocket() {
|
|
831
|
+
const manager = useWebSocketManager();
|
|
832
|
+
const [isConnected, setIsConnected] = useState(manager?.isConnected ?? false);
|
|
833
|
+
useEffect(() => {
|
|
834
|
+
if (!manager) return;
|
|
835
|
+
const interval = setInterval(() => {
|
|
836
|
+
setIsConnected(manager.isConnected);
|
|
837
|
+
}, 500);
|
|
838
|
+
return () => clearInterval(interval);
|
|
839
|
+
}, [manager]);
|
|
840
|
+
return {
|
|
841
|
+
isConnected,
|
|
842
|
+
send: (type, payload) => manager?.send(type, payload),
|
|
843
|
+
subscribe: (room) => manager?.subscribe(room),
|
|
844
|
+
unsubscribe: (room) => manager?.unsubscribe(room),
|
|
845
|
+
getRooms: () => manager?.getRooms() ?? [],
|
|
846
|
+
on: (event, handler) => manager?.on(event, handler),
|
|
847
|
+
off: (event, handler) => manager?.off(event, handler),
|
|
848
|
+
reconnect: () => manager?.reconnect()
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
function useRoom(room) {
|
|
852
|
+
const manager = useWebSocketManager();
|
|
853
|
+
const [isSubscribed, setIsSubscribed] = useState(false);
|
|
854
|
+
useEffect(() => {
|
|
855
|
+
if (!manager) return;
|
|
856
|
+
manager.subscribe(room);
|
|
857
|
+
setIsSubscribed(true);
|
|
858
|
+
return () => {
|
|
859
|
+
manager.unsubscribe(room);
|
|
860
|
+
setIsSubscribed(false);
|
|
861
|
+
};
|
|
862
|
+
}, [room, manager]);
|
|
863
|
+
return { isSubscribed };
|
|
864
|
+
}
|
|
865
|
+
function useRoomEvent(room, event, handler) {
|
|
866
|
+
const manager = useWebSocketManager();
|
|
867
|
+
useEffect(() => {
|
|
868
|
+
if (!manager) return;
|
|
869
|
+
const scoped = (data) => {
|
|
870
|
+
if (data["room"] === room && data["payload"] !== void 0) {
|
|
871
|
+
handler(data["payload"]);
|
|
872
|
+
}
|
|
873
|
+
};
|
|
874
|
+
manager.on(event, scoped);
|
|
875
|
+
return () => {
|
|
876
|
+
manager.off(event, scoped);
|
|
877
|
+
};
|
|
878
|
+
}, [room, event, handler, manager]);
|
|
879
|
+
}
|
|
880
|
+
return { useWebSocketManager, useSocket, useRoom, useRoomEvent };
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// src/theme/hook.ts
|
|
884
|
+
import { atomWithStorage } from "jotai/utils";
|
|
885
|
+
import { useAtom } from "jotai";
|
|
886
|
+
import { useEffect as useEffect2 } from "react";
|
|
887
|
+
var getInitialTheme = () => {
|
|
888
|
+
if (typeof window === "undefined") return "light";
|
|
889
|
+
const stored = localStorage.getItem("snapshot-theme");
|
|
890
|
+
if (stored === "light" || stored === "dark") return stored;
|
|
891
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
892
|
+
};
|
|
893
|
+
var themeAtom = atomWithStorage("snapshot-theme", getInitialTheme());
|
|
894
|
+
var styleInjected = false;
|
|
895
|
+
function ensureNoTransitionStyle() {
|
|
896
|
+
if (styleInjected || typeof document === "undefined") return;
|
|
897
|
+
const style = document.createElement("style");
|
|
898
|
+
style.textContent = ".no-transition, .no-transition * { transition: none !important; }";
|
|
899
|
+
document.head.appendChild(style);
|
|
900
|
+
styleInjected = true;
|
|
901
|
+
}
|
|
902
|
+
function useTheme() {
|
|
903
|
+
const [theme, setTheme] = useAtom(themeAtom);
|
|
904
|
+
useEffect2(() => {
|
|
905
|
+
ensureNoTransitionStyle();
|
|
906
|
+
const root = document.documentElement;
|
|
907
|
+
root.classList.add("no-transition");
|
|
908
|
+
if (theme === "dark") {
|
|
909
|
+
root.classList.add("dark");
|
|
910
|
+
} else {
|
|
911
|
+
root.classList.remove("dark");
|
|
912
|
+
}
|
|
913
|
+
requestAnimationFrame(() => {
|
|
914
|
+
requestAnimationFrame(() => {
|
|
915
|
+
root.classList.remove("no-transition");
|
|
916
|
+
});
|
|
917
|
+
});
|
|
918
|
+
}, [theme]);
|
|
919
|
+
return {
|
|
920
|
+
theme,
|
|
921
|
+
toggle: () => setTheme((t) => t === "dark" ? "light" : "dark"),
|
|
922
|
+
set: (t) => setTheme(t)
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// src/routing/loaders.ts
|
|
927
|
+
import { redirect } from "@tanstack/react-router";
|
|
928
|
+
var AUTH_QUERY_KEY4 = ["auth", "me"];
|
|
929
|
+
function createLoaders(config, api) {
|
|
930
|
+
async function fetchUser(queryClient) {
|
|
931
|
+
try {
|
|
932
|
+
return await queryClient.ensureQueryData({
|
|
933
|
+
queryKey: AUTH_QUERY_KEY4,
|
|
934
|
+
queryFn: async () => {
|
|
935
|
+
try {
|
|
936
|
+
return await api.get("/auth/me");
|
|
937
|
+
} catch {
|
|
938
|
+
return null;
|
|
939
|
+
}
|
|
940
|
+
},
|
|
941
|
+
staleTime: config.staleTime ?? 5 * 60 * 1e3
|
|
942
|
+
});
|
|
943
|
+
} catch {
|
|
944
|
+
return null;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
async function protectedBeforeLoad({ context }) {
|
|
948
|
+
const user = await fetchUser(context.queryClient);
|
|
949
|
+
if (!user) {
|
|
950
|
+
config.onUnauthenticated?.();
|
|
951
|
+
if (!config.loginPath) {
|
|
952
|
+
if (typeof process !== "undefined" && process.env?.["NODE_ENV"] !== "production") {
|
|
953
|
+
throw new Error(
|
|
954
|
+
"[snapshot] protectedBeforeLoad: no loginPath configured. Set loginPath in createSnapshot config."
|
|
955
|
+
);
|
|
956
|
+
}
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
throw redirect({ to: config.loginPath });
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
async function guestBeforeLoad({ context }) {
|
|
963
|
+
const user = await fetchUser(context.queryClient);
|
|
964
|
+
if (user) {
|
|
965
|
+
if (!config.homePath) {
|
|
966
|
+
if (typeof process !== "undefined" && process.env?.["NODE_ENV"] !== "production") {
|
|
967
|
+
throw new Error(
|
|
968
|
+
"[snapshot] guestBeforeLoad: no homePath configured. Set homePath in createSnapshot config."
|
|
969
|
+
);
|
|
970
|
+
}
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
throw redirect({ to: config.homePath });
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
return { protectedBeforeLoad, guestBeforeLoad };
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
// src/providers/QueryProvider.tsx
|
|
980
|
+
import { QueryClientProvider } from "@tanstack/react-query";
|
|
981
|
+
import { jsx } from "react/jsx-runtime";
|
|
982
|
+
function QueryProviderInner({ client, children }) {
|
|
983
|
+
return /* @__PURE__ */ jsx(QueryClientProvider, { client, children });
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// src/create-snapshot.tsx
|
|
987
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
988
|
+
function createSnapshot(config) {
|
|
989
|
+
const api = new ApiClient({
|
|
990
|
+
apiUrl: config.apiUrl,
|
|
991
|
+
auth: config.auth,
|
|
992
|
+
bearerToken: config.bearerToken,
|
|
993
|
+
onUnauthenticated: config.onUnauthenticated,
|
|
994
|
+
onForbidden: config.onForbidden,
|
|
995
|
+
onMfaSetupRequired: config.mfaSetupPath ? () => {
|
|
996
|
+
window.location.href = config.mfaSetupPath;
|
|
997
|
+
} : void 0
|
|
998
|
+
});
|
|
999
|
+
const tokenStorage = createTokenStorage({
|
|
1000
|
+
auth: config.auth,
|
|
1001
|
+
tokenStorage: config.tokenStorage,
|
|
1002
|
+
tokenKey: config.tokenKey
|
|
1003
|
+
});
|
|
1004
|
+
api.setStorage(tokenStorage);
|
|
1005
|
+
const queryClient = new QueryClient({
|
|
1006
|
+
defaultOptions: {
|
|
1007
|
+
queries: {
|
|
1008
|
+
staleTime: config.staleTime ?? 5 * 60 * 1e3,
|
|
1009
|
+
gcTime: config.gcTime ?? 10 * 60 * 1e3,
|
|
1010
|
+
retry: config.retry ?? 1
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
});
|
|
1014
|
+
let wsManager = null;
|
|
1015
|
+
if (config.ws) {
|
|
1016
|
+
wsManager = new WebSocketManager(config.ws);
|
|
1017
|
+
}
|
|
1018
|
+
const pendingMfaChallengeAtom = atom2(null);
|
|
1019
|
+
function usePendingMfaChallenge() {
|
|
1020
|
+
return useAtomValue3(pendingMfaChallengeAtom);
|
|
1021
|
+
}
|
|
1022
|
+
const { useWebSocketManager, useSocket, useRoom, useRoomEvent } = createWsHooks();
|
|
1023
|
+
function useWebSocketManagerWithInit() {
|
|
1024
|
+
const [current, setManager] = useAtom2(wsManagerAtom);
|
|
1025
|
+
if (wsManager !== null && current === null) {
|
|
1026
|
+
setManager(wsManager);
|
|
1027
|
+
}
|
|
1028
|
+
return current;
|
|
1029
|
+
}
|
|
1030
|
+
const { useUser, useLogin, useLogout, useRegister, useForgotPassword } = createAuthHooks({
|
|
1031
|
+
api,
|
|
1032
|
+
storage: tokenStorage,
|
|
1033
|
+
config,
|
|
1034
|
+
pendingMfaChallengeAtom,
|
|
1035
|
+
onLoginSuccess: config.ws?.reconnectOnLogin !== false ? () => wsManager?.reconnect() : void 0
|
|
1036
|
+
});
|
|
1037
|
+
const mfaHooks = createMfaHooks({
|
|
1038
|
+
api,
|
|
1039
|
+
storage: tokenStorage,
|
|
1040
|
+
config,
|
|
1041
|
+
pendingMfaChallengeAtom,
|
|
1042
|
+
onLoginSuccess: config.ws?.reconnectOnLogin !== false ? () => wsManager?.reconnect() : void 0
|
|
1043
|
+
});
|
|
1044
|
+
const accountHooks = createAccountHooks({
|
|
1045
|
+
api,
|
|
1046
|
+
storage: tokenStorage,
|
|
1047
|
+
config,
|
|
1048
|
+
onUnauthenticated: config.onUnauthenticated,
|
|
1049
|
+
queryClient
|
|
1050
|
+
});
|
|
1051
|
+
const oauthHooks = createOAuthHooks({
|
|
1052
|
+
api,
|
|
1053
|
+
storage: tokenStorage,
|
|
1054
|
+
config,
|
|
1055
|
+
onLoginSuccess: config.ws?.reconnectOnLogin !== false ? () => wsManager?.reconnect() : void 0
|
|
1056
|
+
});
|
|
1057
|
+
const webAuthnHooks = createWebAuthnHooks({
|
|
1058
|
+
api,
|
|
1059
|
+
storage: tokenStorage,
|
|
1060
|
+
config,
|
|
1061
|
+
pendingMfaChallengeAtom,
|
|
1062
|
+
onLoginSuccess: config.ws?.reconnectOnLogin !== false ? () => wsManager?.reconnect() : void 0
|
|
1063
|
+
});
|
|
1064
|
+
const { protectedBeforeLoad, guestBeforeLoad } = createLoaders(config, api);
|
|
1065
|
+
function QueryProvider({ children }) {
|
|
1066
|
+
return /* @__PURE__ */ jsx2(QueryProviderInner, { client: queryClient, children });
|
|
1067
|
+
}
|
|
1068
|
+
return {
|
|
1069
|
+
// High-level hooks
|
|
1070
|
+
useUser,
|
|
1071
|
+
useLogin,
|
|
1072
|
+
useLogout,
|
|
1073
|
+
useRegister,
|
|
1074
|
+
useForgotPassword,
|
|
1075
|
+
useSocket,
|
|
1076
|
+
useRoom,
|
|
1077
|
+
useRoomEvent,
|
|
1078
|
+
useTheme,
|
|
1079
|
+
// MFA
|
|
1080
|
+
usePendingMfaChallenge,
|
|
1081
|
+
...mfaHooks,
|
|
1082
|
+
isMfaChallenge,
|
|
1083
|
+
// Account
|
|
1084
|
+
...accountHooks,
|
|
1085
|
+
// OAuth
|
|
1086
|
+
...oauthHooks,
|
|
1087
|
+
// WebAuthn
|
|
1088
|
+
...webAuthnHooks,
|
|
1089
|
+
// Primitives
|
|
1090
|
+
api,
|
|
1091
|
+
tokenStorage,
|
|
1092
|
+
queryClient,
|
|
1093
|
+
useWebSocketManager: useWebSocketManagerWithInit,
|
|
1094
|
+
// Routing
|
|
1095
|
+
protectedBeforeLoad,
|
|
1096
|
+
guestBeforeLoad,
|
|
1097
|
+
// Components
|
|
1098
|
+
QueryProvider
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
export {
|
|
1102
|
+
ApiError,
|
|
1103
|
+
createSnapshot,
|
|
1104
|
+
isMfaChallenge
|
|
1105
|
+
};
|
|
1106
|
+
//# sourceMappingURL=index.js.map
|