@oxyhq/core 3.4.15 → 3.4.17

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.
@@ -2,6 +2,7 @@
2
2
  * User Management Methods Mixin
3
3
  */
4
4
  import type { User, Notification, NotificationPreferences, UserPreferences, SearchProfilesResponse, PrivacySettings } from '../models/interfaces';
5
+ import type { UserNameResponse, UserProfileUpdate } from '@oxyhq/contracts';
5
6
  import type { OxyServicesBase } from '../OxyServices.base';
6
7
  import { type PaginationParams } from '../utils/apiUtils';
7
8
  export declare function OxyServicesUserMixin<T extends typeof OxyServicesBase>(Base: T): {
@@ -12,7 +13,7 @@ export declare function OxyServicesUserMixin<T extends typeof OxyServicesBase>(B
12
13
  getProfileByUsername(username: string): Promise<User>;
13
14
  /**
14
15
  * Lightweight username lookup for login flows.
15
- * Returns minimal public info: exists, color, avatar, displayName.
16
+ * Returns minimal public info: exists, color, avatar, name.displayName.
16
17
  * Faster than getProfileByUsername — no stats, no formatting.
17
18
  */
18
19
  lookupUsername(username: string): Promise<{
@@ -20,7 +21,7 @@ export declare function OxyServicesUserMixin<T extends typeof OxyServicesBase>(B
20
21
  username: string;
21
22
  color: string | null;
22
23
  avatar: string | null;
23
- displayName: string;
24
+ name: UserNameResponse;
24
25
  }>;
25
26
  /**
26
27
  * Search user profiles
@@ -62,11 +63,7 @@ export declare function OxyServicesUserMixin<T extends typeof OxyServicesBase>(B
62
63
  }): Promise<Array<{
63
64
  id: string;
64
65
  username: string;
65
- name?: {
66
- first?: string;
67
- last?: string;
68
- full?: string;
69
- };
66
+ name: UserNameResponse;
70
67
  description?: string;
71
68
  isFederated?: boolean;
72
69
  isAgent?: boolean;
@@ -111,7 +108,7 @@ export declare function OxyServicesUserMixin<T extends typeof OxyServicesBase>(B
111
108
  *
112
109
  * TanStack Query handles offline queuing automatically.
113
110
  */
114
- updateProfile(updates: Partial<User>): Promise<User>;
111
+ updateProfile(updates: UserProfileUpdate): Promise<User>;
115
112
  /**
116
113
  * Get privacy settings for a user
117
114
  * @param userId - The user ID (defaults to current user)
@@ -81,26 +81,27 @@ export interface User {
81
81
  publicKey: string;
82
82
  username: string;
83
83
  email?: string;
84
- avatar?: string;
85
- color?: string;
84
+ avatar?: string | null;
85
+ color?: string | null;
86
86
  privacySettings?: PrivacySettings;
87
87
  /**
88
- * Structured human name. The canonical wire shape ({@link UserNameResponse}):
89
- * `{ first?, last?, full? }` where `full` is a Mongoose virtual (present only
90
- * when the query materialised virtuals). The single source of truth lives in
91
- * `@oxyhq/contracts` — do NOT re-declare a bare `string` here.
88
+ * Structured human name. `name.displayName` is the canonical display string
89
+ * resolved by the API; consumers render it directly instead of recomposing
90
+ * names from `first` / `last` / `full` / `username`.
92
91
  */
93
- name?: UserNameResponse;
92
+ name: UserNameResponse;
94
93
  bio?: string;
95
94
  location?: string;
96
95
  website?: string;
97
96
  createdAt?: string;
98
97
  updatedAt?: string;
99
- links?: Array<{
98
+ links?: string[];
99
+ linksMetadata?: Array<{
100
+ url: string;
100
101
  title?: string;
101
102
  description?: string;
102
103
  image?: string;
103
- link: string;
104
+ id?: string;
104
105
  }>;
105
106
  _count?: {
106
107
  followers?: number;
@@ -539,7 +540,7 @@ export interface RefreshAllAccountUser {
539
540
  * string. The server projects `name` verbatim from the user document. The
540
541
  * single source of truth is `@oxyhq/contracts`.
541
542
  */
542
- name?: UserNameResponse;
543
+ name: UserNameResponse;
543
544
  avatar?: string | null;
544
545
  email?: string;
545
546
  color?: string | null;
@@ -1,3 +1,4 @@
1
+ import type { UserNameResponse } from '@oxyhq/contracts';
1
2
  export interface ClientSession {
2
3
  sessionId: string;
3
4
  deviceId: string;
@@ -21,6 +22,7 @@ export interface StorageKeys {
21
22
  export interface MinimalUserData {
22
23
  id: string;
23
24
  username: string;
25
+ name: UserNameResponse;
24
26
  avatar?: string;
25
27
  }
26
28
  export interface SessionLoginResponse {
@@ -27,18 +27,13 @@ export interface QuickAccount {
27
27
  /** Minimal user shape accepted by display-name helpers. Avoids importing the full User type. */
28
28
  export interface DisplayNameUserShape {
29
29
  name?: string | {
30
+ displayName?: string;
30
31
  first?: string;
31
32
  last?: string;
32
33
  full?: string;
33
34
  [key: string]: unknown;
34
35
  };
35
- /**
36
- * Pre-resolved display name as emitted by the server's `displayName` virtual
37
- * (raw `/users/me` responses). NOTE: the server virtual resolves to
38
- * `username || truncatedPublicKey || 'Anonymous'` — it does NOT compose the
39
- * structured `name`. It is therefore preferred only AFTER a real structured
40
- * name, so a first-name-only account never collapses to its username/key.
41
- */
36
+ /** Pre-normalized account-row display name, not the API User DTO field. */
42
37
  displayName?: string;
43
38
  username?: string;
44
39
  publicKey?: string;
@@ -52,16 +47,13 @@ export declare const formatPublicKeyHandle: (publicKey: string) => string;
52
47
  * Resolve a friendly display name for a user.
53
48
  *
54
49
  * Order of preference:
55
- * 1. `name.full`, or composed `name.first name.last` (FIRST-NAME-ONLY SAFE —
56
- * a user with only a first name resolves to that first name, never to the
57
- * lowercase username; this is the exact drift bug the auth app hit).
58
- * 2. `name` (when stored as a plain string)
59
- * 3. `displayName` (server `displayName` virtual — `username || truncatedKey`).
60
- * Placed AFTER the structured name on purpose: the server virtual ignores
61
- * `name`, so preferring it first would re-introduce the first-only bug.
62
- * 4. `username`
63
- * 5. `Account 0x12345678…` (derived from publicKey, when present)
64
- * 6. Translated fallback (e.g. "Unnamed")
50
+ * 1. `name.displayName` from the API user contract.
51
+ * 2. `name.full`, or composed `name.first name.last` for local unsaved shapes.
52
+ * 3. `name` when passed as a plain string by local non-DTO call sites.
53
+ * 4. pre-normalized account-row `displayName`.
54
+ * 5. `username`
55
+ * 6. `Account 0x12345678…` (derived from publicKey, when present)
56
+ * 7. Translated fallback (e.g. "Unnamed")
65
57
  *
66
58
  * The translation key `common.unnamed` is used for the final fallback. If the
67
59
  * caller does not pass a locale, the default English translation is used.
@@ -89,6 +81,7 @@ export declare const buildAccountsArray: (accounts: Record<string, QuickAccount>
89
81
  */
90
82
  export declare const createQuickAccount: (sessionId: string, userData: {
91
83
  name?: string | {
84
+ displayName?: string;
92
85
  full?: string;
93
86
  first?: string;
94
87
  last?: string;
@@ -99,7 +92,7 @@ export declare const createQuickAccount: (sessionId: string, userData: {
99
92
  _id?: {
100
93
  toString(): string;
101
94
  } | string;
102
- avatar?: string;
95
+ avatar?: string | null;
103
96
  }, existingAccount?: QuickAccount, getFileDownloadUrl?: (fileId: string, variant: string) => string) => QuickAccount;
104
97
  /**
105
98
  * Merge a fresh `/auth/refresh-all` snapshot into an existing QuickAccount
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxyhq/core",
3
- "version": "3.4.15",
3
+ "version": "3.4.17",
4
4
  "description": "OxyHQ SDK Foundation — API client, authentication, cryptographic identity, and shared utilities",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -97,7 +97,7 @@
97
97
  }
98
98
  },
99
99
  "dependencies": {
100
- "@oxyhq/contracts": "^0.1.0",
100
+ "@oxyhq/contracts": "^0.1.1",
101
101
  "bip39": "^3.1.0",
102
102
  "buffer": "^6.0.3",
103
103
  "elliptic": "^6.6.1",
@@ -497,7 +497,7 @@ export class AuthManager {
497
497
  this.oxyServices.httpService.setTokens(session.accessToken);
498
498
  }
499
499
 
500
- if (session.user && typeof (session.user as any).id === 'string' && (session.user as any).id.length > 0) {
500
+ if (session.user && typeof session.user.id === 'string' && session.user.id.length > 0) {
501
501
  this.currentUser = session.user;
502
502
  }
503
503
 
@@ -514,6 +514,7 @@ export class AuthManager {
514
514
  user: {
515
515
  id: session.user.id,
516
516
  username: session.user.username,
517
+ name: session.user.name,
517
518
  avatar: session.user.avatar ?? null,
518
519
  },
519
520
  accessToken: session.accessToken,
@@ -758,6 +759,7 @@ export class AuthManager {
758
759
  return {
759
760
  id: account.user.id,
760
761
  username: account.user.username,
762
+ name: account.user.name,
761
763
  avatar: account.user.avatar ?? undefined,
762
764
  };
763
765
  }
@@ -806,6 +808,7 @@ export class AuthManager {
806
808
  this.currentUser = {
807
809
  id: hydrated.id,
808
810
  username: hydrated.username,
811
+ name: hydrated.name,
809
812
  avatar: hydrated.avatar ?? undefined,
810
813
  };
811
814
  this.notifyListeners();
@@ -1022,6 +1025,7 @@ export class AuthManager {
1022
1025
  ? {
1023
1026
  id: updated.user.id,
1024
1027
  username: updated.user.username,
1028
+ name: updated.user.name,
1025
1029
  avatar: updated.user.avatar ?? undefined,
1026
1030
  }
1027
1031
  : null;
@@ -1083,6 +1087,7 @@ export class AuthManager {
1083
1087
  ? {
1084
1088
  id: next.user.id,
1085
1089
  username: next.user.username,
1090
+ name: next.user.name,
1086
1091
  avatar: next.user.avatar ?? undefined,
1087
1092
  }
1088
1093
  : null;
@@ -684,7 +684,7 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
684
684
  continue;
685
685
  }
686
686
  const userId = e.user.id ?? e.user._id;
687
- if (!userId || !e.user.username) {
687
+ if (!userId || !e.user.username || !e.user.name?.displayName) {
688
688
  continue;
689
689
  }
690
690
  if (typeof e.authuser !== 'number') {
@@ -25,6 +25,7 @@
25
25
 
26
26
  import type { OxyServicesBase } from '../OxyServices.base';
27
27
  import type { SessionLoginResponse, MinimalUserData } from '../models/session';
28
+ import type { UserNameResponse } from '@oxyhq/contracts';
28
29
  import { createDebugLogger } from '../shared/utils/debugUtils';
29
30
 
30
31
  const debug = createDebugLogger('SSO');
@@ -40,6 +41,7 @@ interface SsoExchangeWireResponse {
40
41
  id?: string;
41
42
  _id?: string;
42
43
  username?: string;
44
+ name?: UserNameResponse;
43
45
  avatar?: string;
44
46
  };
45
47
  expiresAt?: string;
@@ -140,13 +142,14 @@ export function OxyServicesSsoMixin<T extends typeof OxyServicesBase>(Base: T) {
140
142
  }
141
143
 
142
144
  const userId = payload.user?.id ?? payload.user?._id;
143
- if (!userId || typeof payload.user?.username !== 'string') {
145
+ if (!userId || typeof payload.user?.username !== 'string' || typeof payload.user.name?.displayName !== 'string') {
144
146
  throw this.handleError(new Error('SSO exchange returned an invalid user'));
145
147
  }
146
148
 
147
149
  const user: MinimalUserData = {
148
150
  id: userId,
149
151
  username: payload.user.username,
152
+ name: payload.user.name,
150
153
  avatar: payload.user.avatar,
151
154
  };
152
155
 
@@ -10,6 +10,7 @@ import type {
10
10
  PaginationInfo,
11
11
  PrivacySettings,
12
12
  } from '../models/interfaces';
13
+ import type { UserNameResponse, UserProfileUpdate } from '@oxyhq/contracts';
13
14
  import type { OxyServicesBase } from '../OxyServices.base';
14
15
  import { buildSearchParams, buildPaginationParams, type PaginationParams } from '../utils/apiUtils';
15
16
  import { KeyManager } from '../crypto/keyManager';
@@ -38,7 +39,7 @@ export function OxyServicesUserMixin<T extends typeof OxyServicesBase>(Base: T)
38
39
 
39
40
  /**
40
41
  * Lightweight username lookup for login flows.
41
- * Returns minimal public info: exists, color, avatar, displayName.
42
+ * Returns minimal public info: exists, color, avatar, name.displayName.
42
43
  * Faster than getProfileByUsername — no stats, no formatting.
43
44
  */
44
45
  async lookupUsername(username: string): Promise<{
@@ -46,7 +47,7 @@ export function OxyServicesUserMixin<T extends typeof OxyServicesBase>(Base: T)
46
47
  username: string;
47
48
  color: string | null;
48
49
  avatar: string | null;
49
- displayName: string;
50
+ name: UserNameResponse;
50
51
  }> {
51
52
  return await this.makeRequest('GET', `/auth/lookup/${encodeURIComponent(username)}`, undefined, {
52
53
  cache: true,
@@ -150,7 +151,7 @@ export function OxyServicesUserMixin<T extends typeof OxyServicesBase>(Base: T)
150
151
  }): Promise<Array<{
151
152
  id: string;
152
153
  username: string;
153
- name?: { first?: string; last?: string; full?: string };
154
+ name: UserNameResponse;
154
155
  description?: string;
155
156
  isFederated?: boolean;
156
157
  isAgent?: boolean;
@@ -225,7 +226,7 @@ export function OxyServicesUserMixin<T extends typeof OxyServicesBase>(Base: T)
225
226
  *
226
227
  * TanStack Query handles offline queuing automatically.
227
228
  */
228
- async updateProfile(updates: Partial<User>): Promise<User> {
229
+ async updateProfile(updates: UserProfileUpdate): Promise<User> {
229
230
  try {
230
231
  const result = normalizeUserIdentity(
231
232
  await this.makeRequest<User>('PUT', '/users/me', updates, { cache: false }),
@@ -89,28 +89,29 @@ export interface User {
89
89
  username: string;
90
90
  email?: string;
91
91
  // Avatar file id (asset id)
92
- avatar?: string;
92
+ avatar?: string | null;
93
93
  // Named color preset (e.g. 'teal', 'blue', 'purple')
94
- color?: string;
94
+ color?: string | null;
95
95
  // Privacy and security settings
96
96
  privacySettings?: PrivacySettings;
97
97
  /**
98
- * Structured human name. The canonical wire shape ({@link UserNameResponse}):
99
- * `{ first?, last?, full? }` where `full` is a Mongoose virtual (present only
100
- * when the query materialised virtuals). The single source of truth lives in
101
- * `@oxyhq/contracts` — do NOT re-declare a bare `string` here.
98
+ * Structured human name. `name.displayName` is the canonical display string
99
+ * resolved by the API; consumers render it directly instead of recomposing
100
+ * names from `first` / `last` / `full` / `username`.
102
101
  */
103
- name?: UserNameResponse;
102
+ name: UserNameResponse;
104
103
  bio?: string;
105
104
  location?: string;
106
105
  website?: string;
107
106
  createdAt?: string;
108
107
  updatedAt?: string;
109
- links?: Array<{
108
+ links?: string[];
109
+ linksMetadata?: Array<{
110
+ url: string;
110
111
  title?: string;
111
112
  description?: string;
112
113
  image?: string;
113
- link: string;
114
+ id?: string;
114
115
  }>;
115
116
  // Social counts
116
117
  _count?: {
@@ -651,7 +652,7 @@ export interface RefreshAllAccountUser {
651
652
  * string. The server projects `name` verbatim from the user document. The
652
653
  * single source of truth is `@oxyhq/contracts`.
653
654
  */
654
- name?: UserNameResponse;
655
+ name: UserNameResponse;
655
656
  avatar?: string | null;
656
657
  email?: string;
657
658
  color?: string | null;
@@ -1,3 +1,5 @@
1
+ import type { UserNameResponse } from '@oxyhq/contracts';
2
+
1
3
  export interface ClientSession {
2
4
  sessionId: string;
3
5
  deviceId: string;
@@ -23,6 +25,7 @@ export interface StorageKeys {
23
25
  export interface MinimalUserData {
24
26
  id: string;
25
27
  username: string;
28
+ name: UserNameResponse;
26
29
  avatar?: string; // file id
27
30
  }
28
31
 
@@ -4,16 +4,16 @@ import {
4
4
  formatPublicKeyHandle,
5
5
  } from '../accountUtils';
6
6
 
7
- /**
8
- * Regression coverage for the auth-app display-name drift (Phase 1 of the
9
- * contract-centralisation refactor).
10
- *
11
- * The auth app previously required BOTH `name.first` AND `name.last` to compose
12
- * a display name, falling back to the lowercase `username` for first-name-only
13
- * accounts. The canonical resolver in core MUST be first-name-only safe: a user
14
- * with only a first name resolves to that first name, never to the username.
15
- */
16
7
  describe('getAccountDisplayName', () => {
8
+ it('prefers the API name.displayName when present', () => {
9
+ expect(
10
+ getAccountDisplayName({
11
+ name: { first: 'Nate', displayName: 'Nate Isern' },
12
+ username: 'nateus',
13
+ }),
14
+ ).toBe('Nate Isern');
15
+ });
16
+
17
17
  it('returns first name for first-name-only accounts (NOT the username)', () => {
18
18
  const result = getAccountDisplayName({
19
19
  name: { first: 'Nate' },
@@ -50,20 +50,7 @@ describe('getAccountDisplayName', () => {
50
50
  ).toBe('Nathaniel Isern');
51
51
  });
52
52
 
53
- it('uses the structured name over a server displayName virtual', () => {
54
- // Server `displayName` virtual = `username || truncatedKey`; it ignores
55
- // the structured name. A real name must win so a first-only account is
56
- // never collapsed to its username.
57
- expect(
58
- getAccountDisplayName({
59
- name: { first: 'Nate' },
60
- displayName: 'nateus',
61
- username: 'nateus',
62
- }),
63
- ).toBe('Nate');
64
- });
65
-
66
- it('uses displayName when there is no structured name', () => {
53
+ it('uses pre-normalized account-row displayName when there is no structured name', () => {
67
54
  expect(
68
55
  getAccountDisplayName({
69
56
  displayName: 'Cool Display',
@@ -30,14 +30,14 @@ export interface QuickAccount {
30
30
 
31
31
  /** Minimal user shape accepted by display-name helpers. Avoids importing the full User type. */
32
32
  export interface DisplayNameUserShape {
33
- name?: string | { first?: string; last?: string; full?: string; [key: string]: unknown };
34
- /**
35
- * Pre-resolved display name as emitted by the server's `displayName` virtual
36
- * (raw `/users/me` responses). NOTE: the server virtual resolves to
37
- * `username || truncatedPublicKey || 'Anonymous'` — it does NOT compose the
38
- * structured `name`. It is therefore preferred only AFTER a real structured
39
- * name, so a first-name-only account never collapses to its username/key.
40
- */
33
+ name?: string | {
34
+ displayName?: string;
35
+ first?: string;
36
+ last?: string;
37
+ full?: string;
38
+ [key: string]: unknown;
39
+ };
40
+ /** Pre-normalized account-row display name, not the API User DTO field. */
41
41
  displayName?: string;
42
42
  username?: string;
43
43
  publicKey?: string;
@@ -57,16 +57,13 @@ export const formatPublicKeyHandle = (publicKey: string): string => {
57
57
  * Resolve a friendly display name for a user.
58
58
  *
59
59
  * Order of preference:
60
- * 1. `name.full`, or composed `name.first name.last` (FIRST-NAME-ONLY SAFE —
61
- * a user with only a first name resolves to that first name, never to the
62
- * lowercase username; this is the exact drift bug the auth app hit).
63
- * 2. `name` (when stored as a plain string)
64
- * 3. `displayName` (server `displayName` virtual — `username || truncatedKey`).
65
- * Placed AFTER the structured name on purpose: the server virtual ignores
66
- * `name`, so preferring it first would re-introduce the first-only bug.
67
- * 4. `username`
68
- * 5. `Account 0x12345678…` (derived from publicKey, when present)
69
- * 6. Translated fallback (e.g. "Unnamed")
60
+ * 1. `name.displayName` from the API user contract.
61
+ * 2. `name.full`, or composed `name.first name.last` for local unsaved shapes.
62
+ * 3. `name` when passed as a plain string by local non-DTO call sites.
63
+ * 4. pre-normalized account-row `displayName`.
64
+ * 5. `username`
65
+ * 6. `Account 0x12345678…` (derived from publicKey, when present)
66
+ * 7. Translated fallback (e.g. "Unnamed")
70
67
  *
71
68
  * The translation key `common.unnamed` is used for the final fallback. If the
72
69
  * caller does not pass a locale, the default English translation is used.
@@ -80,6 +77,9 @@ export const getAccountDisplayName = (
80
77
  const { name, displayName, username, publicKey } = user;
81
78
 
82
79
  if (name && typeof name === 'object') {
80
+ if (typeof name.displayName === 'string' && name.displayName.trim()) {
81
+ return name.displayName.trim();
82
+ }
83
83
  if (typeof name.full === 'string' && name.full.trim()) return name.full.trim();
84
84
  const first = typeof name.first === 'string' ? name.first.trim() : '';
85
85
  const last = typeof name.last === 'string' ? name.last.trim() : '';
@@ -146,12 +146,12 @@ export const buildAccountsArray = (
146
146
  export const createQuickAccount = (
147
147
  sessionId: string,
148
148
  userData: {
149
- name?: string | { full?: string; first?: string; last?: string };
149
+ name?: string | { displayName?: string; full?: string; first?: string; last?: string };
150
150
  username?: string;
151
151
  publicKey?: string;
152
152
  id?: string;
153
153
  _id?: { toString(): string } | string;
154
- avatar?: string;
154
+ avatar?: string | null;
155
155
  },
156
156
  existingAccount?: QuickAccount,
157
157
  getFileDownloadUrl?: (fileId: string, variant: string) => string
@@ -161,10 +161,11 @@ export const createQuickAccount = (
161
161
 
162
162
  // Preserve existing avatarUrl if avatar hasn't changed (prevents image reload)
163
163
  let avatarUrl: string | undefined;
164
- if (existingAccount && existingAccount.avatar === userData.avatar && existingAccount.avatarUrl) {
164
+ const avatar = userData.avatar ?? undefined;
165
+ if (existingAccount && existingAccount.avatar === avatar && existingAccount.avatarUrl) {
165
166
  avatarUrl = existingAccount.avatarUrl;
166
- } else if (userData.avatar && getFileDownloadUrl) {
167
- avatarUrl = getFileDownloadUrl(userData.avatar, 'thumb');
167
+ } else if (avatar && getFileDownloadUrl) {
168
+ avatarUrl = getFileDownloadUrl(avatar, 'thumb');
168
169
  }
169
170
 
170
171
  return {
@@ -172,7 +173,7 @@ export const createQuickAccount = (
172
173
  userId,
173
174
  username: userData.username || '',
174
175
  displayName,
175
- avatar: userData.avatar,
176
+ avatar,
176
177
  avatarUrl,
177
178
  };
178
179
  };
@@ -277,4 +277,4 @@ export async function retryOnError<T>(
277
277
  const errorCode = error?.code || error?.status || error?.message;
278
278
  return retryableErrors.includes(errorCode);
279
279
  });
280
- }
280
+ }