@oxyhq/core 14.0.0 → 15.0.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 (37) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/crypto/recoveryPhrase.js +32 -66
  3. package/dist/cjs/index.js +7 -0
  4. package/dist/cjs/mixins/OxyServices.deviceBoot.js +76 -0
  5. package/dist/cjs/mixins/OxyServices.reputation.js +47 -2
  6. package/dist/cjs/mixins/OxyServices.user.js +3 -4
  7. package/dist/cjs/session/accountProjection.js +4 -1
  8. package/dist/esm/.tsbuildinfo +1 -1
  9. package/dist/esm/crypto/recoveryPhrase.js +32 -33
  10. package/dist/esm/index.js +7 -0
  11. package/dist/esm/mixins/OxyServices.deviceBoot.js +77 -1
  12. package/dist/esm/mixins/OxyServices.reputation.js +47 -2
  13. package/dist/esm/mixins/OxyServices.user.js +3 -4
  14. package/dist/esm/session/accountProjection.js +4 -1
  15. package/dist/types/.tsbuildinfo +1 -1
  16. package/dist/types/crypto/recoveryPhrase.d.ts +6 -0
  17. package/dist/types/index.d.ts +0 -1
  18. package/dist/types/mixins/OxyServices.deviceBoot.d.ts +61 -1
  19. package/dist/types/mixins/OxyServices.identityBackup.d.ts +1 -1
  20. package/dist/types/mixins/OxyServices.reputation.d.ts +43 -276
  21. package/dist/types/mixins/OxyServices.user.d.ts +4 -1
  22. package/dist/types/models/interfaces.d.ts +6 -0
  23. package/package.json +3 -3
  24. package/src/crypto/__tests__/keyManager.test.ts +3 -2
  25. package/src/crypto/recoveryPhrase.ts +33 -34
  26. package/src/index.ts +5 -24
  27. package/src/mixins/OxyServices.deviceBoot.ts +85 -0
  28. package/src/mixins/OxyServices.identityBackup.ts +1 -1
  29. package/src/mixins/OxyServices.reputation.ts +88 -326
  30. package/src/mixins/OxyServices.user.ts +7 -3
  31. package/src/mixins/__tests__/OxyServices.deviceBoot.test.ts +59 -2
  32. package/src/mixins/__tests__/backgroundCredential.httpIntegration.test.ts +135 -0
  33. package/src/mixins/__tests__/followGraphPagination.test.ts +21 -0
  34. package/src/mixins/__tests__/reputation.test.ts +115 -1
  35. package/src/models/interfaces.ts +2 -0
  36. package/src/session/accountProjection.ts +5 -1
  37. package/src/types/bip39.d.ts +0 -32
@@ -6,43 +6,17 @@
6
6
  * for backing up and restoring user identities.
7
7
  *
8
8
  * Note: This module requires the polyfill to be loaded first (done via crypto/index.ts)
9
+ *
10
+ * Oxy recovery phrases are English-only, so the English wordlist is imported by
11
+ * its own subpath. Never reach for a package that exposes its wordlists through
12
+ * a barrel: `bip39`'s `_wordlists` hard-requires all ten languages, which put
13
+ * ~265 KB of unreachable wordlists into the initial chunk of every consuming
14
+ * app. Adding another language means one more subpath import, ideally lazy.
9
15
  */
10
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
11
- if (k2 === undefined) k2 = k;
12
- var desc = Object.getOwnPropertyDescriptor(m, k);
13
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
14
- desc = { enumerable: true, get: function() { return m[k]; } };
15
- }
16
- Object.defineProperty(o, k2, desc);
17
- }) : (function(o, m, k, k2) {
18
- if (k2 === undefined) k2 = k;
19
- o[k2] = m[k];
20
- }));
21
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
22
- Object.defineProperty(o, "default", { enumerable: true, value: v });
23
- }) : function(o, v) {
24
- o["default"] = v;
25
- });
26
- var __importStar = (this && this.__importStar) || (function () {
27
- var ownKeys = function(o) {
28
- ownKeys = Object.getOwnPropertyNames || function (o) {
29
- var ar = [];
30
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
31
- return ar;
32
- };
33
- return ownKeys(o);
34
- };
35
- return function (mod) {
36
- if (mod && mod.__esModule) return mod;
37
- var result = {};
38
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
39
- __setModuleDefault(result, mod);
40
- return result;
41
- };
42
- })();
43
16
  Object.defineProperty(exports, "__esModule", { value: true });
