@kasuvia/sdk 0.0.1
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/LICENSE +201 -0
- package/README.md +112 -0
- package/dist/index.d.mts +1891 -0
- package/dist/index.d.ts +1891 -0
- package/dist/index.js +3540 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +3314 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +59 -0
- package/src/client/authClient.ts +135 -0
- package/src/client/graphqlClient.ts +53 -0
- package/src/client/httpClient.ts +298 -0
- package/src/config/index.ts +102 -0
- package/src/graphql/collections.graphql.ts +56 -0
- package/src/graphql/index.ts +7 -0
- package/src/graphql/products.graphql.ts +710 -0
- package/src/graphql/wishlist.graphql.ts +33 -0
- package/src/hooks/index.ts +26 -0
- package/src/hooks/use-auth.ts +182 -0
- package/src/hooks/use-bespoke-availability.ts +20 -0
- package/src/hooks/use-cart.ts +207 -0
- package/src/hooks/use-cms.ts +165 -0
- package/src/hooks/use-collections.ts +40 -0
- package/src/hooks/use-consultation-stream.ts +141 -0
- package/src/hooks/use-consultations.ts +76 -0
- package/src/hooks/use-countries.ts +24 -0
- package/src/hooks/use-currencies.ts +27 -0
- package/src/hooks/use-google-translate.ts +164 -0
- package/src/hooks/use-is-mounted.ts +9 -0
- package/src/hooks/use-languages.ts +33 -0
- package/src/hooks/use-order-stream.ts +195 -0
- package/src/hooks/use-orders.ts +64 -0
- package/src/hooks/use-payment.ts +68 -0
- package/src/hooks/use-products.ts +113 -0
- package/src/hooks/use-service-booking.ts +118 -0
- package/src/hooks/use-states.ts +28 -0
- package/src/hooks/use-wishlist.ts +121 -0
- package/src/index.ts +65 -0
- package/src/services/app.service.ts +17 -0
- package/src/services/auth.service.ts +332 -0
- package/src/services/bespoke.service.ts +17 -0
- package/src/services/cart.service.ts +245 -0
- package/src/services/cms.service.ts +112 -0
- package/src/services/collection.service.ts +77 -0
- package/src/services/config.service.ts +42 -0
- package/src/services/consultation.service.ts +51 -0
- package/src/services/index.ts +21 -0
- package/src/services/logistics.service.ts +95 -0
- package/src/services/notifications.service.ts +50 -0
- package/src/services/order.service.ts +169 -0
- package/src/services/payment.service.ts +127 -0
- package/src/services/product.service.ts +678 -0
- package/src/services/service-booking.service.ts +86 -0
- package/src/services/wishlist.service.ts +84 -0
- package/src/types/auth/index.ts +126 -0
- package/src/types/bespoke/index.ts +39 -0
- package/src/types/business/index.ts +6 -0
- package/src/types/cms/index.ts +77 -0
- package/src/types/consultations/index.ts +79 -0
- package/src/types/index.ts +27 -0
- package/src/types/media/index.ts +41 -0
- package/src/types/payments/index.ts +120 -0
- package/src/types/services/index.ts +80 -0
- package/src/types/store/index.ts +282 -0
- package/src/types/store/shipping.ts +38 -0
- package/src/types/ui/index.ts +32 -0
- package/src/utils/cache.ts +33 -0
- package/src/utils/checkout.ts +39 -0
- package/src/utils/customer.ts +20 -0
- package/src/utils/error-handler.ts +14 -0
- package/src/utils/index.ts +10 -0
- package/src/utils/logger.ts +79 -0
- package/src/utils/sanitize-url.ts +55 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth Client - Token Store and Refresh Logic
|
|
3
|
+
*
|
|
4
|
+
* - Access token: in-memory only (XSS protection)
|
|
5
|
+
* - Refresh token: localStorage (persists across page loads), keyed per tenant
|
|
6
|
+
*/
|
|
7
|
+
import axios from 'axios'
|
|
8
|
+
import { getRuntimeConfig, getBackendUrl, isInitialized } from '../config'
|
|
9
|
+
|
|
10
|
+
let _accessToken: string | null = null
|
|
11
|
+
let _isRefreshing = false
|
|
12
|
+
let _pendingQueue: Array<{
|
|
13
|
+
resolve: (newAccessToken: string) => void
|
|
14
|
+
reject: (error: unknown) => void
|
|
15
|
+
}> = []
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Returns a BID-prefixed key for storage isolation.
|
|
19
|
+
* Prevents data leakage when multiple storefronts run on the same domain.
|
|
20
|
+
*/
|
|
21
|
+
function getStorageKey(): string {
|
|
22
|
+
if (!isInitialized()) return 'kasuvia-sf-anon-refresh'
|
|
23
|
+
const config = getRuntimeConfig()
|
|
24
|
+
return `kasuvia-sf-${config.businessId}-refresh`
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function drainQueue(error: unknown, newToken: string | null = null): void {
|
|
28
|
+
_pendingQueue.forEach(({ resolve, reject }) => {
|
|
29
|
+
if (newToken) resolve(newToken)
|
|
30
|
+
else reject(error)
|
|
31
|
+
})
|
|
32
|
+
_pendingQueue = []
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const tokenStore = {
|
|
36
|
+
getAccessToken: (): string | null => _accessToken,
|
|
37
|
+
|
|
38
|
+
setAccessToken: (token: string | null): void => {
|
|
39
|
+
_accessToken = token
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
getRefreshToken: (): string | null => {
|
|
43
|
+
if (typeof window === 'undefined') return null
|
|
44
|
+
const key = getStorageKey()
|
|
45
|
+
return localStorage.getItem(key) || sessionStorage.getItem(key)
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
setRefreshToken: (token: string | null, remember = true): void => {
|
|
49
|
+
if (typeof window === 'undefined') return
|
|
50
|
+
const key = getStorageKey()
|
|
51
|
+
localStorage.removeItem(key)
|
|
52
|
+
sessionStorage.removeItem(key)
|
|
53
|
+
if (token) {
|
|
54
|
+
const store = remember ? localStorage : sessionStorage
|
|
55
|
+
store.setItem(key, token)
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
clearAll: (): void => {
|
|
60
|
+
_accessToken = null
|
|
61
|
+
if (typeof window !== 'undefined') {
|
|
62
|
+
const key = getStorageKey()
|
|
63
|
+
localStorage.removeItem(key)
|
|
64
|
+
sessionStorage.removeItem(key)
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
hasSession: (): boolean => {
|
|
69
|
+
if (typeof window === 'undefined') return false
|
|
70
|
+
const key = getStorageKey()
|
|
71
|
+
return !!(localStorage.getItem(key) || sessionStorage.getItem(key))
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
isPersistent: (): boolean => {
|
|
75
|
+
if (typeof window === 'undefined') return true
|
|
76
|
+
const key = getStorageKey()
|
|
77
|
+
return !!localStorage.getItem(key)
|
|
78
|
+
},
|
|
79
|
+
} as const
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Refresh-only Axios instance.
|
|
83
|
+
* CRITICAL: NO auth interceptors — avoids infinite recursion when the 401
|
|
84
|
+
* interceptor on authAxios calls refreshToken() and itself receives a 401.
|
|
85
|
+
* baseURL is set eagerly from the static BACKEND_URLS constant.
|
|
86
|
+
*/
|
|
87
|
+
const refreshAxios = axios.create({
|
|
88
|
+
baseURL: getBackendUrl('auth'),
|
|
89
|
+
headers: {
|
|
90
|
+
'Content-Type': 'application/json',
|
|
91
|
+
Accept: 'application/json',
|
|
92
|
+
},
|
|
93
|
+
withCredentials: true,
|
|
94
|
+
timeout: 30_000,
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Perform token refresh with queue-coalescing to avoid parallel refresh races.
|
|
99
|
+
*/
|
|
100
|
+
export async function refreshToken(): Promise<string> {
|
|
101
|
+
if (_isRefreshing) {
|
|
102
|
+
// Queue subsequent callers so they all get the new token from a single refresh
|
|
103
|
+
return new Promise((resolve, reject) => {
|
|
104
|
+
_pendingQueue.push({ resolve, reject })
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
_isRefreshing = true
|
|
109
|
+
|
|
110
|
+
const refresh = tokenStore.getRefreshToken()
|
|
111
|
+
if (!refresh) {
|
|
112
|
+
_isRefreshing = false
|
|
113
|
+
tokenStore.clearAll()
|
|
114
|
+
throw new Error('[kasuvia SDK] No refresh token available')
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
const { data } = await refreshAxios.post('/auth/token/refresh/', { refresh })
|
|
119
|
+
const newAccess: string = data.access
|
|
120
|
+
tokenStore.setAccessToken(newAccess)
|
|
121
|
+
|
|
122
|
+
if (data.refresh) {
|
|
123
|
+
tokenStore.setRefreshToken(data.refresh, tokenStore.isPersistent())
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
drainQueue(null, newAccess)
|
|
127
|
+
return newAccess
|
|
128
|
+
} catch (err) {
|
|
129
|
+
drainQueue(err)
|
|
130
|
+
tokenStore.clearAll()
|
|
131
|
+
throw err
|
|
132
|
+
} finally {
|
|
133
|
+
_isRefreshing = false
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GraphQL Client
|
|
3
|
+
*
|
|
4
|
+
* Provides a GraphQL client with tenant and auth headers for product queries.
|
|
5
|
+
* The endpoint URL is derived from the static BACKEND_URLS constant (eager,
|
|
6
|
+
* module-load safe). Auth headers are injected fresh on each request via a
|
|
7
|
+
* factory so the token is always current.
|
|
8
|
+
*/
|
|
9
|
+
import { GraphQLClient } from 'graphql-request'
|
|
10
|
+
import { getRuntimeConfig, getBackendUrl, isInitialized } from '../config'
|
|
11
|
+
import { tokenStore } from './authClient'
|
|
12
|
+
|
|
13
|
+
const GRAPHQL_URL = `${getBackendUrl('store')}/graphql`
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Build fresh request headers.
|
|
17
|
+
* Called on every getGraphQLClient() so token changes are always reflected.
|
|
18
|
+
*/
|
|
19
|
+
function buildHeaders(): Record<string, string> {
|
|
20
|
+
const headers: Record<string, string> = {
|
|
21
|
+
'Content-Type': 'application/json',
|
|
22
|
+
Accept: 'application/json',
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (isInitialized()) {
|
|
26
|
+
const config = getRuntimeConfig()
|
|
27
|
+
headers['X-Storefront-Key'] = config.storefrontApiKey
|
|
28
|
+
headers['X-Business-ID'] = config.businessId
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const token = tokenStore.getAccessToken()
|
|
32
|
+
if (token) headers['Authorization'] = `Bearer ${token}`
|
|
33
|
+
|
|
34
|
+
return headers
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Returns a GraphQL client instance with up-to-date auth headers.
|
|
39
|
+
* A new client is created on each call so token/config changes take effect
|
|
40
|
+
* immediately without requiring an explicit reset.
|
|
41
|
+
*/
|
|
42
|
+
export function getGraphQLClient(): GraphQLClient {
|
|
43
|
+
return new GraphQLClient(GRAPHQL_URL, { headers: buildHeaders() })
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* No-op — kept for backward compatibility.
|
|
48
|
+
* Clients are now stateless (created fresh on each call), so there is no
|
|
49
|
+
* cached instance to reset.
|
|
50
|
+
*/
|
|
51
|
+
export function resetGraphQLClient(): void {
|
|
52
|
+
// No-op: clients are built fresh on every call.
|
|
53
|
+
}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP Client - Pre-configured Axios instances
|
|
3
|
+
*
|
|
4
|
+
* Each Axios instance is created with a default baseURL based on the static
|
|
5
|
+
* service URL. The baseURLs are complete URLs including the API version,
|
|
6
|
+
* selected based on the configured environment (development/production).
|
|
7
|
+
*/
|
|
8
|
+
import axios, { type AxiosInstance } from 'axios'
|
|
9
|
+
import { getRuntimeConfig, getBackendUrl, isInitialized } from '../config'
|
|
10
|
+
import { tokenStore, refreshToken } from './authClient'
|
|
11
|
+
|
|
12
|
+
// ── Guest session management ─────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
let _guestSessionId: string | null = null
|
|
15
|
+
|
|
16
|
+
export function getOrCreateGuestSessionId(): string {
|
|
17
|
+
if (_guestSessionId) return _guestSessionId
|
|
18
|
+
|
|
19
|
+
const storageKey = isInitialized()
|
|
20
|
+
? `kasuvia-guest-${getRuntimeConfig().businessId}`
|
|
21
|
+
: 'kasuvia-guest-anon'
|
|
22
|
+
|
|
23
|
+
if (typeof window !== 'undefined') {
|
|
24
|
+
_guestSessionId = sessionStorage.getItem(storageKey) || crypto.randomUUID()
|
|
25
|
+
sessionStorage.setItem(storageKey, _guestSessionId)
|
|
26
|
+
} else {
|
|
27
|
+
_guestSessionId = crypto.randomUUID()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return _guestSessionId
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function getGuestSessionId(): string | null {
|
|
34
|
+
if (!_guestSessionId && typeof window !== 'undefined') {
|
|
35
|
+
const storageKey = isInitialized()
|
|
36
|
+
? `kasuvia-guest-${getRuntimeConfig().businessId}`
|
|
37
|
+
: 'kasuvia-guest-anon'
|
|
38
|
+
_guestSessionId = sessionStorage.getItem(storageKey)
|
|
39
|
+
}
|
|
40
|
+
return _guestSessionId
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function clearGuestSessionId(): void {
|
|
44
|
+
_guestSessionId = null
|
|
45
|
+
if (typeof window !== 'undefined') {
|
|
46
|
+
const storageKey = isInitialized()
|
|
47
|
+
? `kasuvia-guest-${getRuntimeConfig().businessId}`
|
|
48
|
+
: 'kasuvia-guest-anon'
|
|
49
|
+
sessionStorage.removeItem(storageKey)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ── Store Management Axios ───────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
export const storeManagementAxios = axios.create({
|
|
56
|
+
baseURL: getBackendUrl('store'),
|
|
57
|
+
headers: {
|
|
58
|
+
'Content-Type': 'application/json',
|
|
59
|
+
Accept: 'application/json',
|
|
60
|
+
},
|
|
61
|
+
timeout: 30_000,
|
|
62
|
+
}) as AxiosInstance
|
|
63
|
+
|
|
64
|
+
storeManagementAxios.interceptors.request.use((cfg) => {
|
|
65
|
+
const config = getRuntimeConfig()
|
|
66
|
+
|
|
67
|
+
const token = tokenStore.getAccessToken()
|
|
68
|
+
if (token) {
|
|
69
|
+
cfg.headers = cfg.headers ?? {}
|
|
70
|
+
cfg.headers['Authorization'] = `Bearer ${token}`
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
cfg.headers = cfg.headers ?? {}
|
|
74
|
+
cfg.headers['X-Storefront-Key'] = config.storefrontApiKey
|
|
75
|
+
cfg.headers['X-Business-ID'] = config.businessId
|
|
76
|
+
|
|
77
|
+
if (!token) {
|
|
78
|
+
const sid = getOrCreateGuestSessionId()
|
|
79
|
+
if (sid) cfg.headers['X-Guest-Session'] = sid
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return cfg
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
storeManagementAxios.interceptors.response.use(
|
|
86
|
+
(res) => res,
|
|
87
|
+
async (err) => {
|
|
88
|
+
const originalRequest = err.config
|
|
89
|
+
if (!originalRequest) return Promise.reject(err)
|
|
90
|
+
|
|
91
|
+
const status = err.response?.status
|
|
92
|
+
const isUnauthorized = status === 401
|
|
93
|
+
const isRefreshEndpoint = originalRequest.url?.includes('/auth/token/refresh/')
|
|
94
|
+
const alreadyRetried = originalRequest._retry === true
|
|
95
|
+
|
|
96
|
+
if (isUnauthorized && isRefreshEndpoint) {
|
|
97
|
+
tokenStore.clearAll()
|
|
98
|
+
return Promise.reject(err)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!isUnauthorized || alreadyRetried) return Promise.reject(err)
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const newToken = await refreshToken()
|
|
105
|
+
originalRequest._retry = true
|
|
106
|
+
if (originalRequest.headers) {
|
|
107
|
+
originalRequest.headers['Authorization'] = `Bearer ${newToken}`
|
|
108
|
+
}
|
|
109
|
+
return storeManagementAxios(originalRequest)
|
|
110
|
+
} catch (refreshErr) {
|
|
111
|
+
tokenStore.clearAll()
|
|
112
|
+
return Promise.reject(refreshErr)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
// ── Public Store Axios ───────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
export const publicStoreAxios = axios.create({
|
|
120
|
+
baseURL: getBackendUrl('store'),
|
|
121
|
+
headers: {
|
|
122
|
+
'Content-Type': 'application/json',
|
|
123
|
+
Accept: 'application/json',
|
|
124
|
+
},
|
|
125
|
+
timeout: 30_000,
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
publicStoreAxios.interceptors.request.use((cfg) => {
|
|
129
|
+
const config = getRuntimeConfig()
|
|
130
|
+
cfg.headers = cfg.headers ?? {}
|
|
131
|
+
cfg.headers['X-Storefront-Key'] = config.storefrontApiKey
|
|
132
|
+
cfg.headers['X-Business-ID'] = config.businessId
|
|
133
|
+
return cfg
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
publicStoreAxios.interceptors.response.use(
|
|
137
|
+
(res) => res,
|
|
138
|
+
(err) => Promise.reject(err)
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
// ── Auth Axios ───────────────────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
export const authAxios = axios.create({
|
|
144
|
+
baseURL: getBackendUrl('auth'),
|
|
145
|
+
headers: {
|
|
146
|
+
'Content-Type': 'application/json',
|
|
147
|
+
Accept: 'application/json',
|
|
148
|
+
},
|
|
149
|
+
withCredentials: true,
|
|
150
|
+
timeout: 30_000,
|
|
151
|
+
}) as AxiosInstance
|
|
152
|
+
|
|
153
|
+
authAxios.interceptors.request.use((cfg) => {
|
|
154
|
+
const config = getRuntimeConfig()
|
|
155
|
+
|
|
156
|
+
const token = tokenStore.getAccessToken()
|
|
157
|
+
if (token) {
|
|
158
|
+
cfg.headers = cfg.headers ?? {}
|
|
159
|
+
cfg.headers['Authorization'] = `Bearer ${token}`
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
cfg.headers = cfg.headers ?? {}
|
|
163
|
+
cfg.headers['X-Storefront-Key'] = config.storefrontApiKey
|
|
164
|
+
cfg.headers['X-Business-ID'] = config.businessId
|
|
165
|
+
|
|
166
|
+
return cfg
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
authAxios.interceptors.response.use(
|
|
170
|
+
(res) => res,
|
|
171
|
+
async (err) => {
|
|
172
|
+
const originalRequest = err.config
|
|
173
|
+
if (!originalRequest) return Promise.reject(err)
|
|
174
|
+
|
|
175
|
+
const status = err.response?.status
|
|
176
|
+
const isUnauthorized = status === 401
|
|
177
|
+
const isRefreshEndpoint = originalRequest.url?.includes('/auth/token/refresh/')
|
|
178
|
+
const alreadyRetried = originalRequest._retry === true
|
|
179
|
+
|
|
180
|
+
if (isUnauthorized && isRefreshEndpoint) {
|
|
181
|
+
tokenStore.clearAll()
|
|
182
|
+
return Promise.reject(err)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (!isUnauthorized || alreadyRetried) return Promise.reject(err)
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
const newToken = await refreshToken()
|
|
189
|
+
originalRequest._retry = true
|
|
190
|
+
if (originalRequest.headers) {
|
|
191
|
+
originalRequest.headers['Authorization'] = `Bearer ${newToken}`
|
|
192
|
+
}
|
|
193
|
+
return authAxios(originalRequest)
|
|
194
|
+
} catch (refreshErr) {
|
|
195
|
+
tokenStore.clearAll()
|
|
196
|
+
return Promise.reject(refreshErr)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
// ── Payment Axios ────────────────────────────────────────────────────────────
|
|
202
|
+
|
|
203
|
+
export const paymentAxios = axios.create({
|
|
204
|
+
baseURL: getBackendUrl('payment'),
|
|
205
|
+
headers: {
|
|
206
|
+
'Content-Type': 'application/json',
|
|
207
|
+
Accept: 'application/json',
|
|
208
|
+
},
|
|
209
|
+
timeout: 30_000,
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
paymentAxios.interceptors.request.use((cfg) => {
|
|
213
|
+
const config = getRuntimeConfig()
|
|
214
|
+
cfg.headers = cfg.headers ?? {}
|
|
215
|
+
cfg.headers['X-Storefront-Key'] = config.storefrontApiKey
|
|
216
|
+
cfg.headers['X-Business-ID'] = config.businessId
|
|
217
|
+
return cfg
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
paymentAxios.interceptors.response.use(
|
|
221
|
+
(res) => res,
|
|
222
|
+
(err) => Promise.reject(err)
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
// ── Notifications Axios ──────────────────────────────────────────────────────
|
|
226
|
+
|
|
227
|
+
export const notificationsAxios = axios.create({
|
|
228
|
+
baseURL: getBackendUrl('notifications'),
|
|
229
|
+
headers: {
|
|
230
|
+
'Content-Type': 'application/json',
|
|
231
|
+
Accept: 'application/json',
|
|
232
|
+
},
|
|
233
|
+
timeout: 30_000,
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
notificationsAxios.interceptors.response.use(
|
|
237
|
+
(res) => res,
|
|
238
|
+
(err) => Promise.reject(err)
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
// ── CMS Axios ───────────────────────────────────────────────────────────────
|
|
242
|
+
|
|
243
|
+
export const cmsAxios = axios.create({
|
|
244
|
+
baseURL: getBackendUrl('cms'),
|
|
245
|
+
headers: {
|
|
246
|
+
'Content-Type': 'application/json',
|
|
247
|
+
Accept: 'application/json',
|
|
248
|
+
},
|
|
249
|
+
timeout: 30_000,
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
cmsAxios.interceptors.request.use((cfg) => {
|
|
253
|
+
const config = getRuntimeConfig()
|
|
254
|
+
cfg.headers = cfg.headers ?? {}
|
|
255
|
+
cfg.headers['X-Storefront-Key'] = config.storefrontApiKey
|
|
256
|
+
cfg.headers['X-Business-ID'] = config.businessId
|
|
257
|
+
return cfg
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
cmsAxios.interceptors.response.use(
|
|
261
|
+
(res) => res,
|
|
262
|
+
(err) => Promise.reject(err)
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
// ── Refresh Axios ────────────────────────────────────────────────────────────
|
|
266
|
+
|
|
267
|
+
export const refreshAxios = axios.create({
|
|
268
|
+
baseURL: getBackendUrl('auth'),
|
|
269
|
+
headers: {
|
|
270
|
+
'Content-Type': 'application/json',
|
|
271
|
+
Accept: 'application/json',
|
|
272
|
+
},
|
|
273
|
+
withCredentials: true,
|
|
274
|
+
timeout: 30_000,
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* configureHttpClients()
|
|
279
|
+
* Updates the baseURL of all Axios instances to match the configured environment.
|
|
280
|
+
* This must be called after initSdk() to ensure the correct environment URLs are used.
|
|
281
|
+
*/
|
|
282
|
+
export function configureHttpClients(): void {
|
|
283
|
+
if (!isInitialized()) return
|
|
284
|
+
|
|
285
|
+
const storeUrl = getBackendUrl('store')
|
|
286
|
+
const authUrl = getBackendUrl('auth')
|
|
287
|
+
const paymentUrl = getBackendUrl('payment')
|
|
288
|
+
const notificationsUrl = getBackendUrl('notifications')
|
|
289
|
+
const cmsUrl = getBackendUrl('cms')
|
|
290
|
+
|
|
291
|
+
storeManagementAxios.defaults.baseURL = storeUrl
|
|
292
|
+
publicStoreAxios.defaults.baseURL = storeUrl
|
|
293
|
+
authAxios.defaults.baseURL = authUrl
|
|
294
|
+
paymentAxios.defaults.baseURL = paymentUrl
|
|
295
|
+
notificationsAxios.defaults.baseURL = notificationsUrl
|
|
296
|
+
cmsAxios.defaults.baseURL = cmsUrl
|
|
297
|
+
refreshAxios.defaults.baseURL = authUrl
|
|
298
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kasuvia SDK Configuration
|
|
3
|
+
*
|
|
4
|
+
* This module manages SDK initialization and runtime configuration.
|
|
5
|
+
* The SDK is client-based and runs in the browser.
|
|
6
|
+
* All backend URLs are bundled with the SDK.
|
|
7
|
+
*
|
|
8
|
+
* SECURITY NOTE: The storefront API key has limited permissions - it only provides
|
|
9
|
+
* customer-based resources from a business owner to customers, not full platform access.
|
|
10
|
+
* This key is safe to expose to the browser as a NEXT_PUBLIC_ environment variable.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { logger, setEnvironmentMode, setLoggerEnvironmentMode } from "../utils"
|
|
14
|
+
|
|
15
|
+
export interface SdkConfig {
|
|
16
|
+
businessId: string
|
|
17
|
+
storefrontApiKey: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface SdkRuntimeConfig {
|
|
21
|
+
businessId: string
|
|
22
|
+
storefrontApiKey: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let runtimeConfig: SdkRuntimeConfig | null = null
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Backend URLs - bundled with the SDK
|
|
29
|
+
* These are the complete URLs (including API version) that the SDK uses internally.
|
|
30
|
+
* The storefront never sees or configures these URLs.
|
|
31
|
+
*
|
|
32
|
+
* Currently set to localhost development URLs.
|
|
33
|
+
* Update to production URLs when ready for deployment.
|
|
34
|
+
*/
|
|
35
|
+
const BACKEND_URLS = {
|
|
36
|
+
auth: 'https://127.0.0.1:8000/api/v1',
|
|
37
|
+
store: 'https://127.0.0.1:8001/api/v1',
|
|
38
|
+
payment: 'https://127.0.0.1:8002/api/v1',
|
|
39
|
+
notifications: 'https://127.0.0.1:8007/api/v1',
|
|
40
|
+
cms: 'https://127.0.0.1:8003/api/v1',
|
|
41
|
+
} as const
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Initialize the SDK with runtime credentials
|
|
45
|
+
* This must be called before any SDK operations
|
|
46
|
+
*
|
|
47
|
+
* IMPORTANT: In Next.js, this should be called in layout.tsx (server component)
|
|
48
|
+
* using private environment variables (kasuvia_BUSINESS_ID, kasuvia_STOREFRONT_API_KEY).
|
|
49
|
+
* The config is stored in module scope and will be available to client components
|
|
50
|
+
* through the SDK's bundled code.
|
|
51
|
+
*
|
|
52
|
+
* @param config - SDK configuration including credentials
|
|
53
|
+
*/
|
|
54
|
+
export function initSdk(config: SdkConfig): void {
|
|
55
|
+
runtimeConfig = {
|
|
56
|
+
businessId: config.businessId,
|
|
57
|
+
storefrontApiKey: config.storefrontApiKey,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Determine environment from NODE_ENV for logging/cache behavior
|
|
61
|
+
// In browser (window exists), default to production behavior
|
|
62
|
+
// In Node.js (window undefined), check NODE_ENV
|
|
63
|
+
const isDevelopment = typeof window === 'undefined' &&
|
|
64
|
+
(globalThis as any).process?.env?.NODE_ENV === 'development'
|
|
65
|
+
setEnvironmentMode(isDevelopment)
|
|
66
|
+
setLoggerEnvironmentMode(isDevelopment)
|
|
67
|
+
|
|
68
|
+
logger.log('SDK initialized with businessId:', config.businessId)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Get the current runtime configuration
|
|
73
|
+
* Throws if SDK is not initialized
|
|
74
|
+
*/
|
|
75
|
+
export function getRuntimeConfig(): SdkRuntimeConfig {
|
|
76
|
+
if (!runtimeConfig) {
|
|
77
|
+
throw new Error('SDK not initialized. Call initSdk() before using SDK functions.')
|
|
78
|
+
}
|
|
79
|
+
return runtimeConfig
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Get backend URL for a specific service
|
|
84
|
+
* Internal use only - never exposed to storefront
|
|
85
|
+
*/
|
|
86
|
+
export function getBackendUrl(service: 'auth' | 'store' | 'payment' | 'notifications' | 'cms'): string {
|
|
87
|
+
return BACKEND_URLS[service]
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Check if SDK is initialized
|
|
92
|
+
*/
|
|
93
|
+
export function isInitialized(): boolean {
|
|
94
|
+
return runtimeConfig !== null
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Reset SDK configuration (mainly for testing)
|
|
99
|
+
*/
|
|
100
|
+
export function resetConfig(): void {
|
|
101
|
+
runtimeConfig = null
|
|
102
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GraphQL queries for collections
|
|
3
|
+
*/
|
|
4
|
+
import { gql } from 'graphql-request'
|
|
5
|
+
|
|
6
|
+
export const GET_STOREFRONT_COLLECTIONS = gql`
|
|
7
|
+
query GetStorefrontCollections($page: Int = 1, $pageSize: Int = 20) {
|
|
8
|
+
storefront_collections(page: $page, pageSize: $pageSize) {
|
|
9
|
+
items {
|
|
10
|
+
id
|
|
11
|
+
business_id
|
|
12
|
+
name
|
|
13
|
+
slug
|
|
14
|
+
description
|
|
15
|
+
collection_type
|
|
16
|
+
rules
|
|
17
|
+
image {
|
|
18
|
+
id
|
|
19
|
+
url
|
|
20
|
+
alt
|
|
21
|
+
}
|
|
22
|
+
status
|
|
23
|
+
sort_order
|
|
24
|
+
created_at
|
|
25
|
+
updated_at
|
|
26
|
+
}
|
|
27
|
+
total
|
|
28
|
+
page
|
|
29
|
+
page_size
|
|
30
|
+
total_pages
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
`
|
|
34
|
+
|
|
35
|
+
export const GET_STOREFRONT_COLLECTION_BY_SLUG = gql`
|
|
36
|
+
query GetStorefrontCollectionBySlug($slug: String!) {
|
|
37
|
+
storefront_collection_by_slug(slug: $slug) {
|
|
38
|
+
id
|
|
39
|
+
business_id
|
|
40
|
+
name
|
|
41
|
+
slug
|
|
42
|
+
description
|
|
43
|
+
collection_type
|
|
44
|
+
rules
|
|
45
|
+
image {
|
|
46
|
+
id
|
|
47
|
+
url
|
|
48
|
+
alt
|
|
49
|
+
}
|
|
50
|
+
status
|
|
51
|
+
sort_order
|
|
52
|
+
created_at
|
|
53
|
+
updated_at
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
`
|