@oxyhq/core 3.4.7 → 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.
- package/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/HttpService.js +9 -7
- package/dist/cjs/index.js +8 -3
- package/dist/cjs/mixins/OxyServices.auth.js +29 -7
- package/dist/cjs/mixins/OxyServices.fedcm.js +5 -1
- package/dist/cjs/mixins/OxyServices.user.js +12 -7
- package/dist/cjs/utils/userIdentity.js +41 -0
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/HttpService.js +9 -7
- package/dist/esm/index.js +1 -0
- package/dist/esm/mixins/OxyServices.auth.js +29 -7
- package/dist/esm/mixins/OxyServices.fedcm.js +5 -1
- package/dist/esm/mixins/OxyServices.user.js +12 -7
- package/dist/esm/utils/userIdentity.js +36 -0
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/mixins/OxyServices.user.d.ts +3 -1
- package/dist/types/utils/userIdentity.d.ts +12 -0
- package/package.json +1 -1
- package/src/HttpService.ts +10 -8
- package/src/__tests__/httpServiceCsrf.test.ts +92 -0
- package/src/__tests__/linkedClient.test.ts +27 -0
- package/src/__tests__/userIdentity.test.ts +75 -0
- package/src/index.ts +1 -0
- package/src/mixins/OxyServices.auth.ts +36 -7
- package/src/mixins/OxyServices.fedcm.ts +5 -1
- package/src/mixins/OxyServices.user.ts +14 -8
- package/src/utils/userIdentity.ts +51 -0
package/dist/esm/HttpService.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
|
241
|
-
//
|
|
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,
|
|
@@ -599,7 +599,9 @@ export class HttpService {
|
|
|
599
599
|
* Build full URL with query params
|
|
600
600
|
*/
|
|
601
601
|
buildURL(url, params) {
|
|
602
|
-
const base =
|
|
602
|
+
const base = /^https?:\/\//i.test(url)
|
|
603
|
+
? url
|
|
604
|
+
: `${this.baseURL.replace(/\/+$/, '')}/${url.replace(/^\/+/, '')}`;
|
|
603
605
|
if (!params || Object.keys(params).length === 0) {
|
|
604
606
|
return base;
|
|
605
607
|
}
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|