44
17
  exports.RecoveryPhraseService = exports.BACKUP_MATERIAL_LENGTH = exports.BACKUP_KDF_LOOKUP_INFO = exports.BACKUP_KDF_ENCRYPTION_INFO = exports.BACKUP_KDF_SALT = void 0;
45
- const bip39 = __importStar(require("bip39"));
18
+ const bip39_1 = require("@scure/bip39");
19
+ const english_1 = require("@scure/bip39/wordlists/english");
46
20
  const keyManager_1 = require("./keyManager");
47
21
  const kdf_1 = require("./kdf");
48
22
  /**
@@ -84,13 +58,12 @@ class RecoveryPhraseService {
84
58
  */
85
59
  static async generateIdentityWithRecovery(options) {
86
60
  // Generate 128-bit entropy for 12-word mnemonic
87
- const mnemonic = bip39.generateMnemonic(128);
61
+ const mnemonic = (0, bip39_1.generateMnemonic)(english_1.wordlist, 128);
88
62
  // Derive private key from mnemonic
89
63
  // Using the seed directly as the private key (simplified approach)
90
- const seed = await bip39.mnemonicToSeed(mnemonic);
64
+ const seed = await (0, bip39_1.mnemonicToSeed)(mnemonic);
91
65
  // Use first 32 bytes of seed as private key
92
- const seedSlice = seed.subarray ? seed.subarray(0, 32) : seed.slice(0, 32);
93
- const privateKeyHex = toHex(seedSlice);
66
+ const privateKeyHex = toHex(seed.subarray(0, 32));
94
67
  // Import the derived key pair. KeyManager.importKeyPair will refuse to
95
68
  // clobber an existing identity unless overwrite is explicitly requested.
96
69
  const publicKey = await keyManager_1.KeyManager.importKeyPair(privateKeyHex, {
@@ -109,10 +82,9 @@ class RecoveryPhraseService {
109
82
  */
110
83
  static async generateIdentityWithRecovery24(options) {
111
84
  // Generate 256-bit entropy for 24-word mnemonic
112
- const mnemonic = bip39.generateMnemonic(256);
113
- const seed = await bip39.mnemonicToSeed(mnemonic);
114
- const seedSlice = seed.subarray ? seed.subarray(0, 32) : seed.slice(0, 32);
115
- const privateKeyHex = toHex(seedSlice);
85
+ const mnemonic = (0, bip39_1.generateMnemonic)(english_1.wordlist, 256);
86
+ const seed = await (0, bip39_1.mnemonicToSeed)(mnemonic);
87
+ const privateKeyHex = toHex(seed.subarray(0, 32));
116
88
  const publicKey = await keyManager_1.KeyManager.importKeyPair(privateKeyHex, {
117
89
  overwrite: options?.overwrite === true,
118
90
  });
@@ -136,10 +108,9 @@ class RecoveryPhraseService {
136
108
  * committed anywhere — if it is lost the account becomes unrecoverable.
137
109
  */
138
110
  static async derivePendingIdentity() {
139
- const mnemonic = bip39.generateMnemonic(128);
140
- const seed = await bip39.mnemonicToSeed(mnemonic);
141
- const seedSlice = seed.subarray ? seed.subarray(0, 32) : seed.slice(0, 32);
142
- const privateKey = toHex(seedSlice);
111
+ const mnemonic = (0, bip39_1.generateMnemonic)(english_1.wordlist, 128);
112
+ const seed = await (0, bip39_1.mnemonicToSeed)(mnemonic);
113
+ const privateKey = toHex(seed.subarray(0, 32));
143
114
  const publicKey = keyManager_1.KeyManager.derivePublicKey(privateKey);
144
115
  return {
145
116
  phrase: mnemonic,
@@ -158,12 +129,11 @@ class RecoveryPhraseService {
158
129
  */
159
130
  static async derivePrivateKeyFromPhrase(phrase) {
160
131
  const normalizedPhrase = phrase.trim().toLowerCase();
161
- if (!bip39.validateMnemonic(normalizedPhrase)) {
132
+ if (!(0, bip39_1.validateMnemonic)(normalizedPhrase, english_1.wordlist)) {
162
133
  throw new Error('Invalid recovery phrase');
163
134
  }
164
- const seed = await bip39.mnemonicToSeed(normalizedPhrase);
165
- const seedSlice = seed.subarray ? seed.subarray(0, 32) : seed.slice(0, 32);
166
- return toHex(seedSlice);
135
+ const seed = await (0, bip39_1.mnemonicToSeed)(normalizedPhrase);
136
+ return toHex(seed.subarray(0, 32));
167
137
  }
168
138
  /**
169
139
  * Derive the encrypted-backup key material from a recovery phrase (b3 Feature
@@ -184,10 +154,10 @@ class RecoveryPhraseService {
184
154
  */
185
155
  static async deriveBackupMaterial(phrase) {
186
156
  const normalizedPhrase = phrase.trim().toLowerCase();
187
- if (!bip39.validateMnemonic(normalizedPhrase)) {
157
+ if (!(0, bip39_1.validateMnemonic)(normalizedPhrase, english_1.wordlist)) {
188
158
  throw new Error('Invalid recovery phrase. Please check the words and try again.');
189
159
  }
190
- const seed = await bip39.mnemonicToSeed(normalizedPhrase);
160
+ const seed = await (0, bip39_1.mnemonicToSeed)(normalizedPhrase);
191
161
  const salt = utf8(exports.BACKUP_KDF_SALT);
192
162
  const backupKey = (0, kdf_1.hkdfSha256)(seed, salt, utf8(exports.BACKUP_KDF_ENCRYPTION_INFO), exports.BACKUP_MATERIAL_LENGTH);
193
163
  const lookupId = toHex((0, kdf_1.hkdfSha256)(seed, salt, utf8(exports.BACKUP_KDF_LOOKUP_INFO), exports.BACKUP_MATERIAL_LENGTH));
@@ -204,13 +174,12 @@ class RecoveryPhraseService {
204
174
  static async restoreFromPhrase(phrase, options) {
205
175
  // Normalize and validate the phrase
206
176
  const normalizedPhrase = phrase.trim().toLowerCase();
207
- if (!bip39.validateMnemonic(normalizedPhrase)) {
177
+ if (!(0, bip39_1.validateMnemonic)(normalizedPhrase, english_1.wordlist)) {
208
178
  throw new Error('Invalid recovery phrase. Please check the words and try again.');
209
179
  }
210
180
  // Derive the same private key from the mnemonic
211
- const seed = await bip39.mnemonicToSeed(normalizedPhrase);
212
- const seedSlice = seed.subarray ? seed.subarray(0, 32) : seed.slice(0, 32);
213
- const privateKeyHex = toHex(seedSlice);
181
+ const seed = await (0, bip39_1.mnemonicToSeed)(normalizedPhrase);
182
+ const privateKeyHex = toHex(seed.subarray(0, 32));
214
183
  // Import and store the key pair
215
184
  const publicKey = await keyManager_1.KeyManager.importKeyPair(privateKeyHex, {
216
185
  overwrite: options?.overwrite === true,
@@ -222,28 +191,26 @@ class RecoveryPhraseService {
222
191
  */
223
192
  static validatePhrase(phrase) {
224
193
  const normalizedPhrase = phrase.trim().toLowerCase();
225
- return bip39.validateMnemonic(normalizedPhrase);
194
+ return (0, bip39_1.validateMnemonic)(normalizedPhrase, english_1.wordlist);
226
195
  }
227
196
  /**
228
197
  * Get the word list for autocomplete/validation
229
198
  */
230
199
  static getWordList() {
231
- return bip39.wordlists.english;
200
+ return english_1.wordlist;
232
201
  }
233
202
  /**
234
203
  * Check if a word is valid in the BIP39 word list
235
204
  */
236
205
  static isValidWord(word) {
237
- return bip39.wordlists.english.includes(word.toLowerCase());
206
+ return english_1.wordlist.includes(word.toLowerCase());
238
207
  }
239
208
  /**
240
209
  * Get suggestions for a partial word
241
210
  */
242
211
  static getSuggestions(partial, limit = 5) {
243
212
  const lowerPartial = partial.toLowerCase();
244
- return bip39.wordlists.english
245
- .filter((word) => word.startsWith(lowerPartial))
246
- .slice(0, limit);
213
+ return english_1.wordlist.filter((word) => word.startsWith(lowerPartial)).slice(0, limit);
247
214
  }
248
215
  /**
249
216
  * Derive the public key from a phrase without storing
@@ -251,12 +218,11 @@ class RecoveryPhraseService {
251
218
  */
252
219
  static async derivePublicKeyFromPhrase(phrase) {
253
220
  const normalizedPhrase = phrase.trim().toLowerCase();
254
- if (!bip39.validateMnemonic(normalizedPhrase)) {
221
+ if (!(0, bip39_1.validateMnemonic)(normalizedPhrase, english_1.wordlist)) {
255
222
  throw new Error('Invalid recovery phrase');
256
223
  }
257
- const seed = await bip39.mnemonicToSeed(normalizedPhrase);
258
- const seedSlice = seed.subarray ? seed.subarray(0, 32) : seed.slice(0, 32);
259
- const privateKeyHex = toHex(seedSlice);
224
+ const seed = await (0, bip39_1.mnemonicToSeed)(normalizedPhrase);
225
+ const privateKeyHex = toHex(seed.subarray(0, 32));
260
226
  return keyManager_1.KeyManager.derivePublicKey(privateKeyHex);
261
227
  }
262
228
  /**
package/dist/cjs/index.js CHANGED
@@ -67,6 +67,13 @@ Object.defineProperty(exports, "normalizeProfileLinks", { enumerable: true, get:
67
67
  var OxyServices_accounts_1 = require("./mixins/OxyServices.accounts");
68
68
  Object.defineProperty(exports, "ORGANIZATION_CATEGORIES", { enumerable: true, get: function () { return OxyServices_accounts_1.ORGANIZATION_CATEGORIES; } });
69
69
  // ---------------------------------------------------------------------------
70
+ // Reputation (Oxy Trust: ledger, balances, disputes, rules, influence).
71
+ // The whole type family — the closed value sets, the two balance views and the
72
+ // `isFullReputationBalance` narrowing guard, the ledger/dispute/rule/leaderboard
73
+ // shapes, and the write-endpoint inputs — is owned by `@oxyhq/contracts`, which
74
+ // the API's serializers are validated against. Import them from there.
75
+ // ---------------------------------------------------------------------------
76
+ // ---------------------------------------------------------------------------
70
77
  // Self-sovereign identity (DID, signed records, auth-method ↔ VM mapping,
71
78
  // verified domains). Wire shapes (DidDocument, SignedRecordEnvelope,
72
79
  // AuthMethodsResponse, VerifiedDomain, DomainVerificationInstructions,
@@ -16,8 +16,14 @@ exports.OxyServicesDeviceBootMixin = OxyServicesDeviceBootMixin;
16
16
  * This method carries NO persistence or token-planting side effects of its own;
17
17
  * the cold boot / re-mint handler own persistence and `setTokens`, so the same
18
18
  * primitive can be reused from either without double-planting.
19
+ *
20
+ * `provisionBackgroundCredential` is the mixin's second, adjacent call: it hands
21
+ * native background code (no JS runtime) its own non-rotating credential so that
22
+ * code never has to mint from — and therefore never rotates — the device secret
23
+ * JS depends on.
19
24
  */
20
25
  const contracts_1 = require("@oxyhq/contracts");
26
+ const errorUtils_1 = require("../utils/errorUtils");
21
27
  /**
22
28
  * The server's `401 account_not_on_device` for a PINNED mint: the requested
23
29
  * `accountId` is not (or is no longer) a live account of this device session.
@@ -110,5 +116,75 @@ function OxyServicesDeviceBootMixin(Base) {
110
116
  throw normalized;
111
117
  }
112
118
  }
119
+ /**
120
+ * Provision a NON-rotating background credential for the caller's account on
121
+ * this device — the credential native background code (an Android widget
122
+ * worker, which runs with no JS runtime) presents to mint its own access
123
+ * tokens without any JS involvement.
124
+ *
125
+ * It exists precisely so background code never touches the device secret:
126
+ * `POST /session/device/token` ROTATES that secret on every mint (the
127
+ * presented one stays valid only for a short grace), so a worker minting
128
+ * from it would become a second writer of the value JS depends on and could
129
+ * silently sign the user out. The background credential is a separate,
130
+ * non-rotating value minted server-side, so the two lanes never contend.
131
+ *
132
+ * Bearer required and NO body: the server derives both the `deviceId` and
133
+ * the account from the validated bearer. This is the only way a background
134
+ * credential comes into existence, so background code can EXTEND a session
135
+ * the user established in-app but can never bootstrap one from nothing.
136
+ *
137
+ * Unlike the mint above this is NOT a control-plane call — it runs while a
138
+ * session is already live — so it takes the normal authenticated path: no
139
+ * `skipAuth` (a 401 should go through the ordinary re-mint lane) and no
140
+ * `bypassQueue` (nothing in the auth lane is parked awaiting it).
141
+ *
142
+ * There is deliberately no JS counterpart that MINTS from the returned
143
+ * credential: the native side owns that call, and a symmetric-looking JS
144
+ * method would be dead code plus a second implementation of the failure
145
+ * rules. The asymmetry is the design.
146
+ *
147
+ * **Call this from NATIVE only.** There is no background worker on web to
148
+ * consume the credential, and handing a browser origin a long-lived
149
+ * non-rotating secret to persist is strictly weaker than the rotating device
150
+ * secret it already holds. The 404 degrade below is also native-shaped: a
151
+ * browser attaches `Origin`, which a server predating this route answers
152
+ * `403 BAD_ORIGIN` from its router-wide same-site guard rather than 404, so
153
+ * the quiet degrade would not fire there. A native client sends no `Origin`
154
+ * and gets the 404. Gate the caller by platform; do not widen the degrade to
155
+ * 403, which would also swallow a genuine origin misconfiguration.
156
+ *
157
+ * That paragraph is LOAD-BEARING, not belt-and-braces: the route sits above
158
+ * oxy-api's router-wide origin guard (deliberately, so a native client with
159
+ * no `Origin` is not rejected), so as of this writing NOTHING server-side
160
+ * refuses a browser caller that presents a valid bearer. Until a server-side
161
+ * check lands, caller discipline is the only control — which is also why a
162
+ * doc note cannot be the whole answer to browser XSS minting a long-lived
163
+ * credential with the victim's bearer.
164
+ *
165
+ * @returns the provisioned credential, or `null` when the endpoint is absent
166
+ * (404). The API deploy leads the SDK release, so a client on a newer SDK
167
+ * than the server degrades to "no background session" quietly instead of
168
+ * surfacing an error. The status is read off the RAW rejection because
169
+ * `handleError` only preserves it for errors carrying `HttpService`'s
170
+ * annotations.
171
+ * @throws if the response does not match {@link deviceBackgroundCredentialResponseSchema}.
172
+ */
173
+ async provisionBackgroundCredential() {
174
+ try {
175
+ const res = await this.makeRequest('POST', '/session/device/background-credential', undefined, { cache: false });
176
+ const parsed = (0, contracts_1.safeParseContract)(contracts_1.deviceBackgroundCredentialResponseSchema, res);
177
+ if (!parsed) {
178
+ throw new Error('session/device/background-credential returned an unexpected response shape');
179
+ }
180
+ return parsed;
181
+ }
182
+ catch (error) {
183
+ if ((0, errorUtils_1.extractErrorStatus)(error) === 404) {
184
+ return null;
185
+ }
186
+ throw this.handleError(error);
187
+ }
188
+ }
113
189
  };
114
190
  }
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.OxyServicesReputationMixin = OxyServicesReputationMixin;
4
+ const contracts_1 = require("@oxyhq/contracts");
5
+ const OxyServices_errors_1 = require("../OxyServices.errors");
4
6
  const mixinHelpers_1 = require("./mixinHelpers");
5
7
  /** Cache-key prefix for every cached `GET /reputation/...` response. */
6
8
  const REPUTATION_CACHE_PREFIX = 'GET:/reputation/';
@@ -10,8 +12,16 @@ function OxyServicesReputationMixin(Base) {
10
12
  super(...args);
11
13
  }
12
14
  /**
13
- * Get a user's cached reputation balance derived totals, per-category
14
- * breakdown, trust tier, capped influence weights, and reliability signals.
15
+ * Get ANY user's reputation balance, in whichever view the server serves the
16
+ * caller.
17
+ *
18
+ * A third party gets `userId`, `total` and `trustTier` and nothing else, so
19
+ * the return type is a {@link ReputationBalanceView} union: narrow it with
20
+ * {@link isFullReputationBalance} before touching `breakdown`, `influence`
21
+ * or `reliability`. To read your OWN balance, call
22
+ * {@link getMyReputationBalance} instead — it returns the full shape with no
23
+ * narrowing.
24
+ *
15
25
  * @param userId - The subject user's `_id` or publicKey.
16
26
  */
17
27
  async getReputationBalance(userId) {
@@ -22,6 +32,37 @@ function OxyServicesReputationMixin(Base) {
22
32
  throw this.handleError(error);
23
33
  }
24
34
  }
35
+ /**
36
+ * Get the SIGNED-IN user's own reputation balance, in full.
37
+ *
38
+ * The subject view is the only one carrying `breakdown`, `influence` and
39
+ * `reliability`, and the subject is the common caller, so this is the
40
+ * ergonomic path: no id to pass, no narrowing to do.
41
+ *
42
+ * Throws rather than returning a half-populated object when the request was
43
+ * not authenticated as the subject — with no signed-in user, and when the
44
+ * server answered `200` with the public view anyway (which it does for an
45
+ * absent or lapsed token, since the endpoint's auth is optional). Both mean
46
+ * the private blocks are simply absent, and a thrown error is the only
47
+ * honest report of that.
48
+ */
49
+ async getMyReputationBalance() {
50
+ const userId = this.getCurrentUserId();
51
+ if (!userId) {
52
+ throw new OxyServices_errors_1.OxyAuthenticationError('Reading your own reputation balance requires a signed-in user');
53
+ }
54
+ let balance;
55
+ try {
56
+ balance = await this.makeRequest('GET', `/reputation/${encodeURIComponent(userId)}/balance`, undefined, { cache: true, cacheTTL: mixinHelpers_1.CACHE_TIMES.MEDIUM });
57
+ }
58
+ catch (error) {
59
+ throw this.handleError(error);
60
+ }
61
+ if (!(0, contracts_1.isFullReputationBalance)(balance)) {
62
+ throw new OxyServices_errors_1.OxyAuthenticationError('The reputation balance came back as the public view — the request was not authenticated as its subject');
63
+ }
64
+ return balance;
65
+ }
25
66
  /**
26
67
  * Get the reputation leaderboard, ordered by lifetime total descending.
27
68
  * @param limit - Page size (server-capped).
@@ -192,6 +233,10 @@ function OxyServicesReputationMixin(Base) {
192
233
  /**
193
234
  * Force a recompute of a user's balance snapshot from their active ledger
194
235
  * (staff only). Invalidates cached reputation reads.
236
+ *
237
+ * Staff-gated, so the response is always the full subject view — no
238
+ * narrowing needed.
239
+ *
195
240
  * @param userId - The subject user's `_id` or publicKey.
196
241
  */
197
242
  async recalculateReputation(userId) {
@@ -220,10 +220,9 @@ function OxyServicesUserMixin(Base) {
220
220
  /**
221
221
  * Get profiles similar to a given user, based on co-follower overlap.
222
222
  */
223
- async getSimilarProfiles(userId, limit) {
224
- const params = {};
225
- if (limit)
226
- params.limit = String(limit);
223
+ async getSimilarProfiles(userId, limitOrParams) {
224
+ const pagination = typeof limitOrParams === 'number' ? { limit: limitOrParams } : limitOrParams ?? {};
225
+ const params = (0, apiUtils_1.buildQueryParams)(pagination);
227
226
  const users = await this.makeRequest('GET', `/profiles/${userId}/similar`, params, {
228
227
  cache: true,
229
228
  cacheTTL: 5 * 60 * 1000, // 5 min cache
@@ -21,6 +21,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
21
21
  exports.projectSwitchableAccounts = projectSwitchableAccounts;
22
22
  exports.switchableAccountIds = switchableAccountIds;
23
23
  const accountUtils_1 = require("../utils/accountUtils");
24
+ const userHandle_1 = require("../utils/userHandle");
24
25
  /**
25
26
  * Pure union of device sign-ins and account-graph nodes into the flat
26
27
  * {@link SwitchableAccount}[] every switcher renders.
@@ -47,7 +48,9 @@ function projectSwitchableAccounts(input) {
47
48
  kind: opts.kind,
48
49
  parentAccountId: opts.parentAccountId,
49
50
  callerMembership: opts.callerMembership,
50
- displayName: (0, accountUtils_1.getAccountDisplayName)(accountUser, locale),
51
+ displayName: accountUser.name?.displayName ??
52
+ (0, userHandle_1.getNormalizedUserHandle)(accountUser) ??
53
+ (0, accountUtils_1.getAccountDisplayName)(null, locale),
51
54
  // Real email, or the `@handle` fallback (NEVER synthesized).
52
55
  email: accountUser.email ?? secondaryHandle,
53
56
  avatarUrl: resolveAvatarUrl(accountUser.avatar),