@strands.gg/accui 2.1.5 → 2.1.7

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 (173) hide show
  1. package/dist/index.d.ts +1 -6
  2. package/dist/js/auth-components-BJhPF4dS.js +8195 -0
  3. package/dist/js/auth-components-BJhPF4dS.js.br +0 -0
  4. package/dist/js/auth-components-BJhPF4dS.js.gz +0 -0
  5. package/dist/js/auth-components-CXT-W3fG.js +1 -0
  6. package/dist/js/auth-components-CXT-W3fG.js.br +0 -0
  7. package/dist/js/auth-components-CXT-W3fG.js.gz +0 -0
  8. package/dist/js/composables-BqIRpDGP.js +1609 -0
  9. package/dist/js/composables-BqIRpDGP.js.br +0 -0
  10. package/dist/js/composables-BqIRpDGP.js.gz +0 -0
  11. package/dist/js/composables-BqLSXNon.js +1 -0
  12. package/dist/js/composables-BqLSXNon.js.br +0 -0
  13. package/dist/js/composables-BqLSXNon.js.gz +0 -0
  14. package/dist/js/icons-CQ3z-CUv.js +398 -0
  15. package/dist/js/icons-CQ3z-CUv.js.br +0 -0
  16. package/dist/js/icons-CQ3z-CUv.js.gz +0 -0
  17. package/dist/js/icons-MGGFfB13.js +1 -0
  18. package/dist/js/index-87OecKtg.js +1 -0
  19. package/dist/js/index-B113vWue.js +106 -0
  20. package/dist/js/nuxt/module-BA8ZjBpp.js +1 -0
  21. package/dist/{nuxt/module.es.js → js/nuxt/module-YfdoaPCb.js} +2 -0
  22. package/dist/{nuxt/runtime/composables/useAuthenticatedFetch.es.js → js/nuxt/runtime/composables/useAuthenticatedFetch-DToDCO6a.js} +1 -1
  23. package/dist/js/nuxt/runtime/composables/useAuthenticatedFetch-qYkd06Dm.js +1 -0
  24. package/dist/{nuxt/runtime/composables/useStrandsAuth.es.js → js/nuxt/runtime/composables/useStrandsAuth-8b_bomlH.js} +1 -1
  25. package/dist/js/nuxt/runtime/composables/useStrandsAuth-Dn15YEQw.js +1 -0
  26. package/dist/{nuxt/runtime/middleware/auth.global.es.js → js/nuxt/runtime/middleware/auth.global-_2xq-SvK.js} +1 -1
  27. package/dist/js/nuxt/runtime/middleware/auth.global-dZpk-3-I.js +1 -0
  28. package/dist/js/nuxt/runtime/plugin.client-D7S6ulTg.js +1 -0
  29. package/dist/{nuxt/runtime/plugin.client.es.js → js/nuxt/runtime/plugin.client-KK_pCEfA.js} +2 -4
  30. package/dist/js/nuxt/runtime/plugin.server-BpSAN5Gh.js +1 -0
  31. package/dist/{nuxt/runtime/plugin.server.es.js → js/nuxt/runtime/plugin.server-CGdeJOoz.js} +1 -1
  32. package/dist/js/nuxt/runtime/plugins/auth-interceptor.client-BQu0eR16.js +1 -0
  33. package/dist/{nuxt/runtime/plugins/auth-interceptor.client.es.js → js/nuxt/runtime/plugins/auth-interceptor.client-DIa4pC59.js} +1 -1
  34. package/dist/js/nuxt-DqhAWeo-.js +1 -0
  35. package/dist/{nuxt.es.js → js/nuxt-MUpTW0D9.js} +3 -3
  36. package/dist/{nuxt-v4/module.es.js → js/nuxt-v4/module-CCvkwSrU.js} +2 -0
  37. package/dist/js/nuxt-v4/module-CDtxGAn8.js +1 -0
  38. package/dist/{nuxt-v4/runtime/composables/useAuthenticatedFetch.es.js → js/nuxt-v4/runtime/composables/useAuthenticatedFetch-I0BKB43E.js} +1 -1
  39. package/dist/js/nuxt-v4/runtime/composables/useAuthenticatedFetch-fAO6ieYd.js +1 -0
  40. package/dist/{nuxt-v4/runtime/composables/useStrandsAuth.es.js → js/nuxt-v4/runtime/composables/useStrandsAuth-BZe8CTx0.js} +1 -1
  41. package/dist/js/nuxt-v4/runtime/composables/useStrandsAuth-DPGVMZ-e.js +1 -0
  42. package/dist/js/nuxt-v4/runtime/middleware/auth.global-B0pyByUu.js +1 -0
  43. package/dist/{nuxt-v4/runtime/middleware/auth.global.es.js → js/nuxt-v4/runtime/middleware/auth.global-C_saJaMU.js} +1 -1
  44. package/dist/{nuxt-v4/runtime/plugin.client.es.js → js/nuxt-v4/runtime/plugin.client-CxLmYQfu.js} +2 -4
  45. package/dist/js/nuxt-v4/runtime/plugin.client-DGO53i-e.js +1 -0
  46. package/dist/{nuxt-v4/runtime/plugin.server.es.js → js/nuxt-v4/runtime/plugin.server-BM6HYyiZ.js} +1 -1
  47. package/dist/js/nuxt-v4/runtime/plugin.server-D_dUMHYo.js +1 -0
  48. package/dist/js/nuxt-v4/runtime/plugins/auth-interceptor.client-CUNOTFBp.js +1 -0
  49. package/dist/{nuxt-v4/runtime/plugins/auth-interceptor.client.es.js → js/nuxt-v4/runtime/plugins/auth-interceptor.client-Qht8QSLW.js} +1 -1
  50. package/dist/{nuxt-v4.es.js → js/nuxt-v4-C5JN4uyM.js} +3 -3
  51. package/dist/js/nuxt-v4-DuwGUuhX.js +1 -0
  52. package/dist/js/ui-components-BC1UK39i.js +5204 -0
  53. package/dist/js/ui-components-BC1UK39i.js.br +0 -0
  54. package/dist/js/ui-components-BC1UK39i.js.gz +0 -0
  55. package/dist/js/ui-components-CocLXkUI.js +1 -0
  56. package/dist/js/ui-components-CocLXkUI.js.br +0 -0
  57. package/dist/js/ui-components-CocLXkUI.js.gz +0 -0
  58. package/dist/js/utils-B9zcSW52.js +1 -0
  59. package/dist/js/utils-B9zcSW52.js.br +0 -0
  60. package/dist/js/utils-B9zcSW52.js.gz +0 -0
  61. package/dist/js/utils-BZHeJkZf.js +1080 -0
  62. package/dist/js/utils-BZHeJkZf.js.br +0 -0
  63. package/dist/js/utils-BZHeJkZf.js.gz +0 -0
  64. package/dist/nuxt/module.d.ts +1 -5
  65. package/dist/nuxt/runtime/composables/useAuthenticatedFetch.d.ts +1 -20
  66. package/dist/nuxt/runtime/composables/useStrandsAuth.d.ts +1 -142
  67. package/dist/nuxt/runtime/middleware/auth.global.d.ts +1 -3
  68. package/dist/nuxt/runtime/plugin.client.d.ts +1 -3
  69. package/dist/nuxt/runtime/plugin.server.d.ts +1 -3
  70. package/dist/nuxt/runtime/plugins/auth-interceptor.client.d.ts +1 -3
  71. package/dist/nuxt-v4/module.d.ts +1 -5
  72. package/dist/nuxt-v4/runtime/composables/useAuthenticatedFetch.d.ts +1 -20
  73. package/dist/nuxt-v4/runtime/composables/useStrandsAuth.d.ts +1 -27
  74. package/dist/nuxt-v4/runtime/middleware/auth.global.d.ts +1 -3
  75. package/dist/nuxt-v4/runtime/plugin.client.d.ts +1 -3
  76. package/dist/nuxt-v4/runtime/plugin.server.d.ts +1 -3
  77. package/dist/nuxt-v4/runtime/plugins/auth-interceptor.client.d.ts +1 -3
  78. package/dist/nuxt-v4.d.ts +1 -4
  79. package/dist/nuxt.d.ts +1 -4
  80. package/dist/robots.txt +4 -0
  81. package/dist/sitemap.xml +9 -0
  82. package/dist/styles/accui-DF7zRGFD.css +1 -0
  83. package/dist/styles/accui-DF7zRGFD.css.br +0 -0
  84. package/dist/styles/accui-DF7zRGFD.css.gz +0 -0
  85. package/package.json +5 -1
  86. package/dist/accui.css +0 -1
  87. package/dist/nuxt/module.cjs.js +0 -1
  88. package/dist/nuxt/runtime/composables/useAuthenticatedFetch.cjs.js +0 -1
  89. package/dist/nuxt/runtime/composables/useStrandsAuth.cjs.js +0 -1
  90. package/dist/nuxt/runtime/middleware/auth.d.ts +0 -7
  91. package/dist/nuxt/runtime/middleware/auth.global.cjs.js +0 -1
  92. package/dist/nuxt/runtime/middleware/guest.d.ts +0 -7
  93. package/dist/nuxt/runtime/plugin.client.cjs.js +0 -1
  94. package/dist/nuxt/runtime/plugin.server.cjs.js +0 -1
  95. package/dist/nuxt/runtime/plugins/auth-interceptor.client.cjs.js +0 -1
  96. package/dist/nuxt/types.d.ts +0 -45
  97. package/dist/nuxt-v4/module.cjs.js +0 -1
  98. package/dist/nuxt-v4/runtime/composables/useAuthenticatedFetch.cjs.js +0 -1
  99. package/dist/nuxt-v4/runtime/composables/useStrandsAuth.cjs.js +0 -1
  100. package/dist/nuxt-v4/runtime/middleware/auth.global.cjs.js +0 -1
  101. package/dist/nuxt-v4/runtime/plugin.client.cjs.js +0 -1
  102. package/dist/nuxt-v4/runtime/plugin.server.cjs.js +0 -1
  103. package/dist/nuxt-v4/runtime/plugins/auth-interceptor.client.cjs.js +0 -1
  104. package/dist/nuxt-v4/types.d.ts +0 -63
  105. package/dist/nuxt-v4.cjs.js +0 -1
  106. package/dist/nuxt.cjs.js +0 -1
  107. package/dist/shared/defaults.d.ts +0 -2
  108. package/dist/strands-auth-ui.cjs.js +0 -1
  109. package/dist/strands-auth-ui.es.js +0 -10561
  110. package/dist/types/index.d.ts +0 -236
  111. package/dist/useStrandsAuth-CTlaiFqK.cjs +0 -1
  112. package/dist/useStrandsAuth-Cev-PTun.js +0 -899
  113. package/dist/useStrandsConfig-Cxb360Os.js +0 -179
  114. package/dist/useStrandsConfig-Z9_36OcV.cjs +0 -1
  115. package/dist/utils/index.d.ts +0 -1
  116. package/dist/utils/slots.d.ts +0 -1
  117. package/dist/utils/validation.d.ts +0 -12
  118. package/dist/vue/components/SignedIn.vue.d.ts +0 -54
  119. package/dist/vue/components/SignedOut.vue.d.ts +0 -54
  120. package/dist/vue/components/StrandsAuth.vue.d.ts +0 -25
  121. package/dist/vue/components/StrandsBackupCodesModal.vue.d.ts +0 -12
  122. package/dist/vue/components/StrandsCompleteSignUp.vue.d.ts +0 -21
  123. package/dist/vue/components/StrandsConfigProvider.vue.d.ts +0 -22
  124. package/dist/vue/components/StrandsConfirmModal.vue.d.ts +0 -22
  125. package/dist/vue/components/StrandsEmailMfaSetupModal.vue.d.ts +0 -12
  126. package/dist/vue/components/StrandsHardwareKeySetupModal.vue.d.ts +0 -15
  127. package/dist/vue/components/StrandsLogo.vue.d.ts +0 -8
  128. package/dist/vue/components/StrandsMFASetup.vue.d.ts +0 -16
  129. package/dist/vue/components/StrandsMfaModal.vue.d.ts +0 -12
  130. package/dist/vue/components/StrandsMfaVerification.vue.d.ts +0 -17
  131. package/dist/vue/components/StrandsPasswordReset.vue.d.ts +0 -15
  132. package/dist/vue/components/StrandsSecuredFooter.vue.d.ts +0 -22
  133. package/dist/vue/components/StrandsSessionsModal.vue.d.ts +0 -14
  134. package/dist/vue/components/StrandsSettingsModal.vue.d.ts +0 -18
  135. package/dist/vue/components/StrandsSignIn.vue.d.ts +0 -21
  136. package/dist/vue/components/StrandsSignUp.vue.d.ts +0 -19
  137. package/dist/vue/components/StrandsTotpSetupModal.vue.d.ts +0 -12
  138. package/dist/vue/components/StrandsUserButton.vue.d.ts +0 -30
  139. package/dist/vue/components/StrandsUserProfile.vue.d.ts +0 -26
  140. package/dist/vue/components/SvgIcon.vue.d.ts +0 -17
  141. package/dist/vue/components/VirtualList.vue.d.ts +0 -37
  142. package/dist/vue/components/icons/IconGithub.vue.d.ts +0 -3
  143. package/dist/vue/components/icons/IconGoogle.vue.d.ts +0 -3
  144. package/dist/vue/components/icons/index.d.ts +0 -2
  145. package/dist/vue/components/index.d.ts +0 -26
  146. package/dist/vue/composables/useAuthenticatedFetch.d.ts +0 -20
  147. package/dist/vue/composables/useOAuthProviders.d.ts +0 -74
  148. package/dist/vue/composables/useStrandsAuth.d.ts +0 -130
  149. package/dist/vue/composables/useStrandsConfig.d.ts +0 -11
  150. package/dist/vue/composables/useStrandsMfa.d.ts +0 -38
  151. package/dist/vue/index.d.ts +0 -12
  152. package/dist/vue/plugins/StrandsUIPlugin.d.ts +0 -19
  153. package/dist/vue/ui/UiAlert.vue.d.ts +0 -31
  154. package/dist/vue/ui/UiAvatarEditor.vue.d.ts +0 -25
  155. package/dist/vue/ui/UiButton.vue.d.ts +0 -54
  156. package/dist/vue/ui/UiCard.vue.d.ts +0 -29
  157. package/dist/vue/ui/UiInput.vue.d.ts +0 -48
  158. package/dist/vue/ui/UiLevelProgress.vue.d.ts +0 -19
  159. package/dist/vue/ui/UiLink.vue.d.ts +0 -42
  160. package/dist/vue/ui/UiLoader.vue.d.ts +0 -15
  161. package/dist/vue/ui/UiModal.vue.d.ts +0 -33
  162. package/dist/vue/ui/UiTabs.vue.d.ts +0 -17
  163. package/dist/vue/ui/UiToggle.vue.d.ts +0 -15
  164. package/dist/vue/ui/index.d.ts +0 -29
  165. package/dist/vue/utils/contrast.d.ts +0 -79
  166. package/dist/vue/utils/debounce.d.ts +0 -12
  167. package/dist/vue/utils/fontPreloader.d.ts +0 -19
  168. package/dist/vue/utils/iconProps.d.ts +0 -9
  169. package/dist/vue/utils/lazyComponents.d.ts +0 -2
  170. package/dist/vue/utils/levels.d.ts +0 -27
  171. package/dist/vue/utils/performanceInit.d.ts +0 -40
  172. package/dist/vue/utils/requestCache.d.ts +0 -49
  173. package/dist/vue/utils/sounds.d.ts +0 -56
