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/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
+
@@ -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
+