@codingfactory/socialkit-vue 0.1.0

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.
Files changed (56) hide show
  1. package/dist/composables/useAuth.d.ts +27 -0
  2. package/dist/composables/useAuth.d.ts.map +1 -0
  3. package/dist/composables/useAuth.js +137 -0
  4. package/dist/composables/useAuth.js.map +1 -0
  5. package/dist/index.d.ts +25 -0
  6. package/dist/index.d.ts.map +1 -0
  7. package/dist/index.js +16 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/plugins/terminology/index.d.ts +11 -0
  10. package/dist/plugins/terminology/index.d.ts.map +1 -0
  11. package/dist/plugins/terminology/index.js +91 -0
  12. package/dist/plugins/terminology/index.js.map +1 -0
  13. package/dist/plugins/terminology/terms.d.ts +15 -0
  14. package/dist/plugins/terminology/terms.d.ts.map +1 -0
  15. package/dist/plugins/terminology/terms.js +72 -0
  16. package/dist/plugins/terminology/terms.js.map +1 -0
  17. package/dist/plugins/terminology/types.d.ts +32 -0
  18. package/dist/plugins/terminology/types.d.ts.map +1 -0
  19. package/dist/plugins/terminology/types.js +2 -0
  20. package/dist/plugins/terminology/types.js.map +1 -0
  21. package/dist/services/api.d.ts +50 -0
  22. package/dist/services/api.d.ts.map +1 -0
  23. package/dist/services/api.js +305 -0
  24. package/dist/services/api.js.map +1 -0
  25. package/dist/services/auth.d.ts +127 -0
  26. package/dist/services/auth.d.ts.map +1 -0
  27. package/dist/services/auth.js +562 -0
  28. package/dist/services/auth.js.map +1 -0
  29. package/dist/stores/auth.d.ts +174 -0
  30. package/dist/stores/auth.d.ts.map +1 -0
  31. package/dist/stores/auth.js +262 -0
  32. package/dist/stores/auth.js.map +1 -0
  33. package/dist/types/api.d.ts +52 -0
  34. package/dist/types/api.d.ts.map +1 -0
  35. package/dist/types/api.js +7 -0
  36. package/dist/types/api.js.map +1 -0
  37. package/dist/types/user.d.ts +42 -0
  38. package/dist/types/user.d.ts.map +1 -0
  39. package/dist/types/user.js +45 -0
  40. package/dist/types/user.js.map +1 -0
  41. package/dist/utils/tokenStorage.d.ts +41 -0
  42. package/dist/utils/tokenStorage.d.ts.map +1 -0
  43. package/dist/utils/tokenStorage.js +300 -0
  44. package/dist/utils/tokenStorage.js.map +1 -0
  45. package/package.json +40 -0
  46. package/src/composables/useAuth.ts +164 -0
  47. package/src/index.ts +118 -0
  48. package/src/plugins/terminology/index.ts +114 -0
  49. package/src/plugins/terminology/terms.ts +104 -0
  50. package/src/plugins/terminology/types.ts +28 -0
  51. package/src/services/api.ts +472 -0
  52. package/src/services/auth.ts +874 -0
  53. package/src/stores/auth.ts +400 -0
  54. package/src/types/api.ts +56 -0
  55. package/src/types/user.ts +94 -0
  56. package/src/utils/tokenStorage.ts +394 -0
