@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
@@ -5,9 +5,16 @@
5
5
  * for backing up and restoring user identities.
6
6
  *
7
7
  * Note: This module requires the polyfill to be loaded first (done via crypto/index.ts)
8
+ *
9
+ * Oxy recovery phrases are English-only, so the English wordlist is imported by
10
+ * its own subpath. Never reach for a package that exposes its wordlists through
11
+ * a barrel: `bip39`'s `_wordlists` hard-requires all ten languages, which put
12
+ * ~265 KB of unreachable wordlists into the initial chunk of every consuming
13
+ * app. Adding another language means one more subpath import, ideally lazy.
8
14
  */
9
15
 
10
- import * as bip39 from 'bip39';
16
+ import { generateMnemonic, mnemonicToSeed, validateMnemonic } from '@scure/bip39';
17
+ import { wordlist } from '@scure/bip39/wordlists/english';
11
18
  import { KeyManager } from './keyManager';
12
19
  import { hkdfSha256 } from './kdf';
13
20
 
@@ -108,15 +115,14 @@ export class RecoveryPhraseService {
108
115
  options?: GenerateIdentityOptions,
109
116
  ): Promise<RecoveryPhraseResult> {
110
117
  // Generate 128-bit entropy for 12-word mnemonic
111
- const mnemonic = bip39.generateMnemonic(128);
118
+ const mnemonic = generateMnemonic(wordlist, 128);
112
119
 
113
120
  // Derive private key from mnemonic
114
121
  // Using the seed directly as the private key (simplified approach)
115
- const seed = await bip39.mnemonicToSeed(mnemonic);
122
+ const seed = await mnemonicToSeed(mnemonic);
116
123
 
117
124
  // Use first 32 bytes of seed as private key
118
- const seedSlice = seed.subarray ? seed.subarray(0, 32) : seed.slice(0, 32);
119
- const privateKeyHex = toHex(seedSlice);
125
+ const privateKeyHex = toHex(seed.subarray(0, 32));
120
126
 
121
127
  // Import the derived key pair. KeyManager.importKeyPair will refuse to
122
128
  // clobber an existing identity unless overwrite is explicitly requested.
@@ -140,11 +146,10 @@ export class RecoveryPhraseService {
140
146
  options?: GenerateIdentityOptions,
141
147
  ): Promise<RecoveryPhraseResult> {
142
148
  // Generate 256-bit entropy for 24-word mnemonic
143
- const mnemonic = bip39.generateMnemonic(256);
149
+ const mnemonic = generateMnemonic(wordlist, 256);
144
150
 
145
- const seed = await bip39.mnemonicToSeed(mnemonic);
146
- const seedSlice = seed.subarray ? seed.subarray(0, 32) : seed.slice(0, 32);
147
- const privateKeyHex = toHex(seedSlice);
151
+ const seed = await mnemonicToSeed(mnemonic);
152
+ const privateKeyHex = toHex(seed.subarray(0, 32));
148
153
  const publicKey = await KeyManager.importKeyPair(privateKeyHex, {
149
154
  overwrite: options?.overwrite === true,
150
155
  });
@@ -170,10 +175,9 @@ export class RecoveryPhraseService {
170
175
  * committed anywhere — if it is lost the account becomes unrecoverable.
171
176
  */
172
177
  static async derivePendingIdentity(): Promise<PendingIdentityResult> {
173
- const mnemonic = bip39.generateMnemonic(128);
174
- const seed = await bip39.mnemonicToSeed(mnemonic);
175
- const seedSlice = seed.subarray ? seed.subarray(0, 32) : seed.slice(0, 32);
176
- const privateKey = toHex(seedSlice);
178
+ const mnemonic = generateMnemonic(wordlist, 128);
179
+ const seed = await mnemonicToSeed(mnemonic);
180
+ const privateKey = toHex(seed.subarray(0, 32));
177
181
  const publicKey = KeyManager.derivePublicKey(privateKey);
178
182
 
179
183
  return {
@@ -195,13 +199,12 @@ export class RecoveryPhraseService {
195
199
  static async derivePrivateKeyFromPhrase(phrase: string): Promise<string> {
196
200
  const normalizedPhrase = phrase.trim().toLowerCase();
197
201
 
198
- if (!bip39.validateMnemonic(normalizedPhrase)) {
202
+ if (!validateMnemonic(normalizedPhrase, wordlist)) {
199
203
  throw new Error('Invalid recovery phrase');
200
204
  }
201
205
 
202
- const seed = await bip39.mnemonicToSeed(normalizedPhrase);
203
- const seedSlice = seed.subarray ? seed.subarray(0, 32) : seed.slice(0, 32);
204
- return toHex(seedSlice);
206
+ const seed = await mnemonicToSeed(normalizedPhrase);
207
+ return toHex(seed.subarray(0, 32));
205
208
  }
206
209
 
207
210
  /**
@@ -224,11 +227,11 @@ export class RecoveryPhraseService {
224
227
  static async deriveBackupMaterial(phrase: string): Promise<BackupMaterial> {
225
228
  const normalizedPhrase = phrase.trim().toLowerCase();
226
229
 
227
- if (!bip39.validateMnemonic(normalizedPhrase)) {
230
+ if (!validateMnemonic(normalizedPhrase, wordlist)) {
228
231
  throw new Error('Invalid recovery phrase. Please check the words and try again.');
229
232
  }
230
233
 
231
- const seed = await bip39.mnemonicToSeed(normalizedPhrase);
234
+ const seed = await mnemonicToSeed(normalizedPhrase);
232
235
  const salt = utf8(BACKUP_KDF_SALT);
233
236
  const backupKey = hkdfSha256(seed, salt, utf8(BACKUP_KDF_ENCRYPTION_INFO), BACKUP_MATERIAL_LENGTH);
234
237
  const lookupId = toHex(hkdfSha256(seed, salt, utf8(BACKUP_KDF_LOOKUP_INFO), BACKUP_MATERIAL_LENGTH));
@@ -251,14 +254,13 @@ export class RecoveryPhraseService {
251
254
  // Normalize and validate the phrase
252
255
  const normalizedPhrase = phrase.trim().toLowerCase();
253
256
 
254
- if (!bip39.validateMnemonic(normalizedPhrase)) {
257
+ if (!validateMnemonic(normalizedPhrase, wordlist)) {
255
258
  throw new Error('Invalid recovery phrase. Please check the words and try again.');
256
259
  }
257
260
 
258
261
  // Derive the same private key from the mnemonic
259
- const seed = await bip39.mnemonicToSeed(normalizedPhrase);
260
- const seedSlice = seed.subarray ? seed.subarray(0, 32) : seed.slice(0, 32);
261
- const privateKeyHex = toHex(seedSlice);
262
+ const seed = await mnemonicToSeed(normalizedPhrase);
263
+ const privateKeyHex = toHex(seed.subarray(0, 32));
262
264
 
263
265
  // Import and store the key pair
264
266
  const publicKey = await KeyManager.importKeyPair(privateKeyHex, {
@@ -273,21 +275,21 @@ export class RecoveryPhraseService {
273
275
  */
274
276
  static validatePhrase(phrase: string): boolean {
275
277
  const normalizedPhrase = phrase.trim().toLowerCase();
276
- return bip39.validateMnemonic(normalizedPhrase);
278
+ return validateMnemonic(normalizedPhrase, wordlist);
277
279
  }
278
280
 
279
281
  /**
280
282
  * Get the word list for autocomplete/validation
281
283
  */
282
284
  static getWordList(): string[] {
283
- return bip39.wordlists.english;
285
+ return wordlist;
284
286
  }
285
287
 
286
288
  /**
287
289
  * Check if a word is valid in the BIP39 word list
288
290
  */
289
291
  static isValidWord(word: string): boolean {
290
- return bip39.wordlists.english.includes(word.toLowerCase());
292
+ return wordlist.includes(word.toLowerCase());
291
293
  }
292
294
 
293
295
  /**
@@ -295,9 +297,7 @@ export class RecoveryPhraseService {
295
297
  */
296
298
  static getSuggestions(partial: string, limit = 5): string[] {
297
299
  const lowerPartial = partial.toLowerCase();
298
- return bip39.wordlists.english
299
- .filter((word: string) => word.startsWith(lowerPartial))
300
- .slice(0, limit);
300
+ return wordlist.filter((word: string) => word.startsWith(lowerPartial)).slice(0, limit);
301
301
  }
302
302
 
303
303
  /**
@@ -307,14 +307,13 @@ export class RecoveryPhraseService {
307
307
  static async derivePublicKeyFromPhrase(phrase: string): Promise<string> {
308
308
  const normalizedPhrase = phrase.trim().toLowerCase();
309
309
 
310
- if (!bip39.validateMnemonic(normalizedPhrase)) {
310
+ if (!validateMnemonic(normalizedPhrase, wordlist)) {
311
311
  throw new Error('Invalid recovery phrase');
312
312
  }
313
313
 
314
- const seed = await bip39.mnemonicToSeed(normalizedPhrase);
315
- const seedSlice = seed.subarray ? seed.subarray(0, 32) : seed.slice(0, 32);
316
- const privateKeyHex = toHex(seedSlice);
317
-
314
+ const seed = await mnemonicToSeed(normalizedPhrase);
315
+ const privateKeyHex = toHex(seed.subarray(0, 32));
316
+
318
317
  return KeyManager.derivePublicKey(privateKeyHex);
319
318
  }
320
319
 
package/src/index.ts CHANGED
@@ -167,31 +167,12 @@ export type {
167
167
  export { ORGANIZATION_CATEGORIES } from './mixins/OxyServices.accounts';
168
168
 
169
169
  // ---------------------------------------------------------------------------
170
- // Reputation (Oxy Trust: ledger, balances, disputes, rules, influence)
170
+ // Reputation (Oxy Trust: ledger, balances, disputes, rules, influence).
171
+ // The whole type family — the closed value sets, the two balance views and the
172
+ // `isFullReputationBalance` narrowing guard, the ledger/dispute/rule/leaderboard
173
+ // shapes, and the write-endpoint inputs — is owned by `@oxyhq/contracts`, which
174
+ // the API's serializers are validated against. Import them from there.
171
175
  // ---------------------------------------------------------------------------
172
- export type {
173
- ReputationCategory,
174
- TrustTier,
175
- ReputationTransactionStatus,
176
- ReputationTargetEntityType,
177
- ReputationDisputeStatus,
178
- ReputationInfluenceContext,
179
- ReputationTransaction,
180
- ReputationBalanceBreakdown,
181
- ReputationInfluence,
182
- ReputationReliability,
183
- ReputationBalance,
184
- ReputationDispute,
185
- ReputationRule,
186
- ReputationLeaderboardEntry,
187
- ReputationInfluenceResult,
188
- ReverseReputationTransactionResult,
189
- AwardReputationInput,
190
- CreateReputationDisputeInput,
191
- ResolveReputationDisputeInput,
192
- UpsertReputationRuleInput,
193
- ReverseReputationTransactionInput,
194
- } from './mixins/OxyServices.reputation';
195
176
 
196
177
  // ---------------------------------------------------------------------------
197
178
  // Self-sovereign identity (DID, signed records, auth-method ↔ VM mapping,
@@ -12,13 +12,21 @@
12
12
  * This method carries NO persistence or token-planting side effects of its own;
13
13
  * the cold boot / re-mint handler own persistence and `setTokens`, so the same
14
14
  * primitive can be reused from either without double-planting.
15
+ *
16
+ * `provisionBackgroundCredential` is the mixin's second, adjacent call: it hands
17
+ * native background code (no JS runtime) its own non-rotating credential so that
18
+ * code never has to mint from — and therefore never rotates — the device secret
19
+ * JS depends on.
15
20
  */
16
21
  import {
22
+ deviceBackgroundCredentialResponseSchema,
17
23
  deviceTokenMintResponseSchema,
18
24
  safeParseContract,
25
+ type DeviceBackgroundCredentialResponse,
19
26
  type DeviceTokenMintResponse,
20
27
  } from '@oxyhq/contracts';
21
28
  import type { OxyServicesBase } from '../OxyServices.base';
29
+ import { extractErrorStatus } from '../utils/errorUtils';
22
30
 
23
31
  /**
24
32
  * The server's `401 account_not_on_device` for a PINNED mint: the requested
@@ -120,5 +128,64 @@ export function OxyServicesDeviceBootMixin<T extends typeof OxyServicesBase>(Bas
120
128
  throw normalized;
121
129
  }
122
130
  }
131
+
132
+ /**
133
+ * Provision a NON-rotating background credential for the caller's account on
134
+ * this device — the credential native background code (an Android widget
135
+ * worker, which runs with no JS runtime) presents to mint its own access
136
+ * tokens without any JS involvement.
137
+ *
138
+ * It exists precisely so background code never touches the device secret:
139
+ * `POST /session/device/token` ROTATES that secret on every mint (the
140
+ * presented one stays valid only for a short grace), so a worker minting
141
+ * from it would become a second writer of the value JS depends on and could
142
+ * silently sign the user out. The background credential is a separate,
143
+ * non-rotating value minted server-side, so the two lanes never contend.
144
+ *
145
+ * Bearer required and NO body: the server derives both the `deviceId` and
146
+ * the account from the validated bearer. This is the only way a background
147
+ * credential comes into existence, so background code can EXTEND a session
148
+ * the user established in-app but can never bootstrap one from nothing.
149
+ *
150
+ * Unlike the mint above this is NOT a control-plane call — it runs while a
151
+ * session is already live — so it takes the normal authenticated path: no
152
+ * `skipAuth` (a 401 should go through the ordinary re-mint lane) and no
153
+ * `bypassQueue` (nothing in the auth lane is parked awaiting it).
154
+ *
155
+ * There is deliberately no JS counterpart that MINTS from the returned
156
+ * credential: the native side owns that call, and a symmetric-looking JS
157
+ * method would be dead code plus a second implementation of the failure
158
+ * rules. The asymmetry is the design.
159
+ *
160
+ * @returns the provisioned credential, or `null` when the endpoint is absent
161
+ * (404). The API deploy leads the SDK release, so a client on a newer SDK
162
+ * than the server degrades to "no background session" quietly instead of
163
+ * surfacing an error. The status is read off the RAW rejection because
164
+ * `handleError` only preserves it for errors carrying `HttpService`'s
165
+ * annotations.
166
+ * @throws if the response does not match {@link deviceBackgroundCredentialResponseSchema}.
167
+ */
168
+ async provisionBackgroundCredential(): Promise<DeviceBackgroundCredentialResponse | null> {
169
+ try {
170
+ const res = await this.makeRequest<unknown>(
171
+ 'POST',
172
+ '/session/device/background-credential',
173
+ undefined,
174
+ { cache: false },
175
+ );
176
+ const parsed = safeParseContract(deviceBackgroundCredentialResponseSchema, res);
177
+ if (!parsed) {
178
+ throw new Error(
179
+ 'session/device/background-credential returned an unexpected response shape',
180
+ );
181
+ }
182
+ return parsed;
183
+ } catch (error) {
184
+ if (extractErrorStatus(error) === 404) {
185
+ return null;
186
+ }
187
+ throw this.handleError(error);
188
+ }
189
+ }
123
190
  };
124
191
  }
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * Key schedule (from the recovery phrase; see
9
9
  * {@link RecoveryPhraseService.deriveBackupMaterial}):
10
- * seed = bip39.mnemonicToSeed(phrase) // 64 bytes, UNCHANGED
10
+ * seed = mnemonicToSeed(phrase) // 64 bytes, UNCHANGED
11
11
  * backupKey = HKDF(seed, 'oxy-identity-backup-v1', 'oxy-backup-encryption-key')
12
12
  * lookupId = HKDF(seed, 'oxy-identity-backup-v1', 'oxy-backup-lookup-id') // hex
13
13
  *