@@ -0,0 +1,1609 @@
1
+ import { computed, ref, onMounted, watch, inject, provide, onUnmounted } from "vue";
2
+ import { u as useRequestCache, d as debouncedSetItem } from "./utils-BZHeJkZf.js";
3
+ const currentTheme = ref("system");
4
+ const systemPrefersDark = ref(false);
5
+ const STORAGE_KEY = "strands-ui-theme";
6
+ function getSystemPreference() {
7
+ if (typeof window === "undefined") return false;
8
+ return window.matchMedia("(prefers-color-scheme: dark)").matches;
9
+ }
10
+ function getStoredTheme() {
11
+ if (typeof window === "undefined") return "system";
12
+ try {
13
+ const stored = localStorage.getItem(STORAGE_KEY);
14
+ if (stored && ["light", "dark", "system"].includes(stored)) {
15
+ return stored;
16
+ }
17
+ } catch (error) {
18
+ console.warn("Failed to read theme preference from localStorage:", error);
19
+ }
20
+ return "system";
21
+ }
22
+ function setStoredTheme(theme) {
23
+ if (typeof window === "undefined") return;
24
+ try {
25
+ localStorage.setItem(STORAGE_KEY, theme);
26
+ } catch (error) {
27
+ console.warn("Failed to save theme preference to localStorage:", error);
28
+ }
29
+ }
30
+ function applyTheme(isDark) {
31
+ if (typeof document === "undefined") return;
32
+ if (isDark) {
33
+ document.documentElement.setAttribute("data-theme", "dark");
34
+ document.documentElement.classList.add("dark");
35
+ } else {
36
+ document.documentElement.setAttribute("data-theme", "light");
37
+ document.documentElement.classList.remove("dark");
38
+ }
39
+ }
40
+ function useDarkMode() {
41
+ const isDark = computed(() => {
42
+ if (currentTheme.value === "dark") return true;
43
+ if (currentTheme.value === "light") return false;
44
+ return systemPrefersDark.value;
45
+ });
46
+ const themeLabel = computed(() => {
47
+ switch (currentTheme.value) {
48
+ case "light":
49
+ return "Light";
50
+ case "dark":
51
+ return "Dark";
52
+ case "system":
53
+ return "System";
54
+ default:
55
+ return "System";
56
+ }
57
+ });
58
+ function setTheme(theme) {
59
+ currentTheme.value = theme;
60
+ setStoredTheme(theme);
61
+ }
62
+ function toggle() {
63
+ const newTheme = isDark.value ? "light" : "dark";
64
+ setTheme(newTheme);
65
+ }
66
+ function cycleTheme() {
67
+ switch (currentTheme.value) {
68
+ case "light":
69
+ setTheme("dark");
70
+ break;
71
+ case "dark":
72
+ setTheme("system");
73
+ break;
74
+ case "system":
75
+ setTheme("light");
76
+ break;
77
+ default:
78
+ setTheme("light");
79
+ break;
80
+ }
81
+ }
82
+ function initialize() {
83
+ if (typeof window === "undefined") return;
84
+ systemPrefersDark.value = getSystemPreference();
85
+ const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
86
+ const handleSystemThemeChange = (e) => {
87
+ systemPrefersDark.value = e.matches;
88
+ };
89
+ if (mediaQuery.addEventListener) {
90
+ mediaQuery.addEventListener("change", handleSystemThemeChange);
91
+ } else {
92
+ mediaQuery.addListener(handleSystemThemeChange);
93
+ }
94
+ currentTheme.value = getStoredTheme();
95
+ applyTheme(isDark.value);
96
+ watch(isDark, (newIsDark) => {
97
+ applyTheme(newIsDark);
98
+ }, { immediate: false });
99
+ }
100
+ onMounted(() => {
101
+ initialize();
102
+ });
103
+ return {
104
+ // State
105
+ currentTheme: computed(() => currentTheme.value),
106
+ isDark,
107
+ themeLabel,
108
+ // Actions
109
+ setTheme,
110
+ toggle,
111
+ cycleTheme,
112
+ initialize,
113
+ // Theme options for UI
114
+ themeOptions: [
115
+ { value: "light", label: "Light", icon: "sun" },
116
+ { value: "dark", label: "Dark", icon: "moon" },
117
+ { value: "system", label: "System", icon: "monitor" }
118
+ ]
119
+ };
120
+ }
121
+ let globalInstance = null;
122
+ function useGlobalDarkMode() {
123
+ if (!globalInstance) {
124
+ globalInstance = useDarkMode();
125
+ if (typeof window !== "undefined") {
126
+ globalInstance.initialize();
127
+ }
128
+ }
129
+ return globalInstance;
130
+ }
131
+ const STRANDS_AUTH_DEFAULTS = {
132
+ baseUrl: "https://your-api.example.com",
133
+ accentColor: "#EA00A8",
134
+ redirectUrl: "/",
135
+ onSignInUrl: "/dashboard",
136
+ onSignOutUrl: "/",
137
+ autoRefresh: true,
138
+ refreshInterval: 4,
139
+ protectedRoutes: [],
140
+ guestOnlyRoutes: ["/auth", "/login", "/register"],
141
+ devMode: false,
142
+ styles: true,
143
+ endpoints: {},
144
+ useSquircle: true
145
+ };
146
+ const DEFAULT_ENDPOINTS = {
147
+ signIn: "/api/v1/auth/sign-in",
148
+ signUp: "/api/v1/auth/sign-up",
149
+ signOut: "/api/v1/auth/sign-out",
150
+ refresh: "/api/v1/auth/refresh",
151
+ passwordReset: "/api/v1/auth/password-reset",
152
+ passwordResetConfirm: "/api/v1/auth/password-reset/confirm",
153
+ completeRegistration: "/api/v1/auth/complete-registration",
154
+ profile: "/api/v1/user/profile",
155
+ verifyEmail: "/api/v1/auth/verify-email",
156
+ oauthProviders: "/api/v1/oauth/providers",
157
+ oauthProvider: "/api/v1/oauth/providers/{provider_id}",
158
+ changeEmail: "/api/v1/user/change-email",
159
+ avatar: "/api/v1/user/avatar",
160
+ settings: "/api/v1/user/settings",
161
+ // Username endpoints
162
+ changeUsername: "/api/v1/user/username",
163
+ usernameCooldown: "/api/v1/user/username/cooldown",
164
+ checkUsernameAvailability: "/api/v1/username/{username}/available",
165
+ // MFA endpoints
166
+ mfaDevices: "/api/v1/mfa/devices",
167
+ mfaTotpSetup: "/api/v1/mfa/totp/setup",
168
+ mfaTotpVerify: "/api/v1/mfa/totp/verify",
169
+ mfaEmailSetup: "/api/v1/mfa/email/setup",
170
+ mfaEmailSend: "/api/v1/mfa/email/send",
171
+ mfaEmailVerify: "/api/v1/mfa/email/verify",
172
+ mfaDeviceDisable: "/api/v1/mfa/device/disable",
173
+ mfaBackupCodes: "/api/v1/mfa/backup-codes/regenerate",
174
+ // Hardware key endpoints
175
+ mfaHardwareStartRegistration: "/api/v1/mfa/hardware/start-registration",
176
+ mfaHardwareCompleteRegistration: "/api/v1/mfa/hardware/complete-registration",
177
+ // MFA sign-in specific endpoints
178
+ mfaSigninSendEmail: "/api/v1/auth/mfa/email/send",
179
+ mfaSigninVerify: "/api/v1/auth/mfa/verify",
180
+ mfaBackupCodeVerify: "/api/v1/auth/mfa/backup-code/verify",
181
+ mfaWebAuthnChallenge: "/api/v1/auth/mfa/webauthn/challenge",
182
+ // Session management endpoints
183
+ sessions: "/api/v1/sessions",
184
+ sessionsStats: "/api/v1/sessions/stats",
185
+ sessionRevoke: "/api/v1/sessions/{session_id}/revoke",
186
+ sessionsRevokeAll: "/api/v1/sessions/revoke-all"
187
+ };
188
+ const STRANDS_CONFIG_KEY = Symbol("strands-config");
189
+ const globalConfig = ref(null);
190
+ function generateColorShades(accentColor) {
191
+ if (typeof window === "undefined" || !document.documentElement) return;
192
+ document.documentElement.style.setProperty("--strands-accent", accentColor);
193
+ document.documentElement.style.setProperty("--accui-strands-accent", accentColor);
194
+ document.documentElement.style.setProperty("--accui-strands-50", `color-mix(in srgb, ${accentColor} 10%, white)`);
195
+ document.documentElement.style.setProperty("--accui-strands-100", `color-mix(in srgb, ${accentColor} 20%, white)`);
196
+ document.documentElement.style.setProperty("--accui-strands-200", `color-mix(in srgb, ${accentColor} 30%, white)`);
197
+ document.documentElement.style.setProperty("--accui-strands-300", `color-mix(in srgb, ${accentColor} 40%, white)`);
198
+ document.documentElement.style.setProperty("--accui-strands-400", `color-mix(in srgb, ${accentColor} 70%, white)`);
199
+ document.documentElement.style.setProperty("--accui-strands-500", accentColor);
200
+ document.documentElement.style.setProperty("--accui-strands-600", `color-mix(in srgb, ${accentColor} 85%, black)`);
201
+ document.documentElement.style.setProperty("--accui-strands-700", `color-mix(in srgb, ${accentColor} 70%, black)`);
202
+ document.documentElement.style.setProperty("--accui-strands-800", `color-mix(in srgb, ${accentColor} 55%, black)`);
203
+ document.documentElement.style.setProperty("--accui-strands-900", `color-mix(in srgb, ${accentColor} 40%, black)`);
204
+ document.documentElement.style.setProperty("--accui-strands-950", `color-mix(in srgb, ${accentColor} 25%, black)`);
205
+ }
206
+ function provideStrandsConfig(config) {
207
+ globalConfig.value = config;
208
+ if (typeof window !== "undefined" && document.documentElement) {
209
+ const useSquircle = config.useSquircle !== void 0 ? config.useSquircle : true;
210
+ document.documentElement.style.setProperty("--strands-allow-squircle", useSquircle ? "1" : "0");
211
+ if (config.accentColor) {
212
+ generateColorShades(config.accentColor);
213
+ }
214
+ }
215
+ try {
216
+ provide(STRANDS_CONFIG_KEY, config);
217
+ } catch (error) {
218
+ console.warn("[Strands Auth] Could not provide config via Vue provide/inject. Config available via global state only.", error);
219
+ }
220
+ }
221
+ function useStrandsConfig(fallbackConfig) {
222
+ const injectedConfig = inject(STRANDS_CONFIG_KEY, null);
223
+ let nuxtConfig = null;
224
+ try {
225
+ if (typeof window !== "undefined") {
226
+ if (window.__STRANDS_CONFIG__) {
227
+ nuxtConfig = window.__STRANDS_CONFIG__;
228
+ } else if (window.__NUXT__) {
229
+ const nuxtData = window.__NUXT__;
230
+ nuxtConfig = nuxtData?.config?.public?.strandsAuth || nuxtData?.public?.strandsAuth || nuxtData?.strandsAuth;
231
+ }
232
+ }
233
+ } catch (error) {
234
+ console.error("[Strands Auth] Error accessing runtime configuration:", error);
235
+ }
236
+ const activeConfig = computed(() => {
237
+ const config = {
238
+ ...STRANDS_AUTH_DEFAULTS,
239
+ ...fallbackConfig || {},
240
+ ...injectedConfig || {},
241
+ ...globalConfig.value || {},
242
+ ...nuxtConfig || {}
243
+ };
244
+ if (config.baseUrl === STRANDS_AUTH_DEFAULTS.baseUrl && typeof window === "undefined") {
245
+ console.warn("[Strands Auth] No baseUrl configured for SSR. Please provide a baseUrl in your strandsAuth configuration.");
246
+ }
247
+ if (typeof window !== "undefined" && document.documentElement) {
248
+ const useSquircle = config.useSquircle !== void 0 ? config.useSquircle : true;
249
+ document.documentElement.style.setProperty("--strands-allow-squircle", useSquircle ? "1" : "0");
250
+ if (config.accentColor) {
251
+ document.documentElement.style.setProperty("--strands-accent", config.accentColor);
252
+ }
253
+ }
254
+ return config;
255
+ });
256
+ const endpoints = computed(() => {
257
+ const config = activeConfig.value;
258
+ const customEndpoints = config.endpoints || {};
259
+ return {
260
+ signIn: customEndpoints.signIn || DEFAULT_ENDPOINTS.signIn,
261
+ signUp: customEndpoints.signUp || DEFAULT_ENDPOINTS.signUp,
262
+ signOut: customEndpoints.signOut || DEFAULT_ENDPOINTS.signOut,
263
+ refresh: customEndpoints.refresh || DEFAULT_ENDPOINTS.refresh,
264
+ passwordReset: customEndpoints.passwordReset || DEFAULT_ENDPOINTS.passwordReset,
265
+ passwordResetConfirm: customEndpoints.passwordResetConfirm || DEFAULT_ENDPOINTS.passwordResetConfirm,
266
+ completeRegistration: customEndpoints.completeRegistration || DEFAULT_ENDPOINTS.completeRegistration,
267
+ profile: customEndpoints.profile || DEFAULT_ENDPOINTS.profile,
268
+ verifyEmail: customEndpoints.verifyEmail || DEFAULT_ENDPOINTS.verifyEmail,
269
+ oauthProviders: customEndpoints.oauthProviders || DEFAULT_ENDPOINTS.oauthProviders,
270
+ oauthProvider: customEndpoints.oauthProvider || DEFAULT_ENDPOINTS.oauthProvider,
271
+ changeEmail: customEndpoints.changeEmail || DEFAULT_ENDPOINTS.changeEmail,
272
+ avatar: customEndpoints.avatar || DEFAULT_ENDPOINTS.avatar,
273
+ settings: customEndpoints.settings || DEFAULT_ENDPOINTS.settings,
274
+ // Username endpoints
275
+ changeUsername: customEndpoints.changeUsername || DEFAULT_ENDPOINTS.changeUsername,
276
+ usernameCooldown: customEndpoints.usernameCooldown || DEFAULT_ENDPOINTS.usernameCooldown,
277
+ checkUsernameAvailability: customEndpoints.checkUsernameAvailability || DEFAULT_ENDPOINTS.checkUsernameAvailability,
278
+ // MFA endpoints
279
+ mfaDevices: customEndpoints.mfaDevices || DEFAULT_ENDPOINTS.mfaDevices,
280
+ mfaTotpSetup: customEndpoints.mfaTotpSetup || DEFAULT_ENDPOINTS.mfaTotpSetup,
281
+ mfaTotpVerify: customEndpoints.mfaTotpVerify || DEFAULT_ENDPOINTS.mfaTotpVerify,
282
+ mfaEmailSetup: customEndpoints.mfaEmailSetup || DEFAULT_ENDPOINTS.mfaEmailSetup,
283
+ mfaEmailSend: customEndpoints.mfaEmailSend || DEFAULT_ENDPOINTS.mfaEmailSend,
284
+ mfaEmailVerify: customEndpoints.mfaEmailVerify || DEFAULT_ENDPOINTS.mfaEmailVerify,
285
+ mfaDeviceDisable: customEndpoints.mfaDeviceDisable || DEFAULT_ENDPOINTS.mfaDeviceDisable,
286
+ mfaBackupCodes: customEndpoints.mfaBackupCodes || DEFAULT_ENDPOINTS.mfaBackupCodes,
287
+ // Hardware key endpoints
288
+ mfaHardwareStartRegistration: customEndpoints.mfaHardwareStartRegistration || DEFAULT_ENDPOINTS.mfaHardwareStartRegistration,
289
+ mfaHardwareCompleteRegistration: customEndpoints.mfaHardwareCompleteRegistration || DEFAULT_ENDPOINTS.mfaHardwareCompleteRegistration,
290
+ // MFA sign-in specific endpoints
291
+ mfaSigninSendEmail: customEndpoints.mfaSigninSendEmail || DEFAULT_ENDPOINTS.mfaSigninSendEmail,
292
+ mfaSigninVerify: customEndpoints.mfaSigninVerify || DEFAULT_ENDPOINTS.mfaSigninVerify,
293
+ mfaBackupCodeVerify: customEndpoints.mfaBackupCodeVerify || DEFAULT_ENDPOINTS.mfaBackupCodeVerify,
294
+ mfaWebAuthnChallenge: customEndpoints.mfaWebAuthnChallenge || DEFAULT_ENDPOINTS.mfaWebAuthnChallenge,
295
+ // Session management endpoints
296
+ sessions: customEndpoints.sessions || DEFAULT_ENDPOINTS.sessions,
297
+ sessionsStats: customEndpoints.sessionsStats || DEFAULT_ENDPOINTS.sessionsStats,
298
+ sessionRevoke: customEndpoints.sessionRevoke || DEFAULT_ENDPOINTS.sessionRevoke,
299
+ sessionsRevokeAll: customEndpoints.sessionsRevokeAll || DEFAULT_ENDPOINTS.sessionsRevokeAll
300
+ };
301
+ });
302
+ const getUrl = (endpoint) => {
303
+ const config = activeConfig.value;
304
+ if (!config.baseUrl) {
305
+ throw new Error("Base URL is required in configuration");
306
+ }
307
+ let endpointPath;
308
+ if (typeof endpoint === "string" && endpoint in endpoints.value) {
309
+ endpointPath = endpoints.value[endpoint];
310
+ } else if (typeof endpoint === "string") {
311
+ endpointPath = endpoint;
312
+ } else {
313
+ endpointPath = endpoints.value[endpoint];
314
+ }
315
+ const baseUrl = config.baseUrl.replace(/\/$/, "");
316
+ const path = endpointPath.startsWith("/") ? endpointPath : `/${endpointPath}`;
317
+ console.debug(`[Strands Auth] Constructing URL for endpoint "${endpoint}": ${baseUrl}${path}`);
318
+ return `${baseUrl}${path}`;
319
+ };
320
+ const getSupportEmail = () => {
321
+ const config = activeConfig.value;
322
+ return config.supportEmail || null;
323
+ };
324
+ return {
325
+ config: activeConfig,
326
+ endpoints,
327
+ getUrl,
328
+ getSupportEmail
329
+ };
330
+ }
331
+ function setStrandsConfig(config) {
332
+ globalConfig.value = config;
333
+ if (typeof window !== "undefined" && document.documentElement) {
334
+ const useSquircle = config.useSquircle !== void 0 ? config.useSquircle : true;
335
+ document.documentElement.style.setProperty("--strands-allow-squircle", useSquircle ? "1" : "0");
336
+ if (config.accentColor) {
337
+ generateColorShades(config.accentColor);
338
+ }
339
+ }
340
+ }
341
+ const mapApiUserToFrontend = (apiUser) => {
342
+ return {
343
+ id: apiUser.id,
344
+ email: apiUser.email,
345
+ firstName: apiUser.first_name || apiUser.firstName || "",
346
+ lastName: apiUser.last_name || apiUser.lastName || "",
347
+ avatar: apiUser.avatar_url || apiUser.avatar,
348
+ mfaEnabled: apiUser.mfa_enabled ?? apiUser.mfaEnabled ?? false,
349
+ emailVerified: apiUser.email_verified ?? apiUser.emailVerified ?? false,
350
+ passwordUpdatedAt: apiUser.password_updated_at || apiUser.passwordUpdatedAt,
351
+ settings: apiUser.settings || {},
352
+ xp: apiUser.xp || 0,
353
+ level: apiUser.level || 1,
354
+ next_level_xp: apiUser.next_level_xp || apiUser.next_level_xp || 4,
355
+ username: apiUser.username,
356
+ usernameLastChangedAt: apiUser.username_last_changed_at || apiUser.usernameLastChangedAt,
357
+ createdAt: apiUser.created_at || apiUser.createdAt,
358
+ updatedAt: apiUser.updated_at || apiUser.updatedAt || (/* @__PURE__ */ new Date()).toISOString()
359
+ };
360
+ };
361
+ const globalState = {
362
+ currentUser: ref(null),
363
+ currentSession: ref(null),
364
+ loadingStates: ref({
365
+ initializing: true,
366
+ signingIn: false,
367
+ signingUp: false,
368
+ signingOut: false,
369
+ refreshingToken: false,
370
+ sendingMfaEmail: false,
371
+ verifyingMfa: false,
372
+ loadingProfile: false
373
+ }),
374
+ isInitialized: ref(false),
375
+ mfaRequired: ref(false),
376
+ mfaSessionId: ref(null),
377
+ availableMfaMethods: ref([])
378
+ };
379
+ let refreshTimer = null;
380
+ let refreshPromise = null;
381
+ function useStrandsAuth() {
382
+ const { getUrl } = useStrandsConfig();
383
+ const { fetch: cachedFetch, clear: clearCache, invalidate } = useRequestCache();
384
+ const { currentUser, currentSession, loadingStates, isInitialized, mfaRequired, mfaSessionId, availableMfaMethods } = globalState;
385
+ const isInitializing = computed(() => loadingStates.value.initializing);
386
+ const isSigningIn = computed(() => loadingStates.value.signingIn);
387
+ const isSigningUp = computed(() => loadingStates.value.signingUp);
388
+ const isSigningOut = computed(() => loadingStates.value.signingOut);
389
+ const isRefreshingToken = computed(() => loadingStates.value.refreshingToken);
390
+ const isSendingMfaEmail = computed(() => loadingStates.value.sendingMfaEmail);
391
+ const isVerifyingMfa = computed(() => loadingStates.value.verifyingMfa);
392
+ computed(() => loadingStates.value.loadingProfile);
393
+ const loading2 = computed(
394
+ () => loadingStates.value.signingIn || loadingStates.value.signingUp || loadingStates.value.signingOut || loadingStates.value.refreshingToken || loadingStates.value.sendingMfaEmail || loadingStates.value.verifyingMfa || loadingStates.value.loadingProfile
395
+ );
396
+ const isLoading = computed(() => loadingStates.value.initializing || loading2.value);
397
+ const loadingMessage = computed(() => {
398
+ const states = loadingStates.value;
399
+ if (states.initializing) return "Checking authentication...";
400
+ if (states.signingIn) return "Signing you in...";
401
+ if (states.signingUp) return "Creating your account...";
402
+ if (states.signingOut) return "Signing you out...";
403
+ if (states.refreshingToken) return "Refreshing session...";
404
+ if (states.sendingMfaEmail) return "Sending verification code...";
405
+ if (states.verifyingMfa) return "Verifying code...";
406
+ if (states.loadingProfile) return "Loading profile...";
407
+ return "Loading...";
408
+ });
409
+ const getAuthHeaders = () => {
410
+ const headers = {};
411
+ if (currentSession.value?.accessToken) {
412
+ headers["Authorization"] = `Bearer ${currentSession.value.accessToken}`;
413
+ }
414
+ if (currentSession.value?.refreshToken) {
415
+ headers["x-refresh-token"] = currentSession.value.refreshToken;
416
+ }
417
+ return headers;
418
+ };
419
+ const completeHardwareKeyRegistration = async (deviceId, credential, accessToken) => {
420
+ const response = await fetch(getUrl("mfaHardwareCompleteRegistration"), {
421
+ method: "POST",
422
+ headers: {
423
+ "Content-Type": "application/json",
424
+ "Authorization": `Bearer ${currentSession.value.accessToken}`
425
+ },
426
+ body: JSON.stringify({
427
+ device_id: deviceId,
428
+ credential
429
+ })
430
+ });
431
+ if (!response.ok) {
432
+ const errorText = await response.text();
433
+ let errorMessage = "Failed to complete hardware key registration";
434
+ try {
435
+ const errorData = JSON.parse(errorText);
436
+ errorMessage = errorData.message || errorData.error || errorText;
437
+ } catch {
438
+ errorMessage = errorText || "Failed to complete hardware key registration";
439
+ }
440
+ throw new Error(errorMessage);
441
+ }
442
+ return response.json();
443
+ };
444
+ const registerHardwareKey = async (deviceName, accessToken, deviceType = "hardware") => {
445
+ const response = await fetch(getUrl("mfaHardwareStartRegistration"), {
446
+ method: "POST",
447
+ headers: {
448
+ "Content-Type": "application/json",
449
+ "Authorization": `Bearer ${currentSession.value.accessToken}`
450
+ },
451
+ body: JSON.stringify({
452
+ device_name: deviceName,
453
+ device_type: deviceType
454
+ })
455
+ });
456
+ if (!response.ok) {
457
+ const errorText = await response.text();
458
+ let errorMessage = "Failed to start hardware key registration";
459
+ try {
460
+ const errorData = JSON.parse(errorText);
461
+ errorMessage = errorData.message || errorData.error || errorText;
462
+ } catch {
463
+ errorMessage = errorText || "Failed to start hardware key registration";
464
+ }
465
+ throw new Error(errorMessage);
466
+ }
467
+ return response.json();
468
+ };
469
+ const isAuthenticated = computed(() => currentUser.value !== null);
470
+ const signIn = async (credentials) => {
471
+ loadingStates.value.signingIn = true;
472
+ try {
473
+ mfaRequired.value = false;
474
+ mfaSessionId.value = null;
475
+ availableMfaMethods.value = [];
476
+ const headers = {
477
+ "Content-Type": "application/json"
478
+ };
479
+ if (typeof window !== "undefined" && window.location) {
480
+ headers["Origin"] = window.location.origin;
481
+ }
482
+ const response = await fetch(getUrl("signIn"), {
483
+ method: "POST",
484
+ headers,
485
+ body: JSON.stringify(credentials)
486
+ });
487
+ if (!response.ok) {
488
+ if (response.status === 401) {
489
+ throw new Error("Invalid email or password");
490
+ } else if (response.status === 403) {
491
+ throw new Error("Please verify your email address before signing in");
492
+ } else {
493
+ throw new Error(`Sign in failed: ${response.status} ${response.statusText}`);
494
+ }
495
+ }
496
+ const authData = await response.json();
497
+ if (authData.mfa_required) {
498
+ mfaRequired.value = true;
499
+ mfaSessionId.value = authData.mfa_session_id || null;
500
+ const methods = (authData.available_mfa_methods || []).map((method) => {
501
+ let fallbackName = `${method.device_type.charAt(0).toUpperCase() + method.device_type.slice(1)} Authentication`;
502
+ if (method.device_type === "hardware") {
503
+ fallbackName = method.device_name || "Security Key";
504
+ } else if (method.device_type === "totp") {
505
+ fallbackName = method.device_name || "Authenticator App";
506
+ } else if (method.device_type === "email") {
507
+ fallbackName = method.device_name || "Email Verification";
508
+ }
509
+ return {
510
+ id: method.device_id,
511
+ device_type: method.device_type,
512
+ device_name: method.device_name || fallbackName,
513
+ is_active: true,
514
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
515
+ last_used_at: method.last_used_at,
516
+ // Pass through additional metadata if available
517
+ credential_id: method.credential_id,
518
+ device_info: method.device_info
519
+ };
520
+ });
521
+ availableMfaMethods.value = methods;
522
+ loadingStates.value.signingIn = false;
523
+ return authData;
524
+ }
525
+ await setAuthData(authData);
526
+ return authData;
527
+ } catch (error) {
528
+ throw error;
529
+ } finally {
530
+ loadingStates.value.signingIn = false;
531
+ }
532
+ };
533
+ const signUp = async (userData) => {
534
+ loadingStates.value.signingUp = true;
535
+ try {
536
+ throw new Error("Sign up not implemented - please integrate with auth SDK");
537
+ } finally {
538
+ loadingStates.value.signingUp = false;
539
+ }
540
+ };
541
+ const signOut = async () => {
542
+ loadingStates.value.signingOut = true;
543
+ try {
544
+ stopTokenRefreshTimer();
545
+ refreshPromise = null;
546
+ clearCache();
547
+ currentUser.value = null;
548
+ currentSession.value = null;
549
+ mfaRequired.value = false;
550
+ mfaSessionId.value = null;
551
+ availableMfaMethods.value = [];
552
+ if (typeof window !== "undefined") {
553
+ localStorage.removeItem("strands_auth_session");
554
+ localStorage.removeItem("strands_auth_user");
555
+ }
556
+ } finally {
557
+ loadingStates.value.signingOut = false;
558
+ }
559
+ };
560
+ const refreshToken = async () => {
561
+ if (!currentSession.value?.refreshToken) {
562
+ return false;
563
+ }
564
+ if (refreshPromise) {
565
+ console.log("Token refresh already in progress, waiting for completion...");
566
+ return await refreshPromise;
567
+ }
568
+ refreshPromise = (async () => {
569
+ loadingStates.value.refreshingToken = true;
570
+ try {
571
+ const response = await fetch(getUrl("refresh"), {
572
+ method: "POST",
573
+ headers: {
574
+ "Content-Type": "application/json"
575
+ },
576
+ body: JSON.stringify({
577
+ refresh_token: currentSession.value.refreshToken
578
+ })
579
+ });
580
+ if (!response.ok) {
581
+ if (response.status === 401) {
582
+ await signOut();
583
+ return false;
584
+ }
585
+ throw new Error(`Token refresh failed: ${response.status} ${response.statusText}`);
586
+ }
587
+ const tokenData = await response.json();
588
+ if (tokenData.user) {
589
+ currentUser.value = mapApiUserToFrontend(tokenData.user);
590
+ if (currentUser.value && typeof window !== "undefined") {
591
+ localStorage.setItem("strands_auth_user", JSON.stringify(currentUser.value));
592
+ }
593
+ }
594
+ const newSession = {
595
+ accessToken: tokenData.access_token,
596
+ refreshToken: tokenData.refresh_token,
597
+ expiresAt: new Date(Date.now() + 5 * 60 * 1e3),
598
+ // 5 minutes from now
599
+ userId: tokenData.user?.id || currentUser.value?.id
600
+ };
601
+ currentSession.value = newSession;
602
+ if (typeof window !== "undefined") {
603
+ localStorage.setItem("strands_auth_session", JSON.stringify(newSession));
604
+ }
605
+ startTokenRefreshTimer();
606
+ invalidate(`sessions:${currentSession.value.accessToken.slice(0, 20)}`);
607
+ return true;
608
+ } catch (error) {
609
+ await signOut();
610
+ return false;
611
+ } finally {
612
+ loadingStates.value.refreshingToken = false;
613
+ }
614
+ })();
615
+ const result = await refreshPromise;
616
+ refreshPromise = null;
617
+ return result;
618
+ };
619
+ const fetchProfile = async () => {
620
+ const cacheKey = `profile:${currentSession.value.accessToken.slice(0, 20)}`;
621
+ loadingStates.value.loadingProfile = true;
622
+ try {
623
+ return await cachedFetch(cacheKey, async () => {
624
+ const response = await fetch(getUrl("profile"), {
625
+ method: "GET",
626
+ headers: {
627
+ "Content-Type": "application/json",
628
+ "Authorization": `Bearer ${currentSession.value?.accessToken}`
629
+ }
630
+ });
631
+ if (!response.ok) {
632
+ if (response.status === 401) {
633
+ throw new Error("Authentication expired. Please sign in again.");
634
+ } else {
635
+ throw new Error(`Failed to fetch profile: ${response.status} ${response.statusText}`);
636
+ }
637
+ }
638
+ const userData = await response.json();
639
+ currentUser.value = mapApiUserToFrontend(userData);
640
+ if (currentUser.value && typeof window !== "undefined") {
641
+ localStorage.setItem("strands_auth_user", JSON.stringify(currentUser.value));
642
+ }
643
+ return currentUser.value;
644
+ });
645
+ } finally {
646
+ loadingStates.value.loadingProfile = false;
647
+ }
648
+ };
649
+ const updateProfile = async (profileData) => {
650
+ loadingStates.value.loadingProfile = true;
651
+ try {
652
+ const response = await fetch(getUrl("profile"), {
653
+ method: "POST",
654
+ headers: {
655
+ "Content-Type": "application/json",
656
+ "Authorization": `Bearer ${currentSession.value.accessToken}`
657
+ },
658
+ body: JSON.stringify({
659
+ first_name: profileData.firstName,
660
+ last_name: profileData.lastName
661
+ })
662
+ });
663
+ if (!response.ok) {
664
+ if (response.status === 401) {
665
+ throw new Error("Authentication expired. Please sign in again.");
666
+ } else {
667
+ throw new Error(`Profile update failed: ${response.status} ${response.statusText}`);
668
+ }
669
+ }
670
+ const updatedUserData = await response.json();
671
+ currentUser.value = mapApiUserToFrontend(updatedUserData);
672
+ if (currentUser.value) {
673
+ debouncedSetItem("strands_auth_user", JSON.stringify(currentUser.value));
674
+ }
675
+ return currentUser.value;
676
+ } finally {
677
+ loadingStates.value.loadingProfile = false;
678
+ }
679
+ };
680
+ const updateUserSettings = async (settings) => {
681
+ loadingStates.value.loadingProfile = true;
682
+ try {
683
+ const response = await fetch(getUrl("settings"), {
684
+ method: "POST",
685
+ headers: {
686
+ "Content-Type": "application/json",
687
+ "Authorization": `Bearer ${currentSession.value.accessToken}`
688
+ },
689
+ body: JSON.stringify({
690
+ settings
691
+ })
692
+ });
693
+ if (!response.ok) {
694
+ if (response.status === 401) {
695
+ throw new Error("Authentication expired. Please sign in again.");
696
+ } else {
697
+ throw new Error(`Settings update failed: ${response.status} ${response.statusText}`);
698
+ }
699
+ }
700
+ const updatedUserData = await response.json();
701
+ currentUser.value = mapApiUserToFrontend(updatedUserData);
702
+ if (currentUser.value) {
703
+ debouncedSetItem("strands_auth_user", JSON.stringify(currentUser.value));
704
+ }
705
+ return currentUser.value;
706
+ } finally {
707
+ loadingStates.value.loadingProfile = false;
708
+ }
709
+ };
710
+ const changeEmail = async (newEmail, password) => {
711
+ loadingStates.value.loadingProfile = true;
712
+ try {
713
+ const response = await fetch(getUrl("changeEmail"), {
714
+ method: "POST",
715
+ headers: {
716
+ "Content-Type": "application/json",
717
+ "Authorization": `Bearer ${currentSession.value.accessToken}`
718
+ },
719
+ body: JSON.stringify({
720
+ new_email: newEmail,
721
+ password
722
+ })
723
+ });
724
+ if (!response.ok) {
725
+ if (response.status === 401) {
726
+ throw new Error("Authentication expired. Please sign in again.");
727
+ } else {
728
+ const errorData = await response.json().catch(() => ({}));
729
+ throw new Error(errorData.message || `Email change failed: ${response.status} ${response.statusText}`);
730
+ }
731
+ }
732
+ const result = await response.json();
733
+ if (currentUser.value) {
734
+ currentUser.value = {
735
+ ...currentUser.value,
736
+ email: newEmail,
737
+ emailVerified: false,
738
+ // Email needs to be verified again
739
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
740
+ };
741
+ if (typeof window !== "undefined") {
742
+ localStorage.setItem("strands_auth_user", JSON.stringify(currentUser.value));
743
+ }
744
+ }
745
+ return result;
746
+ } finally {
747
+ loadingStates.value.loadingProfile = false;
748
+ }
749
+ };
750
+ const verifyMfa = async (deviceId, code, isBackupCode = false) => {
751
+ if (!mfaSessionId.value) {
752
+ throw new Error("No MFA session available");
753
+ }
754
+ loadingStates.value.verifyingMfa = true;
755
+ try {
756
+ const endpoint = isBackupCode ? getUrl("mfaBackupCodeVerify") : getUrl("mfaSigninVerify");
757
+ const body = isBackupCode ? { mfa_session_id: mfaSessionId.value, backup_code: code } : { mfa_session_id: mfaSessionId.value, device_id: deviceId, code };
758
+ const response = await fetch(endpoint, {
759
+ method: "POST",
760
+ headers: { "Content-Type": "application/json" },
761
+ body: JSON.stringify(body)
762
+ });
763
+ if (!response.ok) {
764
+ const errorText = await response.text();
765
+ let errorMessage = "MFA verification failed";
766
+ try {
767
+ const errorData = JSON.parse(errorText);
768
+ errorMessage = errorData.message || errorData.error || errorText;
769
+ } catch {
770
+ errorMessage = errorText || "MFA verification failed";
771
+ }
772
+ throw new Error(errorMessage);
773
+ }
774
+ const authData = await response.json();
775
+ mfaRequired.value = false;
776
+ mfaSessionId.value = null;
777
+ availableMfaMethods.value = [];
778
+ await setAuthData(authData);
779
+ return authData;
780
+ } finally {
781
+ loadingStates.value.verifyingMfa = false;
782
+ }
783
+ };
784
+ const sendMfaEmailCode = async (deviceId) => {
785
+ if (!mfaSessionId.value) {
786
+ throw new Error("No MFA session available");
787
+ }
788
+ loadingStates.value.sendingMfaEmail = true;
789
+ try {
790
+ const response = await fetch(getUrl("mfaSigninSendEmail"), {
791
+ method: "POST",
792
+ headers: { "Content-Type": "application/json" },
793
+ body: JSON.stringify({
794
+ mfa_session_id: mfaSessionId.value,
795
+ device_id: deviceId
796
+ })
797
+ });
798
+ if (!response.ok) {
799
+ const errorText = await response.text();
800
+ let errorMessage = "Failed to send MFA email code";
801
+ try {
802
+ const errorData = JSON.parse(errorText);
803
+ errorMessage = errorData.message || errorData.error || errorText;
804
+ } catch {
805
+ errorMessage = errorText || "Failed to send MFA email code";
806
+ }
807
+ throw new Error(errorMessage);
808
+ }
809
+ const result = await response.json();
810
+ return result;
811
+ } finally {
812
+ loadingStates.value.sendingMfaEmail = false;
813
+ }
814
+ };
815
+ const getMfaWebAuthnChallenge = async (deviceId) => {
816
+ if (!mfaSessionId.value) {
817
+ throw new Error("No MFA session available");
818
+ }
819
+ const response = await fetch(getUrl("mfaWebAuthnChallenge"), {
820
+ method: "POST",
821
+ headers: { "Content-Type": "application/json" },
822
+ body: JSON.stringify({
823
+ mfa_session_id: mfaSessionId.value,
824
+ device_id: deviceId
825
+ })
826
+ });
827
+ if (!response.ok) {
828
+ const errorText = await response.text();
829
+ let errorMessage = "Failed to get WebAuthn challenge";
830
+ try {
831
+ const errorData = JSON.parse(errorText);
832
+ errorMessage = errorData.message || errorData.error || errorText;
833
+ } catch {
834
+ errorMessage = errorText || errorMessage;
835
+ }
836
+ throw new Error(errorMessage);
837
+ }
838
+ return response.json();
839
+ };
840
+ const setAuthData = async (authResponse) => {
841
+ try {
842
+ if (authResponse.user) {
843
+ currentUser.value = mapApiUserToFrontend(authResponse.user);
844
+ }
845
+ const session = {
846
+ accessToken: authResponse.access_token,
847
+ refreshToken: authResponse.refresh_token,
848
+ expiresAt: new Date(Date.now() + 5 * 60 * 1e3),
849
+ // 5 minutes from now (matching API token expiry)
850
+ userId: currentUser.value?.id || authResponse.user?.id
851
+ };
852
+ currentSession.value = session;
853
+ if (typeof window !== "undefined") {
854
+ localStorage.setItem("strands_auth_session", JSON.stringify(session));
855
+ if (currentUser.value) {
856
+ localStorage.setItem("strands_auth_user", JSON.stringify(currentUser.value));
857
+ }
858
+ }
859
+ startTokenRefreshTimer();
860
+ } catch (error) {
861
+ }
862
+ };
863
+ const startTokenRefreshTimer = () => {
864
+ if (refreshTimer) {
865
+ clearTimeout(refreshTimer);
866
+ }
867
+ if (!currentSession.value) return;
868
+ if (typeof document !== "undefined" && document.visibilityState === "hidden") {
869
+ return;
870
+ }
871
+ const now = /* @__PURE__ */ new Date();
872
+ const expiresAt = currentSession.value.expiresAt;
873
+ const timeUntilRefresh = expiresAt.getTime() - now.getTime() - 1 * 60 * 1e3;
874
+ if (timeUntilRefresh <= 0) {
875
+ refreshToken();
876
+ return;
877
+ }
878
+ refreshTimer = setTimeout(async () => {
879
+ if (typeof document === "undefined" || document.visibilityState === "visible") {
880
+ const success = await refreshToken();
881
+ if (success) {
882
+ startTokenRefreshTimer();
883
+ }
884
+ }
885
+ }, timeUntilRefresh);
886
+ };
887
+ const stopTokenRefreshTimer = () => {
888
+ if (refreshTimer) {
889
+ clearTimeout(refreshTimer);
890
+ refreshTimer = null;
891
+ }
892
+ };
893
+ const initialize = async () => {
894
+ if (isInitialized.value) return;
895
+ loadingStates.value.initializing = true;
896
+ try {
897
+ if (typeof window !== "undefined") {
898
+ const storedSession = localStorage.getItem("strands_auth_session");
899
+ const storedUser = localStorage.getItem("strands_auth_user");
900
+ if (storedSession && storedUser) {
901
+ try {
902
+ const session = JSON.parse(storedSession);
903
+ const user = JSON.parse(storedUser);
904
+ session.expiresAt = new Date(session.expiresAt);
905
+ if (session.expiresAt > /* @__PURE__ */ new Date()) {
906
+ currentSession.value = session;
907
+ currentUser.value = user;
908
+ startTokenRefreshTimer();
909
+ } else {
910
+ localStorage.removeItem("strands_auth_session");
911
+ localStorage.removeItem("strands_auth_user");
912
+ }
913
+ } catch (error) {
914
+ localStorage.removeItem("strands_auth_session");
915
+ localStorage.removeItem("strands_auth_user");
916
+ }
917
+ }
918
+ }
919
+ isInitialized.value = true;
920
+ await new Promise((resolve) => setTimeout(resolve, 50));
921
+ } catch (error) {
922
+ } finally {
923
+ loadingStates.value.initializing = false;
924
+ }
925
+ };
926
+ const changeUsername = async (newUsername) => {
927
+ loadingStates.value.loadingProfile = true;
928
+ try {
929
+ const response = await fetch(getUrl("changeUsername"), {
930
+ method: "POST",
931
+ headers: {
932
+ "Content-Type": "application/json",
933
+ "Authorization": `Bearer ${currentSession.value.accessToken}`
934
+ },
935
+ body: JSON.stringify({
936
+ username: newUsername
937
+ })
938
+ });
939
+ if (!response.ok) {
940
+ const errorData = await response.json().catch(() => ({}));
941
+ if (response.status === 409) {
942
+ throw new Error("Username is already taken");
943
+ } else if (errorData.cooldown_end) {
944
+ throw new Error(`You can only change your username once every 30 days. You can change it again on ${new Date(errorData.cooldown_end).toLocaleDateString()}`);
945
+ }
946
+ throw new Error(errorData.message || `Username change failed: ${response.status} ${response.statusText}`);
947
+ }
948
+ const result = await response.json();
949
+ if (currentUser.value) {
950
+ currentUser.value = {
951
+ ...currentUser.value,
952
+ username: newUsername,
953
+ usernameLastChangedAt: (/* @__PURE__ */ new Date()).toISOString(),
954
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
955
+ };
956
+ if (typeof window !== "undefined") {
957
+ localStorage.setItem("strands_auth_user", JSON.stringify(currentUser.value));
958
+ }
959
+ }
960
+ return result;
961
+ } finally {
962
+ loadingStates.value.loadingProfile = false;
963
+ }
964
+ };
965
+ const getUsernameCooldown = async () => {
966
+ const response = await fetch(getUrl("usernameCooldown"), {
967
+ method: "GET",
968
+ headers: {
969
+ "Authorization": `Bearer ${currentSession.value.accessToken}`
970
+ }
971
+ });
972
+ if (!response.ok) {
973
+ throw new Error(`Failed to get username cooldown: ${response.status} ${response.statusText}`);
974
+ }
975
+ return response.json();
976
+ };
977
+ const checkUsernameAvailability = async (username) => {
978
+ const url = getUrl("checkUsernameAvailability").replace("{username}", encodeURIComponent(username));
979
+ const response = await fetch(url, {
980
+ method: "GET",
981
+ headers: {
982
+ "Content-Type": "application/json"
983
+ }
984
+ });
985
+ if (!response.ok) {
986
+ throw new Error(`Failed to check username availability: ${response.status} ${response.statusText}`);
987
+ }
988
+ return response.json();
989
+ };
990
+ const getUserSessions = async () => {
991
+ const cacheKey = `sessions:${currentSession.value?.accessToken?.slice(0, 20) || "no-token"}`;
992
+ try {
993
+ return await cachedFetch(cacheKey, async () => {
994
+ const headers = getAuthHeaders();
995
+ const response = await fetch(getUrl("sessions"), {
996
+ method: "GET",
997
+ headers
998
+ });
999
+ if (!response.ok) {
1000
+ await response.text();
1001
+ throw new Error(`Failed to get user sessions: ${response.status} ${response.statusText}`);
1002
+ }
1003
+ return response.json();
1004
+ }, 2 * 60 * 1e3);
1005
+ } catch (error) {
1006
+ throw error;
1007
+ }
1008
+ };
1009
+ const getSessionStats = async () => {
1010
+ const response = await fetch(getUrl("sessionsStats"), {
1011
+ method: "GET",
1012
+ headers: getAuthHeaders()
1013
+ });
1014
+ if (!response.ok) {
1015
+ throw new Error(`Failed to get session stats: ${response.status} ${response.statusText}`);
1016
+ }
1017
+ return response.json();
1018
+ };
1019
+ const revokeSession = async (sessionId) => {
1020
+ const url = getUrl("sessionRevoke").replace("{session_id}", encodeURIComponent(sessionId));
1021
+ const response = await fetch(url, {
1022
+ method: "POST",
1023
+ headers: getAuthHeaders()
1024
+ });
1025
+ if (!response.ok) {
1026
+ throw new Error(`Failed to revoke session: ${response.status} ${response.statusText}`);
1027
+ }
1028
+ return response.status === 200;
1029
+ };
1030
+ const revokeAllOtherSessions = async () => {
1031
+ const response = await fetch(getUrl("sessionsRevokeAll"), {
1032
+ method: "POST",
1033
+ headers: getAuthHeaders()
1034
+ });
1035
+ if (!response.ok) {
1036
+ throw new Error(`Failed to revoke all other sessions: ${response.status} ${response.statusText}`);
1037
+ }
1038
+ return response.status === 200;
1039
+ };
1040
+ if (typeof document !== "undefined") {
1041
+ document.addEventListener("visibilitychange", () => {
1042
+ if (document.visibilityState === "visible" && currentSession.value) {
1043
+ startTokenRefreshTimer();
1044
+ } else if (document.visibilityState === "hidden") {
1045
+ stopTokenRefreshTimer();
1046
+ }
1047
+ });
1048
+ }
1049
+ const cleanup = () => {
1050
+ stopTokenRefreshTimer();
1051
+ clearCache();
1052
+ if (typeof document !== "undefined") {
1053
+ document.removeEventListener("visibilitychange", () => {
1054
+ });
1055
+ }
1056
+ };
1057
+ try {
1058
+ onUnmounted(cleanup);
1059
+ } catch (e) {
1060
+ }
1061
+ if (!isInitialized.value) {
1062
+ initialize();
1063
+ }
1064
+ return {
1065
+ // State
1066
+ user: computed(() => currentUser.value),
1067
+ currentUser: computed(() => currentUser.value),
1068
+ currentSession: computed(() => currentSession.value),
1069
+ isAuthenticated,
1070
+ isLoading: computed(() => isLoading.value || !isInitialized.value),
1071
+ loading: computed(() => loading2.value),
1072
+ loadingMessage,
1073
+ // Specific loading states
1074
+ isInitializing,
1075
+ isSigningIn,
1076
+ isSigningUp,
1077
+ isSigningOut,
1078
+ isRefreshingToken,
1079
+ isSendingMfaEmail,
1080
+ isVerifyingMfa,
1081
+ // MFA State
1082
+ mfaRequired: computed(() => mfaRequired.value),
1083
+ mfaSessionId: computed(() => mfaSessionId.value),
1084
+ availableMfaMethods: computed(() => availableMfaMethods.value),
1085
+ // Methods
1086
+ signIn,
1087
+ signUp,
1088
+ signOut,
1089
+ refreshToken,
1090
+ fetchProfile,
1091
+ updateProfile,
1092
+ updateUserSettings,
1093
+ changeEmail,
1094
+ changeUsername,
1095
+ getUsernameCooldown,
1096
+ checkUsernameAvailability,
1097
+ // Session management
1098
+ getUserSessions,
1099
+ getSessionStats,
1100
+ revokeSession,
1101
+ revokeAllOtherSessions,
1102
+ initialize,
1103
+ setAuthData,
1104
+ verifyMfa,
1105
+ sendMfaEmailCode,
1106
+ getMfaWebAuthnChallenge,
1107
+ registerHardwareKey,
1108
+ completeHardwareKeyRegistration,
1109
+ // Token management
1110
+ startTokenRefreshTimer,
1111
+ stopTokenRefreshTimer,
1112
+ getAuthHeaders,
1113
+ // Force re-initialization (useful for testing or navigation)
1114
+ forceReInit: () => {
1115
+ isInitialized.value = false;
1116
+ loadingStates.value.initializing = true;
1117
+ initialize();
1118
+ }
1119
+ };
1120
+ }
1121
+ const mfaDevices = ref([]);
1122
+ const mfaEnabled = ref(false);
1123
+ const loading = ref(false);
1124
+ function useStrandsMfa() {
1125
+ const { getUrl } = useStrandsConfig();
1126
+ const { currentSession } = useStrandsAuth();
1127
+ const hasMfaDevices = computed(() => mfaDevices.value.length > 0);
1128
+ const activeMfaDevices = computed(
1129
+ () => mfaDevices.value.filter(
1130
+ (d) => d.is_active && d.device_type !== "hardware" && d.device_type !== "passkey"
1131
+ )
1132
+ );
1133
+ const makeAuthenticatedRequest = async (url, options = {}) => {
1134
+ const headers = {
1135
+ "Content-Type": "application/json",
1136
+ ...options.headers
1137
+ };
1138
+ if (currentSession.value?.accessToken) {
1139
+ headers["Authorization"] = `Bearer ${currentSession.value.accessToken}`;
1140
+ }
1141
+ const response = await fetch(url, {
1142
+ ...options,
1143
+ headers
1144
+ });
1145
+ if (!response.ok) {
1146
+ const errorText = await response.text();
1147
+ let errorMessage = `Request failed: ${response.status} ${response.statusText}`;
1148
+ try {
1149
+ const errorData = JSON.parse(errorText);
1150
+ errorMessage = errorData.message || errorMessage;
1151
+ } catch {
1152
+ if (errorText) {
1153
+ errorMessage = errorText;
1154
+ }
1155
+ }
1156
+ throw new Error(errorMessage);
1157
+ }
1158
+ return response.json();
1159
+ };
1160
+ const fetchMfaDevices = async () => {
1161
+ loading.value = true;
1162
+ try {
1163
+ const response = await makeAuthenticatedRequest(getUrl("mfaDevices"), {
1164
+ method: "GET"
1165
+ });
1166
+ mfaDevices.value = response.devices || [];
1167
+ mfaEnabled.value = response.mfa_enabled || false;
1168
+ return response;
1169
+ } finally {
1170
+ loading.value = false;
1171
+ }
1172
+ };
1173
+ const setupTotp = async (deviceName) => {
1174
+ loading.value = true;
1175
+ try {
1176
+ const response = await makeAuthenticatedRequest(getUrl("mfaTotpSetup"), {
1177
+ method: "POST",
1178
+ body: JSON.stringify({ device_name: deviceName })
1179
+ });
1180
+ await fetchMfaDevices();
1181
+ return response;
1182
+ } finally {
1183
+ loading.value = false;
1184
+ }
1185
+ };
1186
+ const verifyTotpSetup = async (deviceId, totpCode) => {
1187
+ loading.value = true;
1188
+ try {
1189
+ await makeAuthenticatedRequest(getUrl("mfaTotpVerify"), {
1190
+ method: "POST",
1191
+ body: JSON.stringify({
1192
+ device_id: deviceId,
1193
+ totp_code: totpCode
1194
+ })
1195
+ });
1196
+ await fetchMfaDevices();
1197
+ } finally {
1198
+ loading.value = false;
1199
+ }
1200
+ };
1201
+ const setupEmailMfa = async (deviceName) => {
1202
+ loading.value = true;
1203
+ try {
1204
+ const response = await makeAuthenticatedRequest(getUrl("mfaEmailSetup"), {
1205
+ method: "POST",
1206
+ body: JSON.stringify({ device_name: deviceName })
1207
+ });
1208
+ await fetchMfaDevices();
1209
+ return response;
1210
+ } finally {
1211
+ loading.value = false;
1212
+ }
1213
+ };
1214
+ const sendEmailMfaCode = async (deviceId) => {
1215
+ loading.value = true;
1216
+ try {
1217
+ await makeAuthenticatedRequest(getUrl("mfaEmailSend"), {
1218
+ method: "POST",
1219
+ body: JSON.stringify({ device_id: deviceId })
1220
+ });
1221
+ } finally {
1222
+ loading.value = false;
1223
+ }
1224
+ };
1225
+ const verifyEmailMfaCode = async (deviceId, code) => {
1226
+ loading.value = true;
1227
+ try {
1228
+ const response = await makeAuthenticatedRequest(getUrl("mfaEmailVerify"), {
1229
+ method: "POST",
1230
+ body: JSON.stringify({
1231
+ device_id: deviceId,
1232
+ code
1233
+ })
1234
+ });
1235
+ return response.verified || false;
1236
+ } finally {
1237
+ loading.value = false;
1238
+ }
1239
+ };
1240
+ const disableMfaDevice = async (deviceId) => {
1241
+ loading.value = true;
1242
+ try {
1243
+ await makeAuthenticatedRequest(getUrl("mfaDeviceDisable"), {
1244
+ method: "POST",
1245
+ body: JSON.stringify({ device_id: deviceId })
1246
+ });
1247
+ await fetchMfaDevices();
1248
+ } finally {
1249
+ loading.value = false;
1250
+ }
1251
+ };
1252
+ const regenerateBackupCodes = async (deviceId) => {
1253
+ loading.value = true;
1254
+ try {
1255
+ const response = await makeAuthenticatedRequest(getUrl("mfaBackupCodes"), {
1256
+ method: "POST",
1257
+ body: JSON.stringify({ device_id: deviceId })
1258
+ });
1259
+ return response;
1260
+ } finally {
1261
+ loading.value = false;
1262
+ }
1263
+ };
1264
+ const getDeviceTypeIcon = (deviceType) => {
1265
+ switch (deviceType) {
1266
+ case "totp":
1267
+ return "📱";
1268
+ // Authenticator app
1269
+ case "email":
1270
+ return "📧";
1271
+ // Email
1272
+ case "hardware":
1273
+ return "🔑";
1274
+ // Hardware key
1275
+ case "passkey":
1276
+ return "🔐";
1277
+ // Passkey
1278
+ default:
1279
+ return "🔒";
1280
+ }
1281
+ };
1282
+ const getDeviceTypeName = (deviceType) => {
1283
+ switch (deviceType) {
1284
+ case "totp":
1285
+ return "Authenticator App";
1286
+ case "email":
1287
+ return "Email Verification";
1288
+ case "hardware":
1289
+ return "Hardware Key";
1290
+ case "passkey":
1291
+ return "Passkey";
1292
+ default:
1293
+ return "Unknown";
1294
+ }
1295
+ };
1296
+ const formatLastUsed = (lastUsedAt) => {
1297
+ if (!lastUsedAt) return "Never";
1298
+ const date = new Date(lastUsedAt);
1299
+ const now = /* @__PURE__ */ new Date();
1300
+ const diffMs = now.getTime() - date.getTime();
1301
+ const diffDays = Math.floor(diffMs / (1e3 * 60 * 60 * 24));
1302
+ if (diffDays === 0) return "Today";
1303
+ if (diffDays === 1) return "Yesterday";
1304
+ if (diffDays < 30) return `${diffDays} days ago`;
1305
+ return date.toLocaleDateString();
1306
+ };
1307
+ return {
1308
+ // State
1309
+ mfaDevices: computed(() => mfaDevices.value),
1310
+ mfaEnabled: computed(() => mfaEnabled.value),
1311
+ loading: computed(() => loading.value),
1312
+ hasMfaDevices,
1313
+ activeMfaDevices,
1314
+ // Methods
1315
+ fetchMfaDevices,
1316
+ setupTotp,
1317
+ verifyTotpSetup,
1318
+ setupEmailMfa,
1319
+ sendEmailMfaCode,
1320
+ verifyEmailMfaCode,
1321
+ disableMfaDevice,
1322
+ regenerateBackupCodes,
1323
+ // Helper methods
1324
+ getDeviceTypeIcon,
1325
+ getDeviceTypeName,
1326
+ formatLastUsed
1327
+ };
1328
+ }
1329
+ const providersCache = /* @__PURE__ */ new Map();
1330
+ const CACHE_DURATION = 5 * 60 * 1e3;
1331
+ function useOAuthProviders(options = {}) {
1332
+ const { getUrl, config } = useStrandsConfig();
1333
+ const providers = ref([]);
1334
+ const loading2 = ref(false);
1335
+ const error = ref(null);
1336
+ const enabledProviders = computed(
1337
+ () => providers.value.filter((provider) => provider.enabled)
1338
+ );
1339
+ const fetchProviders = async () => {
1340
+ const cacheKey = JSON.stringify(options);
1341
+ const cached = providersCache.get(cacheKey);
1342
+ if (cached && Date.now() - cached.timestamp < CACHE_DURATION) {
1343
+ providers.value = cached.data;
1344
+ return cached.data;
1345
+ }
1346
+ loading2.value = true;
1347
+ error.value = null;
1348
+ try {
1349
+ let url = getUrl("oauthProviders");
1350
+ const redirectUrl = options.redirectUrl || config.value?.oauth2RedirectUrl;
1351
+ if (redirectUrl) {
1352
+ const params = new URLSearchParams();
1353
+ let absoluteRedirectUrl = redirectUrl;
1354
+ if (redirectUrl.startsWith("/")) {
1355
+ const currentOrigin = window.location.origin;
1356
+ absoluteRedirectUrl = `${currentOrigin}${redirectUrl}`;
1357
+ }
1358
+ params.append("redirect_url", absoluteRedirectUrl);
1359
+ url = `${url}?${params.toString()}`;
1360
+ }
1361
+ const response = await fetch(url, {
1362
+ method: "GET",
1363
+ headers: {
1364
+ "Content-Type": "application/json"
1365
+ }
1366
+ });
1367
+ if (!response.ok) {
1368
+ const errorData = await response.json().catch(() => ({}));
1369
+ throw new Error(errorData.error?.message || `HTTP ${response.status}: ${response.statusText}`);
1370
+ }
1371
+ const result = await response.json();
1372
+ providers.value = result.providers || [];
1373
+ providersCache.set(cacheKey, {
1374
+ data: providers.value,
1375
+ timestamp: Date.now()
1376
+ });
1377
+ } catch (err) {
1378
+ const errorMessage = err instanceof Error ? err.message : "Failed to fetch OAuth providers";
1379
+ error.value = errorMessage;
1380
+ } finally {
1381
+ loading2.value = false;
1382
+ }
1383
+ };
1384
+ const getProviderAuthUrl = async (providerId, customOptions) => {
1385
+ const mergedOptions = { ...options, ...customOptions };
1386
+ const params = new URLSearchParams();
1387
+ if (mergedOptions.redirectUrl) {
1388
+ let absoluteRedirectUrl = mergedOptions.redirectUrl;
1389
+ if (mergedOptions.redirectUrl.startsWith("/")) {
1390
+ const currentOrigin = window.location.origin;
1391
+ absoluteRedirectUrl = `${currentOrigin}${mergedOptions.redirectUrl}`;
1392
+ }
1393
+ params.append("redirect_url", absoluteRedirectUrl);
1394
+ }
1395
+ if (mergedOptions.scopes && mergedOptions.scopes.length > 0) {
1396
+ params.append("scopes", mergedOptions.scopes.join(","));
1397
+ }
1398
+ const queryString = params.toString();
1399
+ const providerUrl = getUrl("oauthProvider").replace("{provider_id}", providerId);
1400
+ const fullUrl = queryString ? `${providerUrl}?${queryString}` : providerUrl;
1401
+ try {
1402
+ const response = await fetch(fullUrl, {
1403
+ method: "GET",
1404
+ headers: {
1405
+ "Content-Type": "application/json"
1406
+ }
1407
+ });
1408
+ if (!response.ok) {
1409
+ const errorData = await response.json().catch(() => ({}));
1410
+ throw new Error(errorData.error?.message || `HTTP ${response.status}: ${response.statusText}`);
1411
+ }
1412
+ const result = await response.json();
1413
+ if (!result.success) {
1414
+ throw new Error(result.error?.message || "Failed to get OAuth auth URL");
1415
+ }
1416
+ return result.data.authUrl;
1417
+ } catch (err) {
1418
+ const errorMessage = err instanceof Error ? err.message : "Failed to get OAuth auth URL";
1419
+ throw new Error(errorMessage);
1420
+ }
1421
+ };
1422
+ const redirectToProvider = async (providerId, customOptions) => {
1423
+ try {
1424
+ const provider = providers.value.find((p) => p.id === providerId);
1425
+ if (!provider) {
1426
+ throw new Error(`OAuth provider '${providerId}' not found`);
1427
+ }
1428
+ if (!provider.auth_url) {
1429
+ throw new Error(`No auth URL configured for provider '${providerId}'`);
1430
+ }
1431
+ window.location.href = provider.auth_url;
1432
+ } catch (err) {
1433
+ error.value = err instanceof Error ? err.message : "Failed to redirect to OAuth provider";
1434
+ throw err;
1435
+ }
1436
+ };
1437
+ const getProviderById = (providerId) => {
1438
+ return providers.value.find((provider) => provider.id === providerId);
1439
+ };
1440
+ const getProviderIcon = (provider) => {
1441
+ if (provider.iconUrl) {
1442
+ return provider.iconUrl;
1443
+ }
1444
+ switch (provider.id.toLowerCase()) {
1445
+ case "google":
1446
+ return "data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGZpbGw9IiM0Mjg1RjQiIGQ9Ik0yMi41NiAxMi4yNWMwLS43OC0uMDctMS41My0uMi0yLjI1SDEydjQuMjZoNS45MmMtLjI2IDEuMzctMS4wNCAyLjUzLTIuMjEgMy4zMXYyLjc3aDMuNTdjMi4wOC0xLjkyIDMuMjgtNC43NCAzLjI4LTguMDl6Ii8+PHBhdGggZmlsbD0iIzM0QTg1MyIgZD0iTTEyIDIzYzIuOTcgMCA1LjQ2LS45OCA3LjI4LTIuNjZsLTMuNTctMi43N2MtLjk4LjY2LTIuMjMgMS4wNi0zLjcxIDEuMDYtMi44NiAwLTUuMjktMS45My02LjE2LTQuNTNIMi4xOHYyLjg0QzMuOTkgMjAuNTMgNy43IDIzIDEyIDIzeiIvPjxwYXRoIGZpbGw9IiNGQkJDMDUiIGQ9Ik01Ljg0IDE0LjA5Yy0uMjItLjY2LS4zNS0xLjM2LS4zNS0yLjA5cy4xMy0xLjQzLjM1LTIuMDlWNy4wN0gyLjE4QzEuNDMgOC41NSAxIDEwLjIyIDEgMTJzLjQzIDMuNDUgMS4xOCA0LjkzbDIuODUtMi4yMi44MS0uNjJ6Ii8+PHBhdGggZmlsbD0iI0VBNDMzNSIgZD0iTTEyIDUuMzhjMS42MiAwIDMuMDYuNTYgNC4yMSAxLjY0bDMuMTUtMy4xNUMxNy40NSAyLjA5IDE0Ljk3IDEgMTIgMSA3LjcgMSAzLjk5IDMuNDcgMi4xOCA3LjA3bDMuNjYgMi44NGMuODctMi42IDMuMy00LjUzIDYuMTYtNC41M3oiLz48L3N2Zz4=";
1447
+ case "github":
1448
+ return "data:image/svg+xml;base64,PHN2ZyBmaWxsPSJjdXJyZW50Q29sb3IiIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTEyIDBoLTEyYy02LjYyNiAwLTEyIDUuMzczLTEyIDEyIDAgNS4zMDIgMy40MzggOS44IDguMjA3IDExLjM4Ny41OTkuMTExLjc5My0uMjYxLjc5My0uNTc3di0yLjIzNGMtMy4zMzguNzI2LTQuMDMzLTEuNDE2LTQuMDMzLTEuNDE2LS41NDYtMS4zODctMS4zMzMtMS43NTYtMS4zMzMtMS43NTYtMS4wODktLjc0NS4wODMtLjcyOS4wODMtLjcyOSAxLjIwNS4wODQgMS44MzkgMS4yMzcgMS44MzkgMS4yMzcgMS4wNyAxLjgzNCAyLjgwNyAxLjMwNCAzLjQ5Mi45OTcuMTA3LS43NzUuNDE4LTEuMzA1Ljc2Mi0xLjYwNC0yLjY2NS0uMzA1LTUuNDY3LTEuMzM0LTUuNDY3LTUuOTMxIDAtMS4zMTEuNDY5LTIuMzgxIDEuMjM2LTMuMjIxLS4xMjQtLjMwMy0uNTM1LTEuNTI0LjExNy0zLjE3NiAwIDAgMS4wMDgtLjMyMiAzLjMwMSAxLjIzLjk1Ny0uMjY2IDEuOTgzLS4zOTkgMy4wMDMtLjQwNCAxLjAyLjAwNSAyLjA0Ny4xMzggMy4wMDYuNDA0IDIuMjkxLTEuNTUyIDMuMjk3LTEuMjMgMy4yOTctMS4yMy42NTMgMS42NTMuMjQyIDIuODc0LjExOCAzLjE3Ni43Ny44NCAxLjIzNSAxLjkxMSAxLjIzNSAzLjIyMSAwIDQuNjA5LTIuODA3IDUuNjI0LTUuNDc5IDUuOTIxLjQzLjM3Mi44MjMgMS4xMDIuODIzIDIuMjIydjMuMjkzYzAgLjMxOS4xOTIuNjk0LjgwMS41NzYgNC43NjUtMS41ODkgOC4xOTktNi4wODYgOC4xOTktMTEuMzg2IDAtNi42MjctNS4zNzMtMTItMTItMTJ6Ii8+PC9zdmc+";
1449
+ case "microsoft":
1450
+ case "azure":
1451
+ return "data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiPjxyZWN0IGZpbGw9IiNmMjUwMjIiIHdpZHRoPSIxMSIgaGVpZ2h0PSIxMSIvPjxyZWN0IGZpbGw9IiM3ZmJhMDAiIHg9IjEzIiB3aWR0aD0iMTEiIGhlaWdodD0iMTEiLz48cmVjdCBmaWxsPSIjMDBhNGVmIiB5PSIxMyIgd2lkdGg9IjExIiBoZWlnaHQ9IjExIi8+PHJlY3QgZmlsbD0iI2ZmYjkwMCIgeD0iMTMiIHk9IjEzIiB3aWR0aD0iMTEiIGhlaWdodD0iMTEiLz48L3N2Zz4=";
1452
+ case "discord":
1453
+ return "data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIGZpbGw9IiM1ODY1RjIiPjxwYXRoIGQ9Ik0yMC4zMTcgNC4zNjk2QTE5Ljc5MTMgMTkuNzkxMyAwIDAgMCAxOC4yMDU4IDMuMzY5NmMtLjk2NDIuMzU2NC0xLjk5NTguNjE1NC0zLjA1OC43NzNhMTQuNzI0NCAxNC43MjQ0IDAgMCAwLTYuMjk1NCAwIDEzLjU0OTQgMTMuNTQ5NCAwIDAgMC0zLjA1OC0uNzczIDEzLjY5MyAxMy42OTMgMCAwIDAtNS4wNjc2LTEuMjc5NCAyMC4xOTg0IDIwLjE5ODQgMCAwIDAtMi42NDk3IDkuMTE2MyAyMC4yODE4IDIwLjI4MTggMCAwIDAgMi4zNzU4IDIuNjNjMS4zMzIyLjEyNTggMi42NjUuMTcyMiA0LjA0NC4xNzIyaDEuMzE1NGMxLjM3OSAwIDIuNzExOC0uMDQ2NCA0LjA0NC0uMTcyMmEyMC4yODE4IDIwLjI4MTggMCAwIDAgMi4zNzU4LTIuNjMgMjAuMTk4NCAyMC4xOTg0IDAgMCAwIDIuNjQ5Ny05LjExNjNBMTkuNzkxMyAxOS43OTEzIDAgMCAwIDIwLjMxNyA0LjM2OTZ6TTE4LjE5IDEyQzE3LjY3IDE0LjQ5IDEzLjk5IDE2IDEyIDE2UzYuMzMgMTQuNDkgNS44MSAxMmMuNTItMi40OSA0LjE5LTQgNi4xOS00UzE3LjY3IDkuNTEgMTguMTkgMTJ6Ii8+PC9zdmc+";
1454
+ default:
1455
+ return "data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIGZpbGw9ImN1cnJlbnRDb2xvciI+PGNpcmNsZSBjeD0iMTIiIGN5PSIxMiIgcj0iMTAiLz48dGV4dCB4PSIxMiIgeT0iMTciIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IndoaXRlIiBmb250LXNpemU9IjEyIj57cHJvdmlkZXIuaWQuY2hhckF0KDApLnRvVXBwZXJDYXNlKCl9PC90ZXh0Pjwvc3ZnPg==";
1456
+ }
1457
+ };
1458
+ return {
1459
+ providers: computed(() => providers.value),
1460
+ enabledProviders,
1461
+ loading: computed(() => loading2.value),
1462
+ error: computed(() => error.value),
1463
+ fetchProviders,
1464
+ getProviderAuthUrl,
1465
+ redirectToProvider,
1466
+ getProviderById,
1467
+ getProviderIcon
1468
+ };
1469
+ }
1470
+ function useAuthenticatedFetch() {
1471
+ const { config } = useStrandsConfig();
1472
+ const { currentSession, refreshToken, getAuthHeaders } = useStrandsAuth();
1473
+ const authenticatedFetch = async (url, options = {}) => {
1474
+ const {
1475
+ autoRefresh = true,
1476
+ requireAuth = true,
1477
+ baseURL,
1478
+ ...fetchOptions
1479
+ } = options;
1480
+ if (requireAuth && !currentSession.value?.accessToken) {
1481
+ throw new Error("User is not authenticated");
1482
+ }
1483
+ let fullUrl = url;
1484
+ const resolvedBaseURL = baseURL || config.value.baseUrl;
1485
+ if (resolvedBaseURL && typeof url === "string" && !url.startsWith("http")) {
1486
+ fullUrl = new URL(url, resolvedBaseURL).toString();
1487
+ }
1488
+ const headers = new Headers(fetchOptions.headers);
1489
+ if (currentSession.value?.accessToken) {
1490
+ try {
1491
+ const authHeaders = getAuthHeaders();
1492
+ Object.entries(authHeaders).forEach(([key, value]) => {
1493
+ headers.set(key, value);
1494
+ });
1495
+ } catch (error) {
1496
+ console.warn("[Strands Auth] Failed to get auth headers:", error);
1497
+ if (requireAuth) {
1498
+ throw error;
1499
+ }
1500
+ }
1501
+ }
1502
+ const enhancedOptions = {
1503
+ ...fetchOptions,
1504
+ headers
1505
+ };
1506
+ let response = await fetch(fullUrl, enhancedOptions);
1507
+ if (response.status === 401 && autoRefresh && currentSession.value?.refreshToken) {
1508
+ console.log("[Strands Auth] Request failed with 401, attempting token refresh...");
1509
+ try {
1510
+ const refreshed = await refreshToken();
1511
+ if (refreshed && currentSession.value?.accessToken) {
1512
+ const newAuthHeaders = getAuthHeaders();
1513
+ Object.entries(newAuthHeaders).forEach(([key, value]) => {
1514
+ headers.set(key, value);
1515
+ });
1516
+ console.log("[Strands Auth] Retrying request with refreshed token");
1517
+ response = await fetch(fullUrl, { ...enhancedOptions, headers });
1518
+ }
1519
+ } catch (refreshError) {
1520
+ console.error("[Strands Auth] Token refresh failed:", refreshError);
1521
+ }
1522
+ }
1523
+ return response;
1524
+ };
1525
+ const get = (url, options) => {
1526
+ return authenticatedFetch(url, { ...options, method: "GET" });
1527
+ };
1528
+ const post = (url, body, options) => {
1529
+ const headers = new Headers(options?.headers);
1530
+ if (body && typeof body === "object" && !headers.has("Content-Type")) {
1531
+ headers.set("Content-Type", "application/json");
1532
+ }
1533
+ return authenticatedFetch(url, {
1534
+ ...options,
1535
+ method: "POST",
1536
+ headers,
1537
+ body: typeof body === "object" ? JSON.stringify(body) : body
1538
+ });
1539
+ };
1540
+ const put = (url, body, options) => {
1541
+ const headers = new Headers(options?.headers);
1542
+ if (body && typeof body === "object" && !headers.has("Content-Type")) {
1543
+ headers.set("Content-Type", "application/json");
1544
+ }
1545
+ return authenticatedFetch(url, {
1546
+ ...options,
1547
+ method: "PUT",
1548
+ headers,
1549
+ body: typeof body === "object" ? JSON.stringify(body) : body
1550
+ });
1551
+ };
1552
+ const del = (url, options) => {
1553
+ return authenticatedFetch(url, { ...options, method: "DELETE" });
1554
+ };
1555
+ const patch = (url, body, options) => {
1556
+ const headers = new Headers(options?.headers);
1557
+ if (body && typeof body === "object" && !headers.has("Content-Type")) {
1558
+ headers.set("Content-Type", "application/json");
1559
+ }
1560
+ return authenticatedFetch(url, {
1561
+ ...options,
1562
+ method: "PATCH",
1563
+ headers,
1564
+ body: typeof body === "object" ? JSON.stringify(body) : body
1565
+ });
1566
+ };
1567
+ return {
1568
+ authenticatedFetch,
1569
+ get,
1570
+ post,
1571
+ put,
1572
+ delete: del,
1573
+ patch
1574
+ };
1575
+ }
1576
+ const $authFetch = {
1577
+ get: async (url, options) => {
1578
+ const { get } = useAuthenticatedFetch();
1579
+ return get(url, options);
1580
+ },
1581
+ post: async (url, body, options) => {
1582
+ const { post } = useAuthenticatedFetch();
1583
+ return post(url, body, options);
1584
+ },
1585
+ put: async (url, body, options) => {
1586
+ const { put } = useAuthenticatedFetch();
1587
+ return put(url, body, options);
1588
+ },
1589
+ delete: async (url, options) => {
1590
+ const { delete: del } = useAuthenticatedFetch();
1591
+ return del(url, options);
1592
+ },
1593
+ patch: async (url, body, options) => {
1594
+ const { patch } = useAuthenticatedFetch();
1595
+ return patch(url, body, options);
1596
+ }
1597
+ };
1598
+ export {
1599
+ $authFetch as $,
1600
+ STRANDS_AUTH_DEFAULTS as S,
1601
+ useStrandsMfa as a,
1602
+ useStrandsAuth as b,
1603
+ useOAuthProviders as c,
1604
+ useGlobalDarkMode as d,
1605
+ useAuthenticatedFetch as e,
1606
+ provideStrandsConfig as p,
1607
+ setStrandsConfig as s,
1608
+ useStrandsConfig as u
1609
+ };