@@ -0,0 +1,394 @@
1
+ /**
2
+ * Token storage utility for SocialKit-powered frontends.
3
+ *
4
+ * Tokens are cached in memory and persisted to a configurable browser storage
5
+ * backend. Use `createTokenStorage()` to get a configured instance.
6
+ */
7
+
8
+ import axios from 'axios'
9
+
10
+ /** Configuration for creating a token storage instance. */
11
+ export interface TokenStorageConfig {
12
+ /** Storage key for the token (default: 'auth_token'). */
13
+ storageKey?: string
14
+ /** API base URL used by the refresh client (default: '/api'). */
15
+ apiBaseUrl?: string
16
+ /** Endpoints to call when refreshing a token (default: ['/v1/auth/refresh-token']). */
17
+ refreshEndpoints?: string[]
18
+ /** Endpoints where the Authorization header should NOT be sent. */
19
+ anonymousEndpoints?: string[]
20
+ /** When true, persist in localStorage and share across tabs; otherwise sessionStorage. */
21
+ shareSessions?: boolean
22
+ }
23
+
24
+ /** Public token storage interface. */
25
+ export interface TokenStorage {
26
+ getToken(): string | null
27
+ setToken(token: string): void
28
+ removeToken(): void
29
+ hasToken(): boolean
30
+ shouldSkipAuth(url: string | undefined): boolean
31
+ isRefreshTokenEndpoint(url: string | undefined): boolean
32
+ tryRefreshToken(): Promise<string | null>
33
+ wasTokenRotatedSince(requestAuthHeaderOrHeaders: unknown): boolean
34
+ }
35
+
36
+ const DEFAULT_STORAGE_KEY = 'auth_token'
37
+ const DEFAULT_REFRESH_ENDPOINTS = ['/v1/auth/refresh-token']
38
+ const DEFAULT_ANONYMOUS_ENDPOINTS = [
39
+ '/sanctum/csrf-cookie',
40
+ '/v1/auth/login',
41
+ '/v1/auth/register',
42
+ '/v1/auth/forgot-password',
43
+ '/v1/auth/reset-password'
44
+ ]
45
+
46
+ interface StoragePair {
47
+ primary: Storage | null
48
+ secondary: Storage | null
49
+ }
50
+
51
+ function normalizeStoredToken(stored: string | null): string | undefined {
52
+ if (typeof stored === 'string' && stored.length > 0) {
53
+ return stored
54
+ }
55
+
56
+ return undefined
57
+ }
58
+
59
+ function getBrowserStorage(kind: 'local' | 'session'): Storage | null {
60
+ if (typeof window === 'undefined') {
61
+ return null
62
+ }
63
+
64
+ try {
65
+ return kind === 'local' ? window.localStorage : window.sessionStorage
66
+ } catch {
67
+ return null
68
+ }
69
+ }
70
+
71
+ function resolveStoragePair(shareSessions: boolean): StoragePair {
72
+ if (shareSessions) {
73
+ return {
74
+ primary: getBrowserStorage('local'),
75
+ secondary: getBrowserStorage('session')
76
+ }
77
+ }
78
+
79
+ return {
80
+ primary: getBrowserStorage('session'),
81
+ secondary: getBrowserStorage('local')
82
+ }
83
+ }
84
+
85
+ function readStorageToken(storage: Storage | null, storageKey: string): string | undefined {
86
+ if (storage === null) {
87
+ return undefined
88
+ }
89
+
90
+ try {
91
+ const stored = storage.getItem(storageKey)
92
+ return normalizeStoredToken(stored)
93
+ } catch {
94
+ return undefined
95
+ }
96
+ }
97
+
98
+ function writeStorageToken(storage: Storage | null, storageKey: string, token: string): void {
99
+ if (storage === null) {
100
+ return
101
+ }
102
+
103
+ try {
104
+ storage.setItem(storageKey, token)
105
+ } catch {
106
+ // Storage full or unavailable — token still works for this session via memory.
107
+ }
108
+ }
109
+
110
+ function removeStorageToken(storage: Storage | null, storageKey: string): void {
111
+ if (storage === null) {
112
+ return
113
+ }
114
+
115
+ try {
116
+ storage.removeItem(storageKey)
117
+ } catch {
118
+ // Storage unavailable — ignore.
119
+ }
120
+ }
121
+
122
+ function isObjectRecord(value: unknown): value is Record<string, unknown> {
123
+ return typeof value === 'object' && value !== null
124
+ }
125
+
126
+ function extractAuthorizationHeaderValue(requestAuthHeaderOrHeaders: unknown): string | null {
127
+ if (typeof requestAuthHeaderOrHeaders === 'string') {
128
+ return requestAuthHeaderOrHeaders
129
+ }
130
+
131
+ if (!isObjectRecord(requestAuthHeaderOrHeaders)) {
132
+ return null
133
+ }
134
+
135
+ const maybeGet = requestAuthHeaderOrHeaders.get
136
+ if (typeof maybeGet === 'function') {
137
+ const uppercaseValue = maybeGet.call(requestAuthHeaderOrHeaders, 'Authorization')
138
+ if (typeof uppercaseValue === 'string' && uppercaseValue.length > 0) {
139
+ return uppercaseValue
140
+ }
141
+
142
+ const lowercaseValue = maybeGet.call(requestAuthHeaderOrHeaders, 'authorization')
143
+ if (typeof lowercaseValue === 'string' && lowercaseValue.length > 0) {
144
+ return lowercaseValue
145
+ }
146
+ }
147
+
148
+ const directUppercase = requestAuthHeaderOrHeaders.Authorization
149
+ if (typeof directUppercase === 'string' && directUppercase.length > 0) {
150
+ return directUppercase
151
+ }
152
+
153
+ const directLowercase = requestAuthHeaderOrHeaders.authorization
154
+ if (typeof directLowercase === 'string' && directLowercase.length > 0) {
155
+ return directLowercase
156
+ }
157
+
158
+ for (const [key, value] of Object.entries(requestAuthHeaderOrHeaders)) {
159
+ if (key.toLowerCase() !== 'authorization') {
160
+ continue
161
+ }
162
+
163
+ if (typeof value === 'string' && value.length > 0) {
164
+ return value
165
+ }
166
+ }
167
+
168
+ return null
169
+ }
170
+
171
+ /**
172
+ * Extract a token string from various API response shapes.
173
+ *
174
+ * Handles:
175
+ * - `{ token: string }`
176
+ * - `{ data: { token: string } }`
177
+ */
178
+ export function extractTokenFromResponse(payload: unknown): string | undefined {
179
+ if (!payload || typeof payload !== 'object') {
180
+ return undefined
181
+ }
182
+
183
+ if ('token' in payload) {
184
+ const token = (payload as { token?: unknown }).token
185
+ if (typeof token === 'string' && token.length > 0) {
186
+ return token
187
+ }
188
+ }
189
+
190
+ if ('data' in payload) {
191
+ return extractTokenFromResponse((payload as { data?: unknown }).data)
192
+ }
193
+
194
+ return undefined
195
+ }
196
+
197
+ /** Create a configured token storage instance. */
198
+ export function createTokenStorage(config: TokenStorageConfig = {}): TokenStorage {
199
+ const storageKey = config.storageKey ?? DEFAULT_STORAGE_KEY
200
+ const refreshEndpoints = config.refreshEndpoints ?? DEFAULT_REFRESH_ENDPOINTS
201
+ const anonymousEndpoints = config.anonymousEndpoints ?? DEFAULT_ANONYMOUS_ENDPOINTS
202
+ const apiBaseUrl = config.apiBaseUrl ?? '/api'
203
+ const shareSessions = config.shareSessions ?? false
204
+ const storagePair = resolveStoragePair(shareSessions)
205
+
206
+ let inMemoryToken: string | undefined
207
+ let refreshPromise: Promise<string | null> | null = null
208
+
209
+ function readPersistedToken(): string | undefined {
210
+ const primaryToken = readStorageToken(storagePair.primary, storageKey)
211
+ const secondaryToken = readStorageToken(storagePair.secondary, storageKey)
212
+
213
+ if (secondaryToken !== undefined) {
214
+ if (primaryToken === undefined) {
215
+ writeStorageToken(storagePair.primary, storageKey, secondaryToken)
216
+ }
217
+
218
+ removeStorageToken(storagePair.secondary, storageKey)
219
+ }
220
+
221
+ return primaryToken ?? secondaryToken
222
+ }
223
+
224
+ function persistToken(token: string): void {
225
+ writeStorageToken(storagePair.primary, storageKey, token)
226
+ removeStorageToken(storagePair.secondary, storageKey)
227
+ }
228
+
229
+ function clearPersistedTokens(): void {
230
+ removeStorageToken(getBrowserStorage('local'), storageKey)
231
+ removeStorageToken(getBrowserStorage('session'), storageKey)
232
+ }
233
+
234
+ function syncInMemoryTokenFromStorage(): void {
235
+ inMemoryToken = readPersistedToken()
236
+ }
237
+
238
+ inMemoryToken = readPersistedToken()
239
+
240
+ if (typeof window !== 'undefined') {
241
+ window.addEventListener('storage', (event: StorageEvent) => {
242
+ const localStorageRef = getBrowserStorage('local')
243
+ const sessionStorageRef = getBrowserStorage('session')
244
+
245
+ if (
246
+ event.storageArea &&
247
+ event.storageArea !== localStorageRef &&
248
+ event.storageArea !== sessionStorageRef
249
+ ) {
250
+ return
251
+ }
252
+
253
+ if (event.key === storageKey) {
254
+ inMemoryToken = normalizeStoredToken(event.newValue)
255
+
256
+ if (inMemoryToken === undefined) {
257
+ clearPersistedTokens()
258
+ }
259
+
260
+ return
261
+ }
262
+
263
+ if (event.key !== null) {
264
+ return
265
+ }
266
+
267
+ syncInMemoryTokenFromStorage()
268
+ })
269
+ }
270
+
271
+ const refreshClient = axios.create({
272
+ baseURL: apiBaseUrl,
273
+ timeout: 30000,
274
+ headers: {
275
+ 'Content-Type': 'application/json',
276
+ Accept: 'application/json'
277
+ },
278
+ withCredentials: false
279
+ })
280
+
281
+ function getToken(): string | null {
282
+ if (inMemoryToken !== undefined) {
283
+ return inMemoryToken
284
+ }
285
+
286
+ const persistedToken = readPersistedToken()
287
+ if (persistedToken !== undefined) {
288
+ inMemoryToken = persistedToken
289
+ return persistedToken
290
+ }
291
+
292
+ return null
293
+ }
294
+
295
+ function setToken(token: string): void {
296
+ inMemoryToken = token
297
+ persistToken(token)
298
+ }
299
+
300
+ function removeToken(): void {
301
+ inMemoryToken = undefined
302
+ clearPersistedTokens()
303
+ }
304
+
305
+ function hasToken(): boolean {
306
+ return getToken() !== null
307
+ }
308
+
309
+ function shouldSkipAuth(url: string | undefined): boolean {
310
+ if (!url) {
311
+ return false
312
+ }
313
+
314
+ return anonymousEndpoints.some((endpoint) => url.includes(endpoint))
315
+ }
316
+
317
+ function isRefreshTokenEndpoint(url: string | undefined): boolean {
318
+ if (!url) {
319
+ return false
320
+ }
321
+
322
+ return refreshEndpoints.some((endpoint) => url.includes(endpoint))
323
+ }
324
+
325
+ async function requestTokenRefresh(): Promise<string | null> {
326
+ const existingToken = getToken()
327
+ if (!existingToken) {
328
+ return null
329
+ }
330
+
331
+ for (const endpoint of refreshEndpoints) {
332
+ try {
333
+ const response = await refreshClient.post(endpoint, {}, {
334
+ headers: {
335
+ Authorization: `Bearer ${existingToken}`
336
+ }
337
+ })
338
+
339
+ const refreshedToken = extractTokenFromResponse(response.data)
340
+ if (refreshedToken) {
341
+ setToken(refreshedToken)
342
+ return refreshedToken
343
+ }
344
+ } catch {
345
+ // Try the next known refresh endpoint.
346
+ }
347
+ }
348
+
349
+ const latestPersistedToken = readPersistedToken()
350
+ if (latestPersistedToken && latestPersistedToken !== existingToken) {
351
+ inMemoryToken = latestPersistedToken
352
+ return latestPersistedToken
353
+ }
354
+
355
+ return null
356
+ }
357
+
358
+ async function tryRefreshToken(): Promise<string | null> {
359
+ if (refreshPromise) {
360
+ return refreshPromise
361
+ }
362
+
363
+ refreshPromise = requestTokenRefresh()
364
+
365
+ try {
366
+ return await refreshPromise
367
+ } finally {
368
+ refreshPromise = null
369
+ }
370
+ }
371
+
372
+ function wasTokenRotatedSince(requestAuthHeaderOrHeaders: unknown): boolean {
373
+ const requestAuthHeader = extractAuthorizationHeaderValue(requestAuthHeaderOrHeaders)
374
+ if (requestAuthHeader === null || !requestAuthHeader.startsWith('Bearer ')) {
375
+ return false
376
+ }
377
+
378
+ const requestToken = requestAuthHeader.slice(7)
379
+ const currentToken = getToken()
380
+
381
+ return requestToken.length > 0 && currentToken !== null && currentToken !== requestToken
382
+ }
383
+
384
+ return {
385
+ getToken,
386
+ setToken,
387
+ removeToken,
388
+ hasToken,
389
+ shouldSkipAuth,
390
+ isRefreshTokenEndpoint,
391
+ tryRefreshToken,
392
+ wasTokenRotatedSince
393
+ }
394
+ }