@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.
- package/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/crypto/recoveryPhrase.js +32 -66
- package/dist/cjs/index.js +7 -0
- package/dist/cjs/mixins/OxyServices.deviceBoot.js +58 -0
- package/dist/cjs/mixins/OxyServices.reputation.js +47 -2
- package/dist/cjs/mixins/OxyServices.user.js +3 -4
- package/dist/cjs/session/accountProjection.js +4 -1
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/crypto/recoveryPhrase.js +32 -33
- package/dist/esm/index.js +7 -0
- package/dist/esm/mixins/OxyServices.deviceBoot.js +59 -1
- package/dist/esm/mixins/OxyServices.reputation.js +47 -2
- package/dist/esm/mixins/OxyServices.user.js +3 -4
- package/dist/esm/session/accountProjection.js +4 -1
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/crypto/recoveryPhrase.d.ts +6 -0
- package/dist/types/index.d.ts +0 -1
- package/dist/types/mixins/OxyServices.deviceBoot.d.ts +43 -1
- package/dist/types/mixins/OxyServices.identityBackup.d.ts +1 -1
- package/dist/types/mixins/OxyServices.reputation.d.ts +43 -276
- package/dist/types/mixins/OxyServices.user.d.ts +4 -1
- package/dist/types/models/interfaces.d.ts +6 -0
- package/package.json +3 -3
- package/src/crypto/__tests__/keyManager.test.ts +3 -2
- package/src/crypto/recoveryPhrase.ts +33 -34
- package/src/index.ts +5 -24
- package/src/mixins/OxyServices.deviceBoot.ts +67 -0
- package/src/mixins/OxyServices.identityBackup.ts +1 -1
- package/src/mixins/OxyServices.reputation.ts +88 -326
- package/src/mixins/OxyServices.user.ts +7 -3
- package/src/mixins/__tests__/OxyServices.deviceBoot.test.ts +59 -2
- package/src/mixins/__tests__/followGraphPagination.test.ts +21 -0
- package/src/mixins/__tests__/reputation.test.ts +115 -1
- package/src/models/interfaces.ts +2 -0
- package/src/session/accountProjection.ts +5 -1
- 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 '
|
|
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', () => {
|
package/src/models/interfaces.ts
CHANGED
|
@@ -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:
|
|
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),
|
package/src/types/bip39.d.ts
DELETED
|
@@ -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
|
-
|