@oxyhq/core 13.2.0 → 15.0.0

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 (36) 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 +58 -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 +59 -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 +43 -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 +67 -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__/followGraphPagination.test.ts +21 -0
  33. package/src/mixins/__tests__/reputation.test.ts +115 -1
  34. package/src/models/interfaces.ts +2 -0
  35. package/src/session/accountProjection.ts +5 -1
  36. package/src/types/bip39.d.ts +0 -32
@@ -10,19 +10,58 @@
10
10
  */
11
11
 
12
12
  import { OxyServices } from '../../OxyServices';
13
+ import { OxyAuthenticationError } from '../../OxyServices.errors';
14
+ import { isFullReputationBalance } from '@oxyhq/contracts';
13
15
  import type {
14
16
  ReputationBalance,
17
+ ReputationBalanceSummary,
18
+ ReputationBalanceView,
15
19
  ReputationTransaction,
16
20
  ReputationDispute,
17
21
  ReputationRule,
18
22
  ReputationLeaderboardEntry,
19
23
  ReputationInfluenceResult,
20
- } from '../OxyServices.reputation';
24
+ } from '@oxyhq/contracts';
21
25
 
22
26
  const setAccessTokenForTest = (oxy: OxyServices): void => {
23
27
  oxy.httpService.setTokens('test-token');
24
28
  };
25
29
 
