@oxyhq/core 3.2.0 → 3.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/AuthManager.js +3 -1
  3. package/dist/cjs/HttpService.js +89 -0
  4. package/dist/cjs/OxyServices.js +1 -1
  5. package/dist/cjs/constants/version.js +1 -1
  6. package/dist/cjs/i18n/locales/en-US.json +44 -44
  7. package/dist/cjs/i18n/locales/es-ES.json +44 -44
  8. package/dist/cjs/i18n/locales/locales/en-US.json +44 -44
  9. package/dist/cjs/i18n/locales/locales/es-ES.json +44 -44
  10. package/dist/cjs/index.js +4 -0
  11. package/dist/cjs/mixins/OxyServices.applications.js +3 -1
  12. package/dist/cjs/mixins/OxyServices.reputation.js +244 -0
  13. package/dist/cjs/mixins/OxyServices.workspaces.js +3 -1
  14. package/dist/cjs/mixins/index.js +2 -2
  15. package/dist/cjs/utils/accountUtils.js +12 -5
  16. package/dist/cjs/utils/ssoReturn.js +80 -33
  17. package/dist/esm/.tsbuildinfo +1 -1
  18. package/dist/esm/AuthManager.js +3 -1
  19. package/dist/esm/HttpService.js +89 -0
  20. package/dist/esm/OxyServices.js +1 -1
  21. package/dist/esm/constants/version.js +1 -1
  22. package/dist/esm/i18n/locales/en-US.json +44 -44
  23. package/dist/esm/i18n/locales/es-ES.json +44 -44
  24. package/dist/esm/i18n/locales/locales/en-US.json +44 -44
  25. package/dist/esm/i18n/locales/locales/es-ES.json +44 -44
  26. package/dist/esm/index.js +4 -0
  27. package/dist/esm/mixins/OxyServices.applications.js +3 -1
  28. package/dist/esm/mixins/OxyServices.reputation.js +241 -0
  29. package/dist/esm/mixins/OxyServices.workspaces.js +3 -1
  30. package/dist/esm/mixins/index.js +2 -2
  31. package/dist/esm/utils/accountUtils.js +12 -5
  32. package/dist/esm/utils/ssoReturn.js +80 -33
  33. package/dist/types/.tsbuildinfo +1 -1
  34. package/dist/types/HttpService.d.ts +57 -0
  35. package/dist/types/OxyServices.d.ts +1 -1
  36. package/dist/types/constants/version.d.ts +2 -2
  37. package/dist/types/index.d.ts +2 -1
  38. package/dist/types/mixins/OxyServices.applications.d.ts +8 -2
  39. package/dist/types/mixins/OxyServices.features.d.ts +0 -1
  40. package/dist/types/mixins/OxyServices.reputation.d.ts +436 -0
  41. package/dist/types/mixins/OxyServices.workspaces.d.ts +8 -2
  42. package/dist/types/mixins/index.d.ts +2 -2
  43. package/dist/types/models/interfaces.d.ts +15 -26
  44. package/dist/types/utils/accountUtils.d.ts +17 -4
  45. package/dist/types/utils/ssoReturn.d.ts +30 -9
  46. package/package.json +2 -1
  47. package/src/AuthManager.ts +3 -1
  48. package/src/HttpService.ts +91 -0
  49. package/src/OxyServices.ts +1 -1
  50. package/src/__tests__/httpServiceCache.test.ts +198 -0
  51. package/src/constants/version.ts +1 -1
  52. package/src/i18n/locales/en-US.json +44 -44
  53. package/src/i18n/locales/es-ES.json +44 -44
  54. package/src/index.ts +32 -4
  55. package/src/mixins/OxyServices.applications.ts +8 -2
  56. package/src/mixins/OxyServices.auth.ts +2 -1
  57. package/src/mixins/OxyServices.features.ts +0 -1
  58. package/src/mixins/OxyServices.reputation.ts +674 -0
  59. package/src/mixins/OxyServices.workspaces.ts +8 -2
  60. package/src/mixins/__tests__/reputation.test.ts +408 -0
  61. package/src/mixins/index.ts +3 -3
  62. package/src/models/interfaces.ts +16 -32
  63. package/src/utils/__tests__/accountUtils.test.ts +142 -0
  64. package/src/utils/__tests__/consumeSsoReturn.test.ts +229 -37
  65. package/src/utils/accountUtils.ts +20 -5
  66. package/src/utils/ssoReturn.ts +98 -37
  67. package/dist/cjs/mixins/OxyServices.developer.js +0 -97
  68. package/dist/cjs/mixins/OxyServices.karma.js +0 -108
  69. package/dist/esm/mixins/OxyServices.developer.js +0 -94
  70. package/dist/esm/mixins/OxyServices.karma.js +0 -105
  71. package/dist/types/mixins/OxyServices.developer.d.ts +0 -106
  72. package/dist/types/mixins/OxyServices.karma.d.ts +0 -92
  73. package/src/mixins/OxyServices.karma.ts +0 -111
