flowlink-auth 2.7.2 → 2.7.4
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.js +46 -1069
- package/package.json +4 -3
- package/src/ErrorBox.jsx +22 -0
- package/src/SignIn.jsx +212 -0
- package/src/SignUp.jsx +260 -0
- package/src/api.js +60 -0
- package/src/createAuthMiddleware.js +69 -0
- package/src/index.d.ts +15 -0
- package/src/index.js +5 -0
- package/src/init.js +100 -0
- package/src/provider.js +261 -0
- package/src/securityUtils.js +151 -0
- package/src/useAuth.js +13 -0
package/src/init.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// src/init.js
|
|
2
|
+
let CONFIG = null
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Initialize the SDK globally (alternative to using flowlinkAuthProvider props).
|
|
6
|
+
* publishableKey: string (required)
|
|
7
|
+
* baseUrl: string (required)
|
|
8
|
+
* opts: { requireServerSecret?: boolean, serverSecret?: string } - serverSecret ONLY for dev/testing
|
|
9
|
+
*/
|
|
10
|
+
export function initFlowlinkAuth({ publishableKey, baseUrl, opts } = {}) {
|
|
11
|
+
if (!publishableKey) throw new Error('flowlink-auth: publishableKey is required in initFlowlinkAuth()')
|
|
12
|
+
if (!baseUrl) throw new Error('flowlink-auth: baseUrl is required in initFlowlinkAuth()')
|
|
13
|
+
|
|
14
|
+
// Security: Validate publishable key format
|
|
15
|
+
if (!publishableKey.startsWith('pk_')) {
|
|
16
|
+
console.warn('flowlink-auth: publishableKey should start with "pk_"')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Security: Enforce HTTPS in production
|
|
20
|
+
if (typeof window !== 'undefined' && window.location.protocol === 'http:' && !baseUrl.includes('localhost')) {
|
|
21
|
+
console.error('flowlink-auth: HTTPS is required for production. Current protocol:', window.location.protocol)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Security: Validate baseUrl is properly formatted
|
|
25
|
+
const urlObj = new URL(baseUrl).href
|
|
26
|
+
if (!urlObj) {
|
|
27
|
+
throw new Error('flowlink-auth: baseUrl must be a valid URL')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
CONFIG = {
|
|
31
|
+
publishableKey,
|
|
32
|
+
baseUrl: baseUrl.replace(/\/+$/, ''),
|
|
33
|
+
requireServerSecret: opts?.requireServerSecret || false,
|
|
34
|
+
secretKey: opts?.serverSecret || null,
|
|
35
|
+
csrfToken: generateCSRFToken() // Add CSRF protection
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (CONFIG.requireServerSecret && !CONFIG.secretKey) {
|
|
39
|
+
console.warn('flowlink-auth: requireServerSecret=true but serverSecret not provided. This is insecure for production.')
|
|
40
|
+
}
|
|
41
|
+
if (CONFIG.secretKey) {
|
|
42
|
+
console.error('flowlink-auth: secretKey should NEVER be provided to client SDK. Use server-side authentication only.')
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Generate CSRF token for form submissions
|
|
48
|
+
*/
|
|
49
|
+
function generateCSRFToken() {
|
|
50
|
+
if (typeof window === 'undefined') return null
|
|
51
|
+
const array = new Uint8Array(32)
|
|
52
|
+
window.crypto.getRandomValues(array)
|
|
53
|
+
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('')
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Called by flowlinkAuthProvider so provider props set the same shared CONFIG.
|
|
58
|
+
* This allows either initflowlinkAuth(...) or provider props to work.
|
|
59
|
+
*/
|
|
60
|
+
export function setConfigFromProvider({ publishableKey, secretKey = null, baseUrl, opts = {} } = {}) {
|
|
61
|
+
if (!publishableKey || !baseUrl) {
|
|
62
|
+
// don't throw here — provider already validates and shows friendly UI
|
|
63
|
+
CONFIG = null
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Security: Validate HTTPS
|
|
68
|
+
if (typeof window !== 'undefined' && window.location.protocol === 'http:' && !baseUrl.includes('localhost')) {
|
|
69
|
+
console.warn('flowlink-auth: HTTPS recommended for production')
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
CONFIG = {
|
|
73
|
+
publishableKey,
|
|
74
|
+
baseUrl: baseUrl.replace(/\/+$/, ''),
|
|
75
|
+
requireServerSecret: opts?.requireServerSecret || false,
|
|
76
|
+
secretKey: secretKey || null,
|
|
77
|
+
csrfToken: generateCSRFToken()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (secretKey) {
|
|
81
|
+
console.error('flowlink-auth: NEVER pass secretKey to client-side code')
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Safe getter used by UI components (returns null if not initialized) */
|
|
86
|
+
export function getConfigSafe() {
|
|
87
|
+
return CONFIG
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Strict getter used by server helpers (throws if not initialized) */
|
|
91
|
+
export function getConfig() {
|
|
92
|
+
if (!CONFIG) throw new Error('flowlink-auth: SDK not initialized. Call initFlowlinkAuth(...) or wrap components in FlowlinkAuthProvider with keys.')
|
|
93
|
+
return CONFIG
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Get CSRF token for forms */
|
|
97
|
+
export function getCSRFToken() {
|
|
98
|
+
return CONFIG?.csrfToken || null
|
|
99
|
+
}
|
|
100
|
+
|
package/src/provider.js
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
// provider.js
|
|
2
|
+
'use client'
|
|
3
|
+
import React, { createContext, useContext, useEffect, useState, useCallback, useRef } from 'react'
|
|
4
|
+
import { checkSecureContext, validateEmail, getSafeErrorMessage } from './securityUtils.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* flowlinkAuthProvider (cookie-based)
|
|
8
|
+
*
|
|
9
|
+
* - Backend sets HttpOnly cookie (flowlink_token) on login/signup
|
|
10
|
+
* - Frontend calls /api/sdk/me with credentials:'include' to validate session
|
|
11
|
+
* - completeLogin() waits for /me and then optionally redirects (replace by default)
|
|
12
|
+
* - logout() clears client state and calls server logout (if available)
|
|
13
|
+
* - other tabs are notified via localStorage events (flowlink_login / flowlink_logout)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const AuthContext = createContext(null)
|
|
17
|
+
|
|
18
|
+
const FlowlinkAuthProvider = ({ children, publishableKey, baseUrl, redirect }) => {
|
|
19
|
+
const [ready, setReady] = useState(false)
|
|
20
|
+
const [error, setError] = useState(null)
|
|
21
|
+
|
|
22
|
+
const [user, setUser] = useState(null)
|
|
23
|
+
const [loadingUser, setLoadingUser] = useState(true)
|
|
24
|
+
const [sessionTimeout, setSessionTimeout] = useState(null)
|
|
25
|
+
|
|
26
|
+
const redirectedRef = useRef(false) // prevent multiple automatic redirects
|
|
27
|
+
const sessionTimerRef = useRef(null) // track session timeout timer
|
|
28
|
+
|
|
29
|
+
// Security: Check HTTPS context
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
checkSecureContext()
|
|
32
|
+
}, [])
|
|
33
|
+
|
|
34
|
+
/* Validate config */
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (!publishableKey || !publishableKey.trim()) {
|
|
37
|
+
setError('Missing publishable key')
|
|
38
|
+
setReady(false)
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
if (!baseUrl || !baseUrl.trim()) {
|
|
42
|
+
setError('Missing baseUrl')
|
|
43
|
+
setReady(false)
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Security: Validate publishable key format
|
|
48
|
+
if (!publishableKey.startsWith('pk_')) {
|
|
49
|
+
console.warn('flowlink-auth: publishableKey should start with "pk_"')
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
setError(null)
|
|
53
|
+
setReady(true)
|
|
54
|
+
}, [publishableKey, baseUrl])
|
|
55
|
+
|
|
56
|
+
const normalizedBase = useCallback(() => {
|
|
57
|
+
return baseUrl?.replace(/\/+$/, '') || ''
|
|
58
|
+
}, [baseUrl])
|
|
59
|
+
|
|
60
|
+
const redirectTo = (url, { replace = true } = {}) => {
|
|
61
|
+
if (!url) return
|
|
62
|
+
if (typeof window === 'undefined') return
|
|
63
|
+
|
|
64
|
+
// Security: Validate redirect URL (prevent open redirect)
|
|
65
|
+
if (url.startsWith('//') || url.startsWith('http')) {
|
|
66
|
+
console.error('flowlink-auth: Redirect URL must be relative')
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
if (replace) window.location.replace(url)
|
|
72
|
+
else window.location.assign(url)
|
|
73
|
+
} catch (_) {
|
|
74
|
+
window.location.href = url
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Security: Reset session timeout on activity
|
|
79
|
+
const resetSessionTimeout = useCallback(() => {
|
|
80
|
+
if (sessionTimerRef.current) {
|
|
81
|
+
clearTimeout(sessionTimerRef.current)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Set 24-hour session timeout
|
|
85
|
+
sessionTimerRef.current = setTimeout(() => {
|
|
86
|
+
setSessionTimeout(true)
|
|
87
|
+
logout()
|
|
88
|
+
}, 24 * 60 * 60 * 1000)
|
|
89
|
+
}, [])
|
|
90
|
+
|
|
91
|
+
/* Fetch /api/sdk/me using cookie (HttpOnly token) */
|
|
92
|
+
const fetchMe = useCallback(async () => {
|
|
93
|
+
setLoadingUser(true)
|
|
94
|
+
try {
|
|
95
|
+
const base = normalizedBase()
|
|
96
|
+
if (!base) {
|
|
97
|
+
setUser(null)
|
|
98
|
+
setLoadingUser(false)
|
|
99
|
+
return null
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Security: Validate URL
|
|
103
|
+
const fullUrl = new URL(`${base}/api/sdk/me`).href
|
|
104
|
+
|
|
105
|
+
const res = await fetch(fullUrl, {
|
|
106
|
+
method: 'GET',
|
|
107
|
+
credentials: 'include',
|
|
108
|
+
headers: {
|
|
109
|
+
'Content-Type': 'application/json'
|
|
110
|
+
},
|
|
111
|
+
referrerPolicy: 'strict-origin-when-cross-origin'
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
if (!res.ok) {
|
|
115
|
+
setUser(null)
|
|
116
|
+
setLoadingUser(false)
|
|
117
|
+
return null
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const data = await res.json()
|
|
121
|
+
const u = data?.user ?? null
|
|
122
|
+
setUser(u)
|
|
123
|
+
setLoadingUser(false)
|
|
124
|
+
|
|
125
|
+
// Reset session timeout on successful fetch
|
|
126
|
+
resetSessionTimeout()
|
|
127
|
+
|
|
128
|
+
return u
|
|
129
|
+
} catch (err) {
|
|
130
|
+
console.error('Failed to fetch user:', getSafeErrorMessage(err))
|
|
131
|
+
setUser(null)
|
|
132
|
+
setLoadingUser(false)
|
|
133
|
+
return null
|
|
134
|
+
}
|
|
135
|
+
}, [normalizedBase, resetSessionTimeout])
|
|
136
|
+
|
|
137
|
+
/* Initial bootstrap + storage listener (to sync across tabs) */
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
if (!ready) return
|
|
140
|
+
fetchMe()
|
|
141
|
+
|
|
142
|
+
const onStorage = (e) => {
|
|
143
|
+
if (!e.key) return
|
|
144
|
+
if (e.key === 'flowlink_login' || e.key === 'flowlink_logout') {
|
|
145
|
+
// revalidate session when other tab signals a change
|
|
146
|
+
fetchMe()
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (typeof window !== 'undefined') {
|
|
151
|
+
window.addEventListener('storage', onStorage)
|
|
152
|
+
}
|
|
153
|
+
return () => {
|
|
154
|
+
if (typeof window !== 'undefined') {
|
|
155
|
+
window.removeEventListener('storage', onStorage)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}, [ready, fetchMe])
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Auto-redirect when user becomes authenticated.
|
|
162
|
+
* Runs once per mount when loadingUser turns false and user exists.
|
|
163
|
+
*/
|
|
164
|
+
useEffect(() => {
|
|
165
|
+
if (!ready) return
|
|
166
|
+
if (loadingUser) return
|
|
167
|
+
if (!user) {
|
|
168
|
+
redirectedRef.current = false // reset flag when logged out
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
if (user && redirect && !redirectedRef.current) {
|
|
172
|
+
redirectedRef.current = true
|
|
173
|
+
redirectTo(redirect, { replace: true })
|
|
174
|
+
}
|
|
175
|
+
}, [ready, loadingUser, user, redirect])
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* completeLogin(opts)
|
|
179
|
+
* - call after server login/signup that sets HttpOnly cookie
|
|
180
|
+
* - will re-fetch /me, notify other tabs, and optionally redirect
|
|
181
|
+
* opts:
|
|
182
|
+
* - redirectTo: URL to navigate after successful validation (overrides provider redirect)
|
|
183
|
+
* - replace: boolean (default true)
|
|
184
|
+
*/
|
|
185
|
+
const completeLogin = useCallback(async (opts = {}) => {
|
|
186
|
+
const { redirectTo: redirectUrl, replace = true } = opts
|
|
187
|
+
const u = await fetchMe()
|
|
188
|
+
// notify other tabs
|
|
189
|
+
try { localStorage.setItem('flowlink_login', String(Date.now())) } catch (_) {}
|
|
190
|
+
// only redirect if we have a valid user
|
|
191
|
+
const dest = redirectUrl ?? redirect
|
|
192
|
+
if (u && dest) {
|
|
193
|
+
redirectTo(dest, { replace })
|
|
194
|
+
}
|
|
195
|
+
return u
|
|
196
|
+
}, [fetchMe, redirect])
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* logout(opts)
|
|
200
|
+
* - callServer: whether to call /api/sdk/logout (default true)
|
|
201
|
+
* - redirectTo: optional URL to go to after logout (overrides provider redirect)
|
|
202
|
+
*/
|
|
203
|
+
const logout = useCallback(async (opts = {}) => {
|
|
204
|
+
const { callServer = true, redirectTo: redirectUrl, replace = true } = opts
|
|
205
|
+
const base = normalizedBase()
|
|
206
|
+
|
|
207
|
+
if (callServer && base) {
|
|
208
|
+
try {
|
|
209
|
+
await fetch(`${base}/api/sdk/logout`, {
|
|
210
|
+
method: 'POST',
|
|
211
|
+
credentials: 'include',
|
|
212
|
+
headers: { 'Content-Type': 'application/json' }
|
|
213
|
+
})
|
|
214
|
+
} catch (_) {
|
|
215
|
+
// ignore server errors; proceed to client cleanup
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
setUser(null)
|
|
220
|
+
try { localStorage.setItem('flowlink_logout', String(Date.now())) } catch (_) {}
|
|
221
|
+
|
|
222
|
+
const dest = redirectUrl ?? redirect
|
|
223
|
+
if (dest) redirectTo(dest, { replace })
|
|
224
|
+
}, [normalizedBase, redirect])
|
|
225
|
+
|
|
226
|
+
/* Provide context value */
|
|
227
|
+
const value = {
|
|
228
|
+
publishableKey,
|
|
229
|
+
baseUrl,
|
|
230
|
+
redirectTo: (url, opts) => redirectTo(url, opts),
|
|
231
|
+
user,
|
|
232
|
+
setUser,
|
|
233
|
+
loadingUser,
|
|
234
|
+
fetchMe,
|
|
235
|
+
logout,
|
|
236
|
+
completeLogin
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return (
|
|
240
|
+
<AuthContext.Provider value={value}>
|
|
241
|
+
{error ? (
|
|
242
|
+
<div style={{ padding:'20px', background:'#220000', color:'white' }}>
|
|
243
|
+
<h2>flowlink Auth Error</h2>
|
|
244
|
+
<p>{error}</p>
|
|
245
|
+
</div>
|
|
246
|
+
) : !ready ? null : (
|
|
247
|
+
children
|
|
248
|
+
)}
|
|
249
|
+
</AuthContext.Provider>
|
|
250
|
+
)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const useAuth = () => {
|
|
254
|
+
const ctx = useContext(AuthContext)
|
|
255
|
+
if (!ctx) throw new Error('useAuth must be used within FlowlinkAuthProvider')
|
|
256
|
+
return ctx
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export default FlowlinkAuthProvider
|
|
260
|
+
export { useAuth }
|
|
261
|
+
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// src/securityUtils.js
|
|
2
|
+
/**
|
|
3
|
+
* Security utilities for flowlink-auth SDK
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Sanitize user input to prevent XSS
|
|
8
|
+
*/
|
|
9
|
+
export function sanitizeInput(input) {
|
|
10
|
+
if (typeof input !== 'string') return input
|
|
11
|
+
|
|
12
|
+
const div = document.createElement('div')
|
|
13
|
+
div.textContent = input
|
|
14
|
+
return div.innerHTML
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Validate email format
|
|
19
|
+
*/
|
|
20
|
+
export function validateEmail(email) {
|
|
21
|
+
if (typeof email !== 'string') return false
|
|
22
|
+
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
|
23
|
+
return regex.test(email) && email.length <= 255
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Validate password strength
|
|
28
|
+
*/
|
|
29
|
+
export function validatePasswordStrength(password) {
|
|
30
|
+
if (typeof password !== 'string') return false
|
|
31
|
+
if (password.length < 12) return false
|
|
32
|
+
|
|
33
|
+
const hasUpper = /[A-Z]/.test(password)
|
|
34
|
+
const hasLower = /[a-z]/.test(password)
|
|
35
|
+
const hasNumber = /[0-9]/.test(password)
|
|
36
|
+
const hasSpecial = /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password)
|
|
37
|
+
|
|
38
|
+
return hasUpper && hasLower && hasNumber && hasSpecial
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get password strength feedback
|
|
43
|
+
*/
|
|
44
|
+
export function getPasswordFeedback(password) {
|
|
45
|
+
const feedback = []
|
|
46
|
+
|
|
47
|
+
if (password.length < 12) {
|
|
48
|
+
feedback.push('At least 12 characters')
|
|
49
|
+
}
|
|
50
|
+
if (!/[A-Z]/.test(password)) {
|
|
51
|
+
feedback.push('One uppercase letter')
|
|
52
|
+
}
|
|
53
|
+
if (!/[a-z]/.test(password)) {
|
|
54
|
+
feedback.push('One lowercase letter')
|
|
55
|
+
}
|
|
56
|
+
if (!/[0-9]/.test(password)) {
|
|
57
|
+
feedback.push('One number')
|
|
58
|
+
}
|
|
59
|
+
if (!/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password)) {
|
|
60
|
+
feedback.push('One special character')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return feedback
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Check if running on HTTPS (except localhost)
|
|
68
|
+
*/
|
|
69
|
+
export function isSecureContext() {
|
|
70
|
+
if (typeof window === 'undefined') return true
|
|
71
|
+
|
|
72
|
+
const isLocalhost = window.location.hostname === 'localhost' ||
|
|
73
|
+
window.location.hostname === '127.0.0.1' ||
|
|
74
|
+
window.location.hostname === '[::1]'
|
|
75
|
+
|
|
76
|
+
const isHttps = window.location.protocol === 'https:'
|
|
77
|
+
|
|
78
|
+
return isHttps || isLocalhost
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Warn if not on HTTPS in production
|
|
83
|
+
*/
|
|
84
|
+
export function checkSecureContext() {
|
|
85
|
+
if (!isSecureContext()) {
|
|
86
|
+
console.warn(
|
|
87
|
+
'flowlink-auth: HTTPS is required for production. ' +
|
|
88
|
+
'Your connection is not secure. Authentication may fail.'
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Safe error message handler (don't leak sensitive info)
|
|
95
|
+
*/
|
|
96
|
+
export function getSafeErrorMessage(error) {
|
|
97
|
+
if (typeof error === 'string') {
|
|
98
|
+
// Check for sensitive keywords
|
|
99
|
+
if (error.includes('password') || error.includes('token') || error.includes('secret')) {
|
|
100
|
+
return 'An error occurred. Please try again.'
|
|
101
|
+
}
|
|
102
|
+
return error
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (error?.message) {
|
|
106
|
+
return getSafeErrorMessage(error.message)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return 'An error occurred. Please try again.'
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Generate nonce for CSP
|
|
114
|
+
*/
|
|
115
|
+
export function generateNonce() {
|
|
116
|
+
const array = new Uint8Array(16)
|
|
117
|
+
if (typeof window !== 'undefined' && window.crypto) {
|
|
118
|
+
window.crypto.getRandomValues(array)
|
|
119
|
+
}
|
|
120
|
+
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('')
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Validate origin matches expected domain
|
|
125
|
+
*/
|
|
126
|
+
export function validateOrigin(expectedOrigin) {
|
|
127
|
+
if (typeof window === 'undefined') return true
|
|
128
|
+
|
|
129
|
+
const currentOrigin = window.location.origin
|
|
130
|
+
|
|
131
|
+
return currentOrigin === expectedOrigin
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Check for XSS vulnerabilities in stored data
|
|
136
|
+
*/
|
|
137
|
+
export function hasXSSPatterns(input) {
|
|
138
|
+
if (typeof input !== 'string') return false
|
|
139
|
+
|
|
140
|
+
const xssPatterns = [
|
|
141
|
+
/<script[^>]*>.*?<\/script>/gi,
|
|
142
|
+
/javascript:/gi,
|
|
143
|
+
/on\w+\s*=/gi,
|
|
144
|
+
/<iframe/gi,
|
|
145
|
+
/<object/gi,
|
|
146
|
+
/<embed/gi
|
|
147
|
+
]
|
|
148
|
+
|
|
149
|
+
return xssPatterns.some(pattern => pattern.test(input))
|
|
150
|
+
}
|
|
151
|
+
|
package/src/useAuth.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// 'use client'
|
|
2
|
+
import { useAuth } from './provider.js'
|
|
3
|
+
import { checkSecureContext } from './securityUtils.js'
|
|
4
|
+
|
|
5
|
+
// Check security context on module load
|
|
6
|
+
if (typeof window !== 'undefined') {
|
|
7
|
+
checkSecureContext()
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Just re-export the hook from provider.js
|
|
11
|
+
export { useAuth }
|
|
12
|
+
|
|
13
|
+
|