@instroc/auth 1.3.1 → 2.0.0-alpha.2
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/dist/chunk-26ODM5TP.js +958 -0
- package/dist/forms.d.ts +1 -1
- package/dist/forms.js +1 -1
- package/dist/{index-iglVgSQI.d.ts → index-BsFMnPty.d.ts} +1 -1
- package/dist/index.d.ts +36 -11
- package/dist/index.js +36 -2
- package/package.json +11 -10
- package/dist/chunk-4FR5RRCB.js +0 -1036
|
@@ -0,0 +1,958 @@
|
|
|
1
|
+
// src/forms/use-login-form.ts
|
|
2
|
+
import { useState as useState2, useCallback, useRef, useEffect as useEffect2 } from "react";
|
|
3
|
+
|
|
4
|
+
// src/use-auth.ts
|
|
5
|
+
import { useEffect, useState } from "react";
|
|
6
|
+
import { setInstrocProjectId, useInstrocConfig } from "@instroc/client";
|
|
7
|
+
|
|
8
|
+
// src/auth-actions.ts
|
|
9
|
+
import { getInstrocConfig as getInstrocConfig2 } from "@instroc/client";
|
|
10
|
+
|
|
11
|
+
// src/errors.ts
|
|
12
|
+
var AuthError = class _AuthError extends Error {
|
|
13
|
+
constructor(message, status, code) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = "AuthError";
|
|
16
|
+
this.status = status;
|
|
17
|
+
this.code = code;
|
|
18
|
+
Object.setPrototypeOf(this, _AuthError.prototype);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
function authErrorFromResponse(response, data, fallbackMessage) {
|
|
22
|
+
const body = data ?? {};
|
|
23
|
+
const message = body.error || fallbackMessage;
|
|
24
|
+
return new AuthError(message, response.status, body.code);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// src/auth-fetch.ts
|
|
28
|
+
import { getInstrocConfig } from "@instroc/client";
|
|
29
|
+
|
|
30
|
+
// src/version.ts
|
|
31
|
+
var SDK_NAME = "instroc-auth";
|
|
32
|
+
var SDK_VERSION = true ? "2.0.0-alpha.2" : "0.0.0-dev";
|
|
33
|
+
function withSdkHeader(init) {
|
|
34
|
+
const headers = new Headers(init?.headers ?? {});
|
|
35
|
+
headers.set("X-Instroc-SDK", `${SDK_NAME}/${SDK_VERSION}`);
|
|
36
|
+
return { ...init, headers };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// src/auth-fetch.ts
|
|
40
|
+
var WARNING_SESSION_KEY = "instroc_auth_sdk_warning_seen";
|
|
41
|
+
function noteDeprecationWarning(response) {
|
|
42
|
+
const warning = response.headers?.get?.("X-Instroc-SDK-Warning");
|
|
43
|
+
if (!warning)
|
|
44
|
+
return;
|
|
45
|
+
if (typeof window === "undefined")
|
|
46
|
+
return;
|
|
47
|
+
try {
|
|
48
|
+
if (sessionStorage.getItem(WARNING_SESSION_KEY))
|
|
49
|
+
return;
|
|
50
|
+
sessionStorage.setItem(WARNING_SESSION_KEY, "1");
|
|
51
|
+
} catch {
|
|
52
|
+
}
|
|
53
|
+
console.warn(`[instroc-auth] ${warning}`);
|
|
54
|
+
}
|
|
55
|
+
function buildAuthUrl(endpoint) {
|
|
56
|
+
const { projectId, baseUrl } = getInstrocConfig();
|
|
57
|
+
if (!projectId) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
"Instroc SDK is not configured \u2014 call initInstroc({ projectId }) before using auth."
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
return `${baseUrl}/${projectId}/auth/${endpoint}`;
|
|
63
|
+
}
|
|
64
|
+
async function authFetch(url, init) {
|
|
65
|
+
const response = await fetch(url, {
|
|
66
|
+
...withSdkHeader(init),
|
|
67
|
+
credentials: "include"
|
|
68
|
+
});
|
|
69
|
+
noteDeprecationWarning(response);
|
|
70
|
+
return response;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// src/auth-store.ts
|
|
74
|
+
import { useSyncExternalStore } from "react";
|
|
75
|
+
import { createStore } from "zustand/vanilla";
|
|
76
|
+
var INITIAL_STATE = {
|
|
77
|
+
user: null,
|
|
78
|
+
session: null,
|
|
79
|
+
// Starts true so guards like `if (loading) showSpinner` don't flash the
|
|
80
|
+
// logged-out UI before the bootstrap fetch resolves.
|
|
81
|
+
loading: true,
|
|
82
|
+
error: null,
|
|
83
|
+
authConfig: null,
|
|
84
|
+
visibilityConfig: null
|
|
85
|
+
};
|
|
86
|
+
var authStore = createStore(() => ({
|
|
87
|
+
...INITIAL_STATE
|
|
88
|
+
}));
|
|
89
|
+
function setAuthState(patch) {
|
|
90
|
+
authStore.setState(patch);
|
|
91
|
+
}
|
|
92
|
+
function getAuthState() {
|
|
93
|
+
return authStore.getState();
|
|
94
|
+
}
|
|
95
|
+
var authStateListeners = /* @__PURE__ */ new Set();
|
|
96
|
+
function subscribeAuthStateChange(listener) {
|
|
97
|
+
authStateListeners.add(listener);
|
|
98
|
+
return () => {
|
|
99
|
+
authStateListeners.delete(listener);
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function notifyAuthStateChange(user) {
|
|
103
|
+
for (const listener of authStateListeners) {
|
|
104
|
+
try {
|
|
105
|
+
listener(user);
|
|
106
|
+
} catch (err) {
|
|
107
|
+
console.error("[instroc-auth] onAuthStateChange listener threw:", err);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function useAuthStoreSelector(selector) {
|
|
112
|
+
return useSyncExternalStore(
|
|
113
|
+
authStore.subscribe,
|
|
114
|
+
() => selector(authStore.getState()),
|
|
115
|
+
() => selector(authStore.getState())
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// src/auth-actions.ts
|
|
120
|
+
var refreshTimeout = null;
|
|
121
|
+
function clearRefreshTimer() {
|
|
122
|
+
if (refreshTimeout) {
|
|
123
|
+
clearTimeout(refreshTimeout);
|
|
124
|
+
refreshTimeout = null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function inMemorySession(session) {
|
|
128
|
+
if (!session)
|
|
129
|
+
return null;
|
|
130
|
+
return {
|
|
131
|
+
// Tokens stay empty in JS — XSS can't read them, period.
|
|
132
|
+
access_token: "",
|
|
133
|
+
refresh_token: "",
|
|
134
|
+
expires_at: session.expires_at
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function scheduleRefresh(expiresAt) {
|
|
138
|
+
clearRefreshTimer();
|
|
139
|
+
const expiresTime = new Date(expiresAt).getTime();
|
|
140
|
+
const now = Date.now();
|
|
141
|
+
const refreshIn = expiresTime - now - 5 * 60 * 1e3;
|
|
142
|
+
if (refreshIn > 0) {
|
|
143
|
+
refreshTimeout = setTimeout(async () => {
|
|
144
|
+
try {
|
|
145
|
+
await refreshSession();
|
|
146
|
+
} catch (err) {
|
|
147
|
+
console.warn("[instroc-auth] Token refresh failed:", err);
|
|
148
|
+
updateAuthState(null, null);
|
|
149
|
+
}
|
|
150
|
+
}, refreshIn);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function updateAuthState(newUser, newSession) {
|
|
154
|
+
const stripped = inMemorySession(newSession);
|
|
155
|
+
setAuthState({ user: newUser, session: stripped });
|
|
156
|
+
if (stripped) {
|
|
157
|
+
scheduleRefresh(stripped.expires_at);
|
|
158
|
+
} else {
|
|
159
|
+
clearRefreshTimer();
|
|
160
|
+
}
|
|
161
|
+
notifyAuthStateChange(newUser);
|
|
162
|
+
}
|
|
163
|
+
async function fetchUser() {
|
|
164
|
+
const { projectId } = getInstrocConfig2();
|
|
165
|
+
if (!projectId) {
|
|
166
|
+
setAuthState({ loading: false });
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
try {
|
|
170
|
+
const response = await authFetch(buildAuthUrl("me"));
|
|
171
|
+
if (response.ok) {
|
|
172
|
+
const data = await response.json();
|
|
173
|
+
updateAuthState(data.user ?? null, data.session ?? null);
|
|
174
|
+
} else if (response.status === 401) {
|
|
175
|
+
const refreshResponse = await authFetch(buildAuthUrl("refresh"), {
|
|
176
|
+
method: "POST",
|
|
177
|
+
headers: { "Content-Type": "application/json" },
|
|
178
|
+
body: "{}"
|
|
179
|
+
});
|
|
180
|
+
if (refreshResponse.ok) {
|
|
181
|
+
const meResponse = await authFetch(buildAuthUrl("me"));
|
|
182
|
+
if (meResponse.ok) {
|
|
183
|
+
const data = await meResponse.json();
|
|
184
|
+
updateAuthState(data.user ?? null, data.session ?? null);
|
|
185
|
+
} else {
|
|
186
|
+
updateAuthState(null, null);
|
|
187
|
+
}
|
|
188
|
+
} else {
|
|
189
|
+
updateAuthState(null, null);
|
|
190
|
+
}
|
|
191
|
+
} else {
|
|
192
|
+
updateAuthState(null, null);
|
|
193
|
+
}
|
|
194
|
+
} catch (err) {
|
|
195
|
+
console.error("[instroc-auth] Failed to fetch user:", err);
|
|
196
|
+
updateAuthState(null, null);
|
|
197
|
+
} finally {
|
|
198
|
+
setAuthState({ loading: false });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
async function fetchAuthConfig() {
|
|
202
|
+
const { projectId, baseUrl } = getInstrocConfig2();
|
|
203
|
+
if (!projectId)
|
|
204
|
+
return;
|
|
205
|
+
try {
|
|
206
|
+
const response = await authFetch(`${baseUrl}/${projectId}/auth/config`);
|
|
207
|
+
if (response.ok) {
|
|
208
|
+
const data = await response.json();
|
|
209
|
+
setAuthState({
|
|
210
|
+
authConfig: data.config || null,
|
|
211
|
+
visibilityConfig: data.visibility || {
|
|
212
|
+
projectName: "App",
|
|
213
|
+
visibility: "public",
|
|
214
|
+
requireLogin: false,
|
|
215
|
+
logoUrl: null,
|
|
216
|
+
welcomeMessage: null
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
} catch (err) {
|
|
221
|
+
console.error("[instroc-auth] Failed to fetch auth config:", err);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
async function login(credentials) {
|
|
225
|
+
setAuthState({ error: null, loading: true });
|
|
226
|
+
try {
|
|
227
|
+
const response = await authFetch(buildAuthUrl("login"), {
|
|
228
|
+
method: "POST",
|
|
229
|
+
headers: { "Content-Type": "application/json" },
|
|
230
|
+
body: JSON.stringify(credentials)
|
|
231
|
+
});
|
|
232
|
+
const data = await response.json();
|
|
233
|
+
if (!response.ok) {
|
|
234
|
+
throw authErrorFromResponse(response, data, "Login failed");
|
|
235
|
+
}
|
|
236
|
+
const authResponse = data;
|
|
237
|
+
updateAuthState(authResponse.user, authResponse.session);
|
|
238
|
+
} catch (err) {
|
|
239
|
+
const message = err instanceof Error ? err.message : "Login failed";
|
|
240
|
+
setAuthState({ error: message });
|
|
241
|
+
throw err;
|
|
242
|
+
} finally {
|
|
243
|
+
setAuthState({ loading: false });
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
async function signup(credentials) {
|
|
247
|
+
setAuthState({ error: null, loading: true });
|
|
248
|
+
try {
|
|
249
|
+
const response = await authFetch(buildAuthUrl("signup"), {
|
|
250
|
+
method: "POST",
|
|
251
|
+
headers: { "Content-Type": "application/json" },
|
|
252
|
+
body: JSON.stringify(credentials)
|
|
253
|
+
});
|
|
254
|
+
const data = await response.json();
|
|
255
|
+
if (!response.ok) {
|
|
256
|
+
throw authErrorFromResponse(response, data, "Signup failed");
|
|
257
|
+
}
|
|
258
|
+
const authResponse = data;
|
|
259
|
+
if (authResponse.needsVerification) {
|
|
260
|
+
return { status: "needs_verification", email: credentials.email };
|
|
261
|
+
}
|
|
262
|
+
if (authResponse.needsApproval) {
|
|
263
|
+
updateAuthState(authResponse.user, authResponse.session);
|
|
264
|
+
return { status: "needs_approval", user: authResponse.user };
|
|
265
|
+
}
|
|
266
|
+
if (authResponse.session.access_token) {
|
|
267
|
+
updateAuthState(authResponse.user, authResponse.session);
|
|
268
|
+
return { status: "authenticated", user: authResponse.user };
|
|
269
|
+
}
|
|
270
|
+
return { status: "needs_verification", email: credentials.email };
|
|
271
|
+
} catch (err) {
|
|
272
|
+
const message = err instanceof Error ? err.message : "Signup failed";
|
|
273
|
+
setAuthState({ error: message });
|
|
274
|
+
throw err;
|
|
275
|
+
} finally {
|
|
276
|
+
setAuthState({ loading: false });
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
async function logout() {
|
|
280
|
+
try {
|
|
281
|
+
await authFetch(buildAuthUrl("logout"), { method: "POST" });
|
|
282
|
+
} catch (err) {
|
|
283
|
+
console.error("[instroc-auth] Logout error:", err);
|
|
284
|
+
} finally {
|
|
285
|
+
updateAuthState(null, null);
|
|
286
|
+
clearRefreshTimer();
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
async function updateProfile(data) {
|
|
290
|
+
const { user } = getAuthState();
|
|
291
|
+
if (!user) {
|
|
292
|
+
throw new AuthError("Not authenticated", 401);
|
|
293
|
+
}
|
|
294
|
+
setAuthState({ error: null });
|
|
295
|
+
try {
|
|
296
|
+
const response = await authFetch(buildAuthUrl("me"), {
|
|
297
|
+
method: "PATCH",
|
|
298
|
+
headers: { "Content-Type": "application/json" },
|
|
299
|
+
body: JSON.stringify(data)
|
|
300
|
+
});
|
|
301
|
+
const result = await response.json();
|
|
302
|
+
if (!response.ok) {
|
|
303
|
+
throw authErrorFromResponse(response, result, "Update failed");
|
|
304
|
+
}
|
|
305
|
+
setAuthState({ user: result.user });
|
|
306
|
+
} catch (err) {
|
|
307
|
+
const message = err instanceof Error ? err.message : "Update failed";
|
|
308
|
+
setAuthState({ error: message });
|
|
309
|
+
throw err;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
async function refreshSession() {
|
|
313
|
+
const { user, session } = getAuthState();
|
|
314
|
+
if (!session)
|
|
315
|
+
return;
|
|
316
|
+
const response = await authFetch(buildAuthUrl("refresh"), {
|
|
317
|
+
method: "POST",
|
|
318
|
+
headers: { "Content-Type": "application/json" },
|
|
319
|
+
body: "{}"
|
|
320
|
+
});
|
|
321
|
+
const data = await response.json();
|
|
322
|
+
if (!response.ok) {
|
|
323
|
+
throw authErrorFromResponse(response, data, "Refresh failed");
|
|
324
|
+
}
|
|
325
|
+
const refreshData = data;
|
|
326
|
+
updateAuthState(user, {
|
|
327
|
+
access_token: "",
|
|
328
|
+
refresh_token: "",
|
|
329
|
+
expires_at: refreshData.expires_at
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
async function forgotPassword(email) {
|
|
333
|
+
setAuthState({ error: null });
|
|
334
|
+
try {
|
|
335
|
+
const response = await authFetch(buildAuthUrl("forgot-password"), {
|
|
336
|
+
method: "POST",
|
|
337
|
+
headers: { "Content-Type": "application/json" },
|
|
338
|
+
body: JSON.stringify({ email })
|
|
339
|
+
});
|
|
340
|
+
const data = await response.json();
|
|
341
|
+
if (!response.ok) {
|
|
342
|
+
throw authErrorFromResponse(response, data, "Request failed");
|
|
343
|
+
}
|
|
344
|
+
} catch (err) {
|
|
345
|
+
const message = err instanceof Error ? err.message : "Request failed";
|
|
346
|
+
setAuthState({ error: message });
|
|
347
|
+
throw err;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
async function resetPassword(token, newPassword) {
|
|
351
|
+
setAuthState({ error: null });
|
|
352
|
+
try {
|
|
353
|
+
const response = await authFetch(buildAuthUrl("reset-password"), {
|
|
354
|
+
method: "POST",
|
|
355
|
+
headers: { "Content-Type": "application/json" },
|
|
356
|
+
body: JSON.stringify({ token, password: newPassword })
|
|
357
|
+
});
|
|
358
|
+
const data = await response.json();
|
|
359
|
+
if (!response.ok) {
|
|
360
|
+
throw authErrorFromResponse(response, data, "Reset failed");
|
|
361
|
+
}
|
|
362
|
+
} catch (err) {
|
|
363
|
+
const message = err instanceof Error ? err.message : "Reset failed";
|
|
364
|
+
setAuthState({ error: message });
|
|
365
|
+
throw err;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
function signInWithOAuth(provider) {
|
|
369
|
+
const { projectId, baseUrl } = getInstrocConfig2();
|
|
370
|
+
if (!projectId) {
|
|
371
|
+
setAuthState({ error: "Project ID is required" });
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
const currentUrl = window.location.origin + window.location.pathname;
|
|
375
|
+
const oauthUrl = `${baseUrl}/${projectId}/auth/oauth/${provider}?redirect_uri=${encodeURIComponent(currentUrl)}`;
|
|
376
|
+
window.location.href = oauthUrl;
|
|
377
|
+
}
|
|
378
|
+
async function verifyOTP(email, code) {
|
|
379
|
+
setAuthState({ error: null, loading: true });
|
|
380
|
+
try {
|
|
381
|
+
const response = await authFetch(buildAuthUrl("verify-email"), {
|
|
382
|
+
method: "POST",
|
|
383
|
+
headers: { "Content-Type": "application/json" },
|
|
384
|
+
body: JSON.stringify({ email, otp: code })
|
|
385
|
+
});
|
|
386
|
+
const data = await response.json();
|
|
387
|
+
if (!response.ok) {
|
|
388
|
+
throw authErrorFromResponse(response, data, "Verification failed");
|
|
389
|
+
}
|
|
390
|
+
if (data.session) {
|
|
391
|
+
updateAuthState(data.user, data.session);
|
|
392
|
+
}
|
|
393
|
+
} catch (err) {
|
|
394
|
+
const message = err instanceof Error ? err.message : "Verification failed";
|
|
395
|
+
setAuthState({ error: message });
|
|
396
|
+
throw err;
|
|
397
|
+
} finally {
|
|
398
|
+
setAuthState({ loading: false });
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
async function resendOTP(email) {
|
|
402
|
+
setAuthState({ error: null });
|
|
403
|
+
try {
|
|
404
|
+
const response = await authFetch(buildAuthUrl("resend-verification"), {
|
|
405
|
+
method: "POST",
|
|
406
|
+
headers: { "Content-Type": "application/json" },
|
|
407
|
+
body: JSON.stringify({ email })
|
|
408
|
+
});
|
|
409
|
+
const data = await response.json();
|
|
410
|
+
if (!response.ok) {
|
|
411
|
+
throw authErrorFromResponse(response, data, "Failed to resend code");
|
|
412
|
+
}
|
|
413
|
+
} catch (err) {
|
|
414
|
+
const message = err instanceof Error ? err.message : "Failed to resend code";
|
|
415
|
+
setAuthState({ error: message });
|
|
416
|
+
throw err;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
function parseOAuthCallback() {
|
|
420
|
+
if (typeof window === "undefined")
|
|
421
|
+
return false;
|
|
422
|
+
const hash = window.location.hash;
|
|
423
|
+
if (hash && (hash.startsWith("#auth=") || hash.startsWith("#access_token="))) {
|
|
424
|
+
window.history.replaceState({}, "", window.location.pathname);
|
|
425
|
+
return true;
|
|
426
|
+
}
|
|
427
|
+
const params = new URLSearchParams(window.location.search);
|
|
428
|
+
const accessToken = params.get("access_token");
|
|
429
|
+
const refreshToken = params.get("refresh_token");
|
|
430
|
+
const verifyEmailToken = params.get("verify_email");
|
|
431
|
+
const { projectId } = getInstrocConfig2();
|
|
432
|
+
if (verifyEmailToken && projectId) {
|
|
433
|
+
fetch(buildAuthUrl("verify-email"), {
|
|
434
|
+
method: "POST",
|
|
435
|
+
headers: { "Content-Type": "application/json" },
|
|
436
|
+
body: JSON.stringify({ token: verifyEmailToken }),
|
|
437
|
+
credentials: "include"
|
|
438
|
+
}).then(() => window.history.replaceState({}, "", window.location.pathname)).catch(
|
|
439
|
+
() => window.history.replaceState({}, "", window.location.pathname)
|
|
440
|
+
);
|
|
441
|
+
return true;
|
|
442
|
+
}
|
|
443
|
+
if (accessToken || refreshToken) {
|
|
444
|
+
window.history.replaceState({}, "", window.location.pathname);
|
|
445
|
+
return true;
|
|
446
|
+
}
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// src/auth-bootstrap.ts
|
|
451
|
+
import { configStore, getInstrocConfig as getInstrocConfig3 } from "@instroc/client";
|
|
452
|
+
var bootstrapped = false;
|
|
453
|
+
var lastBootstrappedProjectId = null;
|
|
454
|
+
var unsubscribe = null;
|
|
455
|
+
function runBootstrapForCurrentConfig() {
|
|
456
|
+
const { projectId } = getInstrocConfig3();
|
|
457
|
+
if (projectId === lastBootstrappedProjectId)
|
|
458
|
+
return;
|
|
459
|
+
lastBootstrappedProjectId = projectId;
|
|
460
|
+
if (!projectId) {
|
|
461
|
+
setAuthState({ loading: false });
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
parseOAuthCallback();
|
|
465
|
+
void fetchUser();
|
|
466
|
+
void fetchAuthConfig();
|
|
467
|
+
}
|
|
468
|
+
function ensureAuthBootstrap() {
|
|
469
|
+
if (bootstrapped)
|
|
470
|
+
return;
|
|
471
|
+
bootstrapped = true;
|
|
472
|
+
unsubscribe = configStore.subscribe(() => {
|
|
473
|
+
runBootstrapForCurrentConfig();
|
|
474
|
+
});
|
|
475
|
+
runBootstrapForCurrentConfig();
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// src/use-auth.ts
|
|
479
|
+
function useAuth() {
|
|
480
|
+
ensureAuthBootstrap();
|
|
481
|
+
const { user, session, loading, error, authConfig, visibilityConfig } = useAuthStoreSelector((state) => state);
|
|
482
|
+
const { projectId } = useInstrocConfig();
|
|
483
|
+
return {
|
|
484
|
+
user,
|
|
485
|
+
session,
|
|
486
|
+
loading,
|
|
487
|
+
error,
|
|
488
|
+
authConfig,
|
|
489
|
+
visibilityConfig,
|
|
490
|
+
projectId,
|
|
491
|
+
login,
|
|
492
|
+
signup,
|
|
493
|
+
logout,
|
|
494
|
+
signInWithOAuth,
|
|
495
|
+
verifyOTP,
|
|
496
|
+
resendOTP,
|
|
497
|
+
updateProfile,
|
|
498
|
+
refreshSession,
|
|
499
|
+
forgotPassword,
|
|
500
|
+
resetPassword,
|
|
501
|
+
setProjectId: setInstrocProjectId
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
var useAuthContext = useAuth;
|
|
505
|
+
function useUser() {
|
|
506
|
+
const { user, loading } = useAuth();
|
|
507
|
+
return { user, loading };
|
|
508
|
+
}
|
|
509
|
+
function useSession() {
|
|
510
|
+
const { session, loading } = useAuth();
|
|
511
|
+
return { session, loading };
|
|
512
|
+
}
|
|
513
|
+
function useAuthRequired() {
|
|
514
|
+
const { user, loading, session } = useAuth();
|
|
515
|
+
if (loading) {
|
|
516
|
+
return { user: null, session: null, loading: true };
|
|
517
|
+
}
|
|
518
|
+
if (!user) {
|
|
519
|
+
throw new Error("Authentication required");
|
|
520
|
+
}
|
|
521
|
+
return { user, session, loading: false };
|
|
522
|
+
}
|
|
523
|
+
function useIsOwner() {
|
|
524
|
+
const { user, session, loading } = useAuth();
|
|
525
|
+
if (loading || !user || !session)
|
|
526
|
+
return false;
|
|
527
|
+
if (user.is_owner === true)
|
|
528
|
+
return true;
|
|
529
|
+
const claim = decodeIsOwnerClaim(session.access_token);
|
|
530
|
+
return claim === true;
|
|
531
|
+
}
|
|
532
|
+
function useIsWorkspaceMember() {
|
|
533
|
+
const [isMember, setIsMember] = useState(() => isDevHost());
|
|
534
|
+
useEffect(() => {
|
|
535
|
+
if (typeof window === "undefined")
|
|
536
|
+
return;
|
|
537
|
+
let cancelled = false;
|
|
538
|
+
(async () => {
|
|
539
|
+
try {
|
|
540
|
+
const res = await fetch("/__instroc/workspace-status", {
|
|
541
|
+
credentials: "include",
|
|
542
|
+
cache: "no-store"
|
|
543
|
+
});
|
|
544
|
+
if (cancelled)
|
|
545
|
+
return;
|
|
546
|
+
if (!res.ok) {
|
|
547
|
+
setIsMember(isDevHost());
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
const data = await res.json();
|
|
551
|
+
setIsMember(data.isWorkspaceMember === true);
|
|
552
|
+
} catch {
|
|
553
|
+
if (!cancelled)
|
|
554
|
+
setIsMember(isDevHost());
|
|
555
|
+
}
|
|
556
|
+
})();
|
|
557
|
+
return () => {
|
|
558
|
+
cancelled = true;
|
|
559
|
+
};
|
|
560
|
+
}, []);
|
|
561
|
+
return isMember;
|
|
562
|
+
}
|
|
563
|
+
function isDevHost() {
|
|
564
|
+
if (typeof window === "undefined")
|
|
565
|
+
return false;
|
|
566
|
+
const h = window.location.hostname;
|
|
567
|
+
return h === "localhost" || h === "127.0.0.1" || h.endsWith(".fly.dev") || h.endsWith(".workers.dev") || h === "preview.instroc.app";
|
|
568
|
+
}
|
|
569
|
+
function decodeIsOwnerClaim(accessToken) {
|
|
570
|
+
try {
|
|
571
|
+
const parts = accessToken.split(".");
|
|
572
|
+
if (parts.length !== 3)
|
|
573
|
+
return void 0;
|
|
574
|
+
const payload = parts[1];
|
|
575
|
+
const base64 = payload.replace(/-/g, "+").replace(/_/g, "/");
|
|
576
|
+
const padded = base64 + "=".repeat((4 - base64.length % 4) % 4);
|
|
577
|
+
const json = typeof atob !== "undefined" ? atob(padded) : Buffer.from(padded, "base64").toString("utf-8");
|
|
578
|
+
const decoded = JSON.parse(json);
|
|
579
|
+
return decoded.is_owner;
|
|
580
|
+
} catch {
|
|
581
|
+
return void 0;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// src/forms/use-form-action.ts
|
|
586
|
+
function resolveErrorMessage(err, messages, fallback) {
|
|
587
|
+
if (err instanceof AuthError) {
|
|
588
|
+
return messages?.[err.status] ?? err.message ?? fallback;
|
|
589
|
+
}
|
|
590
|
+
if (err instanceof Error)
|
|
591
|
+
return err.message;
|
|
592
|
+
return fallback;
|
|
593
|
+
}
|
|
594
|
+
function applyErrorResult(err, onError, messages, fallback, setError) {
|
|
595
|
+
const override = onError?.(err);
|
|
596
|
+
if (override === null) {
|
|
597
|
+
setError(null);
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
if (typeof override === "string") {
|
|
601
|
+
setError(override);
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
setError(resolveErrorMessage(err, messages, fallback));
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// src/forms/use-login-form.ts
|
|
608
|
+
function useLoginForm(options = {}) {
|
|
609
|
+
const { login: login2, user } = useAuth();
|
|
610
|
+
const { onSuccess, onNeedsVerification, onError, messages } = options;
|
|
611
|
+
const [loading, setLoading] = useState2(false);
|
|
612
|
+
const [error, setError] = useState2(null);
|
|
613
|
+
const mountedRef = useRef(true);
|
|
614
|
+
useEffect2(() => {
|
|
615
|
+
mountedRef.current = true;
|
|
616
|
+
return () => {
|
|
617
|
+
mountedRef.current = false;
|
|
618
|
+
};
|
|
619
|
+
}, []);
|
|
620
|
+
const safeSetError = useCallback((msg) => {
|
|
621
|
+
if (mountedRef.current)
|
|
622
|
+
setError(msg);
|
|
623
|
+
}, []);
|
|
624
|
+
const clearError = useCallback(() => safeSetError(null), [safeSetError]);
|
|
625
|
+
const submit = useCallback(
|
|
626
|
+
async (credentials) => {
|
|
627
|
+
if (mountedRef.current) {
|
|
628
|
+
setLoading(true);
|
|
629
|
+
setError(null);
|
|
630
|
+
}
|
|
631
|
+
try {
|
|
632
|
+
await login2(credentials);
|
|
633
|
+
onSuccess?.(user ?? { id: "", email: credentials.email });
|
|
634
|
+
} catch (err) {
|
|
635
|
+
if (err instanceof AuthError && err.status === 403 && onNeedsVerification) {
|
|
636
|
+
safeSetError(null);
|
|
637
|
+
onNeedsVerification(credentials.email);
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
applyErrorResult(
|
|
641
|
+
err,
|
|
642
|
+
onError,
|
|
643
|
+
messages,
|
|
644
|
+
"Login failed. Please try again.",
|
|
645
|
+
safeSetError
|
|
646
|
+
);
|
|
647
|
+
} finally {
|
|
648
|
+
if (mountedRef.current)
|
|
649
|
+
setLoading(false);
|
|
650
|
+
}
|
|
651
|
+
},
|
|
652
|
+
[
|
|
653
|
+
login2,
|
|
654
|
+
user,
|
|
655
|
+
onSuccess,
|
|
656
|
+
onNeedsVerification,
|
|
657
|
+
onError,
|
|
658
|
+
messages,
|
|
659
|
+
safeSetError
|
|
660
|
+
]
|
|
661
|
+
);
|
|
662
|
+
return { submit, loading, error, setError: safeSetError, clearError };
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// src/forms/use-signup-form.ts
|
|
666
|
+
import { useState as useState3, useCallback as useCallback2, useRef as useRef2, useEffect as useEffect3 } from "react";
|
|
667
|
+
function useSignupForm(options = {}) {
|
|
668
|
+
const { signup: signup2 } = useAuth();
|
|
669
|
+
const { onSuccess, onNeedsVerification, onNeedsApproval, onError, messages } = options;
|
|
670
|
+
const [loading, setLoading] = useState3(false);
|
|
671
|
+
const [error, setError] = useState3(null);
|
|
672
|
+
const mountedRef = useRef2(true);
|
|
673
|
+
useEffect3(() => {
|
|
674
|
+
mountedRef.current = true;
|
|
675
|
+
return () => {
|
|
676
|
+
mountedRef.current = false;
|
|
677
|
+
};
|
|
678
|
+
}, []);
|
|
679
|
+
const safeSetError = useCallback2((msg) => {
|
|
680
|
+
if (mountedRef.current)
|
|
681
|
+
setError(msg);
|
|
682
|
+
}, []);
|
|
683
|
+
const clearError = useCallback2(() => safeSetError(null), [safeSetError]);
|
|
684
|
+
const submit = useCallback2(
|
|
685
|
+
async (credentials) => {
|
|
686
|
+
if (mountedRef.current) {
|
|
687
|
+
setLoading(true);
|
|
688
|
+
setError(null);
|
|
689
|
+
}
|
|
690
|
+
try {
|
|
691
|
+
const result = await signup2(credentials);
|
|
692
|
+
switch (result.status) {
|
|
693
|
+
case "authenticated":
|
|
694
|
+
onSuccess?.(result.user);
|
|
695
|
+
return;
|
|
696
|
+
case "needs_verification":
|
|
697
|
+
onNeedsVerification?.(result.email);
|
|
698
|
+
return;
|
|
699
|
+
case "needs_approval":
|
|
700
|
+
onNeedsApproval?.(result.user);
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
} catch (err) {
|
|
704
|
+
applyErrorResult(
|
|
705
|
+
err,
|
|
706
|
+
onError,
|
|
707
|
+
messages,
|
|
708
|
+
"Signup failed. Please try again.",
|
|
709
|
+
safeSetError
|
|
710
|
+
);
|
|
711
|
+
} finally {
|
|
712
|
+
if (mountedRef.current)
|
|
713
|
+
setLoading(false);
|
|
714
|
+
}
|
|
715
|
+
},
|
|
716
|
+
[
|
|
717
|
+
signup2,
|
|
718
|
+
onSuccess,
|
|
719
|
+
onNeedsVerification,
|
|
720
|
+
onNeedsApproval,
|
|
721
|
+
onError,
|
|
722
|
+
messages,
|
|
723
|
+
safeSetError
|
|
724
|
+
]
|
|
725
|
+
);
|
|
726
|
+
return { submit, loading, error, setError: safeSetError, clearError };
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// src/forms/use-otp-form.ts
|
|
730
|
+
import { useState as useState4, useCallback as useCallback3, useRef as useRef3, useEffect as useEffect4 } from "react";
|
|
731
|
+
function useOtpForm(options) {
|
|
732
|
+
const { verifyOTP: verifyOTP2, resendOTP: resendOTP2 } = useAuth();
|
|
733
|
+
const {
|
|
734
|
+
email,
|
|
735
|
+
onSuccess,
|
|
736
|
+
onResendSuccess,
|
|
737
|
+
onError,
|
|
738
|
+
messages,
|
|
739
|
+
resendCooldownSeconds = 60
|
|
740
|
+
} = options;
|
|
741
|
+
const [loading, setLoading] = useState4(false);
|
|
742
|
+
const [resending, setResending] = useState4(false);
|
|
743
|
+
const [resendCooldown, setResendCooldown] = useState4(0);
|
|
744
|
+
const [error, setError] = useState4(null);
|
|
745
|
+
const mountedRef = useRef3(true);
|
|
746
|
+
useEffect4(() => {
|
|
747
|
+
mountedRef.current = true;
|
|
748
|
+
return () => {
|
|
749
|
+
mountedRef.current = false;
|
|
750
|
+
};
|
|
751
|
+
}, []);
|
|
752
|
+
useEffect4(() => {
|
|
753
|
+
if (resendCooldown <= 0)
|
|
754
|
+
return;
|
|
755
|
+
const id = setTimeout(() => setResendCooldown((s) => s - 1), 1e3);
|
|
756
|
+
return () => clearTimeout(id);
|
|
757
|
+
}, [resendCooldown]);
|
|
758
|
+
const safeSetError = useCallback3((msg) => {
|
|
759
|
+
if (mountedRef.current)
|
|
760
|
+
setError(msg);
|
|
761
|
+
}, []);
|
|
762
|
+
const clearError = useCallback3(() => safeSetError(null), [safeSetError]);
|
|
763
|
+
const submit = useCallback3(
|
|
764
|
+
async (code) => {
|
|
765
|
+
if (mountedRef.current) {
|
|
766
|
+
setLoading(true);
|
|
767
|
+
setError(null);
|
|
768
|
+
}
|
|
769
|
+
try {
|
|
770
|
+
await verifyOTP2(email, code);
|
|
771
|
+
onSuccess?.();
|
|
772
|
+
} catch (err) {
|
|
773
|
+
applyErrorResult(
|
|
774
|
+
err,
|
|
775
|
+
onError,
|
|
776
|
+
messages,
|
|
777
|
+
"Verification failed. Please try again.",
|
|
778
|
+
safeSetError
|
|
779
|
+
);
|
|
780
|
+
} finally {
|
|
781
|
+
if (mountedRef.current)
|
|
782
|
+
setLoading(false);
|
|
783
|
+
}
|
|
784
|
+
},
|
|
785
|
+
[verifyOTP2, email, onSuccess, onError, messages, safeSetError]
|
|
786
|
+
);
|
|
787
|
+
const resend = useCallback3(async () => {
|
|
788
|
+
if (resendCooldown > 0)
|
|
789
|
+
return;
|
|
790
|
+
if (mountedRef.current) {
|
|
791
|
+
setResending(true);
|
|
792
|
+
setError(null);
|
|
793
|
+
}
|
|
794
|
+
try {
|
|
795
|
+
await resendOTP2(email);
|
|
796
|
+
if (mountedRef.current)
|
|
797
|
+
setResendCooldown(resendCooldownSeconds);
|
|
798
|
+
onResendSuccess?.();
|
|
799
|
+
} catch (err) {
|
|
800
|
+
applyErrorResult(
|
|
801
|
+
err,
|
|
802
|
+
onError,
|
|
803
|
+
messages,
|
|
804
|
+
"Could not resend code. Please try again.",
|
|
805
|
+
safeSetError
|
|
806
|
+
);
|
|
807
|
+
} finally {
|
|
808
|
+
if (mountedRef.current)
|
|
809
|
+
setResending(false);
|
|
810
|
+
}
|
|
811
|
+
}, [
|
|
812
|
+
resendCooldown,
|
|
813
|
+
resendOTP2,
|
|
814
|
+
email,
|
|
815
|
+
resendCooldownSeconds,
|
|
816
|
+
onResendSuccess,
|
|
817
|
+
onError,
|
|
818
|
+
messages,
|
|
819
|
+
safeSetError
|
|
820
|
+
]);
|
|
821
|
+
return {
|
|
822
|
+
submit,
|
|
823
|
+
resend,
|
|
824
|
+
loading,
|
|
825
|
+
resending,
|
|
826
|
+
resendCooldown,
|
|
827
|
+
error,
|
|
828
|
+
setError: safeSetError,
|
|
829
|
+
clearError
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// src/forms/use-forgot-password-form.ts
|
|
834
|
+
import { useState as useState5, useCallback as useCallback4, useRef as useRef4, useEffect as useEffect5 } from "react";
|
|
835
|
+
function useForgotPasswordForm(options = {}) {
|
|
836
|
+
const { forgotPassword: forgotPassword2 } = useAuth();
|
|
837
|
+
const { onSuccess, onError, messages } = options;
|
|
838
|
+
const [loading, setLoading] = useState5(false);
|
|
839
|
+
const [submitted, setSubmitted] = useState5(false);
|
|
840
|
+
const [error, setError] = useState5(null);
|
|
841
|
+
const mountedRef = useRef4(true);
|
|
842
|
+
useEffect5(() => {
|
|
843
|
+
mountedRef.current = true;
|
|
844
|
+
return () => {
|
|
845
|
+
mountedRef.current = false;
|
|
846
|
+
};
|
|
847
|
+
}, []);
|
|
848
|
+
const safeSetError = useCallback4((msg) => {
|
|
849
|
+
if (mountedRef.current)
|
|
850
|
+
setError(msg);
|
|
851
|
+
}, []);
|
|
852
|
+
const clearError = useCallback4(() => safeSetError(null), [safeSetError]);
|
|
853
|
+
const reset = useCallback4(() => {
|
|
854
|
+
if (mountedRef.current) {
|
|
855
|
+
setSubmitted(false);
|
|
856
|
+
setError(null);
|
|
857
|
+
}
|
|
858
|
+
}, []);
|
|
859
|
+
const submit = useCallback4(
|
|
860
|
+
async (email) => {
|
|
861
|
+
if (mountedRef.current) {
|
|
862
|
+
setLoading(true);
|
|
863
|
+
setError(null);
|
|
864
|
+
}
|
|
865
|
+
try {
|
|
866
|
+
await forgotPassword2(email);
|
|
867
|
+
if (mountedRef.current)
|
|
868
|
+
setSubmitted(true);
|
|
869
|
+
onSuccess?.(email);
|
|
870
|
+
} catch (err) {
|
|
871
|
+
applyErrorResult(
|
|
872
|
+
err,
|
|
873
|
+
onError,
|
|
874
|
+
messages,
|
|
875
|
+
"Could not send reset link. Please try again.",
|
|
876
|
+
safeSetError
|
|
877
|
+
);
|
|
878
|
+
} finally {
|
|
879
|
+
if (mountedRef.current)
|
|
880
|
+
setLoading(false);
|
|
881
|
+
}
|
|
882
|
+
},
|
|
883
|
+
[forgotPassword2, onSuccess, onError, messages, safeSetError]
|
|
884
|
+
);
|
|
885
|
+
return {
|
|
886
|
+
submit,
|
|
887
|
+
loading,
|
|
888
|
+
submitted,
|
|
889
|
+
error,
|
|
890
|
+
setError: safeSetError,
|
|
891
|
+
clearError,
|
|
892
|
+
reset
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// src/forms/use-reset-password-form.ts
|
|
897
|
+
import { useState as useState6, useCallback as useCallback5, useRef as useRef5, useEffect as useEffect6 } from "react";
|
|
898
|
+
function useResetPasswordForm(options) {
|
|
899
|
+
const { resetPassword: resetPassword2 } = useAuth();
|
|
900
|
+
const { token, onSuccess, onError, messages } = options;
|
|
901
|
+
const [loading, setLoading] = useState6(false);
|
|
902
|
+
const [error, setError] = useState6(null);
|
|
903
|
+
const mountedRef = useRef5(true);
|
|
904
|
+
useEffect6(() => {
|
|
905
|
+
mountedRef.current = true;
|
|
906
|
+
return () => {
|
|
907
|
+
mountedRef.current = false;
|
|
908
|
+
};
|
|
909
|
+
}, []);
|
|
910
|
+
const safeSetError = useCallback5((msg) => {
|
|
911
|
+
if (mountedRef.current)
|
|
912
|
+
setError(msg);
|
|
913
|
+
}, []);
|
|
914
|
+
const clearError = useCallback5(() => safeSetError(null), [safeSetError]);
|
|
915
|
+
const submit = useCallback5(
|
|
916
|
+
async (newPassword) => {
|
|
917
|
+
if (mountedRef.current) {
|
|
918
|
+
setLoading(true);
|
|
919
|
+
setError(null);
|
|
920
|
+
}
|
|
921
|
+
try {
|
|
922
|
+
await resetPassword2(token, newPassword);
|
|
923
|
+
onSuccess?.();
|
|
924
|
+
} catch (err) {
|
|
925
|
+
applyErrorResult(
|
|
926
|
+
err,
|
|
927
|
+
onError,
|
|
928
|
+
messages,
|
|
929
|
+
"Could not reset password. Please try again.",
|
|
930
|
+
safeSetError
|
|
931
|
+
);
|
|
932
|
+
} finally {
|
|
933
|
+
if (mountedRef.current)
|
|
934
|
+
setLoading(false);
|
|
935
|
+
}
|
|
936
|
+
},
|
|
937
|
+
[resetPassword2, token, onSuccess, onError, messages, safeSetError]
|
|
938
|
+
);
|
|
939
|
+
return { submit, loading, error, setError: safeSetError, clearError };
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
export {
|
|
943
|
+
AuthError,
|
|
944
|
+
subscribeAuthStateChange,
|
|
945
|
+
ensureAuthBootstrap,
|
|
946
|
+
useAuth,
|
|
947
|
+
useAuthContext,
|
|
948
|
+
useUser,
|
|
949
|
+
useSession,
|
|
950
|
+
useAuthRequired,
|
|
951
|
+
useIsOwner,
|
|
952
|
+
useIsWorkspaceMember,
|
|
953
|
+
useLoginForm,
|
|
954
|
+
useSignupForm,
|
|
955
|
+
useOtpForm,
|
|
956
|
+
useForgotPasswordForm,
|
|
957
|
+
useResetPasswordForm
|
|
958
|
+
};
|