30
+ /** An unsigned JWT carrying just the `userId` claim `getCurrentUserId` decodes. */
31
+ const signedInToken = (userId: string): string => {
32
+ const encode = (value: object): string =>
33
+ Buffer.from(JSON.stringify(value))
34
+ .toString('base64')
35
+ .replace(/\+/g, '-')
36
+ .replace(/\//g, '_')
37
+ .replace(/=+$/, '');
38
+ return `${encode({ alg: 'none', typ: 'JWT' })}.${encode({ userId })}.`;
39
+ };
40
+
41
+ /**
42
+ * The fields the SUBJECT view adds. Mirrors the private list the API's
43
+ * `reputationReadAuthz` test asserts absent from the public view.
44
+ */
45
+ const FULL_BALANCE_FIELD_NAMES = [
46
+ 'positive',
47
+ 'negative',
48
+ 'breakdown',
49
+ 'influence',
50
+ 'reliability',
51
+ 'recalculatedAt',
52
+ 'updatedAt',
53
+ ] as const;
54
+
55
+ /*
56
+ * The COMPILE-TIME half of this guarantee — that no private field is reachable
57
+ * on `ReputationBalanceView` without narrowing — is asserted in the SOURCE file,
58
+ * not here: `ts-jest` runs with `diagnostics: false` and `tsconfig.json`
59
+ * excludes `**\/__tests__`, so a type-level assertion written in this file could
60
+ * never fail. See `_PrivateFieldsAreUnreachableOnTheView` in
61
+ * `@oxyhq/contracts`' `src/reputation.ts`, which that package's
62
+ * `bun run typescript` and `build:types` both check.
63
+ */
64
+
26
65
  const balanceFixture: ReputationBalance = {
27
66
  userId: 'u1',
28
67
  total: 120,
@@ -53,6 +92,13 @@ const balanceFixture: ReputationBalance = {
53
92
  updatedAt: '2026-06-16T00:00:00.000Z',
54
93
  };
55
94
 
95
+ /** What the API serves a caller who is neither the subject nor staff. */
96
+ const summaryFixture: ReputationBalanceSummary = {
97
+ userId: 'u1',
98
+ total: 120,
99
+ trustTier: 'trusted',
100
+ };
101
+
56
102
  const transactionFixture: ReputationTransaction = {
57
103
  id: 't1',
58
104
  userId: 'u1',
@@ -121,6 +167,74 @@ describe('OxyServices.reputation', () => {
121
167
  await oxy.getReputationBalance('a b/c');
122
168
  expect(makeRequestSpy.mock.calls[0][1]).toBe('/reputation/a%20b%2Fc/balance');
123
169
  });
170
+
171
+ it('passes the public view through unchanged for a third-party subject', async () => {
172
+ makeRequestSpy.mockResolvedValue(summaryFixture);
173
+
174
+ const result = await oxy.getReputationBalance('someone-else');
175
+
176
+ expect(result).toEqual(summaryFixture);
177
+ expect(isFullReputationBalance(result)).toBe(false);
178
+ });
179
+ });
180
+
181
+ describe('isFullReputationBalance', () => {
182
+ it('narrows the subject view', () => {
183
+ const view: ReputationBalanceView = balanceFixture;
184
+ expect(isFullReputationBalance(view)).toBe(true);
185
+ if (isFullReputationBalance(view)) {
186
+ // Reachable ONLY through the guard — the point of the narrowing.
187
+ expect(view.reliability.reportAccuracyScore).toBe(1);
188
+ expect(view.influence.reportWeight).toBe(1.0);
189
+ expect(view.breakdown.content).toBe(80);
190
+ }
191
+ });
192
+
193
+ it('rejects the public view', () => {
194
+ expect(isFullReputationBalance(summaryFixture)).toBe(false);
195
+ });
196
+
197
+ it('rejects a payload missing any single private field', () => {
198
+ for (const field of FULL_BALANCE_FIELD_NAMES) {
199
+ const partial = { ...balanceFixture };
200
+ delete (partial as Record<string, unknown>)[field];
201
+ expect(isFullReputationBalance(partial as ReputationBalanceView)).toBe(false);
202
+ }
203
+ });
204
+ });
205
+
206
+ describe('getMyReputationBalance', () => {
207
+ it('reads the signed-in user id from the token and returns the full shape', async () => {
208
+ oxy.httpService.setTokens(signedInToken('me-123'));
209
+ makeRequestSpy.mockResolvedValue(balanceFixture);
210
+
211
+ const result = await oxy.getMyReputationBalance();
212
+
213
+ // Typed as the full balance with no narrowing — the ergonomic path.
214
+ expect(result.reliability.abuseScore).toBe(0);
215
+ expect(makeRequestSpy).toHaveBeenCalledWith(
216
+ 'GET',
217
+ '/reputation/me-123/balance',
218
+ undefined,
219
+ expect.objectContaining({ cache: true }),
220
+ );
221
+ });
222
+
223
+ it('throws without a signed-in user, and never issues the request', async () => {
224
+ oxy.httpService.clearTokens();
225
+
226
+ await expect(oxy.getMyReputationBalance()).rejects.toThrow(OxyAuthenticationError);
227
+ expect(makeRequestSpy).not.toHaveBeenCalled();
228
+ });
229
+
230
+ it('throws when the server answers 200 with the public view', async () => {
231
+ // What an absent or lapsed token gets: the endpoint's auth is optional, so
232
+ // the read succeeds and silently omits every private block.
233
+ oxy.httpService.setTokens(signedInToken('me-123'));
234
+ makeRequestSpy.mockResolvedValue(summaryFixture);
235
+
236
+ await expect(oxy.getMyReputationBalance()).rejects.toThrow(OxyAuthenticationError);
237
+ });
124
238
  });
125
239
 
126
240
  describe('getReputationLeaderboard', () => {
@@ -246,6 +246,7 @@ export interface BlockedUser {
246
246
  _id: string;
247
247
  username: string;
248
248
  avatar?: string;
249
+ name?: { displayName?: string };
249
250
  };
250
251
  userId: string;
251
252
  createdAt?: string;
@@ -260,6 +261,7 @@ export interface RestrictedUser {
260
261
  _id: string;
261
262
  username: string;
262
263
  avatar?: string;
264
+ name?: { displayName?: string };
263
265
  };
264
266
  userId: string;
265
267
  createdAt?: string;
@@ -26,6 +26,7 @@ import type {
26
26
  AccountMember,
27
27
  } from '../mixins/OxyServices.accounts';
28
28
  import { getAccountDisplayName, getAccountFallbackHandle } from '../utils/accountUtils';
29
+ import { getNormalizedUserHandle } from '../utils/userHandle';
29
30
 
30
31
  /**
31
32
  * The per-account user shape carried by a {@link SwitchableAccount}. The SDK's
@@ -170,7 +171,10 @@ export function projectSwitchableAccounts(input: ProjectSwitchableAccountsInput)
170
171
  kind: opts.kind,
171
172
  parentAccountId: opts.parentAccountId,
172
173
  callerMembership: opts.callerMembership,
173
- displayName: getAccountDisplayName(accountUser, locale),
174
+ displayName:
175
+ accountUser.name?.displayName ??
176
+ getNormalizedUserHandle(accountUser) ??
177
+ getAccountDisplayName(null, locale),
174
178
  // Real email, or the `@handle` fallback (NEVER synthesized).
175
179
  email: accountUser.email ?? secondaryHandle,
176
180
  avatarUrl: resolveAvatarUrl(accountUser.avatar),
@@ -1,32 +0,0 @@
1
- declare module 'bip39' {
2
- export interface Wordlist {
3
- [index: number]: string;
4
- length: number;
5
- getWord(index: number): string;
6
- getWordIndex(word: string): number;
7
- }
8
-
9
- export const wordlists: {
10
- english: string[];
11
- chinese_simplified: string[];
12
- chinese_traditional: string[];
13
- french: string[];
14
- italian: string[];
15
- japanese: string[];
16
- korean: string[];
17
- spanish: string[];
18
- };
19
-
20
- // Use Uint8Array instead of Buffer for React Native compatibility
21
- // In Node.js, Buffer extends Uint8Array so this is compatible
22
- export function generateMnemonic(strength?: number, rng?: (size: number) => Uint8Array, wordlist?: string[]): string;
23
- export function mnemonicToSeed(mnemonic: string, passphrase?: string): Promise<Uint8Array>;
24
- export function mnemonicToSeedSync(mnemonic: string, passphrase?: string): Uint8Array;
25
- export function mnemonicToEntropy(mnemonic: string, wordlist?: string[]): string;
26
- export function entropyToMnemonic(entropy: string, wordlist?: string[]): string;
27
- export function validateMnemonic(mnemonic: string, wordlist?: string[]): boolean;
28
- export function mnemonicToSeedHex(mnemonic: string, passphrase?: string): Promise<string>;
29
- export function mnemonicToSeedHexSync(mnemonic: string, passphrase?: string): string;
30
- }
31
-
32
-