@instroc/auth 1.0.0 → 1.0.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/index.d.ts +54 -2
- package/dist/index.js +63 -30
- package/package.json +4 -2
package/dist/index.d.ts
CHANGED
|
@@ -45,9 +45,26 @@ interface VisibilityConfig {
|
|
|
45
45
|
logoUrl: string | null;
|
|
46
46
|
welcomeMessage: string | null;
|
|
47
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Result of signup() — indicates what post-signup flow the caller should show.
|
|
50
|
+
*
|
|
51
|
+
* - `status: "authenticated"` — user is logged in, session is active. Redirect to app.
|
|
52
|
+
* - `status: "needs_verification"` — email verification OTP was sent. Show OTP screen.
|
|
53
|
+
* - `status: "needs_approval"` — account pending admin approval. Show waiting screen.
|
|
54
|
+
*/
|
|
55
|
+
type SignupResult = {
|
|
56
|
+
status: "authenticated";
|
|
57
|
+
user: AuthUser;
|
|
58
|
+
} | {
|
|
59
|
+
status: "needs_verification";
|
|
60
|
+
email: string;
|
|
61
|
+
} | {
|
|
62
|
+
status: "needs_approval";
|
|
63
|
+
user: AuthUser;
|
|
64
|
+
};
|
|
48
65
|
interface AuthContextValue extends AuthState {
|
|
49
66
|
login: (credentials: LoginCredentials) => Promise<void>;
|
|
50
|
-
signup: (credentials: SignupCredentials) => Promise<
|
|
67
|
+
signup: (credentials: SignupCredentials) => Promise<SignupResult>;
|
|
51
68
|
logout: () => Promise<void>;
|
|
52
69
|
signInWithOAuth: (provider: OAuthProvider) => void;
|
|
53
70
|
verifyOTP: (email: string, code: string) => Promise<void>;
|
|
@@ -76,6 +93,10 @@ interface AuthProviderProps {
|
|
|
76
93
|
interface AuthResponse {
|
|
77
94
|
user: AuthUser;
|
|
78
95
|
session: AuthSession;
|
|
96
|
+
/** Set when signup requires email verification — session is a placeholder with empty tokens. */
|
|
97
|
+
needsVerification?: boolean;
|
|
98
|
+
/** Set when signup succeeded but account is pending admin approval (private apps). */
|
|
99
|
+
needsApproval?: boolean;
|
|
79
100
|
}
|
|
80
101
|
interface RefreshResponse {
|
|
81
102
|
access_token: string;
|
|
@@ -104,4 +125,35 @@ declare function useAuthRequired(): {
|
|
|
104
125
|
loading: boolean;
|
|
105
126
|
};
|
|
106
127
|
|
|
107
|
-
|
|
128
|
+
/**
|
|
129
|
+
* Typed error thrown by every `@instroc/auth` method when the server responds
|
|
130
|
+
* with a non-2xx status. Carries the HTTP status so consumers can branch on
|
|
131
|
+
* behavioural outcomes (429 = cooldown UI, 403 = redirect to OTP page, etc.)
|
|
132
|
+
* without string-matching the message.
|
|
133
|
+
*
|
|
134
|
+
* Usage:
|
|
135
|
+
* try {
|
|
136
|
+
* await login({ email, password });
|
|
137
|
+
* } catch (err) {
|
|
138
|
+
* if (err instanceof AuthError) {
|
|
139
|
+
* if (err.status === 429) { setCooldown(60); return; }
|
|
140
|
+
* if (err.status === 403) { onNeedsVerification?.(email); return; }
|
|
141
|
+
* setLocalError(err.message);
|
|
142
|
+
* } else {
|
|
143
|
+
* setLocalError("An unexpected error occurred.");
|
|
144
|
+
* }
|
|
145
|
+
* }
|
|
146
|
+
*/
|
|
147
|
+
declare class AuthError extends Error {
|
|
148
|
+
/** HTTP status code from the server (400, 401, 403, 404, 409, 429, 500…). */
|
|
149
|
+
readonly status: number;
|
|
150
|
+
/**
|
|
151
|
+
* Optional machine-readable error code from the server (e.g. `"invalid_credentials"`,
|
|
152
|
+
* `"email_not_verified"`). Present when the server includes a `code` field in the
|
|
153
|
+
* error body; otherwise `undefined`.
|
|
154
|
+
*/
|
|
155
|
+
readonly code?: string;
|
|
156
|
+
constructor(message: string, status: number, code?: string);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export { type AuthConfig, type AuthContextValue, AuthError, AuthProvider, type AuthProviderProps, type AuthResponse, type AuthSession, type AuthState, type AuthUser, type LoginCredentials, type OAuthProvider, type RefreshResponse, type SignupCredentials, type SignupResult, type UpdateProfileData, type VisibilityConfig, useAuth, useAuthContext, useAuthRequired, useSession, useUser };
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,24 @@ import {
|
|
|
7
7
|
useEffect,
|
|
8
8
|
useRef
|
|
9
9
|
} from "react";
|
|
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/provider.tsx
|
|
10
28
|
import { jsx } from "react/jsx-runtime";
|
|
11
29
|
var AuthContext = createContext(null);
|
|
12
30
|
var STORAGE_KEY = "instroc_auth_session";
|
|
@@ -36,6 +54,10 @@ function AuthProvider({
|
|
|
36
54
|
},
|
|
37
55
|
[projectId, baseUrl]
|
|
38
56
|
);
|
|
57
|
+
const authFetch = useCallback(
|
|
58
|
+
(url, init) => fetch(url, { ...init, credentials: "include" }),
|
|
59
|
+
[]
|
|
60
|
+
);
|
|
39
61
|
const saveSession = useCallback(
|
|
40
62
|
(sessionData) => {
|
|
41
63
|
if (persistSession && typeof window !== "undefined") {
|
|
@@ -74,7 +96,7 @@ function AuthProvider({
|
|
|
74
96
|
try {
|
|
75
97
|
await refreshSession();
|
|
76
98
|
} catch (err) {
|
|
77
|
-
console.
|
|
99
|
+
console.warn("Token refresh failed:", err);
|
|
78
100
|
setUser(null);
|
|
79
101
|
setSession(null);
|
|
80
102
|
saveSession(null);
|
|
@@ -105,7 +127,7 @@ function AuthProvider({
|
|
|
105
127
|
return;
|
|
106
128
|
}
|
|
107
129
|
try {
|
|
108
|
-
const response = await
|
|
130
|
+
const response = await authFetch(buildUrl("me"), {
|
|
109
131
|
headers: {
|
|
110
132
|
Authorization: `Bearer ${storedSession.access_token}`
|
|
111
133
|
}
|
|
@@ -115,7 +137,7 @@ function AuthProvider({
|
|
|
115
137
|
updateAuthState(data.user, storedSession);
|
|
116
138
|
} else if (response.status === 401) {
|
|
117
139
|
try {
|
|
118
|
-
const refreshResponse = await
|
|
140
|
+
const refreshResponse = await authFetch(buildUrl("refresh"), {
|
|
119
141
|
method: "POST",
|
|
120
142
|
headers: { "Content-Type": "application/json" },
|
|
121
143
|
body: JSON.stringify({ refreshToken: storedSession.refresh_token })
|
|
@@ -127,7 +149,7 @@ function AuthProvider({
|
|
|
127
149
|
access_token: refreshData.access_token,
|
|
128
150
|
expires_at: refreshData.expires_at
|
|
129
151
|
};
|
|
130
|
-
const userResponse = await
|
|
152
|
+
const userResponse = await authFetch(buildUrl("me"), {
|
|
131
153
|
headers: {
|
|
132
154
|
Authorization: `Bearer ${newSession.access_token}`
|
|
133
155
|
}
|
|
@@ -171,14 +193,14 @@ function AuthProvider({
|
|
|
171
193
|
setError(null);
|
|
172
194
|
setLoading(true);
|
|
173
195
|
try {
|
|
174
|
-
const response = await
|
|
196
|
+
const response = await authFetch(buildUrl("login"), {
|
|
175
197
|
method: "POST",
|
|
176
198
|
headers: { "Content-Type": "application/json" },
|
|
177
199
|
body: JSON.stringify(credentials)
|
|
178
200
|
});
|
|
179
201
|
const data = await response.json();
|
|
180
202
|
if (!response.ok) {
|
|
181
|
-
throw
|
|
203
|
+
throw authErrorFromResponse(response, data, "Login failed");
|
|
182
204
|
}
|
|
183
205
|
const authResponse = data;
|
|
184
206
|
updateAuthState(authResponse.user, authResponse.session);
|
|
@@ -197,21 +219,34 @@ function AuthProvider({
|
|
|
197
219
|
setError(null);
|
|
198
220
|
setLoading(true);
|
|
199
221
|
try {
|
|
200
|
-
const response = await
|
|
222
|
+
const response = await authFetch(buildUrl("signup"), {
|
|
201
223
|
method: "POST",
|
|
202
224
|
headers: { "Content-Type": "application/json" },
|
|
203
225
|
body: JSON.stringify(credentials)
|
|
204
226
|
});
|
|
205
227
|
const data = await response.json();
|
|
206
228
|
if (!response.ok) {
|
|
207
|
-
throw
|
|
229
|
+
throw authErrorFromResponse(response, data, "Signup failed");
|
|
208
230
|
}
|
|
209
231
|
const authResponse = data;
|
|
232
|
+
if (authResponse.needsVerification) {
|
|
233
|
+
return {
|
|
234
|
+
status: "needs_verification",
|
|
235
|
+
email: credentials.email
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
if (authResponse.needsApproval) {
|
|
239
|
+
updateAuthState(authResponse.user, authResponse.session);
|
|
240
|
+
return { status: "needs_approval", user: authResponse.user };
|
|
241
|
+
}
|
|
210
242
|
if (authResponse.session.access_token) {
|
|
211
243
|
updateAuthState(authResponse.user, authResponse.session);
|
|
212
|
-
|
|
213
|
-
setUser(authResponse.user);
|
|
244
|
+
return { status: "authenticated", user: authResponse.user };
|
|
214
245
|
}
|
|
246
|
+
return {
|
|
247
|
+
status: "needs_verification",
|
|
248
|
+
email: credentials.email
|
|
249
|
+
};
|
|
215
250
|
} catch (err) {
|
|
216
251
|
const message = err instanceof Error ? err.message : "Signup failed";
|
|
217
252
|
setError(message);
|
|
@@ -225,7 +260,7 @@ function AuthProvider({
|
|
|
225
260
|
const logout = useCallback(async () => {
|
|
226
261
|
try {
|
|
227
262
|
if (session) {
|
|
228
|
-
await
|
|
263
|
+
await authFetch(buildUrl("logout"), {
|
|
229
264
|
method: "POST",
|
|
230
265
|
headers: {
|
|
231
266
|
Authorization: `Bearer ${session.access_token}`
|
|
@@ -244,11 +279,11 @@ function AuthProvider({
|
|
|
244
279
|
const updateProfile = useCallback(
|
|
245
280
|
async (data) => {
|
|
246
281
|
if (!session) {
|
|
247
|
-
throw new
|
|
282
|
+
throw new AuthError("Not authenticated", 401);
|
|
248
283
|
}
|
|
249
284
|
setError(null);
|
|
250
285
|
try {
|
|
251
|
-
const response = await
|
|
286
|
+
const response = await authFetch(buildUrl("me"), {
|
|
252
287
|
method: "PATCH",
|
|
253
288
|
headers: {
|
|
254
289
|
"Content-Type": "application/json",
|
|
@@ -258,7 +293,7 @@ function AuthProvider({
|
|
|
258
293
|
});
|
|
259
294
|
const result = await response.json();
|
|
260
295
|
if (!response.ok) {
|
|
261
|
-
throw
|
|
296
|
+
throw authErrorFromResponse(response, result, "Update failed");
|
|
262
297
|
}
|
|
263
298
|
setUser(result.user);
|
|
264
299
|
} catch (err) {
|
|
@@ -270,17 +305,16 @@ function AuthProvider({
|
|
|
270
305
|
[session, buildUrl]
|
|
271
306
|
);
|
|
272
307
|
const refreshSession = useCallback(async () => {
|
|
273
|
-
if (!session)
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
const response = await fetch(buildUrl("refresh"), {
|
|
308
|
+
if (!session)
|
|
309
|
+
return;
|
|
310
|
+
const response = await authFetch(buildUrl("refresh"), {
|
|
277
311
|
method: "POST",
|
|
278
312
|
headers: { "Content-Type": "application/json" },
|
|
279
313
|
body: JSON.stringify({ refreshToken: session.refresh_token })
|
|
280
314
|
});
|
|
281
315
|
const data = await response.json();
|
|
282
316
|
if (!response.ok) {
|
|
283
|
-
throw
|
|
317
|
+
throw authErrorFromResponse(response, data, "Refresh failed");
|
|
284
318
|
}
|
|
285
319
|
const refreshData = data;
|
|
286
320
|
const newSession = {
|
|
@@ -294,14 +328,14 @@ function AuthProvider({
|
|
|
294
328
|
async (email) => {
|
|
295
329
|
setError(null);
|
|
296
330
|
try {
|
|
297
|
-
const response = await
|
|
331
|
+
const response = await authFetch(buildUrl("forgot-password"), {
|
|
298
332
|
method: "POST",
|
|
299
333
|
headers: { "Content-Type": "application/json" },
|
|
300
334
|
body: JSON.stringify({ email })
|
|
301
335
|
});
|
|
302
336
|
const data = await response.json();
|
|
303
337
|
if (!response.ok) {
|
|
304
|
-
throw
|
|
338
|
+
throw authErrorFromResponse(response, data, "Request failed");
|
|
305
339
|
}
|
|
306
340
|
} catch (err) {
|
|
307
341
|
const message = err instanceof Error ? err.message : "Request failed";
|
|
@@ -315,14 +349,14 @@ function AuthProvider({
|
|
|
315
349
|
async (token, newPassword) => {
|
|
316
350
|
setError(null);
|
|
317
351
|
try {
|
|
318
|
-
const response = await
|
|
352
|
+
const response = await authFetch(buildUrl("reset-password"), {
|
|
319
353
|
method: "POST",
|
|
320
354
|
headers: { "Content-Type": "application/json" },
|
|
321
355
|
body: JSON.stringify({ token, password: newPassword })
|
|
322
356
|
});
|
|
323
357
|
const data = await response.json();
|
|
324
358
|
if (!response.ok) {
|
|
325
|
-
throw
|
|
359
|
+
throw authErrorFromResponse(response, data, "Reset failed");
|
|
326
360
|
}
|
|
327
361
|
} catch (err) {
|
|
328
362
|
const message = err instanceof Error ? err.message : "Reset failed";
|
|
@@ -349,14 +383,14 @@ function AuthProvider({
|
|
|
349
383
|
setError(null);
|
|
350
384
|
setLoading(true);
|
|
351
385
|
try {
|
|
352
|
-
const response = await
|
|
386
|
+
const response = await authFetch(buildUrl("verify-email"), {
|
|
353
387
|
method: "POST",
|
|
354
388
|
headers: { "Content-Type": "application/json" },
|
|
355
389
|
body: JSON.stringify({ email, otp: code })
|
|
356
390
|
});
|
|
357
391
|
const data = await response.json();
|
|
358
392
|
if (!response.ok) {
|
|
359
|
-
throw
|
|
393
|
+
throw authErrorFromResponse(response, data, "Verification failed");
|
|
360
394
|
}
|
|
361
395
|
if (data.session) {
|
|
362
396
|
updateAuthState(data.user, data.session);
|
|
@@ -375,14 +409,14 @@ function AuthProvider({
|
|
|
375
409
|
async (email) => {
|
|
376
410
|
setError(null);
|
|
377
411
|
try {
|
|
378
|
-
const response = await
|
|
412
|
+
const response = await authFetch(buildUrl("resend-verification"), {
|
|
379
413
|
method: "POST",
|
|
380
414
|
headers: { "Content-Type": "application/json" },
|
|
381
415
|
body: JSON.stringify({ email })
|
|
382
416
|
});
|
|
383
417
|
const data = await response.json();
|
|
384
418
|
if (!response.ok) {
|
|
385
|
-
throw
|
|
419
|
+
throw authErrorFromResponse(response, data, "Failed to resend code");
|
|
386
420
|
}
|
|
387
421
|
} catch (err) {
|
|
388
422
|
const message = err instanceof Error ? err.message : "Failed to resend code";
|
|
@@ -396,9 +430,7 @@ function AuthProvider({
|
|
|
396
430
|
if (!projectId)
|
|
397
431
|
return;
|
|
398
432
|
try {
|
|
399
|
-
const response = await fetch(
|
|
400
|
-
`${baseUrl}/${projectId}/auth/config`
|
|
401
|
-
);
|
|
433
|
+
const response = await fetch(`${baseUrl}/${projectId}/auth/config`);
|
|
402
434
|
if (response.ok) {
|
|
403
435
|
const data = await response.json();
|
|
404
436
|
setAuthConfig(data.config || null);
|
|
@@ -527,6 +559,7 @@ function useAuthRequired() {
|
|
|
527
559
|
return { user, session, loading: false };
|
|
528
560
|
}
|
|
529
561
|
export {
|
|
562
|
+
AuthError,
|
|
530
563
|
AuthProvider,
|
|
531
564
|
useAuth,
|
|
532
565
|
useAuthContext,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@instroc/auth",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Authentication hooks for Instroc Cloud — useAuth, useUser, AuthProvider",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
"types": "./dist/index.d.ts"
|
|
12
12
|
}
|
|
13
13
|
},
|
|
14
|
-
"files": [
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
15
17
|
"scripts": {
|
|
16
18
|
"build": "tsup",
|
|
17
19
|
"dev": "tsup --watch"
|