@oxyhq/core 14.0.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
|
@@ -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
|
|
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 =
|
|
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
|
|
122
|
+
const seed = await mnemonicToSeed(mnemonic);
|
|
116
123
|
|
|
117
124
|
// Use first 32 bytes of seed as private key
|
|
118
|
-
const
|
|
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 =
|
|
149
|
+
const mnemonic = generateMnemonic(wordlist, 256);
|
|
144
150
|
|
|
145
|
-
const seed = await
|
|
146
|
-
const
|
|
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 =
|
|
174
|
-
const seed = await
|
|
175
|
-
const
|
|
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 (!
|
|
202
|
+
if (!validateMnemonic(normalizedPhrase, wordlist)) {
|
|
199
203
|
throw new Error('Invalid recovery phrase');
|
|
200
204
|
}
|
|
201
205
|
|
|
202
|
-
const seed = await
|
|
203
|
-
|
|
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 (!
|
|
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
|
|
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 (!
|
|
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
|
|
260
|
-
const
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 (!
|
|
310
|
+
if (!validateMnemonic(normalizedPhrase, wordlist)) {
|
|
311
311
|
throw new Error('Invalid recovery phrase');
|
|
312
312
|
}
|
|
313
313
|
|
|
314
|
-
const seed = await
|
|
315
|
-
const
|
|
316
|
-
|
|
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 =
|
|
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
|
*
|