@oxyhq/core 1.0.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 (277) hide show
  1. package/README.md +50 -0
  2. package/dist/cjs/AuthManager.js +361 -0
  3. package/dist/cjs/CrossDomainAuth.js +258 -0
  4. package/dist/cjs/HttpService.js +618 -0
  5. package/dist/cjs/OxyServices.base.js +263 -0
  6. package/dist/cjs/OxyServices.errors.js +22 -0
  7. package/dist/cjs/OxyServices.js +63 -0
  8. package/dist/cjs/constants/version.js +16 -0
  9. package/dist/cjs/crypto/index.js +20 -0
  10. package/dist/cjs/crypto/keyManager.js +887 -0
  11. package/dist/cjs/crypto/polyfill.js +64 -0
  12. package/dist/cjs/crypto/recoveryPhrase.js +169 -0
  13. package/dist/cjs/crypto/signatureService.js +296 -0
  14. package/dist/cjs/i18n/index.js +73 -0
  15. package/dist/cjs/i18n/locales/ar-SA.json +120 -0
  16. package/dist/cjs/i18n/locales/ca-ES.json +120 -0
  17. package/dist/cjs/i18n/locales/de-DE.json +120 -0
  18. package/dist/cjs/i18n/locales/en-US.json +956 -0
  19. package/dist/cjs/i18n/locales/es-ES.json +944 -0
  20. package/dist/cjs/i18n/locales/fr-FR.json +120 -0
  21. package/dist/cjs/i18n/locales/it-IT.json +120 -0
  22. package/dist/cjs/i18n/locales/ja-JP.json +119 -0
  23. package/dist/cjs/i18n/locales/ko-KR.json +120 -0
  24. package/dist/cjs/i18n/locales/locales/ar-SA.json +120 -0
  25. package/dist/cjs/i18n/locales/locales/ca-ES.json +120 -0
  26. package/dist/cjs/i18n/locales/locales/de-DE.json +120 -0
  27. package/dist/cjs/i18n/locales/locales/en-US.json +956 -0
  28. package/dist/cjs/i18n/locales/locales/es-ES.json +944 -0
  29. package/dist/cjs/i18n/locales/locales/fr-FR.json +120 -0
  30. package/dist/cjs/i18n/locales/locales/it-IT.json +120 -0
  31. package/dist/cjs/i18n/locales/locales/ja-JP.json +119 -0
  32. package/dist/cjs/i18n/locales/locales/ko-KR.json +120 -0
  33. package/dist/cjs/i18n/locales/locales/pt-PT.json +120 -0
  34. package/dist/cjs/i18n/locales/locales/zh-CN.json +120 -0
  35. package/dist/cjs/i18n/locales/pt-PT.json +120 -0
  36. package/dist/cjs/i18n/locales/zh-CN.json +120 -0
  37. package/dist/cjs/index.js +153 -0
  38. package/dist/cjs/mixins/OxyServices.analytics.js +49 -0
  39. package/dist/cjs/mixins/OxyServices.assets.js +380 -0
  40. package/dist/cjs/mixins/OxyServices.auth.js +259 -0
  41. package/dist/cjs/mixins/OxyServices.developer.js +97 -0
  42. package/dist/cjs/mixins/OxyServices.devices.js +116 -0
  43. package/dist/cjs/mixins/OxyServices.features.js +309 -0
  44. package/dist/cjs/mixins/OxyServices.fedcm.js +435 -0
  45. package/dist/cjs/mixins/OxyServices.karma.js +108 -0
  46. package/dist/cjs/mixins/OxyServices.language.js +154 -0
  47. package/dist/cjs/mixins/OxyServices.location.js +43 -0
  48. package/dist/cjs/mixins/OxyServices.payment.js +158 -0
  49. package/dist/cjs/mixins/OxyServices.popup.js +371 -0
  50. package/dist/cjs/mixins/OxyServices.privacy.js +162 -0
  51. package/dist/cjs/mixins/OxyServices.redirect.js +345 -0
  52. package/dist/cjs/mixins/OxyServices.security.js +81 -0
  53. package/dist/cjs/mixins/OxyServices.user.js +355 -0
  54. package/dist/cjs/mixins/OxyServices.utility.js +156 -0
  55. package/dist/cjs/mixins/index.js +79 -0
  56. package/dist/cjs/mixins/mixinHelpers.js +53 -0
  57. package/dist/cjs/models/interfaces.js +20 -0
  58. package/dist/cjs/models/session.js +2 -0
  59. package/dist/cjs/shared/index.js +70 -0
  60. package/dist/cjs/shared/utils/colorUtils.js +153 -0
  61. package/dist/cjs/shared/utils/debugUtils.js +73 -0
  62. package/dist/cjs/shared/utils/errorUtils.js +183 -0
  63. package/dist/cjs/shared/utils/index.js +49 -0
  64. package/dist/cjs/shared/utils/networkUtils.js +183 -0
  65. package/dist/cjs/shared/utils/themeUtils.js +106 -0
  66. package/dist/cjs/utils/apiUtils.js +61 -0
  67. package/dist/cjs/utils/asyncUtils.js +194 -0
  68. package/dist/cjs/utils/cache.js +226 -0
  69. package/dist/cjs/utils/deviceManager.js +205 -0
  70. package/dist/cjs/utils/errorUtils.js +154 -0
  71. package/dist/cjs/utils/index.js +26 -0
  72. package/dist/cjs/utils/languageUtils.js +165 -0
  73. package/dist/cjs/utils/loggerUtils.js +126 -0
  74. package/dist/cjs/utils/platform.js +144 -0
  75. package/dist/cjs/utils/requestUtils.js +209 -0
  76. package/dist/cjs/utils/sessionUtils.js +181 -0
  77. package/dist/cjs/utils/validationUtils.js +173 -0
  78. package/dist/esm/AuthManager.js +356 -0
  79. package/dist/esm/CrossDomainAuth.js +253 -0
  80. package/dist/esm/HttpService.js +614 -0
  81. package/dist/esm/OxyServices.base.js +259 -0
  82. package/dist/esm/OxyServices.errors.js +17 -0
  83. package/dist/esm/OxyServices.js +59 -0
  84. package/dist/esm/constants/version.js +13 -0
  85. package/dist/esm/crypto/index.js +13 -0
  86. package/dist/esm/crypto/keyManager.js +850 -0
  87. package/dist/esm/crypto/polyfill.js +61 -0
  88. package/dist/esm/crypto/recoveryPhrase.js +132 -0
  89. package/dist/esm/crypto/signatureService.js +259 -0
  90. package/dist/esm/i18n/index.js +69 -0
  91. package/dist/esm/i18n/locales/ar-SA.json +120 -0
  92. package/dist/esm/i18n/locales/ca-ES.json +120 -0
  93. package/dist/esm/i18n/locales/de-DE.json +120 -0
  94. package/dist/esm/i18n/locales/en-US.json +956 -0
  95. package/dist/esm/i18n/locales/es-ES.json +944 -0
  96. package/dist/esm/i18n/locales/fr-FR.json +120 -0
  97. package/dist/esm/i18n/locales/it-IT.json +120 -0
  98. package/dist/esm/i18n/locales/ja-JP.json +119 -0
  99. package/dist/esm/i18n/locales/ko-KR.json +120 -0
  100. package/dist/esm/i18n/locales/locales/ar-SA.json +120 -0
  101. package/dist/esm/i18n/locales/locales/ca-ES.json +120 -0
  102. package/dist/esm/i18n/locales/locales/de-DE.json +120 -0
  103. package/dist/esm/i18n/locales/locales/en-US.json +956 -0
  104. package/dist/esm/i18n/locales/locales/es-ES.json +944 -0
  105. package/dist/esm/i18n/locales/locales/fr-FR.json +120 -0
  106. package/dist/esm/i18n/locales/locales/it-IT.json +120 -0
  107. package/dist/esm/i18n/locales/locales/ja-JP.json +119 -0
  108. package/dist/esm/i18n/locales/locales/ko-KR.json +120 -0
  109. package/dist/esm/i18n/locales/locales/pt-PT.json +120 -0
  110. package/dist/esm/i18n/locales/locales/zh-CN.json +120 -0
  111. package/dist/esm/i18n/locales/pt-PT.json +120 -0
  112. package/dist/esm/i18n/locales/zh-CN.json +120 -0
  113. package/dist/esm/index.js +55 -0
  114. package/dist/esm/mixins/OxyServices.analytics.js +46 -0
  115. package/dist/esm/mixins/OxyServices.assets.js +377 -0
  116. package/dist/esm/mixins/OxyServices.auth.js +256 -0
  117. package/dist/esm/mixins/OxyServices.developer.js +94 -0
  118. package/dist/esm/mixins/OxyServices.devices.js +113 -0
  119. package/dist/esm/mixins/OxyServices.features.js +306 -0
  120. package/dist/esm/mixins/OxyServices.fedcm.js +433 -0
  121. package/dist/esm/mixins/OxyServices.karma.js +105 -0
  122. package/dist/esm/mixins/OxyServices.language.js +118 -0
  123. package/dist/esm/mixins/OxyServices.location.js +40 -0
  124. package/dist/esm/mixins/OxyServices.payment.js +155 -0
  125. package/dist/esm/mixins/OxyServices.popup.js +369 -0
  126. package/dist/esm/mixins/OxyServices.privacy.js +159 -0
  127. package/dist/esm/mixins/OxyServices.redirect.js +343 -0
  128. package/dist/esm/mixins/OxyServices.security.js +78 -0
  129. package/dist/esm/mixins/OxyServices.user.js +352 -0
  130. package/dist/esm/mixins/OxyServices.utility.js +153 -0
  131. package/dist/esm/mixins/index.js +76 -0
  132. package/dist/esm/mixins/mixinHelpers.js +48 -0
  133. package/dist/esm/models/interfaces.js +17 -0
  134. package/dist/esm/models/session.js +1 -0
  135. package/dist/esm/shared/index.js +31 -0
  136. package/dist/esm/shared/utils/colorUtils.js +143 -0
  137. package/dist/esm/shared/utils/debugUtils.js +65 -0
  138. package/dist/esm/shared/utils/errorUtils.js +170 -0
  139. package/dist/esm/shared/utils/index.js +15 -0
  140. package/dist/esm/shared/utils/networkUtils.js +173 -0
  141. package/dist/esm/shared/utils/themeUtils.js +98 -0
  142. package/dist/esm/utils/apiUtils.js +55 -0
  143. package/dist/esm/utils/asyncUtils.js +179 -0
  144. package/dist/esm/utils/cache.js +218 -0
  145. package/dist/esm/utils/deviceManager.js +168 -0
  146. package/dist/esm/utils/errorUtils.js +146 -0
  147. package/dist/esm/utils/index.js +7 -0
  148. package/dist/esm/utils/languageUtils.js +158 -0
  149. package/dist/esm/utils/loggerUtils.js +115 -0
  150. package/dist/esm/utils/platform.js +102 -0
  151. package/dist/esm/utils/requestUtils.js +203 -0
  152. package/dist/esm/utils/sessionUtils.js +171 -0
  153. package/dist/esm/utils/validationUtils.js +153 -0
  154. package/dist/types/AuthManager.d.ts +143 -0
  155. package/dist/types/CrossDomainAuth.d.ts +160 -0
  156. package/dist/types/HttpService.d.ts +163 -0
  157. package/dist/types/OxyServices.base.d.ts +126 -0
  158. package/dist/types/OxyServices.d.ts +81 -0
  159. package/dist/types/OxyServices.errors.d.ts +11 -0
  160. package/dist/types/constants/version.d.ts +13 -0
  161. package/dist/types/crypto/index.d.ts +11 -0
  162. package/dist/types/crypto/keyManager.d.ts +189 -0
  163. package/dist/types/crypto/polyfill.d.ts +11 -0
  164. package/dist/types/crypto/recoveryPhrase.d.ts +58 -0
  165. package/dist/types/crypto/signatureService.d.ts +86 -0
  166. package/dist/types/i18n/index.d.ts +3 -0
  167. package/dist/types/index.d.ts +50 -0
  168. package/dist/types/mixins/OxyServices.analytics.d.ts +66 -0
  169. package/dist/types/mixins/OxyServices.assets.d.ts +135 -0
  170. package/dist/types/mixins/OxyServices.auth.d.ts +186 -0
  171. package/dist/types/mixins/OxyServices.developer.d.ts +99 -0
  172. package/dist/types/mixins/OxyServices.devices.d.ts +96 -0
  173. package/dist/types/mixins/OxyServices.features.d.ts +228 -0
  174. package/dist/types/mixins/OxyServices.fedcm.d.ts +200 -0
  175. package/dist/types/mixins/OxyServices.karma.d.ts +85 -0
  176. package/dist/types/mixins/OxyServices.language.d.ts +81 -0
  177. package/dist/types/mixins/OxyServices.location.d.ts +64 -0
  178. package/dist/types/mixins/OxyServices.payment.d.ts +111 -0
  179. package/dist/types/mixins/OxyServices.popup.d.ts +205 -0
  180. package/dist/types/mixins/OxyServices.privacy.d.ts +122 -0
  181. package/dist/types/mixins/OxyServices.redirect.d.ts +245 -0
  182. package/dist/types/mixins/OxyServices.security.d.ts +78 -0
  183. package/dist/types/mixins/OxyServices.user.d.ts +182 -0
  184. package/dist/types/mixins/OxyServices.utility.d.ts +93 -0
  185. package/dist/types/mixins/index.d.ts +30 -0
  186. package/dist/types/mixins/mixinHelpers.d.ts +31 -0
  187. package/dist/types/models/interfaces.d.ts +415 -0
  188. package/dist/types/models/session.d.ts +27 -0
  189. package/dist/types/shared/index.d.ts +28 -0
  190. package/dist/types/shared/utils/colorUtils.d.ts +104 -0
  191. package/dist/types/shared/utils/debugUtils.d.ts +48 -0
  192. package/dist/types/shared/utils/errorUtils.d.ts +97 -0
  193. package/dist/types/shared/utils/index.d.ts +13 -0
  194. package/dist/types/shared/utils/networkUtils.d.ts +139 -0
  195. package/dist/types/shared/utils/themeUtils.d.ts +90 -0
  196. package/dist/types/utils/apiUtils.d.ts +53 -0
  197. package/dist/types/utils/asyncUtils.d.ts +58 -0
  198. package/dist/types/utils/cache.d.ts +127 -0
  199. package/dist/types/utils/deviceManager.d.ts +65 -0
  200. package/dist/types/utils/errorUtils.d.ts +46 -0
  201. package/dist/types/utils/index.d.ts +6 -0
  202. package/dist/types/utils/languageUtils.d.ts +37 -0
  203. package/dist/types/utils/loggerUtils.d.ts +48 -0
  204. package/dist/types/utils/platform.d.ts +40 -0
  205. package/dist/types/utils/requestUtils.d.ts +123 -0
  206. package/dist/types/utils/sessionUtils.d.ts +54 -0
  207. package/dist/types/utils/validationUtils.d.ts +85 -0
  208. package/package.json +84 -0
  209. package/src/AuthManager.ts +436 -0
  210. package/src/CrossDomainAuth.ts +307 -0
  211. package/src/HttpService.ts +752 -0
  212. package/src/OxyServices.base.ts +334 -0
  213. package/src/OxyServices.errors.ts +26 -0
  214. package/src/OxyServices.ts +129 -0
  215. package/src/constants/version.ts +15 -0
  216. package/src/crypto/index.ts +25 -0
  217. package/src/crypto/keyManager.ts +962 -0
  218. package/src/crypto/polyfill.ts +70 -0
  219. package/src/crypto/recoveryPhrase.ts +166 -0
  220. package/src/crypto/signatureService.ts +323 -0
  221. package/src/i18n/index.ts +75 -0
  222. package/src/i18n/locales/ar-SA.json +120 -0
  223. package/src/i18n/locales/ca-ES.json +120 -0
  224. package/src/i18n/locales/de-DE.json +120 -0
  225. package/src/i18n/locales/en-US.json +956 -0
  226. package/src/i18n/locales/es-ES.json +944 -0
  227. package/src/i18n/locales/fr-FR.json +120 -0
  228. package/src/i18n/locales/it-IT.json +120 -0
  229. package/src/i18n/locales/ja-JP.json +119 -0
  230. package/src/i18n/locales/ko-KR.json +120 -0
  231. package/src/i18n/locales/pt-PT.json +120 -0
  232. package/src/i18n/locales/zh-CN.json +120 -0
  233. package/src/index.ts +153 -0
  234. package/src/mixins/OxyServices.analytics.ts +53 -0
  235. package/src/mixins/OxyServices.assets.ts +412 -0
  236. package/src/mixins/OxyServices.auth.ts +358 -0
  237. package/src/mixins/OxyServices.developer.ts +114 -0
  238. package/src/mixins/OxyServices.devices.ts +119 -0
  239. package/src/mixins/OxyServices.features.ts +428 -0
  240. package/src/mixins/OxyServices.fedcm.ts +494 -0
  241. package/src/mixins/OxyServices.karma.ts +111 -0
  242. package/src/mixins/OxyServices.language.ts +127 -0
  243. package/src/mixins/OxyServices.location.ts +46 -0
  244. package/src/mixins/OxyServices.payment.ts +163 -0
  245. package/src/mixins/OxyServices.popup.ts +443 -0
  246. package/src/mixins/OxyServices.privacy.ts +182 -0
  247. package/src/mixins/OxyServices.redirect.ts +397 -0
  248. package/src/mixins/OxyServices.security.ts +103 -0
  249. package/src/mixins/OxyServices.user.ts +392 -0
  250. package/src/mixins/OxyServices.utility.ts +191 -0
  251. package/src/mixins/index.ts +91 -0
  252. package/src/mixins/mixinHelpers.ts +69 -0
  253. package/src/models/interfaces.ts +511 -0
  254. package/src/models/session.ts +30 -0
  255. package/src/shared/index.ts +82 -0
  256. package/src/shared/utils/colorUtils.ts +155 -0
  257. package/src/shared/utils/debugUtils.ts +73 -0
  258. package/src/shared/utils/errorUtils.ts +181 -0
  259. package/src/shared/utils/index.ts +59 -0
  260. package/src/shared/utils/networkUtils.ts +248 -0
  261. package/src/shared/utils/themeUtils.ts +115 -0
  262. package/src/types/bip39.d.ts +32 -0
  263. package/src/types/buffer.d.ts +97 -0
  264. package/src/types/color.d.ts +20 -0
  265. package/src/types/elliptic.d.ts +62 -0
  266. package/src/utils/apiUtils.ts +88 -0
  267. package/src/utils/asyncUtils.ts +252 -0
  268. package/src/utils/cache.ts +264 -0
  269. package/src/utils/deviceManager.ts +198 -0
  270. package/src/utils/errorUtils.ts +216 -0
  271. package/src/utils/index.ts +21 -0
  272. package/src/utils/languageUtils.ts +174 -0
  273. package/src/utils/loggerUtils.ts +153 -0
  274. package/src/utils/platform.ts +117 -0
  275. package/src/utils/requestUtils.ts +237 -0
  276. package/src/utils/sessionUtils.ts +206 -0
  277. package/src/utils/validationUtils.ts +174 -0
