@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 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<void>;
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
- export { type AuthConfig, type AuthContextValue, AuthProvider, type AuthProviderProps, type AuthResponse, type AuthSession, type AuthState, type AuthUser, type LoginCredentials, type OAuthProvider, type RefreshResponse, type SignupCredentials, type UpdateProfileData, type VisibilityConfig, useAuth, useAuthContext, useAuthRequired, useSession, useUser };
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.error("Token refresh failed:", err);
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 fetch(buildUrl("me"), {
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 fetch(buildUrl("refresh"), {
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 fetch(buildUrl("me"), {
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 fetch(buildUrl("login"), {
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 new Error(data.error || "Login failed");
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 fetch(buildUrl("signup"), {
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 new Error(data.error || "Signup failed");
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
- } else {
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 fetch(buildUrl("logout"), {
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 Error("Not authenticated");
282
+ throw new AuthError("Not authenticated", 401);
248
283
  }
249
284
  setError(null);
250
285
  try {
251
- const response = await fetch(buildUrl("me"), {
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 new Error(result.error || "Update failed");
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
- throw new Error("No session to refresh");
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 new Error(data.error || "Refresh failed");
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 fetch(buildUrl("forgot-password"), {
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 new Error(data.error || "Request failed");
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 fetch(buildUrl("reset-password"), {
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 new Error(data.error || "Reset failed");
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 fetch(buildUrl("verify-email"), {
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 new Error(data.error || "Verification failed");
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 fetch(buildUrl("resend-verification"), {
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 new Error(data.error || "Failed to resend code");
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.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": ["dist"],
14
+ "files": [
15
+ "dist"
16
+ ],
15
17
  "scripts": {
16
18
  "build": "tsup",
17
19
  "dev": "tsup --watch"