@stacksjs/browser 0.74.60 → 0.74.62

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.
@@ -1 +1 @@
1
- import{readSessionHandoff,stripSessionHandoff,useStorage}from"@stacksjs/composables";import{ref}from"@stacksjs/stx";import{withCsrfHeader}from"./csrf";const token=useStorage("token",""),refreshToken=useStorage("refresh_token",""),stacksConfig=globalThis.__STACKS_CONFIG__||{},baseUrl=stacksConfig.API_URL||(typeof window<"u"?window.location.origin:""),ME_PATH=stacksConfig.AUTH_ME_PATH||"/api/me",REFRESH_PATH=stacksConfig.AUTH_REFRESH_PATH||"/auth/refresh",user=ref(null),isAuthenticated=ref(!1);let inFlightRefresh=null;function applySessionHandoff(pack){const resolved=pack??(typeof window<"u"?readSessionHandoff(window.location.hash):null);if(!resolved)return!1;token.value=resolved.token;if(resolved.refreshToken)refreshToken.value=resolved.refreshToken;if(resolved.user!==void 0&&resolved.user!==null)user.value=resolved.user;isAuthenticated.value=!0;if(typeof window<"u"&&!pack){const rest=stripSessionHandoff(window.location.hash);window.history.replaceState(null,"",`${window.location.pathname}${window.location.search}${rest}`)}return!0}function clearSession(){token.value="";refreshToken.value="";user.value=null;isAuthenticated.value=!1}async function performRefresh(){if(!refreshToken.value)return!1;const response=await fetch(`${baseUrl}${REFRESH_PATH}`,{method:"POST",credentials:"same-origin",headers:withCsrfHeader({"Content-Type":"application/json",Accept:"application/json"}),body:JSON.stringify({refresh_token:refreshToken.value})});if(!response.ok){clearSession();return!1}const data=await response.json(),nextAccess=data.access_token??data.token;if(!nextAccess){clearSession();return!1}token.value=nextAccess;if(data.refresh_token)refreshToken.value=data.refresh_token;return!0}export async function refreshSession(){if(inFlightRefresh)return inFlightRefresh;inFlightRefresh=performRefresh().catch(()=>{return!1}).finally(()=>{inFlightRefresh=null});return inFlightRefresh}export async function authFetch(input,init={}){const send=()=>fetch(input.startsWith("http")?input:`${baseUrl}${input}`,{...init,headers:{...init.headers,...token.value?{Authorization:`Bearer ${token.value}`}:{},Accept:"application/json"}}),response=await send();if(response.status!==401||!refreshToken.value)return response;if(!await refreshSession())return response;return await send()}export function useAuth(){async function fetchAuthUser(){try{if(!token.value){isAuthenticated.value=!1;user.value=null;return null}const response=await authFetch(ME_PATH);if(!response.ok){clearSession();return null}const data=await response.json();user.value=data;isAuthenticated.value=!0;return data}catch(error){console.error("Error fetching user:",error);isAuthenticated.value=!1;user.value=null;return null}}async function checkAuthentication(){try{return await fetchAuthUser()!==null}catch(error){console.error("Error checking authentication:",error);return!1}}async function register(user){const url=`${baseUrl}/register`,data=await(await fetch(url,{method:"POST",credentials:"same-origin",headers:withCsrfHeader({"Content-Type":"application/json"}),body:JSON.stringify(user)})).json();if(isRegisterError(data))return data;if(isRegisterResponse(data)){token.value=data.token;const refreshed=data.refresh_token;if(refreshed)refreshToken.value=refreshed;return data}return data}function isRegisterError(data){return"errors"in data}function isRegisterResponse(data){return"token"in data&&"user"in data}async function login(user){try{const url=`${baseUrl}/login`,response=await fetch(url,{method:"POST",credentials:"same-origin",headers:withCsrfHeader({"Content-Type":"application/json"}),body:JSON.stringify(user)});if(!response.ok){const errorData=await response.json();throw Error(errorData.message||`HTTP error! status: ${response.status}`)}const data=await response.json();token.value=data.token;const refreshed=data.refresh_token;if(refreshed)refreshToken.value=refreshed;await fetchAuthUser();return data}catch(error){return error}}async function logout(){try{const currentToken=token.value;if(currentToken)await fetch(`${baseUrl}/logout`,{method:"POST",headers:{Authorization:`Bearer ${currentToken}`,Accept:"application/json"}})}catch(error){console.error("Error during logout:",error)}finally{clearSession()}}async function completeSocialLogin(pack){const applied=applySessionHandoff(pack),confirmed=await fetchAuthUser();if(confirmed||applied)return confirmed;return null}return{user,isAuthenticated,token,getToken:()=>token.value,register,login,logout,fetchAuthUser,completeSocialLogin,checkAuthentication,refreshToken,getRefreshToken:()=>refreshToken.value,refreshSession,authFetch}}export function authGuard(options={}){const guest=options.guest??!1,{isAuthenticated}=useAuth();if(guest){if(isAuthenticated.value)window.location.replace("/");return}if(!isAuthenticated.value)window.location.replace("/login")}
1
+ import{readSessionHandoff,stripSessionHandoff,useStorage}from"@stacksjs/composables";import{ref}from"@stacksjs/stx";import{CSRF_COOKIE_NAME,readCsrfToken,withCsrfHeader}from"./csrf";const token=useStorage("token",""),refreshToken=useStorage("refresh_token",""),stacksConfig=globalThis.__STACKS_CONFIG__||{},baseUrl=stacksConfig.API_URL||(typeof window<"u"?window.location.origin:""),ME_PATH=stacksConfig.AUTH_ME_PATH||"/api/me",REFRESH_PATH=stacksConfig.AUTH_REFRESH_PATH||"/auth/refresh",user=ref(null),isAuthenticated=ref(!1);let inFlightRefresh=null;function applySessionHandoff(pack){const resolved=pack??(typeof window<"u"?readSessionHandoff(window.location.hash):null);if(!resolved)return!1;token.value=resolved.token;refreshToken.value=resolved.refreshToken??"";if(resolved.user!==void 0&&resolved.user!==null)user.value=resolved.user;isAuthenticated.value=!0;if(typeof window<"u"&&!pack){const rest=stripSessionHandoff(window.location.hash);window.history.replaceState(null,"",`${window.location.pathname}${window.location.search}${rest}`)}return!0}function clearSession(){token.value="";refreshToken.value="";user.value=null;isAuthenticated.value=!1}async function performRefresh(){if(!refreshToken.value)return!1;const response=await fetch(`${baseUrl}${REFRESH_PATH}`,{method:"POST",credentials:"same-origin",headers:withCsrfHeader({"Content-Type":"application/json",Accept:"application/json"}),body:JSON.stringify({refresh_token:refreshToken.value})});if(!response.ok){clearSession();return!1}const data=await response.json(),nextAccess=data.access_token??data.token;if(!nextAccess){clearSession();return!1}token.value=nextAccess;if(data.refresh_token)refreshToken.value=data.refresh_token;return!0}export async function refreshSession(){if(inFlightRefresh)return inFlightRefresh;inFlightRefresh=performRefresh().catch(()=>{return!1}).finally(()=>{inFlightRefresh=null});return inFlightRefresh}export async function authFetch(input,init={}){const send=()=>{const target=input.startsWith("http")?input:`${baseUrl}${input}`,headers=new Headers(init.headers);if(token.value)headers.set("Authorization",`Bearer ${token.value}`);headers.set("Accept","application/json");const method=(init.method||"GET").toUpperCase(),pageOrigin=typeof window<"u"?window.location?.origin:void 0,sameOrigin=!!pageOrigin&&new URL(target,pageOrigin).origin===pageOrigin;if(!["GET","HEAD","OPTIONS"].includes(method)&&sameOrigin&&!headers.has(CSRF_COOKIE_NAME)){const csrfToken=readCsrfToken();if(csrfToken)headers.set(CSRF_COOKIE_NAME,csrfToken)}return fetch(target,{credentials:"same-origin",...init,headers})},response=await send();if(response.status!==401||!refreshToken.value)return response;if(!await refreshSession())return response;return await send()}export function useAuth(){function isTwoFactorChallenge(data){if(!data||typeof data!=="object")return!1;const candidate=data;return candidate.requires_two_factor===!0&&typeof candidate.challenge_token==="string"&&candidate.challenge_token.length>0}function isLoginResponse(data){if(!data||typeof data!=="object")return!1;const candidate=data;return typeof candidate.token==="string"&&candidate.token.length>0&&typeof candidate.user==="object"&&candidate.user!==null&&(candidate.refresh_token===void 0||typeof candidate.refresh_token==="string"&&candidate.refresh_token.length>0)}function storeTokenPack(data){token.value=data.token;refreshToken.value=data.refresh_token??""}async function fetchAuthUser(){try{const response=await authFetch(ME_PATH);if(!response.ok){clearSession();return null}const data=await response.json();user.value=data;isAuthenticated.value=!0;return data}catch(error){console.error("Error fetching user:",error);isAuthenticated.value=!1;user.value=null;return null}}async function checkAuthentication(){try{return await fetchAuthUser()!==null}catch(error){console.error("Error checking authentication:",error);return!1}}async function register(user){const url=`${baseUrl}/register`,data=await(await fetch(url,{method:"POST",credentials:"same-origin",headers:withCsrfHeader({"Content-Type":"application/json"}),body:JSON.stringify(user)})).json();if(isRegisterError(data))return data;if(isRegisterResponse(data)){token.value=data.token;const refreshed=data.refresh_token;refreshToken.value=refreshed??"";return data}return data}function isRegisterError(data){return"errors"in data}function isRegisterResponse(data){return"token"in data&&"user"in data}async function login(user){try{const url=`${baseUrl}/login`,response=await fetch(url,{method:"POST",credentials:"same-origin",headers:withCsrfHeader({"Content-Type":"application/json"}),body:JSON.stringify(user)});if(!response.ok){const errorData=await response.json();throw Error(errorData.message||`HTTP error! status: ${response.status}`)}const data=await response.json();if(isTwoFactorChallenge(data))return data;if(!isLoginResponse(data))throw Error("The login response did not contain a valid session.");storeTokenPack(data);await fetchAuthUser();return data}catch(error){return error}}async function verifyTwoFactorLogin(challengeToken,code){try{const response=await fetch(`${baseUrl}/verify-two-factor-login`,{method:"POST",credentials:"same-origin",headers:withCsrfHeader({"Content-Type":"application/json"}),body:JSON.stringify({challenge_token:challengeToken,code})});if(!response.ok){const errorData=await response.json();throw Error(errorData.message||`HTTP error! status: ${response.status}`)}const data=await response.json();if(!isLoginResponse(data))throw Error("The two-factor response did not contain a valid session.");storeTokenPack(data);await fetchAuthUser();return data}catch(error){return error}}async function logout(){const currentToken=token.value,response=await fetch(`${baseUrl}/logout`,{method:"POST",credentials:"same-origin",headers:withCsrfHeader({...currentToken?{Authorization:`Bearer ${currentToken}`}:{},Accept:"application/json"})});if(!response.ok)throw Error(`Logout failed with status ${response.status}`);clearSession()}async function completeSocialLogin(pack){const applied=applySessionHandoff(pack),confirmed=await fetchAuthUser();if(confirmed||applied)return confirmed;return null}return{user,isAuthenticated,token,getToken:()=>token.value,register,login,verifyTwoFactorLogin,logout,fetchAuthUser,completeSocialLogin,checkAuthentication,refreshToken,getRefreshToken:()=>refreshToken.value,refreshSession,authFetch}}export function authGuard(options={}){const guest=options.guest??!1,{isAuthenticated}=useAuth();if(guest){if(isAuthenticated.value)window.location.replace("/");return}if(!isAuthenticated.value)window.location.replace("/login")}
@@ -7,7 +7,9 @@ export declare interface ValidationError {
7
7
  }[]