@@ -0,0 +1,494 @@
1
+ import type { OxyServicesBase } from '../OxyServices.base';
2
+ import { OxyAuthenticationError } from '../OxyServices.errors';
3
+ import type { SessionLoginResponse } from '../models/session';
4
+ import { createDebugLogger } from '../shared/utils/debugUtils';
5
+
6
+ const debug = createDebugLogger('FedCM');
7
+
8
+ export interface FedCMAuthOptions {
9
+ nonce?: string;
10
+ context?: 'signin' | 'signup' | 'continue' | 'use';
11
+ }
12
+
13
+ export interface FedCMConfig {
14
+ enabled: boolean;
15
+ configURL: string;
16
+ clientId?: string;
17
+ }
18
+
19
+ // Global lock to prevent concurrent FedCM requests
20
+ // FedCM only allows one navigator.credentials.get request at a time
21
+ let fedCMRequestInProgress = false;
22
+ let fedCMRequestPromise: Promise<any> | null = null;
23
+ let currentMediationMode: string | null = null;
24
+
25
+ /**
26
+ * Federated Credential Management (FedCM) Authentication Mixin
27
+ *
28
+ * Implements the modern browser-native identity federation API that enables
29
+ * Google-style cross-domain authentication without third-party cookies.
30
+ *
31
+ * Browser Support:
32
+ * - Chrome 108+
33
+ * - Safari 16.4+
34
+ * - Edge 108+
35
+ * - Firefox: Not yet supported (fallback required)
36
+ *
37
+ * Key Features:
38
+ * - No redirects or popups required
39
+ * - Browser-native UI prompts
40
+ * - Privacy-preserving (IdP can't track users)
41
+ * - Automatic SSO across domains
42
+ * - Silent re-authentication support
43
+ *
44
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/FedCM_API
45
+ */
46
+ export function OxyServicesFedCMMixin<T extends typeof OxyServicesBase>(Base: T) {
47
+ return class extends Base {
48
+ constructor(...args: any[]) {
49
+ super(...(args as [any]));
50
+ }
51
+ public static readonly DEFAULT_CONFIG_URL = 'https://auth.oxy.so/fedcm.json';
52
+ public static readonly FEDCM_TIMEOUT = 60000; // 1 minute for interactive
53
+ public static readonly FEDCM_SILENT_TIMEOUT = 10000; // 10 seconds for silent mediation
54
+
55
+ /**
56
+ * Check if FedCM is supported in the current browser
57
+ */
58
+ static isFedCMSupported(): boolean {
59
+ if (typeof window === 'undefined') return false;
60
+ return 'IdentityCredential' in window && 'navigator' in window && 'credentials' in navigator;
61
+ }
62
+
63
+ /**
64
+ * Instance method to check FedCM support
65
+ */
66
+ isFedCMSupported(): boolean {
67
+ return (this.constructor as typeof OxyServicesBase & { isFedCMSupported: () => boolean }).isFedCMSupported();
68
+ }
69
+
70
+ /**
71
+ * Sign in using FedCM (Federated Credential Management API)
72
+ *
73
+ * This provides a Google-style authentication experience:
74
+ * - Browser shows native "Sign in with Oxy" prompt
75
+ * - No redirect or popup required
76
+ * - User approves → credential exchange happens in browser
77
+ * - All apps automatically get SSO after first sign-in
78
+ *
79
+ * @param options - Authentication options
80
+ * @returns Session with access token and user data
81
+ * @throws {OxyAuthenticationError} If FedCM not supported or user cancels
82
+ *
83
+ * @example
84
+ * ```typescript
85
+ * try {
86
+ * const session = await oxyServices.signInWithFedCM();
87
+ * console.log('Signed in:', session.user);
88
+ * } catch (error) {
89
+ * // Fallback to popup or redirect auth
90
+ * await oxyServices.signInWithPopup();
91
+ * }
92
+ * ```
93
+ */
94
+ async signInWithFedCM(options: FedCMAuthOptions = {}): Promise<SessionLoginResponse> {
95
+ if (!this.isFedCMSupported()) {
96
+ throw new OxyAuthenticationError(
97
+ 'FedCM not supported in this browser. Please update your browser or use an alternative sign-in method.'
98
+ );
99
+ }
100
+
101
+ try {
102
+ const nonce = options.nonce || this.generateNonce();
103
+ const clientId = this.getClientId();
104
+
105
+ debug.log('Interactive sign-in: Requesting credential for', clientId);
106
+
107
+ // Request credential from browser's native identity flow
108
+ const credential = await this.requestIdentityCredential({
109
+ configURL: (this.constructor as any).DEFAULT_CONFIG_URL,
110
+ clientId,
111
+ nonce,
112
+ context: options.context,
113
+ });
114
+
115
+ if (!credential || !credential.token) {
116
+ throw new OxyAuthenticationError('No credential received from browser');
117
+ }
118
+
119
+ debug.log('Interactive sign-in: Got credential, exchanging for session');
120
+
121
+ // Exchange FedCM ID token for Oxy session
122
+ const session = await this.exchangeIdTokenForSession(credential.token);
123
+
124
+ // Store access token in HttpService (extract from response or get from session)
125
+ if (session && (session as any).accessToken) {
126
+ this.httpService.setTokens((session as any).accessToken);
127
+ }
128
+
129
+ debug.log('Interactive sign-in: Success!', { userId: (session as any)?.user?.id });
130
+
131
+ return session;
132
+ } catch (error) {
133
+ debug.log('Interactive sign-in failed:', error);
134
+ if ((error as any).name === 'AbortError') {
135
+ throw new OxyAuthenticationError('Sign-in was cancelled by user');
136
+ }
137
+ if ((error as any).name === 'NetworkError') {
138
+ throw new OxyAuthenticationError('Network error during sign-in. Please check your connection.');
139
+ }
140
+ throw error;
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Silent sign-in using FedCM
146
+ *
147
+ * Attempts to automatically re-authenticate the user without any UI.
148
+ * This is what enables "instant sign-in" across all Oxy domains after
149
+ * the user has signed in once.
150
+ *
151
+ * The browser will:
152
+ * 1. Check if user has previously signed in to Oxy
153
+ * 2. Check if user is still signed in at auth.oxy.so
154
+ * 3. If yes, automatically provide credential without prompting
155
+ *
156
+ * @returns Session if user is already signed in, null otherwise
157
+ *
158
+ * @example
159
+ * ```typescript
160
+ * // On app startup
161
+ * useEffect(() => {
162
+ * const checkAuth = async () => {
163
+ * const session = await oxyServices.silentSignInWithFedCM();
164
+ * if (session) {
165
+ * setUser(session.user);
166
+ * } else {
167
+ * // Show sign-in button
168
+ * }
169
+ * };
170
+ * checkAuth();
171
+ * }, []);
172
+ * ```
173
+ */
174
+ async silentSignInWithFedCM(): Promise<SessionLoginResponse | null> {
175
+ if (!this.isFedCMSupported()) {
176
+ debug.log('Silent SSO: FedCM not supported in this browser');
177
+ return null;
178
+ }
179
+
180
+ const clientId = this.getClientId();
181
+ debug.log('Silent SSO: Starting for', clientId);
182
+
183
+ // First try silent mediation (no UI) - works if user previously consented
184
+ let credential: { token: string } | null = null;
185
+
186
+ try {
187
+ const nonce = this.generateNonce();
188
+ debug.log('Silent SSO: Attempting silent mediation...');
189
+
190
+ credential = await this.requestIdentityCredential({
191
+ configURL: (this.constructor as any).DEFAULT_CONFIG_URL,
192
+ clientId,
193
+ nonce,
194
+ mediation: 'silent',
195
+ });
196
+
197
+ debug.log('Silent SSO: Silent mediation result:', { hasCredential: !!credential, hasToken: !!credential?.token });
198
+ } catch (silentError) {
199
+ // Silent mediation failed - this is expected if user hasn't consented before or is in quiet period
200
+ const errorName = silentError instanceof Error ? silentError.name : 'Unknown';
201
+ const errorMessage = silentError instanceof Error ? silentError.message : String(silentError);
202
+ debug.log('Silent SSO: Silent mediation error (will try optional):', { name: errorName, message: errorMessage });
203
+ }
204
+
205
+ // If silent failed, try optional mediation which shows browser UI if needed
206
+ if (!credential || !credential.token) {
207
+ try {
208
+ const nonce = this.generateNonce();
209
+ debug.log('Silent SSO: Trying optional mediation (may show browser UI)...');
210
+
211
+ credential = await this.requestIdentityCredential({
212
+ configURL: (this.constructor as any).DEFAULT_CONFIG_URL,
213
+ clientId,
214
+ nonce,
215
+ mediation: 'optional',
216
+ });
217
+
218
+ debug.log('Silent SSO: Optional mediation result:', { hasCredential: !!credential, hasToken: !!credential?.token });
219
+ } catch (optionalError) {
220
+ const errorName = optionalError instanceof Error ? optionalError.name : 'Unknown';
221
+ const errorMessage = optionalError instanceof Error ? optionalError.message : String(optionalError);
222
+ debug.log('Silent SSO: Optional mediation also failed:', { name: errorName, message: errorMessage });
223
+ return null;
224
+ }
225
+ }
226
+
227
+ if (!credential || !credential.token) {
228
+ debug.log('Silent SSO: No credential returned (user may have dismissed prompt or is not logged in at IdP)');
229
+ return null;
230
+ }
231
+
232
+ debug.log('Silent SSO: Got credential, exchanging for session...');
233
+
234
+ let session: SessionLoginResponse;
235
+ try {
236
+ session = await this.exchangeIdTokenForSession(credential.token);
237
+ } catch (exchangeError) {
238
+ debug.error('Silent SSO: Token exchange failed:', exchangeError);
239
+ return null;
240
+ }
241
+
242
+ // Validate session response has required fields
243
+ if (!session) {
244
+ debug.error('Silent SSO: Exchange returned null session');
245
+ return null;
246
+ }
247
+
248
+ if (!session.sessionId) {
249
+ debug.error('Silent SSO: Exchange returned session without sessionId:', session);
250
+ return null;
251
+ }
252
+
253
+ if (!session.user) {
254
+ debug.error('Silent SSO: Exchange returned session without user:', session);
255
+ return null;
256
+ }
257
+
258
+ // Set the access token
259
+ if ((session as any).accessToken) {
260
+ this.httpService.setTokens((session as any).accessToken);
261
+ debug.log('Silent SSO: Access token set');
262
+ } else {
263
+ debug.warn('Silent SSO: No accessToken in session response');
264
+ }
265
+
266
+ debug.log('Silent SSO: Success!', {
267
+ sessionId: session.sessionId?.substring(0, 8) + '...',
268
+ userId: session.user?.id
269
+ });
270
+
271
+ return session;
272
+ }
273
+
274
+ /**
275
+ * Request identity credential from browser using FedCM API
276
+ *
277
+ * Uses a global lock to prevent concurrent requests, as FedCM only
278
+ * allows one navigator.credentials.get request at a time.
279
+ *
280
+ * Interactive requests (optional/required) wait for any silent request to finish first.
281
+ *
282
+ * @private
283
+ */
284
+ public async requestIdentityCredential(options: {
285
+ configURL: string;
286
+ clientId: string;
287
+ nonce: string;
288
+ context?: string;
289
+ mediation?: 'silent' | 'optional' | 'required';
290
+ }): Promise<{ token: string } | null> {
291
+ const requestedMediation = options.mediation || 'optional';
292
+ const isInteractive = requestedMediation !== 'silent';
293
+
294
+ debug.log('requestIdentityCredential called:', {
295
+ mediation: requestedMediation,
296
+ clientId: options.clientId,
297
+ inProgress: fedCMRequestInProgress,
298
+ });
299
+
300
+ // If a request is already in progress...
301
+ if (fedCMRequestInProgress && fedCMRequestPromise) {
302
+ debug.log('Request already in progress, waiting...');
303
+ // If current request is silent and new request is interactive,
304
+ // wait for silent to finish, then make the interactive request
305
+ if (currentMediationMode === 'silent' && isInteractive) {
306
+ try {
307
+ await fedCMRequestPromise;
308
+ } catch {
309
+ // Ignore silent request errors
310
+ }
311
+ // Now fall through to make the interactive request
312
+ } else {
313
+ // Same type of request - wait for the existing one
314
+ try {
315
+ return await fedCMRequestPromise;
316
+ } catch {
317
+ return null;
318
+ }
319
+ }
320
+ }
321
+
322
+ fedCMRequestInProgress = true;
323
+ currentMediationMode = requestedMediation;
324
+ const controller = new AbortController();
325
+ // Use shorter timeout for silent mediation since it should be quick
326
+ const timeoutMs = requestedMediation === 'silent'
327
+ ? (this.constructor as any).FEDCM_SILENT_TIMEOUT
328
+ : (this.constructor as any).FEDCM_TIMEOUT;
329
+ const timeout = setTimeout(() => {
330
+ debug.log('Request timed out after', timeoutMs, 'ms (mediation:', requestedMediation + ')');
331
+ controller.abort();
332
+ }, timeoutMs);
333
+
334
+ fedCMRequestPromise = (async () => {
335
+ try {
336
+ debug.log('Calling navigator.credentials.get with mediation:', requestedMediation);
337
+ // Type assertion needed as FedCM types may not be in all TypeScript versions
338
+ const credential = (await (navigator.credentials as any).get({
339
+ identity: {
340
+ providers: [
341
+ {
342
+ configURL: options.configURL,
343
+ clientId: options.clientId,
344
+ // Send nonce at both levels for backward compatibility
345
+ nonce: options.nonce, // For older browsers
346
+ params: {
347
+ nonce: options.nonce, // For Chrome 145+
348
+ },
349
+ ...(options.context && { loginHint: options.context }),
350
+ },
351
+ ],
352
+ },
353
+ mediation: requestedMediation,
354
+ signal: controller.signal,
355
+ })) as any;
356
+
357
+ debug.log('navigator.credentials.get returned:', {
358
+ hasCredential: !!credential,
359
+ type: credential?.type,
360
+ hasToken: !!credential?.token,
361
+ });
362
+
363
+ if (!credential || credential.type !== 'identity') {
364
+ debug.log('No valid identity credential returned');
365
+ return null;
366
+ }
367
+
368
+ debug.log('Got valid identity credential with token');
369
+ return { token: credential.token };
370
+ } catch (error) {
371
+ const errorName = error instanceof Error ? error.name : 'Unknown';
372
+ const errorMessage = error instanceof Error ? error.message : String(error);
373
+ debug.log('navigator.credentials.get error:', { name: errorName, message: errorMessage });
374
+ throw error;
375
+ } finally {
376
+ clearTimeout(timeout);
377
+ fedCMRequestInProgress = false;
378
+ fedCMRequestPromise = null;
379
+ currentMediationMode = null;
380
+ }
381
+ })();
382
+
383
+ return fedCMRequestPromise;
384
+ }
385
+
386
+ /**
387
+ * Exchange FedCM ID token for Oxy session
388
+ *
389
+ * The ID token is a JWT issued by auth.oxy.so that proves the user's
390
+ * identity. We exchange it for a full Oxy session with access token.
391
+ *
392
+ * @private
393
+ */
394
+ public async exchangeIdTokenForSession(idToken: string): Promise<SessionLoginResponse> {
395
+ debug.log('exchangeIdTokenForSession: Starting exchange...');
396
+ debug.log('exchangeIdTokenForSession: Token length:', idToken?.length);
397
+ debug.log('exchangeIdTokenForSession: Token preview:', idToken?.substring(0, 50) + '...');
398
+
399
+ try {
400
+ const response = await this.makeRequest<SessionLoginResponse>(
401
+ 'POST',
402
+ '/api/fedcm/exchange',
403
+ { id_token: idToken },
404
+ { cache: false }
405
+ );
406
+
407
+ debug.log('exchangeIdTokenForSession: Response received:', {
408
+ hasResponse: !!response,
409
+ hasSessionId: !!(response as any)?.sessionId,
410
+ hasUser: !!(response as any)?.user,
411
+ hasAccessToken: !!(response as any)?.accessToken,
412
+ userId: (response as any)?.user?.id,
413
+ username: (response as any)?.user?.username,
414
+ responseKeys: response ? Object.keys(response) : [],
415
+ });
416
+
417
+ return response;
418
+ } catch (error) {
419
+ debug.error('exchangeIdTokenForSession: Error:', {
420
+ name: error instanceof Error ? error.name : 'Unknown',
421
+ message: error instanceof Error ? error.message : String(error),
422
+ stack: error instanceof Error ? error.stack : undefined,
423
+ });
424
+ throw error;
425
+ }
426
+ }
427
+
428
+ /**
429
+ * Revoke FedCM credential (sign out)
430
+ *
431
+ * This tells the browser to forget the FedCM credential for this app.
432
+ * The user will need to re-authenticate next time.
433
+ */
434
+ async revokeFedCMCredential(): Promise<void> {
435
+ if (!this.isFedCMSupported()) {
436
+ return;
437
+ }
438
+
439
+ try {
440
+ // FedCM logout API (if available)
441
+ if ('IdentityCredential' in window && 'logout' in (window as any).IdentityCredential) {
442
+ const clientId = this.getClientId();
443
+ await (window as any).IdentityCredential.logout({
444
+ configURL: (this.constructor as any).DEFAULT_CONFIG_URL,
445
+ clientId,
446
+ });
447
+ }
448
+ } catch (error) {
449
+ // Silent failure
450
+ }
451
+ }
452
+
453
+ /**
454
+ * Get configuration for FedCM
455
+ *
456
+ * @returns FedCM configuration with browser support info
457
+ */
458
+ getFedCMConfig(): FedCMConfig {
459
+ return {
460
+ enabled: this.isFedCMSupported(),
461
+ configURL: (this.constructor as any).DEFAULT_CONFIG_URL,
462
+ clientId: this.getClientId(),
463
+ };
464
+ }
465
+
466
+ /**
467
+ * Generate a cryptographically secure nonce for FedCM
468
+ *
469
+ * @private
470
+ */
471
+ public generateNonce(): string {
472
+ if (typeof window !== 'undefined' && window.crypto && window.crypto.randomUUID) {
473
+ return window.crypto.randomUUID();
474
+ }
475
+ // Fallback for older browsers
476
+ return `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
477
+ }
478
+
479
+ /**
480
+ * Get the client ID for this origin
481
+ *
482
+ * @private
483
+ */
484
+ public getClientId(): string {
485
+ if (typeof window === 'undefined') {
486
+ return 'unknown';
487
+ }
488
+ return window.location.origin;
489
+ }
490
+ };
491
+ }
492
+
493
+ // Export the mixin function as both named and default
494
+ export { OxyServicesFedCMMixin as FedCMMixin };
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Karma Methods Mixin
3
+ *
4
+ * Provides methods for karma system management
5
+ */
6
+ import type { OxyServicesBase } from '../OxyServices.base';
7
+ import { CACHE_TIMES } from './mixinHelpers';
8
+
9
+ export function OxyServicesKarmaMixin<T extends typeof OxyServicesBase>(Base: T) {
10
+ return class extends Base {
11
+ constructor(...args: any[]) {
12
+ super(...(args as [any]));
13
+ }
14
+ /**
15
+ * Get user karma
16
+ */
17
+ async getUserKarma(userId: string): Promise<any> {
18
+ try {
19
+ return await this.makeRequest('GET', `/api/karma/${userId}`, undefined, {
20
+ cache: true,
21
+ cacheTTL: 2 * 60 * 1000, // 2 minutes cache
22
+ });
23
+ } catch (error) {
24
+ throw this.handleError(error);
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Give karma to user
30
+ */
31
+ async giveKarma(userId: string, amount: number, reason?: string): Promise<any> {
32
+ try {
33
+ return await this.makeRequest('POST', `/api/karma/${userId}/give`, {
34
+ amount,
35
+ reason
36
+ }, { cache: false });
37
+ } catch (error) {
38
+ throw this.handleError(error);
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Get user karma total
44
+ * @param userId - The user ID
45
+ * @returns User karma total
46
+ */
47
+ async getUserKarmaTotal(userId: string): Promise<any> {
48
+ try {
49
+ return await this.makeRequest('GET', `/api/karma/${userId}/total`, undefined, {
50
+ cache: true,
51
+ cacheTTL: CACHE_TIMES.MEDIUM,
52
+ });
53
+ } catch (error) {
54
+ throw this.handleError(error);
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Get user karma history
60
+ * @param userId - The user ID
61
+ * @param limit - Optional limit for results
62
+ * @param offset - Optional offset for pagination
63
+ * @returns User karma history
64
+ */
65
+ async getUserKarmaHistory(userId: string, limit?: number, offset?: number): Promise<any> {
66
+ try {
67
+ const params: any = {};
68
+ if (limit) params.limit = limit;
69
+ if (offset) params.offset = offset;
70
+
71
+ return await this.makeRequest('GET', `/api/karma/${userId}/history`, params, {
72
+ cache: true,
73
+ cacheTTL: CACHE_TIMES.MEDIUM,
74
+ });
75
+ } catch (error) {
76
+ throw this.handleError(error);
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Get karma leaderboard
82
+ * @returns Karma leaderboard
83
+ */
84
+ async getKarmaLeaderboard(): Promise<any> {
85
+ try {
86
+ return await this.makeRequest('GET', '/api/karma/leaderboard', undefined, {
87
+ cache: true,
88
+ cacheTTL: CACHE_TIMES.LONG,
89
+ });
90
+ } catch (error) {
91
+ throw this.handleError(error);
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Get karma rules
97
+ * @returns Karma rules
98
+ */
99
+ async getKarmaRules(): Promise<any> {
100
+ try {
101
+ return await this.makeRequest('GET', '/api/karma/rules', undefined, {
102
+ cache: true,
103
+ cacheTTL: CACHE_TIMES.EXTRA_LONG, // Rules don't change often
104
+ });
105
+ } catch (error) {
106
+ throw this.handleError(error);
107
+ }
108
+ }
109
+ };
110
+ }
111
+