@oxyhq/core 3.4.8 → 3.4.9

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.
@@ -204,9 +204,11 @@ export class HttpService {
204
204
  const fullUrl = this.buildURL(url, params);
205
205
  // Get auth token (with auto-refresh)
206
206
  const authHeader = await this.getAuthHeader();
207
- // Get CSRF token for state-changing requests
207
+ // CSRF protects cookie-authenticated browser writes. Bearer-authenticated
208
+ // SDK clients are not vulnerable to ambient-cookie CSRF, and linked app
209
+ // APIs should not need to implement a duplicate `/csrf-token` route.
208
210
  const isStateChangingMethod = ['POST', 'PUT', 'PATCH', 'DELETE'].includes(method);
209
- const csrfToken = isStateChangingMethod ? await this.fetchCsrfToken() : null;
211
+ const csrfToken = isStateChangingMethod && !authHeader ? await this.fetchCsrfToken() : null;
210
212
  // Determine if data is FormData using robust detection
211
213
  const isFormData = this.isFormData(data);
212
214
  // Make fetch request
@@ -237,10 +239,8 @@ export class HttpService {
237
239
  if (isNativeApp && isStateChangingMethod) {
238
240
  headers['X-Native-App'] = 'true';
239
241
  }
240
- // Debug logging for CSRF issues routed through the SimpleLogger so
241
- // it only fires when consumers opt in via `enableLogging`. Previously
242
- // this was a bare console.log that leaked noise into every host app's
243
- // stdout in development.
242
+ // Debug logging for CSRF issues, routed through SimpleLogger so it only
243
+ // fires when consumers opt in via `enableLogging`.
244
244
  if (isStateChangingMethod) {
245
245
  this.logger.debug('CSRF Debug:', {
246
246
  url,
package/dist/esm/index.js CHANGED
@@ -30,6 +30,7 @@ export { AuthManager, createAuthManager } from './AuthManager.js';
30
30
  export { CrossDomainAuth, createCrossDomainAuth } from './CrossDomainAuth.js';
31
31
  export { ServiceCredentialMismatchError } from './mixins/OxyServices.auth.js';
32
32
  export { OxyAppDataIdentifierError } from './mixins/OxyServices.appData.js';
33
+ export { getNormalizedUserId, normalizeUserIdentity, normalizeUserIdentityOrNull } from './utils/userIdentity.js';
33
34
  // ---------------------------------------------------------------------------
34
35
  // Auth helpers (token refresh, error normalisation, retry policies)
35
36
  // ---------------------------------------------------------------------------
@@ -1,6 +1,7 @@
1
1
  import { OxyAuthenticationError } from '../OxyServices.errors.js';
2
2
  import { loadNodeCrypto } from '../utils/platformCrypto.js';
3
3
  import { logger } from '../utils/loggerUtils.js';
4
+ import { normalizeUserIdentity, normalizeUserIdentityOrNull } from '../utils/userIdentity.js';
4
5
  /**
5
6
  * Sentinel error raised when getServiceToken() is called with a known apiKey
6
7
  * but a non-matching secret. Indicates either credential drift in the caller
@@ -308,7 +309,10 @@ export function OxyServicesAuthMixin(Base) {
308
309
  if (res?.accessToken) {
309
310
  this.setTokens(res.accessToken);
310
311
  }
311
- return res;
312
+ return {
313
+ ...res,
314
+ user: normalizeUserIdentity(res.user),
315
+ };
312
316
  }
313
317
  catch (error) {
314
318
  throw this.handleError(error);
@@ -330,7 +334,8 @@ export function OxyServicesAuthMixin(Base) {
330
334
  */
331
335
  async getUserByPublicKey(publicKey) {
332
336
  try {
333
- return await this.makeRequest('GET', `/auth/user/${encodeURIComponent(publicKey)}`, undefined, { cache: true, cacheTTL: 2 * 60 * 1000 });
337
+ const user = await this.makeRequest('GET', `/auth/user/${encodeURIComponent(publicKey)}`, undefined, { cache: true, cacheTTL: 2 * 60 * 1000 });
338
+ return normalizeUserIdentity(user);
334
339
  }
335
340
  catch (error) {
336
341
  throw this.handleError(error);
@@ -341,10 +346,11 @@ export function OxyServicesAuthMixin(Base) {
341
346
  */
342
347
  async getUserBySession(sessionId) {
343
348
  try {
344
- return await this.makeRequest('GET', `/session/user/${sessionId}`, undefined, {
349
+ const user = await this.makeRequest('GET', `/session/user/${sessionId}`, undefined, {
345
350
  cache: true,
346
351
  cacheTTL: 2 * 60 * 1000,
347
352
  });
353
+ return normalizeUserIdentity(user);
348
354
  }
349
355
  catch (error) {
350
356
  throw this.handleError(error);
@@ -359,11 +365,15 @@ export function OxyServicesAuthMixin(Base) {
359
365
  return [];
360
366
  }
361
367
  const uniqueSessionIds = Array.from(new Set(sessionIds)).sort();
362
- return await this.makeRequest('POST', '/session/users/batch', { sessionIds: uniqueSessionIds }, {
368
+ const users = await this.makeRequest('POST', '/session/users/batch', { sessionIds: uniqueSessionIds }, {
363
369
  cache: true,
364
370
  cacheTTL: 2 * 60 * 1000,
365
371
  deduplicate: true,
366
372
  });
373
+ return users.map((entry) => ({
374
+ ...entry,
375
+ user: normalizeUserIdentityOrNull(entry.user),
376
+ }));
367
377
  }
368
378
  catch (error) {
369
379
  throw this.handleError(error);
@@ -688,7 +698,11 @@ export function OxyServicesAuthMixin(Base) {
688
698
  urlParams.deviceFingerprint = options.deviceFingerprint;
689
699
  if (options.useHeaderValidation)
690
700
  urlParams.useHeaderValidation = 'true';
691
- return await this.makeRequest('GET', `/session/validate/${sessionId}`, urlParams, { cache: false });
701
+ const validation = await this.makeRequest('GET', `/session/validate/${sessionId}`, urlParams, { cache: false });
702
+ return {
703
+ ...validation,
704
+ user: normalizeUserIdentity(validation.user),
705
+ };
692
706
  }
693
707
  catch (error) {
694
708
  // Session is invalid — clear any cached user data for this session (#196)
@@ -723,13 +737,17 @@ export function OxyServicesAuthMixin(Base) {
723
737
  */
724
738
  async signUp(username, email, password, deviceName, deviceFingerprint) {
725
739
  try {
726
- return await this.makeRequest('POST', '/auth/signup', {
740
+ const session = await this.makeRequest('POST', '/auth/signup', {
727
741
  username,
728
742
  email,
729
743
  password,
730
744
  deviceName,
731
745
  deviceFingerprint,
732
746
  }, { cache: false });
747
+ return {
748
+ ...session,
749
+ user: normalizeUserIdentity(session.user),
750
+ };
733
751
  }
734
752
  catch (error) {
735
753
  throw this.handleError(error);
@@ -740,12 +758,16 @@ export function OxyServicesAuthMixin(Base) {
740
758
  */
741
759
  async signIn(identifier, password, deviceName, deviceFingerprint) {
742
760
  try {
743
- return await this.makeRequest('POST', '/auth/login', {
761
+ const session = await this.makeRequest('POST', '/auth/login', {
744
762
  identifier,
745
763
  password,
746
764
  deviceName,
747
765
  deviceFingerprint,
748
766
  }, { cache: false });
767
+ return {
768
+ ...session,
769
+ user: normalizeUserIdentity(session.user),
770
+ };
749
771
  }
750
772
  catch (error) {
751
773
  throw this.handleError(error);
@@ -1,5 +1,6 @@
1
1
  import { OxyAuthenticationError } from '../OxyServices.errors.js';
2
2
  import { createDebugLogger } from '../shared/utils/debugUtils.js';
3
+ import { normalizeUserIdentity } from '../utils/userIdentity.js';
3
4
  const debug = createDebugLogger('FedCM');
4
5
  // Modern (W3C spec) → legacy (Chrome 125–131) mode value mapping. Used to
5
6
  // retry a credential request when an older browser rejects the modern enum.
@@ -585,7 +586,10 @@ export function OxyServicesFedCMMixin(Base) {
585
586
  hasSession: !!response?.sessionId,
586
587
  hasUser: !!response?.user,
587
588
  });
588
- return response;
589
+ return {
590
+ ...response,
591
+ user: normalizeUserIdentity(response.user),
592
+ };
589
593
  }
590
594
  catch (error) {
591
595
  debug.error('Token exchange failed:', error instanceof Error ? error.message : String(error));
@@ -1,6 +1,7 @@
1
1
  import { buildSearchParams, buildPaginationParams } from '../utils/apiUtils.js';
2
2
  import { KeyManager } from '../crypto/keyManager.js';
3
3
  import { SignatureService } from '../crypto/signatureService.js';
4
+ import { normalizeUserIdentity, normalizeUserIdentityOrNull } from '../utils/userIdentity.js';
4
5
  export function OxyServicesUserMixin(Base) {
5
6
  return class extends Base {
6
7
  constructor(...args) {
@@ -11,10 +12,11 @@ export function OxyServicesUserMixin(Base) {
11
12
  */
12
13
  async getProfileByUsername(username) {
13
14
  try {
14
- return await this.makeRequest('GET', `/profiles/username/${username}`, undefined, {
15
+ const user = await this.makeRequest('GET', `/profiles/username/${username}`, undefined, {
15
16
  cache: true,
16
17
  cacheTTL: 5 * 60 * 1000, // 5 minutes cache for profiles
17
18
  });
19
+ return normalizeUserIdentity(user);
18
20
  }
19
21
  catch (error) {
20
22
  throw this.handleError(error);
@@ -77,7 +79,7 @@ export function OxyServicesUserMixin(Base) {
77
79
  cache: true,
78
80
  cacheTTL: 24 * 60 * 60 * 1000, // 24h cache — matches server-side staleness window
79
81
  });
80
- return result ?? null;
82
+ return normalizeUserIdentityOrNull(result);
81
83
  }
82
84
  catch {
83
85
  return null;
@@ -89,7 +91,7 @@ export function OxyServicesUserMixin(Base) {
89
91
  * method — calling services never write user data directly.
90
92
  */
91
93
  async resolveExternalUser(data) {
92
- return this.makeRequest('PUT', '/users/resolve', data);
94
+ return normalizeUserIdentity(await this.makeRequest('PUT', '/users/resolve', data));
93
95
  }
94
96
  /**
95
97
  * Get profile recommendations, optionally filtering out specific user types.
@@ -119,20 +121,22 @@ export function OxyServicesUserMixin(Base) {
119
121
  const params = {};
120
122
  if (limit)
121
123
  params.limit = String(limit);
122
- return await this.makeRequest('GET', `/profiles/${userId}/similar`, params, {
124
+ const users = await this.makeRequest('GET', `/profiles/${userId}/similar`, params, {
123
125
  cache: true,
124
126
  cacheTTL: 5 * 60 * 1000, // 5 min cache
125
127
  });
128
+ return users.map((user) => normalizeUserIdentity(user));
126
129
  }
127
130
  /**
128
131
  * Get user by ID
129
132
  */
130
133
  async getUserById(userId) {
131
134
  try {
132
- return await this.makeRequest('GET', `/users/${userId}`, undefined, {
135
+ const user = await this.makeRequest('GET', `/users/${userId}`, undefined, {
133
136
  cache: true,
134
137
  cacheTTL: 5 * 60 * 1000, // 5 minutes cache
135
138
  });
139
+ return normalizeUserIdentity(user);
136
140
  }
137
141
  catch (error) {
138
142
  throw this.handleError(error);
@@ -143,10 +147,11 @@ export function OxyServicesUserMixin(Base) {
143
147
  */
144
148
  async getCurrentUser() {
145
149
  return this.withAuthRetry(async () => {
146
- return await this.makeRequest('GET', '/users/me', undefined, {
150
+ const user = await this.makeRequest('GET', '/users/me', undefined, {
147
151
  cache: true,
148
152
  cacheTTL: 1 * 60 * 1000, // 1 minute cache for current user
149
153
  });
154
+ return normalizeUserIdentity(user);
150
155
  }, 'getCurrentUser');
151
156
  }
152
157
  /**
@@ -164,7 +169,7 @@ export function OxyServicesUserMixin(Base) {
164
169
  */
165
170
  async updateProfile(updates) {
166
171
  try {
167
- const result = await this.makeRequest('PUT', '/users/me', updates, { cache: false });
172
+ const result = normalizeUserIdentity(await this.makeRequest('PUT', '/users/me', updates, { cache: false }));
168
173
  // Bust every cached representation of the current user. We use a
169
174
  // prefix sweep rather than an enumeration because the SDK never
170
175
  // tracks the set of active session IDs centrally.
@@ -0,0 +1,36 @@
1
+ function stringifyIdentity(value) {
2
+ if (typeof value === 'string') {
3
+ const trimmed = value.trim();
4
+ return trimmed.length > 0 ? trimmed : null;
5
+ }
6
+ if (typeof value === 'number' && Number.isFinite(value)) {
7
+ return String(value);
8
+ }
9
+ if (value && typeof value === 'object' && 'toString' in value) {
10
+ const toStringFn = value.toString;
11
+ if (typeof toStringFn === 'function' && toStringFn !== Object.prototype.toString) {
12
+ const rendered = toStringFn.call(value);
13
+ if (typeof rendered === 'string') {
14
+ const trimmed = rendered.trim();
15
+ return trimmed.length > 0 && trimmed !== '[object Object]' ? trimmed : null;
16
+ }
17
+ }
18
+ }
19
+ return null;
20
+ }
21
+ export function getNormalizedUserId(user) {
22
+ if (!user) {
23
+ return null;
24
+ }
25
+ return stringifyIdentity(user.id) ?? stringifyIdentity(user._id);
26
+ }
27
+ export function normalizeUserIdentity(user) {
28
+ const id = getNormalizedUserId(user);
29
+ if (!id) {
30
+ throw new Error('User response missing id');
31
+ }
32
+ return { ...user, id };
33
+ }
34
+ export function normalizeUserIdentityOrNull(user) {
35
+ return user ? normalizeUserIdentity(user) : null;
36
+ }