@oxyhq/core 3.4.8 → 3.4.10
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 +6 -6
- package/dist/cjs/index.js +8 -3
- package/dist/cjs/mixins/OxyServices.auth.js +29 -7
- package/dist/cjs/mixins/OxyServices.fedcm.js +12 -8
- package/dist/cjs/mixins/OxyServices.user.js +20 -9
- package/dist/cjs/utils/userIdentity.js +41 -0
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/HttpService.js +6 -6
- package/dist/esm/index.js +1 -0
- package/dist/esm/mixins/OxyServices.auth.js +29 -7
- package/dist/esm/mixins/OxyServices.fedcm.js +12 -8
- package/dist/esm/mixins/OxyServices.user.js +20 -9
- 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.fedcm.d.ts +7 -7
- package/dist/types/utils/userIdentity.d.ts +12 -0
- package/package.json +1 -1
- package/src/HttpService.ts +7 -7
- package/src/__tests__/httpServiceCsrf.test.ts +92 -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 +12 -8
- package/src/mixins/OxyServices.user.ts +24 -12
- package/src/utils/userIdentity.ts +51 -0
package/dist/types/index.d.ts
CHANGED
|
@@ -34,6 +34,7 @@ export type { ServiceApp, ServiceActingAsVerification } from './mixins/OxyServic
|
|
|
34
34
|
export type { CreateManagedAccountInput, ManagedAccountManager, ManagedAccount, } from './mixins/OxyServices.managedAccounts';
|
|
35
35
|
export type { ContactDiscoveryMatch, ContactDiscoveryResponse, } from './mixins/OxyServices.contacts';
|
|
36
36
|
export { OxyAppDataIdentifierError } from './mixins/OxyServices.appData';
|
|
37
|
+
export { getNormalizedUserId, normalizeUserIdentity, normalizeUserIdentityOrNull } from './utils/userIdentity';
|
|
37
38
|
export type { Application, PublicApplication, ApplicationMember, ApplicationCredential, ApplicationRole, ApplicationType, ApplicationStatus, ApplicationMemberStatus, ApplicationCredentialType, ApplicationCredentialStatus, ApplicationEnvironment, CreateApplicationInput, UpdateApplicationInput, InviteApplicationMemberInput, UpdateApplicationMemberInput, TransferApplicationOwnershipInput, CreateApplicationCredentialInput, ApplicationCredentialWithSecret, RotateApplicationCredentialResult, ApplicationUsagePeriod, ApplicationUsageSummary, ApplicationUsageByDay, ApplicationUsageByEndpoint, ApplicationUsageStats, ApplicationSuccessResult, } from './mixins/OxyServices.applications';
|
|
38
39
|
export type { Workspace, WorkspaceMember, WorkspaceRole, WorkspaceType, WorkspaceStatus, WorkspaceMemberStatus, CreateWorkspaceInput, UpdateWorkspaceInput, InviteWorkspaceMemberInput, UpdateWorkspaceMemberInput, TransferWorkspaceOwnershipInput, WorkspaceSuccessResult, } from './mixins/OxyServices.workspaces';
|
|
39
40
|
export type { ReputationCategory, TrustTier, ReputationTransactionStatus, ReputationTargetEntityType, ReputationDisputeStatus, ReputationInfluenceContext, ReputationTransaction, ReputationBalanceBreakdown, ReputationInfluence, ReputationReliability, ReputationBalance, ReputationDispute, ReputationRule, ReputationLeaderboardEntry, ReputationInfluenceResult, ReverseReputationTransactionResult, AwardReputationInput, CreateReputationDisputeInput, ResolveReputationDisputeInput, UpsertReputationRuleInput, ReverseReputationTransactionInput, } from './mixins/OxyServices.reputation';
|
|
@@ -71,13 +71,13 @@ export declare function OxyServicesFedCMMixin<T extends typeof OxyServicesBase>(
|
|
|
71
71
|
* @throws {OxyAuthenticationError} If FedCM not supported or user cancels
|
|
72
72
|
*
|
|
73
73
|
* @example
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
74
|
+
* ```typescript
|
|
75
|
+
* try {
|
|
76
|
+
* const session = await oxyServices.signInWithFedCM();
|
|
77
|
+
* const user = session.user;
|
|
78
|
+
* } catch (error) {
|
|
79
|
+
* // Fallback to redirect auth
|
|
80
|
+
* oxyServices.signInWithRedirect();
|
|
81
81
|
* }
|
|
82
82
|
* ```
|
|
83
83
|
*/
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
interface UserIdentityInput {
|
|
2
|
+
id?: unknown;
|
|
3
|
+
_id?: unknown;
|
|
4
|
+
}
|
|
5
|
+
export declare function getNormalizedUserId(user: UserIdentityInput | null | undefined): string | null;
|
|
6
|
+
export declare function normalizeUserIdentity<T extends UserIdentityInput>(user: T): T & {
|
|
7
|
+
id: string;
|
|
8
|
+
};
|
|
9
|
+
export declare function normalizeUserIdentityOrNull<T extends UserIdentityInput>(user: T | null | undefined): (T & {
|
|
10
|
+
id: string;
|
|
11
|
+
}) | null;
|
|
12
|
+
export {};
|
package/package.json
CHANGED
package/src/HttpService.ts
CHANGED
|
@@ -32,7 +32,7 @@ interface JwtPayload {
|
|
|
32
32
|
userId?: string;
|
|
33
33
|
id?: string;
|
|
34
34
|
sessionId?: string;
|
|
35
|
-
[key: string]:
|
|
35
|
+
[key: string]: unknown;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
export type AuthRefreshReason = 'preflight' | 'response-401';
|
|
@@ -314,9 +314,11 @@ export class HttpService {
|
|
|
314
314
|
// Get auth token (with auto-refresh)
|
|
315
315
|
const authHeader = await this.getAuthHeader();
|
|
316
316
|
|
|
317
|
-
//
|
|
317
|
+
// CSRF protects cookie-authenticated browser writes. Bearer-authenticated
|
|
318
|
+
// SDK clients are not vulnerable to ambient-cookie CSRF, and linked app
|
|
319
|
+
// APIs should not need to implement a duplicate `/csrf-token` route.
|
|
318
320
|
const isStateChangingMethod = ['POST', 'PUT', 'PATCH', 'DELETE'].includes(method);
|
|
319
|
-
const csrfToken = isStateChangingMethod ? await this.fetchCsrfToken() : null;
|
|
321
|
+
const csrfToken = isStateChangingMethod && !authHeader ? await this.fetchCsrfToken() : null;
|
|
320
322
|
|
|
321
323
|
// Determine if data is FormData using robust detection
|
|
322
324
|
const isFormData = this.isFormData(data);
|
|
@@ -356,10 +358,8 @@ export class HttpService {
|
|
|
356
358
|
headers['X-Native-App'] = 'true';
|
|
357
359
|
}
|
|
358
360
|
|
|
359
|
-
// Debug logging for CSRF issues
|
|
360
|
-
//
|
|
361
|
-
// this was a bare console.log that leaked noise into every host app's
|
|
362
|
-
// stdout in development.
|
|
361
|
+
// Debug logging for CSRF issues, routed through SimpleLogger so it only
|
|
362
|
+
// fires when consumers opt in via `enableLogging`.
|
|
363
363
|
if (isStateChangingMethod) {
|
|
364
364
|
this.logger.debug('CSRF Debug:', {
|
|
365
365
|
url,
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { HttpService } from '../HttpService';
|
|
2
|
+
|
|
3
|
+
interface FetchCall {
|
|
4
|
+
url: string;
|
|
5
|
+
init: RequestInit;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function createJwt(payload: Record<string, unknown>): string {
|
|
9
|
+
const encode = (value: unknown): string => Buffer.from(JSON.stringify(value)).toString('base64url');
|
|
10
|
+
return `${encode({ alg: 'HS256', typ: 'JWT' })}.${encode(payload)}.signature`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function jsonResponse(data: unknown): Response {
|
|
14
|
+
return new Response(JSON.stringify({ data }), {
|
|
15
|
+
status: 200,
|
|
16
|
+
headers: { 'content-type': 'application/json' },
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function readHeaders(init: RequestInit | undefined): Record<string, string> {
|
|
21
|
+
const headers = init?.headers;
|
|
22
|
+
if (!headers) {
|
|
23
|
+
return {};
|
|
24
|
+
}
|
|
25
|
+
if (headers instanceof Headers) {
|
|
26
|
+
return Object.fromEntries(headers.entries());
|
|
27
|
+
}
|
|
28
|
+
if (Array.isArray(headers)) {
|
|
29
|
+
return Object.fromEntries(headers);
|
|
30
|
+
}
|
|
31
|
+
return headers as Record<string, string>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe('HttpService CSRF behavior', () => {
|
|
35
|
+
const originalFetch = globalThis.fetch;
|
|
36
|
+
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
globalThis.fetch = originalFetch;
|
|
39
|
+
jest.restoreAllMocks();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('does not fetch csrf-token before bearer-authenticated writes', async () => {
|
|
43
|
+
const calls: FetchCall[] = [];
|
|
44
|
+
const fetchMock = jest.fn(async (url: string, init: RequestInit) => {
|
|
45
|
+
calls.push({ url, init });
|
|
46
|
+
return jsonResponse({ ok: true });
|
|
47
|
+
});
|
|
48
|
+
globalThis.fetch = fetchMock as unknown as typeof globalThis.fetch;
|
|
49
|
+
|
|
50
|
+
const http = new HttpService({ baseURL: 'https://api.mention.earth', enableRetry: false });
|
|
51
|
+
const accessToken = createJwt({
|
|
52
|
+
userId: 'user_1',
|
|
53
|
+
exp: Math.floor(Date.now() / 1000) + 3600,
|
|
54
|
+
});
|
|
55
|
+
http.setTokens(accessToken);
|
|
56
|
+
|
|
57
|
+
await http.post('/posts', { text: 'hello' });
|
|
58
|
+
|
|
59
|
+
expect(calls).toHaveLength(1);
|
|
60
|
+
expect(calls[0].url).toBe('https://api.mention.earth/posts');
|
|
61
|
+
const headers = readHeaders(calls[0].init);
|
|
62
|
+
expect(headers.Authorization).toBe(`Bearer ${accessToken}`);
|
|
63
|
+
expect(headers['X-CSRF-Token']).toBeUndefined();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('still fetches csrf-token for cookie-authenticated writes without bearer', async () => {
|
|
67
|
+
const calls: FetchCall[] = [];
|
|
68
|
+
const fetchMock = jest.fn(async (url: string, init: RequestInit) => {
|
|
69
|
+
calls.push({ url, init });
|
|
70
|
+
if (url.endsWith('/csrf-token')) {
|
|
71
|
+
return new Response(JSON.stringify({ csrfToken: 'csrf_1' }), {
|
|
72
|
+
status: 200,
|
|
73
|
+
headers: { 'content-type': 'application/json' },
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return jsonResponse({ ok: true });
|
|
77
|
+
});
|
|
78
|
+
globalThis.fetch = fetchMock as unknown as typeof globalThis.fetch;
|
|
79
|
+
|
|
80
|
+
const http = new HttpService({ baseURL: 'https://api.mention.earth', enableRetry: false });
|
|
81
|
+
|
|
82
|
+
await http.post('/posts', { text: 'hello' });
|
|
83
|
+
|
|
84
|
+
expect(calls.map((call) => call.url)).toEqual([
|
|
85
|
+
'https://api.mention.earth/csrf-token',
|
|
86
|
+
'https://api.mention.earth/posts',
|
|
87
|
+
]);
|
|
88
|
+
const headers = readHeaders(calls[1].init);
|
|
89
|
+
expect(headers.Authorization).toBeUndefined();
|
|
90
|
+
expect(headers['X-CSRF-Token']).toBe('csrf_1');
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { OxyServices } from '../OxyServices';
|
|
2
|
+
import { getNormalizedUserId, normalizeUserIdentity } from '../utils/userIdentity';
|
|
3
|
+
|
|
4
|
+
interface FetchCall {
|
|
5
|
+
url: string;
|
|
6
|
+
init: RequestInit;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function jsonResponse(data: unknown): Response {
|
|
10
|
+
return new Response(JSON.stringify({ data }), {
|
|
11
|
+
status: 200,
|
|
12
|
+
headers: { 'content-type': 'application/json' },
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function createJwt(payload: Record<string, unknown>): string {
|
|
17
|
+
const encode = (value: unknown): string => Buffer.from(JSON.stringify(value)).toString('base64url');
|
|
18
|
+
return `${encode({ alg: 'HS256', typ: 'JWT' })}.${encode(payload)}.signature`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('user identity normalization', () => {
|
|
22
|
+
const originalFetch = globalThis.fetch;
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
globalThis.fetch = originalFetch;
|
|
26
|
+
jest.restoreAllMocks();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('normalizes Mongo _id to id', () => {
|
|
30
|
+
expect(getNormalizedUserId({ _id: 'mongo_id' })).toBe('mongo_id');
|
|
31
|
+
expect(normalizeUserIdentity({ _id: 'mongo_id', username: 'nate' })).toEqual({
|
|
32
|
+
_id: 'mongo_id',
|
|
33
|
+
id: 'mongo_id',
|
|
34
|
+
username: 'nate',
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('normalizes getCurrentUser responses before exposing them to apps', async () => {
|
|
39
|
+
const calls: FetchCall[] = [];
|
|
40
|
+
const fetchMock = jest.fn(async (url: string, init: RequestInit) => {
|
|
41
|
+
calls.push({ url, init });
|
|
42
|
+
return jsonResponse({ _id: 'user_1', username: 'nate', publicKey: 'pub_1' });
|
|
43
|
+
});
|
|
44
|
+
globalThis.fetch = fetchMock as unknown as typeof globalThis.fetch;
|
|
45
|
+
|
|
46
|
+
const oxy = new OxyServices({ baseURL: 'https://api.oxy.so' });
|
|
47
|
+
oxy.setTokens(createJwt({
|
|
48
|
+
userId: 'user_1',
|
|
49
|
+
exp: Math.floor(Date.now() / 1000) + 3600,
|
|
50
|
+
}));
|
|
51
|
+
const user = await oxy.getCurrentUser();
|
|
52
|
+
|
|
53
|
+
expect(user.id).toBe('user_1');
|
|
54
|
+
expect(user.username).toBe('nate');
|
|
55
|
+
expect(calls[0].url).toBe('https://api.oxy.so/users/me');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('normalizes validateSession users before services stores them', async () => {
|
|
59
|
+
const fetchMock = jest.fn(async () =>
|
|
60
|
+
jsonResponse({
|
|
61
|
+
valid: true,
|
|
62
|
+
expiresAt: '2099-01-01T00:00:00.000Z',
|
|
63
|
+
lastActivity: '2026-06-18T00:00:00.000Z',
|
|
64
|
+
user: { _id: 'user_1', username: 'nate', publicKey: 'pub_1' },
|
|
65
|
+
}),
|
|
66
|
+
);
|
|
67
|
+
globalThis.fetch = fetchMock as unknown as typeof globalThis.fetch;
|
|
68
|
+
|
|
69
|
+
const oxy = new OxyServices({ baseURL: 'https://api.oxy.so' });
|
|
70
|
+
const validation = await oxy.validateSession('session_1', { useHeaderValidation: true });
|
|
71
|
+
|
|
72
|
+
expect(validation.user.id).toBe('user_1');
|
|
73
|
+
expect(validation.user.username).toBe('nate');
|
|
74
|
+
});
|
|
75
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -62,6 +62,7 @@ export type {
|
|
|
62
62
|
ContactDiscoveryResponse,
|
|
63
63
|
} from './mixins/OxyServices.contacts';
|
|
64
64
|
export { OxyAppDataIdentifierError } from './mixins/OxyServices.appData';
|
|
65
|
+
export { getNormalizedUserId, normalizeUserIdentity, normalizeUserIdentityOrNull } from './utils/userIdentity';
|
|
65
66
|
|
|
66
67
|
// ---------------------------------------------------------------------------
|
|
67
68
|
// Applications (multi-user apps: membership, roles, credentials)
|
|
@@ -15,6 +15,7 @@ import type { OxyServicesBase } from '../OxyServices.base';
|
|
|
15
15
|
import { OxyAuthenticationError } from '../OxyServices.errors';
|
|
16
16
|
import { loadNodeCrypto } from '../utils/platformCrypto';
|
|
17
17
|
import { logger } from '../utils/loggerUtils';
|
|
18
|
+
import { normalizeUserIdentity, normalizeUserIdentityOrNull } from '../utils/userIdentity';
|
|
18
19
|
|
|
19
20
|
export interface ChallengeResponse {
|
|
20
21
|
challenge: string;
|
|
@@ -451,7 +452,10 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
451
452
|
this.setTokens(res.accessToken);
|
|
452
453
|
}
|
|
453
454
|
|
|
454
|
-
return
|
|
455
|
+
return {
|
|
456
|
+
...res,
|
|
457
|
+
user: normalizeUserIdentity(res.user),
|
|
458
|
+
};
|
|
455
459
|
} catch (error) {
|
|
456
460
|
throw this.handleError(error);
|
|
457
461
|
}
|
|
@@ -478,12 +482,13 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
478
482
|
*/
|
|
479
483
|
async getUserByPublicKey(publicKey: string): Promise<User> {
|
|
480
484
|
try {
|
|
481
|
-
|
|
485
|
+
const user = await this.makeRequest<User>(
|
|
482
486
|
'GET',
|
|
483
487
|
`/auth/user/${encodeURIComponent(publicKey)}`,
|
|
484
488
|
undefined,
|
|
485
489
|
{ cache: true, cacheTTL: 2 * 60 * 1000 }
|
|
486
490
|
);
|
|
491
|
+
return normalizeUserIdentity(user);
|
|
487
492
|
} catch (error) {
|
|
488
493
|
throw this.handleError(error);
|
|
489
494
|
}
|
|
@@ -494,10 +499,11 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
494
499
|
*/
|
|
495
500
|
async getUserBySession(sessionId: string): Promise<User> {
|
|
496
501
|
try {
|
|
497
|
-
|
|
502
|
+
const user = await this.makeRequest<User>('GET', `/session/user/${sessionId}`, undefined, {
|
|
498
503
|
cache: true,
|
|
499
504
|
cacheTTL: 2 * 60 * 1000,
|
|
500
505
|
});
|
|
506
|
+
return normalizeUserIdentity(user);
|
|
501
507
|
} catch (error) {
|
|
502
508
|
throw this.handleError(error);
|
|
503
509
|
}
|
|
@@ -514,7 +520,7 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
514
520
|
|
|
515
521
|
const uniqueSessionIds = Array.from(new Set(sessionIds)).sort();
|
|
516
522
|
|
|
517
|
-
|
|
523
|
+
const users = await this.makeRequest<Array<{ sessionId: string; user: User | null }>>(
|
|
518
524
|
'POST',
|
|
519
525
|
'/session/users/batch',
|
|
520
526
|
{ sessionIds: uniqueSessionIds },
|
|
@@ -524,6 +530,10 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
524
530
|
deduplicate: true,
|
|
525
531
|
}
|
|
526
532
|
);
|
|
533
|
+
return users.map((entry) => ({
|
|
534
|
+
...entry,
|
|
535
|
+
user: normalizeUserIdentityOrNull(entry.user),
|
|
536
|
+
}));
|
|
527
537
|
} catch (error) {
|
|
528
538
|
throw this.handleError(error);
|
|
529
539
|
}
|
|
@@ -909,7 +919,18 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
909
919
|
const urlParams: Record<string, string> = {};
|
|
910
920
|
if (options.deviceFingerprint) urlParams.deviceFingerprint = options.deviceFingerprint;
|
|
911
921
|
if (options.useHeaderValidation) urlParams.useHeaderValidation = 'true';
|
|
912
|
-
|
|
922
|
+
const validation = await this.makeRequest<{
|
|
923
|
+
valid: boolean;
|
|
924
|
+
expiresAt: string;
|
|
925
|
+
lastActivity: string;
|
|
926
|
+
user: User;
|
|
927
|
+
sessionId?: string;
|
|
928
|
+
source?: string;
|
|
929
|
+
}>('GET', `/session/validate/${sessionId}`, urlParams, { cache: false });
|
|
930
|
+
return {
|
|
931
|
+
...validation,
|
|
932
|
+
user: normalizeUserIdentity(validation.user),
|
|
933
|
+
};
|
|
913
934
|
} catch (error) {
|
|
914
935
|
// Session is invalid — clear any cached user data for this session (#196)
|
|
915
936
|
this.clearCacheEntry(`GET:/session/user/${sessionId}`);
|
|
@@ -950,13 +971,17 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
950
971
|
deviceFingerprint?: any
|
|
951
972
|
): Promise<SessionLoginResponse> {
|
|
952
973
|
try {
|
|
953
|
-
|
|
974
|
+
const session = await this.makeRequest<SessionLoginResponse>('POST', '/auth/signup', {
|
|
954
975
|
username,
|
|
955
976
|
email,
|
|
956
977
|
password,
|
|
957
978
|
deviceName,
|
|
958
979
|
deviceFingerprint,
|
|
959
980
|
}, { cache: false });
|
|
981
|
+
return {
|
|
982
|
+
...session,
|
|
983
|
+
user: normalizeUserIdentity(session.user),
|
|
984
|
+
};
|
|
960
985
|
} catch (error) {
|
|
961
986
|
throw this.handleError(error);
|
|
962
987
|
}
|
|
@@ -972,12 +997,16 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
972
997
|
deviceFingerprint?: any
|
|
973
998
|
): Promise<SessionLoginResponse> {
|
|
974
999
|
try {
|
|
975
|
-
|
|
1000
|
+
const session = await this.makeRequest<SessionLoginResponse>('POST', '/auth/login', {
|
|
976
1001
|
identifier,
|
|
977
1002
|
password,
|
|
978
1003
|
deviceName,
|
|
979
1004
|
deviceFingerprint,
|
|
980
1005
|
}, { cache: false });
|
|
1006
|
+
return {
|
|
1007
|
+
...session,
|
|
1008
|
+
user: normalizeUserIdentity(session.user),
|
|
1009
|
+
};
|
|
981
1010
|
} catch (error) {
|
|
982
1011
|
throw this.handleError(error);
|
|
983
1012
|
}
|
|
@@ -2,6 +2,7 @@ import type { OxyServicesBase } from '../OxyServices.base';
|
|
|
2
2
|
import { OxyAuthenticationError } from '../OxyServices.errors';
|
|
3
3
|
import type { SessionLoginResponse } from '../models/session';
|
|
4
4
|
import { createDebugLogger } from '../shared/utils/debugUtils';
|
|
5
|
+
import { normalizeUserIdentity } from '../utils/userIdentity';
|
|
5
6
|
|
|
6
7
|
const debug = createDebugLogger('FedCM');
|
|
7
8
|
|
|
@@ -254,13 +255,13 @@ export function OxyServicesFedCMMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
254
255
|
* @throws {OxyAuthenticationError} If FedCM not supported or user cancels
|
|
255
256
|
*
|
|
256
257
|
* @example
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
258
|
+
* ```typescript
|
|
259
|
+
* try {
|
|
260
|
+
* const session = await oxyServices.signInWithFedCM();
|
|
261
|
+
* const user = session.user;
|
|
262
|
+
* } catch (error) {
|
|
263
|
+
* // Fallback to redirect auth
|
|
264
|
+
* oxyServices.signInWithRedirect();
|
|
264
265
|
* }
|
|
265
266
|
* ```
|
|
266
267
|
*/
|
|
@@ -776,7 +777,10 @@ export function OxyServicesFedCMMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
776
777
|
hasUser: !!response?.user,
|
|
777
778
|
});
|
|
778
779
|
|
|
779
|
-
return
|
|
780
|
+
return {
|
|
781
|
+
...response,
|
|
782
|
+
user: normalizeUserIdentity(response.user),
|
|
783
|
+
};
|
|
780
784
|
} catch (error) {
|
|
781
785
|
debug.error('Token exchange failed:', error instanceof Error ? error.message : String(error));
|
|
782
786
|
throw error;
|
|
@@ -14,6 +14,7 @@ import type { OxyServicesBase } from '../OxyServices.base';
|
|
|
14
14
|
import { buildSearchParams, buildPaginationParams, type PaginationParams } from '../utils/apiUtils';
|
|
15
15
|
import { KeyManager } from '../crypto/keyManager';
|
|
16
16
|
import { SignatureService } from '../crypto/signatureService';
|
|
17
|
+
import { normalizeUserIdentity, normalizeUserIdentityOrNull } from '../utils/userIdentity';
|
|
17
18
|
|
|
18
19
|
export function OxyServicesUserMixin<T extends typeof OxyServicesBase>(Base: T) {
|
|
19
20
|
return class extends Base {
|
|
@@ -25,10 +26,11 @@ export function OxyServicesUserMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
25
26
|
*/
|
|
26
27
|
async getProfileByUsername(username: string): Promise<User> {
|
|
27
28
|
try {
|
|
28
|
-
|
|
29
|
+
const user = await this.makeRequest<User>('GET', `/profiles/username/${username}`, undefined, {
|
|
29
30
|
cache: true,
|
|
30
31
|
cacheTTL: 5 * 60 * 1000, // 5 minutes cache for profiles
|
|
31
32
|
});
|
|
33
|
+
return normalizeUserIdentity(user);
|
|
32
34
|
} catch (error) {
|
|
33
35
|
throw this.handleError(error);
|
|
34
36
|
}
|
|
@@ -109,7 +111,7 @@ export function OxyServicesUserMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
109
111
|
cache: true,
|
|
110
112
|
cacheTTL: 24 * 60 * 60 * 1000, // 24h cache — matches server-side staleness window
|
|
111
113
|
});
|
|
112
|
-
return result
|
|
114
|
+
return normalizeUserIdentityOrNull(result);
|
|
113
115
|
} catch {
|
|
114
116
|
return null;
|
|
115
117
|
}
|
|
@@ -130,7 +132,7 @@ export function OxyServicesUserMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
130
132
|
bio?: string;
|
|
131
133
|
ownerId?: string;
|
|
132
134
|
}): Promise<User> {
|
|
133
|
-
return this.makeRequest<User>('PUT', '/users/resolve', data);
|
|
135
|
+
return normalizeUserIdentity(await this.makeRequest<User>('PUT', '/users/resolve', data));
|
|
134
136
|
}
|
|
135
137
|
|
|
136
138
|
/**
|
|
@@ -175,10 +177,11 @@ export function OxyServicesUserMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
175
177
|
async getSimilarProfiles(userId: string, limit?: number): Promise<User[]> {
|
|
176
178
|
const params: Record<string, string> = {};
|
|
177
179
|
if (limit) params.limit = String(limit);
|
|
178
|
-
|
|
180
|
+
const users = await this.makeRequest<User[]>('GET', `/profiles/${userId}/similar`, params, {
|
|
179
181
|
cache: true,
|
|
180
182
|
cacheTTL: 5 * 60 * 1000, // 5 min cache
|
|
181
183
|
});
|
|
184
|
+
return users.map((user) => normalizeUserIdentity(user));
|
|
182
185
|
}
|
|
183
186
|
|
|
184
187
|
/**
|
|
@@ -186,10 +189,11 @@ export function OxyServicesUserMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
186
189
|
*/
|
|
187
190
|
async getUserById(userId: string): Promise<User> {
|
|
188
191
|
try {
|
|
189
|
-
|
|
192
|
+
const user = await this.makeRequest<User>('GET', `/users/${userId}`, undefined, {
|
|
190
193
|
cache: true,
|
|
191
194
|
cacheTTL: 5 * 60 * 1000, // 5 minutes cache
|
|
192
195
|
});
|
|
196
|
+
return normalizeUserIdentity(user);
|
|
193
197
|
} catch (error) {
|
|
194
198
|
throw this.handleError(error);
|
|
195
199
|
}
|
|
@@ -200,10 +204,11 @@ export function OxyServicesUserMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
200
204
|
*/
|
|
201
205
|
async getCurrentUser(): Promise<User> {
|
|
202
206
|
return this.withAuthRetry(async () => {
|
|
203
|
-
|
|
207
|
+
const user = await this.makeRequest<User>('GET', '/users/me', undefined, {
|
|
204
208
|
cache: true,
|
|
205
209
|
cacheTTL: 1 * 60 * 1000, // 1 minute cache for current user
|
|
206
210
|
});
|
|
211
|
+
return normalizeUserIdentity(user);
|
|
207
212
|
}, 'getCurrentUser');
|
|
208
213
|
}
|
|
209
214
|
|
|
@@ -222,7 +227,9 @@ export function OxyServicesUserMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
222
227
|
*/
|
|
223
228
|
async updateProfile(updates: Partial<User>): Promise<User> {
|
|
224
229
|
try {
|
|
225
|
-
const result =
|
|
230
|
+
const result = normalizeUserIdentity(
|
|
231
|
+
await this.makeRequest<User>('PUT', '/users/me', updates, { cache: false }),
|
|
232
|
+
);
|
|
226
233
|
|
|
227
234
|
// Bust every cached representation of the current user. We use a
|
|
228
235
|
// prefix sweep rather than an enumeration because the SDK never
|
|
@@ -236,12 +243,18 @@ export function OxyServicesUserMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
236
243
|
|
|
237
244
|
return result;
|
|
238
245
|
} catch (error) {
|
|
239
|
-
const errorAny = error as any;
|
|
240
246
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
241
|
-
const
|
|
242
|
-
|
|
247
|
+
const errorRecord = error && typeof error === 'object'
|
|
248
|
+
? error as { status?: unknown; response?: { status?: unknown } }
|
|
249
|
+
: null;
|
|
250
|
+
const status = typeof errorRecord?.status === 'number'
|
|
251
|
+
? errorRecord.status
|
|
252
|
+
: typeof errorRecord?.response?.status === 'number'
|
|
253
|
+
? errorRecord.response.status
|
|
254
|
+
: undefined;
|
|
255
|
+
|
|
243
256
|
// Check if it's an authentication error (401)
|
|
244
|
-
const isAuthError = status === 401 ||
|
|
257
|
+
const isAuthError = status === 401 ||
|
|
245
258
|
errorMessage.includes('Authentication required') ||
|
|
246
259
|
errorMessage.includes('Invalid or missing authorization header');
|
|
247
260
|
|
|
@@ -531,4 +544,3 @@ export function OxyServicesUserMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
531
544
|
}
|
|
532
545
|
};
|
|
533
546
|
}
|
|
534
|
-
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
interface UserIdentityInput {
|
|
2
|
+
id?: unknown;
|
|
3
|
+
_id?: unknown;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function stringifyIdentity(value: unknown): string | null {
|
|
7
|
+
if (typeof value === 'string') {
|
|
8
|
+
const trimmed = value.trim();
|
|
9
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
13
|
+
return String(value);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (value && typeof value === 'object' && 'toString' in value) {
|
|
17
|
+
const toStringFn = value.toString;
|
|
18
|
+
if (typeof toStringFn === 'function' && toStringFn !== Object.prototype.toString) {
|
|
19
|
+
const rendered = toStringFn.call(value);
|
|
20
|
+
if (typeof rendered === 'string') {
|
|
21
|
+
const trimmed = rendered.trim();
|
|
22
|
+
return trimmed.length > 0 && trimmed !== '[object Object]' ? trimmed : null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function getNormalizedUserId(user: UserIdentityInput | null | undefined): string | null {
|
|
31
|
+
if (!user) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return stringifyIdentity(user.id) ?? stringifyIdentity(user._id);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function normalizeUserIdentity<T extends UserIdentityInput>(user: T): T & { id: string } {
|
|
39
|
+
const id = getNormalizedUserId(user);
|
|
40
|
+
if (!id) {
|
|
41
|
+
throw new Error('User response missing id');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return { ...user, id };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function normalizeUserIdentityOrNull<T extends UserIdentityInput>(
|
|
48
|
+
user: T | null | undefined,
|
|
49
|
+
): (T & { id: string }) | null {
|
|
50
|
+
return user ? normalizeUserIdentity(user) : null;
|
|
51
|
+
}
|