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