8
8
  }
9
9
  export declare interface RegisterError {
10
- errors: ResponseError
10
+ errors?: ResponseError
11
+ error?: string
12
+ message?: string
11
13
  }
12
14
  // LoginAction/RegisterAction/AuthUserAction respond via `response.json(...)`,
13
15
  // which serializes flat (see @stacksjs/bun-router's ResponseFactory.json) —
@@ -26,7 +28,7 @@ export declare interface RegisterResponse {
26
28
  // is kept alongside `access_token` for backward compatibility.
27
29
  export declare interface LoginResponse {
28
30
  access_token: string
29
- refresh_token: string
31
+ refresh_token?: string
30
32
  token_type: string
31
33
  expires_in: number
32
34
  token: string
@@ -36,6 +38,10 @@ export declare interface LoginResponse {
36
38
  name: string
37
39
  }
38
40
  }
41
+ export declare interface TwoFactorLoginChallenge {
42
+ requires_two_factor: true
43
+ challenge_token: string
44
+ }
39
45
  export declare interface Response<T> {
40
46
  errors: ResponseError
41
47
  data: T
@@ -43,6 +49,7 @@ export declare interface Response<T> {
43
49
  export declare interface AuthUser {
44
50
  email: string
45
51
  password: string
52
+ remember?: boolean
46
53
  }
47
54
  export declare interface ErrorResponse {
48
55
  message: string
@@ -55,12 +62,13 @@ export declare interface UserData {
55
62
  export declare interface AuthComposable {
56
63
  isAuthenticated: Ref<boolean>
57
64
  user: Ref<UserData | null>
58
- login: (user: AuthUser) => Promise<LoginResponse | LoginError>
65
+ login: (user: AuthUser) => Promise<LoginResult | LoginError>
66
+ verifyTwoFactorLogin: (challengeToken: string, code: string) => Promise<LoginResponse | LoginError>
59
67
  register: (user: AuthUser) => Promise<RegisterResponse | RegisterError>
60
68
  fetchAuthUser: () => Promise<UserData | null>
61
69
  completeSocialLogin: (pack?: SessionHandoffPack | null) => Promise<UserData | null>
62
70
  checkAuthentication: () => Promise<boolean>
63
- logout: () => void
71
+ logout: () => Promise<void>
64
72
  getToken: () => string | null
65
73
  token: Ref<string | null>
66
74
  refreshToken: Ref<string | null>
@@ -72,4 +80,5 @@ export type ResponseError = {
72
80
  error: string
73
81
  } | ValidationError;
74
82
  export type LoginError = RegisterError;
83
+ export type LoginResult = LoginResponse | TwoFactorLoginChallenge;
75
84
  export type MeResponse = UserData;
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/browser",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.74.60",
5
+ "version": "0.74.62",
6
6
  "description": "Stacks core frontend/browser functionalities.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -75,9 +75,9 @@
75
75
  "prepublishOnly": "bun run build"
76
76
  },
77
77
  "dependencies": {
78
- "@stacksjs/composables": "0.74.60",
79
- "@stacksjs/datetime": "0.74.60",
80
- "@stacksjs/strings": "0.74.60",
78
+ "@stacksjs/composables": "0.74.62",
79
+ "@stacksjs/datetime": "0.74.62",
80
+ "@stacksjs/strings": "0.74.62",
81
81
  "@stacksjs/stx": "^0.2.286",
82
82
  "bun-query-builder": "^0.2.70"
83
83
  },
@@ -90,7 +90,7 @@
90
90
  }
91
91
  },
92
92
  "devDependencies": {
93
- "@stacksjs/utils": "0.74.60",
93
+ "@stacksjs/utils": "0.74.62",
94
94
  "@stripe/stripe-js": "^9.10.0",
95
95
  "better-dx": "^0.2.24"
96
96
  }