@@ -87,7 +87,9 @@ function OxyServicesWorkspacesMixin(Base) {
87
87
  /**
88
88
  * Add a member to a workspace.
89
89
  * @param workspaceId - The workspace's Mongo `_id`.
90
- * @param data - Target user id and role (never `owner`).
90
+ * @param data - Target user's username or email and role (never `owner`).
91
+ * The server resolves `usernameOrEmail` to a user; an unknown value yields
92
+ * a 404 "User not found".
91
93
  */
92
94
  async inviteWorkspaceMember(workspaceId, data) {
93
95
  try {
@@ -18,7 +18,7 @@ const OxyServices_user_1 = require("./OxyServices.user");
18
18
  const OxyServices_privacy_1 = require("./OxyServices.privacy");
19
19
  const OxyServices_language_1 = require("./OxyServices.language");
20
20
  const OxyServices_payment_1 = require("./OxyServices.payment");
21
- const OxyServices_karma_1 = require("./OxyServices.karma");
21
+ const OxyServices_reputation_1 = require("./OxyServices.reputation");
22
22
  const OxyServices_assets_1 = require("./OxyServices.assets");
23
23
  const OxyServices_applications_1 = require("./OxyServices.applications");
24
24
  const OxyServices_workspaces_1 = require("./OxyServices.workspaces");
@@ -63,7 +63,7 @@ const MIXIN_PIPELINE = [
63
63
  // Feature mixins
64
64
  OxyServices_language_1.OxyServicesLanguageMixin,
65
65
  OxyServices_payment_1.OxyServicesPaymentMixin,
66
- OxyServices_karma_1.OxyServicesKarmaMixin,
66
+ OxyServices_reputation_1.OxyServicesReputationMixin,
67
67
  OxyServices_assets_1.OxyServicesAssetsMixin,
68
68
  OxyServices_applications_1.OxyServicesApplicationsMixin,
69
69
  OxyServices_workspaces_1.OxyServicesWorkspacesMixin,
@@ -21,11 +21,16 @@ exports.formatPublicKeyHandle = formatPublicKeyHandle;
21
21
  * Resolve a friendly display name for a user.
22
22
  *
23
23
  * Order of preference:
24
- * 1. `name.full`, or composed `name.first name.last`
24
+ * 1. `name.full`, or composed `name.first name.last` (FIRST-NAME-ONLY SAFE —
25
+ * a user with only a first name resolves to that first name, never to the
26
+ * lowercase username; this is the exact drift bug the auth app hit).
25
27
  * 2. `name` (when stored as a plain string)
26
- * 3. `username`
27
- * 4. `Account 0x12345678…` (derived from publicKey, when present)
28
- * 5. Translated fallback (e.g. "Unnamed")
28
+ * 3. `displayName` (server `displayName` virtual — `username || truncatedKey`).
29
+ * Placed AFTER the structured name on purpose: the server virtual ignores
30
+ * `name`, so preferring it first would re-introduce the first-only bug.
31
+ * 4. `username`
32
+ * 5. `Account 0x12345678…` (derived from publicKey, when present)
33
+ * 6. Translated fallback (e.g. "Unnamed")
29
34
  *
30
35
  * The translation key `common.unnamed` is used for the final fallback. If the
31
36
  * caller does not pass a locale, the default English translation is used.
@@ -33,7 +38,7 @@ exports.formatPublicKeyHandle = formatPublicKeyHandle;
33
38
  const getAccountDisplayName = (user, locale) => {
34
39
  if (!user)
35
40
  return (0, i18n_1.translate)(locale, 'common.unnamed');
36
- const { name, username, publicKey } = user;
41
+ const { name, displayName, username, publicKey } = user;
37
42
  if (name && typeof name === 'object') {
38
43
  if (typeof name.full === 'string' && name.full.trim())
39
44
  return name.full.trim();
@@ -46,6 +51,8 @@ const getAccountDisplayName = (user, locale) => {
46
51
  else if (typeof name === 'string' && name.trim()) {
47
52
  return name.trim();
48
53
  }
54
+ if (typeof displayName === 'string' && displayName.trim())
55
+ return displayName.trim();
49
56
  if (typeof username === 'string' && username.trim())
50
57
  return username.trim();
51
58
  if (typeof publicKey === 'string' && publicKey.length > 0) {
@@ -96,14 +96,22 @@ function parseSsoReturnFragment(hash) {
96
96
  * treated exactly like "no session" (never loops, never rethrows).
97
97
  * - On EVERY consumed outcome (ok, none, error, state-mismatch, no-code,
98
98
  * failed-exchange, no-sessionId) — not just ok — if the page landed on
99
- * {@link SSO_CALLBACK_PATH}, the real pre-bounce destination is restored
100
- * from the DEST key so the user is never stranded on the internal callback
101
- * path. Same-origin only (an attacker-planted cross-origin or relative-evil
102
- * dest is rejected). The DEST key is removed unconditionally.
103
- * - After a same-origin dest restore (which uses `history.replaceState`, that
104
- * does NOT itself emit `popstate`), a synthetic `popstate` is dispatched so
105
- * URL-driven routers (Expo Router / React Navigation web) re-sync to the
106
- * restored route. It is NOT dispatched when the dest is rejected/absent.
99
+ * {@link SSO_CALLBACK_PATH}, the user is taken to a same-origin TARGET so
100
+ * they are never stranded on the internal callback path (which is an
101
+ * unregistered route in every consumer router a hard 404). The target is
102
+ * the stored DEST when it parses as same-origin (an attacker-planted
103
+ * cross-origin / protocol-relative dest is rejected), ELSE the app root
104
+ * (`origin + '/'`). The DEST key is removed unconditionally.
105
+ * - For the `ok` outcome the target is applied via a SOFT
106
+ * `history.replaceState` + synthetic `popstate` so the freshly exchanged
107
+ * in-memory session the provider is about to commit is preserved (no
108
+ * reload). `popstate` is dispatched only on the `ok` same-origin restore.
109
+ * - For every NON-`ok` outcome there is no in-memory session to preserve, and
110
+ * the consumer router has ALREADY synchronously rendered its 404 for the
111
+ * unregistered callback route — a soft replaceState+popstate does not
112
+ * reliably make it re-resolve. So these outcomes perform a HARD
113
+ * full-document navigation to the target (`hardRedirect`), which is both
114
+ * safe (nothing to lose) and guaranteed to clear the 404 in every router.
107
115
  *
108
116
  * Total: this function NEVER throws. Off-web it is a no-op returning `null`.
109
117
  *
@@ -137,6 +145,17 @@ async function consumeSsoReturn(oxy, deps = {}) {
137
145
  window.dispatchEvent(new Event('popstate'));
138
146
  }
139
147
  });
148
+ // Default: a hard, full-document navigation used to leave the callback path
149
+ // on non-`ok` outcomes. Feature-detected end to end so it never throws in any
150
+ // environment (SSR / native / a stubbed location without `replace`).
151
+ const hardRedirect = deps.hardRedirect ??
152
+ ((url) => {
153
+ if (typeof window !== 'undefined' &&
154
+ window.location &&
155
+ typeof window.location.replace === 'function') {
156
+ window.location.replace(url);
157
+ }
158
+ });
140
159
  const ret = parseSsoReturnFragment(location.hash);
141
160
  if (!ret) {
142
161
  // Not an oxy_sso fragment — nothing to do (do NOT touch any flags).
@@ -159,42 +178,62 @@ async function consumeSsoReturn(oxy, deps = {}) {
159
178
  // even if some consumer path skipped setting it pre-bounce.
160
179
  storage.setItem((0, ssoBounce_1.ssoAttemptedKey)(origin), '1');
161
180
  };
162
- // Restore the user's real pre-bounce destination so they are never stranded
163
- // on the internal callback path invoked on EVERY consumed outcome, not just
164
- // success. Same-origin only never honour a cross-origin/protocol-relative
165
- // dest that could have been planted to redirect the user. The DEST key is
166
- // removed unconditionally. After a successful same-origin restore a synthetic
167
- // `popstate` is dispatched so URL-driven routers re-sync.
168
- const restoreDest = () => {
169
- if (location.pathname === ssoBounce_1.SSO_CALLBACK_PATH) {
170
- const dest = storage.getItem((0, ssoBounce_1.ssoDestKey)(origin));
171
- if (dest) {
172
- try {
173
- const destUrl = new URL(dest, origin);
174
- if (destUrl.origin === origin) {
175
- history.replaceState(null, '', destUrl.pathname + destUrl.search + destUrl.hash);
176
- dispatchPopState();
177
- }
178
- }
179
- catch {
180
- // Malformed stored destination — leave the URL on the callback path.
181
+ // Compute the same-origin TARGET to leave the callback path for. Returns the
182
+ // stored DEST when present AND it parses as same-origin (never honour a
183
+ // cross-origin / protocol-relative dest that could have been planted to
184
+ // redirect the user), ELSE the app root (`origin + '/'`) so the user is never
185
+ // stranded on the internal callback path even when no dest was stored. The
186
+ // DEST key is removed unconditionally. Returns the relative path+search+hash
187
+ // (so it can be fed to either `history.replaceState` or a `hardRedirect`),
188
+ // or `null` when the page is not on the callback path (nothing to leave).
189
+ const consumeCallbackTarget = () => {
190
+ if (location.pathname !== ssoBounce_1.SSO_CALLBACK_PATH) {
191
+ // Not on the callback path — still drop the dest key (consumed) but there
192
+ // is nothing to navigate away from.
193
+ storage.removeItem((0, ssoBounce_1.ssoDestKey)(origin));
194
+ return null;
195
+ }
196
+ const dest = storage.getItem((0, ssoBounce_1.ssoDestKey)(origin));
197
+ storage.removeItem((0, ssoBounce_1.ssoDestKey)(origin));
198
+ if (dest) {
199
+ try {
200
+ const destUrl = new URL(dest, origin);
201
+ if (destUrl.origin === origin) {
202
+ return destUrl.pathname + destUrl.search + destUrl.hash;
181
203
  }
182
204
  }
205
+ catch {
206
+ // Malformed stored destination — fall through to the app-root fallback.
207
+ }
208
+ }
209
+ // No dest, a cross-origin/protocol-relative dest, or an unparseable dest:
210
+ // fall back to the app root so the router always leaves the 404.
211
+ return '/';
212
+ };
213
+ // Non-`ok` outcomes: there is no in-memory session to preserve, and the
214
+ // consumer router has already rendered its 404 for the unregistered callback
215
+ // route — a soft replaceState+popstate does not reliably make it re-resolve.
216
+ // Perform a HARD full-document navigation to the target (safe: nothing to
217
+ // lose; guaranteed: every router leaves the 404). Off the callback path this
218
+ // is a no-op (target is null).
219
+ const leaveCallbackHard = () => {
220
+ const target = consumeCallbackTarget();
221
+ if (target !== null) {
222
+ hardRedirect(origin + target);
183
223
  }
184
- storage.removeItem((0, ssoBounce_1.ssoDestKey)(origin));
185
224
  };
186
225
  if (ret.kind === 'none' || ret.kind === 'error') {
187
226
  // The central IdP had no session (or the bounce failed). Record it so we do
188
227
  // not bounce again this tab — the definitive loop breaker.
189
228
  markNoSession();
190
- restoreDest();
229
+ leaveCallbackHard();
191
230
  return null;
192
231
  }
193
232
  if (!stateOk || !ret.code) {
194
233
  // Forged / replayed / stale fragment, or a malformed ok with no code. Treat
195
234
  // exactly like "no session": never exchange, never loop.
196
235
  markNoSession();
197
- restoreDest();
236
+ leaveCallbackHard();
198
237
  return null;
199
238
  }
200
239
  let session;
@@ -204,14 +243,22 @@ async function consumeSsoReturn(oxy, deps = {}) {
204
243
  catch (error) {
205
244
  onExchangeError?.(error);
206
245
  markNoSession();
207
- restoreDest();
246
+ leaveCallbackHard();
208
247
  return null;
209
248
  }
210
249
  if (!session?.sessionId) {
211
250
  markNoSession();
212
- restoreDest();
251
+ leaveCallbackHard();
213
252
  return null;
214
253
  }
215
- restoreDest();
254
+ // `ok`: the provider is about to commit the freshly exchanged in-memory
255
+ // session — do NOT hard-redirect (a full navigation would discard it). Use a
256
+ // SOFT `history.replaceState` to the target + a synthetic `popstate` so
257
+ // URL-driven routers re-sync to the restored route without a reload.
258
+ const target = consumeCallbackTarget();
259
+ if (target !== null) {
260
+ history.replaceState(null, '', target);
261
+ dispatchPopState();
262
+ }
216
263
  return session;
217
264
  }