@oxy.so/core 1.9.1 → 1.11.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/README.md +4 -4
- package/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/HttpService.js +56 -163
- package/dist/cjs/OxyServices.base.js +4 -2
- package/dist/cjs/boot/sessionColdBoot.js +3 -0
- package/dist/cjs/crypto/deviceBackup.js +75 -0
- package/dist/cjs/crypto/keyManager.js +285 -12
- package/dist/cjs/mixins/OxyServices.linkedAccounts.js +79 -0
- package/dist/cjs/mixins/OxyServices.user.js +7 -6
- package/dist/cjs/mixins/index.js +4 -0
- package/dist/cjs/server/cors.js +0 -1
- package/dist/cjs/server/securityHeaders.js +3 -0
- package/dist/cjs/session/refresh.js +32 -2
- package/dist/cjs/utils/errorUtils.js +1 -1
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/HttpService.js +56 -163
- package/dist/esm/OxyServices.base.js +4 -2
- package/dist/esm/boot/sessionColdBoot.js +3 -0
- package/dist/esm/crypto/deviceBackup.js +70 -0
- package/dist/esm/crypto/keyManager.js +252 -12
- package/dist/esm/mixins/OxyServices.linkedAccounts.js +76 -0
- package/dist/esm/mixins/OxyServices.user.js +7 -6
- package/dist/esm/mixins/index.js +4 -0
- package/dist/esm/server/cors.js +0 -1
- package/dist/esm/server/securityHeaders.js +3 -0
- package/dist/esm/session/refresh.js +32 -2
- package/dist/esm/utils/errorUtils.js +1 -1
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/HttpService.d.ts +31 -7
- package/dist/types/OxyServices.base.d.ts +3 -1
- package/dist/types/crypto/deviceBackup.d.ts +60 -0
- package/dist/types/crypto/keyManager.d.ts +79 -12
- package/dist/types/index.d.ts +1 -0
- package/dist/types/mixins/OxyServices.civic.d.ts +11 -1
- package/dist/types/mixins/OxyServices.linkedAccounts.d.ts +107 -0
- package/dist/types/mixins/OxyServices.user.d.ts +8 -6
- package/dist/types/mixins/index.d.ts +2 -1
- package/dist/types/models/interfaces.d.ts +30 -0
- package/dist/types/server/securityHeaders.d.ts +5 -0
- package/dist/types/session/refresh.d.ts +7 -0
- package/dist/types/utils/errorUtils.d.ts +1 -1
- package/package.json +2 -2
- package/src/HttpService.ts +59 -187
- package/src/OxyServices.base.ts +4 -2
- package/src/__tests__/httpServiceFormEncoded.test.ts +2 -5
- package/src/__tests__/{httpServiceCsrf.test.ts → httpServiceWrites.test.ts} +49 -109
- package/src/__tests__/inSessionRefresh.test.ts +53 -0
- package/src/__tests__/linkedClient.test.ts +0 -7
- package/src/boot/__tests__/sessionColdBoot.identity.test.ts +1 -1
- package/src/boot/__tests__/sessionColdBoot.sharedDevice.test.ts +1 -1
- package/src/boot/__tests__/sessionColdBoot.test.ts +1 -1
- package/src/boot/sessionColdBoot.ts +3 -0
- package/src/crypto/__tests__/keyManager.deviceBackup.test.ts +310 -0
- package/src/crypto/deviceBackup.ts +108 -0
- package/src/crypto/keyManager.ts +278 -13
- package/src/index.ts +1 -0
- package/src/mixins/OxyServices.linkedAccounts.ts +129 -0
- package/src/mixins/OxyServices.user.ts +9 -7
- package/src/mixins/__tests__/backgroundCredential.httpIntegration.test.ts +5 -10
- package/src/mixins/__tests__/discoveryErrorHandling.test.ts +3 -4
- package/src/mixins/__tests__/linkedAccounts.test.ts +109 -0
- package/src/mixins/__tests__/topics.test.ts +2 -3
- package/src/mixins/index.ts +6 -0
- package/src/models/interfaces.ts +30 -1
- package/src/server/__tests__/securityHeaders.test.ts +7 -0
- package/src/server/cors.ts +0 -1
- package/src/server/securityHeaders.ts +8 -0
- package/src/session/__tests__/refresh.identityPin.test.ts +1 -1
- package/src/session/__tests__/refresh.test.ts +115 -2
- package/src/session/refresh.ts +38 -2
- package/src/utils/errorUtils.ts +1 -1
|
@@ -94,8 +94,6 @@ interface RequestConfig extends RequestOptions {
|
|
|
94
94
|
params?: Record<string, unknown>;
|
|
95
95
|
/** @internal Used to prevent infinite auth retry loops */
|
|
96
96
|
_isAuthRetry?: boolean;
|
|
97
|
-
/** @internal Used to prevent infinite CSRF retry loops */
|
|
98
|
-
_isCsrfRetry?: boolean;
|
|
99
97
|
}
|
|
100
98
|
/**
|
|
101
99
|
* Unified HTTP Service
|
|
@@ -165,6 +163,21 @@ export declare class HttpService {
|
|
|
165
163
|
private authRefreshHandler;
|
|
166
164
|
private accessTokenProvider;
|
|
167
165
|
private deviceSecretMintInFlight;
|
|
166
|
+
/**
|
|
167
|
+
* Bumped by every {@link endSession}. A re-mint captures it before its first
|
|
168
|
+
* await and plants nothing if it moved: a refresh that was already in flight
|
|
169
|
+
* when the user signed out would otherwise complete a moment later and put a
|
|
170
|
+
* bearer — and through the token listeners, the account — back.
|
|
171
|
+
*/
|
|
172
|
+
private sessionEpoch;
|
|
173
|
+
/**
|
|
174
|
+
* Set by {@link endSession}, cleared by the next {@link setTokens}. Read by the
|
|
175
|
+
* native shared-keychain re-mint arm: after a sign-out a 401 is the expected
|
|
176
|
+
* answer, not a cue to sign back in with the identity key. The device-secret
|
|
177
|
+
* arm is NOT gated on it — minting from a credential the store still holds is
|
|
178
|
+
* how a signed-out tab joins a sign-in made in another tab.
|
|
179
|
+
*/
|
|
180
|
+
private sessionEnded;
|
|
168
181
|
/**
|
|
169
182
|
* Epoch (ms) before which a cache-size telemetry warning must not be
|
|
170
183
|
* re-emitted. Throttles the {@link CACHE_SOFT_MAX_ENTRIES} warning to at most
|
|
@@ -307,11 +320,6 @@ export declare class HttpService {
|
|
|
307
320
|
private buildURL;
|
|
308
321
|
private getCredentialsMode;
|
|
309
322
|
private shouldSendCredentials;
|
|
310
|
-
/**
|
|
311
|
-
* Fetch CSRF token from server (with deduplication)
|
|
312
|
-
* Required for state-changing requests (POST, PUT, PATCH, DELETE)
|
|
313
|
-
*/
|
|
314
|
-
private fetchCsrfToken;
|
|
315
323
|
/**
|
|
316
324
|
* Get auth header with automatic token refresh
|
|
317
325
|
*/
|
|
@@ -416,6 +424,22 @@ export declare class HttpService {
|
|
|
416
424
|
patch<T = unknown>(url: string, data?: unknown, config?: Omit<RequestConfig, 'method' | 'url' | 'data'>): Promise<T>;
|
|
417
425
|
delete<T = unknown>(url: string, config?: Omit<RequestConfig, 'method' | 'url'>): Promise<T>;
|
|
418
426
|
setTokens(accessToken: string): void;
|
|
427
|
+
/**
|
|
428
|
+
* End the local session: clear the bearer and abandon every re-mint already
|
|
429
|
+
* in flight, so none of them can plant a token after the sign-out.
|
|
430
|
+
*
|
|
431
|
+
* {@link clearTokens} alone only drops the bearer, which is right for a
|
|
432
|
+
* mirror (a linked client following its parent) but not for a sign-out: a
|
|
433
|
+
* refresh started a moment earlier would finish and plant a new one.
|
|
434
|
+
*/
|
|
435
|
+
endSession(): void;
|
|
436
|
+
/**
|
|
437
|
+
* The current session epoch — see {@link endSession}. A re-mint lane that
|
|
438
|
+
* plants tokens itself (the device-secret arm) compares it across its awaits.
|
|
439
|
+
*/
|
|
440
|
+
getSessionEpoch(): number;
|
|
441
|
+
/** Whether {@link endSession} ran and no token has been planted since. */
|
|
442
|
+
hasSessionEnded(): boolean;
|
|
419
443
|
setAuthRefreshHandler(handler: AuthRefreshHandler | null): void;
|
|
420
444
|
setAccessTokenProvider(provider: AccessTokenProvider | null): void;
|
|
421
445
|
clearTokens(): void;
|
|
@@ -96,7 +96,9 @@ export declare class OxyServicesBase {
|
|
|
96
96
|
*/
|
|
97
97
|
setTokens(accessToken: string): void;
|
|
98
98
|
/**
|
|
99
|
-
* Clear stored authentication tokens
|
|
99
|
+
* Clear stored authentication tokens and end the local session: a re-mint
|
|
100
|
+
* already in flight plants nothing, and no new one runs until a token is set
|
|
101
|
+
* again (see `HttpService.endSession`).
|
|
100
102
|
*/
|
|
101
103
|
clearTokens(): void;
|
|
102
104
|
/**
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The device backup of the self-custody identity: a copy that lives OUTSIDE this
|
|
3
|
+
* app's own keystore, so a keystore wipe cannot take it along with the keys.
|
|
4
|
+
*
|
|
5
|
+
* Why it exists (OxyHQ/oxy#1388): on Android every Oxy app shares the Linux UID
|
|
6
|
+
* `so.oxy.shared`, and the Android Keystore is per UID. Clearing the storage of
|
|
7
|
+
* ANY sibling app (`pm clear`, or Settings › Apps › Storage › Clear storage)
|
|
8
|
+
* wipes the Keystore of the whole UID. Every copy the SDK keeps in
|
|
9
|
+
* expo-secure-store (primary, backup slot, phrase slot) and in the androidx
|
|
10
|
+
* encrypted prefs (the cross-app shared slot) is wrapped by a key in that
|
|
11
|
+
* Keystore, so they all die together, and until now the only way back was the
|
|
12
|
+
* recovery phrase.
|
|
13
|
+
*
|
|
14
|
+
* `@oxy.so/core` never imports a native module, so the store is injected by the
|
|
15
|
+
* app that owns the identity ({@link KeyManager.setDeviceBackupStore}). Commons
|
|
16
|
+
* registers Android Block Store; every other app registers nothing, which keeps
|
|
17
|
+
* every code path below a no-op for them. The design and the threat model are in
|
|
18
|
+
* `docs/identity/device-backup.md`.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Where the device backup is kept. Implementations must hold the value outside
|
|
22
|
+
* the calling app's keystore (that is the whole point) and must never send it
|
|
23
|
+
* anywhere Oxy can read it.
|
|
24
|
+
*
|
|
25
|
+
* `read` resolves `null` when nothing is stored AND when the store cannot be
|
|
26
|
+
* reached (no Google Play services, an old binary without the native module):
|
|
27
|
+
* the caller treats both as "no device backup" and falls back to the phrase.
|
|
28
|
+
*/
|
|
29
|
+
export interface IdentityDeviceBackupStore {
|
|
30
|
+
/** Short name for logs, e.g. `android-block-store`. Never contains secrets. */
|
|
31
|
+
readonly name: string;
|
|
32
|
+
read(): Promise<string | null>;
|
|
33
|
+
write(value: string): Promise<void>;
|
|
34
|
+
clear(): Promise<void>;
|
|
35
|
+
}
|
|
36
|
+
/** The record format. Bump {@link DEVICE_BACKUP_VERSION} on a breaking change. */
|
|
37
|
+
export declare const DEVICE_BACKUP_VERSION = 1;
|
|
38
|
+
export interface DeviceBackupRecord {
|
|
39
|
+
version: typeof DEVICE_BACKUP_VERSION;
|
|
40
|
+
/** Canonical lowercase 64-hex secp256k1 private key. */
|
|
41
|
+
privateKey: string;
|
|
42
|
+
/** Canonical lowercase public key. */
|
|
43
|
+
publicKey: string;
|
|
44
|
+
/**
|
|
45
|
+
* The BIP-39 phrase, present only when it was verified to derive
|
|
46
|
+
* {@link publicKey}. Carried so a restored identity can still reveal its
|
|
47
|
+
* phrase in Settings and create the phrase-keyed Oxy backup; the phrase is
|
|
48
|
+
* never more powerful than the private key already in the record.
|
|
49
|
+
*/
|
|
50
|
+
mnemonic?: string;
|
|
51
|
+
/** ISO-8601 time of the last write. */
|
|
52
|
+
updatedAt: string;
|
|
53
|
+
}
|
|
54
|
+
export declare function serializeDeviceBackup(record: DeviceBackupRecord): string;
|
|
55
|
+
/**
|
|
56
|
+
* Parse a stored record, or `null` for anything that is not a well-formed
|
|
57
|
+
* version-1 record. Checks SHAPE only; the caller still proves the key pair is
|
|
58
|
+
* healthy (well-formed, public derives from private) before trusting it.
|
|
59
|
+
*/
|
|
60
|
+
export declare function parseDeviceBackup(raw: string | null | undefined): DeviceBackupRecord | null;
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* Private keys are stored securely using expo-secure-store and never leave the device.
|
|
6
6
|
*/
|
|
7
7
|
import { type IdentityMarker } from './identityMarker';
|
|
8
|
+
import { type IdentityDeviceBackupStore } from './deviceBackup';
|
|
8
9
|
/**
|
|
9
10
|
* Thrown when an identity-mutating operation (createIdentity / importKeyPair)
|
|
10
11
|
* is invoked while a valid identity already exists on the device.
|
|
@@ -79,10 +80,14 @@ export type IdentityStatus = {
|
|
|
79
80
|
* which independent, `key_v1`-surviving source restored the identity. On failure
|
|
80
81
|
* `reason` distinguishes "wasn't lost", "no surviving source", "a source held a
|
|
81
82
|
* DIFFERENT account" (never silently switched), and "storage unavailable".
|
|
83
|
+
*
|
|
84
|
+
* `device-backup` is the copy held outside this app's keystore (Android Block
|
|
85
|
+
* Store in Commons, see {@link KeyManager.setDeviceBackupStore}); it is the only
|
|
86
|
+
* source that survives a wipe of the whole shared-UID keystore.
|
|
82
87
|
*/
|
|
83
88
|
export type IdentityRecoveryResult = {
|
|
84
89
|
recovered: true;
|
|
85
|
-
source: 'backup' | 'shared';
|
|
90
|
+
source: 'backup' | 'shared' | 'device-backup';
|
|
86
91
|
publicKey: string;
|
|
87
92
|
} | {
|
|
88
93
|
recovered: false;
|
|
@@ -114,6 +119,23 @@ export declare class KeyManager {
|
|
|
114
119
|
*/
|
|
115
120
|
private static slotMigrationPromise;
|
|
116
121
|
private static slotMigrationResult;
|
|
122
|
+
/**
|
|
123
|
+
* The keystore-independent device backup, registered by the app that owns the
|
|
124
|
+
* identity (Commons). `null` in every other app, which makes every device
|
|
125
|
+
* backup path a no-op there. See `./deviceBackup`.
|
|
126
|
+
*/
|
|
127
|
+
private static deviceBackupStore;
|
|
128
|
+
/**
|
|
129
|
+
* Register (or, with `null`, unregister) where the identity's device backup is
|
|
130
|
+
* kept. Call once at startup, before the first identity read, ONLY from the
|
|
131
|
+
* app that holds the self-custody identity: the backup is a full copy of the
|
|
132
|
+
* private key, and it belongs to exactly one app.
|
|
133
|
+
*
|
|
134
|
+
* From then on every create, import, rotation and restore refreshes the
|
|
135
|
+
* backup, a forced delete clears it, and {@link attemptIdentityRecovery}
|
|
136
|
+
* restores from it when the keystore copies are gone.
|
|
137
|
+
*/
|
|
138
|
+
static setDeviceBackupStore(store: IdentityDeviceBackupStore | null): void;
|
|
117
139
|
/**
|
|
118
140
|
* Invalidate cached identity state
|
|
119
141
|
* Called internally when identity is created/deleted/imported
|
|
@@ -347,6 +369,33 @@ export declare class KeyManager {
|
|
|
347
369
|
* @internal
|
|
348
370
|
*/
|
|
349
371
|
private static _persistIdentityAtomic;
|
|
372
|
+
/**
|
|
373
|
+
* Write the device backup for `(privateKey, publicKey)`, keeping the phrase an
|
|
374
|
+
* existing record already carries for the SAME identity. `mnemonic` replaces
|
|
375
|
+
* it, and must already be verified against `publicKey` by the caller.
|
|
376
|
+
* Best-effort: never throws, returns whether the store now holds this pair.
|
|
377
|
+
*
|
|
378
|
+
* @internal
|
|
379
|
+
*/
|
|
380
|
+
private static _writeDeviceBackup;
|
|
381
|
+
/** Remove the device backup. Best-effort: never throws. @internal */
|
|
382
|
+
private static _clearDeviceBackup;
|
|
383
|
+
/** True when `phrase` is a valid BIP-39 phrase that derives `publicKey`. @internal */
|
|
384
|
+
private static _phraseDerivesPublicKey;
|
|
385
|
+
/** The primary pair when it reads back healthy, else `null`. Never throws. @internal */
|
|
386
|
+
private static _readHealthyPrimary;
|
|
387
|
+
/**
|
|
388
|
+
* Make sure the device backup holds the identity on this device, writing it
|
|
389
|
+
* when it is missing or stale. Commons calls this once per launch once the
|
|
390
|
+
* identity reads back healthy: it backfills identities created before the
|
|
391
|
+
* device backup existed, and repairs a backup whose last write failed (a
|
|
392
|
+
* failed write after a key rotation would otherwise leave the OLD key there).
|
|
393
|
+
*
|
|
394
|
+
* Carries the stored recovery phrase along only when it provably derives the
|
|
395
|
+
* current key. Resolves `true` when the backup is current, `false` when there
|
|
396
|
+
* is no store, no healthy identity, or the write failed. Never throws.
|
|
397
|
+
*/
|
|
398
|
+
static ensureDeviceBackup(): Promise<boolean>;
|
|
350
399
|
/**
|
|
351
400
|
* Write/refresh the identity marker after a successful persist. A same-identity
|
|
352
401
|
* re-persist (e.g. backup refresh, idempotent re-import) preserves `createdAt`
|
|
@@ -534,25 +583,43 @@ export declare class KeyManager {
|
|
|
534
583
|
*/
|
|
535
584
|
static restoreIdentityFromBackup(): Promise<boolean>;
|
|
536
585
|
/**
|
|
537
|
-
* Recovery ladder — restore
|
|
538
|
-
*
|
|
586
|
+
* Recovery ladder — restore an identity whose keys are gone from an
|
|
587
|
+
* independent source WITHOUT the user re-entering their recovery phrase.
|
|
539
588
|
*
|
|
540
|
-
*
|
|
541
|
-
*
|
|
542
|
-
*
|
|
543
|
-
*
|
|
544
|
-
*
|
|
545
|
-
* `publicKey === marker.publicKey`, so a source holding a DIFFERENT account is
|
|
546
|
-
* SKIPPED, never restored):
|
|
547
|
-
* 1. the v2 backup slot (independent keychain key from the primary), then
|
|
589
|
+
* For a `lost` verdict (marker present, keys empty) the rungs are tried in
|
|
590
|
+
* order, each fully validated (well-formed + derive-match + `publicKey ===
|
|
591
|
+
* marker.publicKey`, so a source holding a DIFFERENT account is SKIPPED, never
|
|
592
|
+
* restored):
|
|
593
|
+
* 1. the v2 backup slot (independent keychain key from the primary),
|
|
548
594
|
* 2. the cross-app shared slot (Android bridge `getShared` / iOS keychain
|
|
549
|
-
* group) — the copy that survives a primary+backup `key_v1` death
|
|
595
|
+
* group) — the copy that survives a primary+backup `key_v1` death,
|
|
596
|
+
* 3. the device backup ({@link setDeviceBackupStore}; Android Block Store in
|
|
597
|
+
* Commons) — the only copy that survives a wipe of the whole shared-UID
|
|
598
|
+
* Android Keystore, which takes rungs 1 and 2 with it (oxy#1388).
|
|
599
|
+
*
|
|
600
|
+
* For an `absent` verdict (no keys AND no marker: this app's own data was
|
|
601
|
+
* cleared, or it was reinstalled) only rung 3 applies: a device backup there
|
|
602
|
+
* means an identity lived on this device and was never deleted (every delete
|
|
603
|
+
* clears the backup), so it is restored instead of letting onboarding create a
|
|
604
|
+
* new identity over it. Without a registered store the result stays
|
|
605
|
+
* `not-lost`, exactly as before.
|
|
606
|
+
*
|
|
607
|
+
* `present` → `not-lost`; `unavailable` (keychain locked) → `unavailable`.
|
|
608
|
+
* Everything here is on-device: no network, so it works offline.
|
|
550
609
|
*
|
|
551
610
|
* On success it re-persists via {@link _persistIdentityAtomic} (origin
|
|
552
611
|
* `'restore'`) and invalidates the cache so routing re-reads `present`. When no
|
|
553
612
|
* rung matches, the UI proceeds to recovery-phrase entry.
|
|
554
613
|
*/
|
|
555
614
|
static attemptIdentityRecovery(): Promise<IdentityRecoveryResult>;
|
|
615
|
+
/** Read the device backup as a healthy, canonical candidate, or null. Never throws. @internal */
|
|
616
|
+
private static _readDeviceBackupCandidate;
|
|
617
|
+
/**
|
|
618
|
+
* Persist a device-backup candidate, then put its phrase back in the
|
|
619
|
+
* re-reveal slot (which died with the keystore) when it derives the key.
|
|
620
|
+
* @internal
|
|
621
|
+
*/
|
|
622
|
+
private static _commitDeviceBackupRecovery;
|
|
556
623
|
/** Read the active-layout backup slot as a healthy candidate, or null. @internal */
|
|
557
624
|
private static _readBackupCandidate;
|
|
558
625
|
/** Read the cross-app shared slot as a healthy candidate, or null. @internal */
|
package/dist/types/index.d.ts
CHANGED
|
@@ -61,6 +61,7 @@ export { KeyManager, IdentityAlreadyExistsError, IdentityPersistError, IdentityU
|
|
|
61
61
|
export type { KeyPair, IdentityStatus, IdentityRecoveryResult } from './crypto/keyManager';
|
|
62
62
|
export { readIdentityMarker, updateIdentityMarker, } from './crypto/identityMarker';
|
|
63
63
|
export type { IdentityMarker } from './crypto/identityMarker';
|
|
64
|
+
export type { IdentityDeviceBackupStore } from './crypto/deviceBackup';
|
|
64
65
|
export { SignatureService } from './crypto/signatureService';
|
|
65
66
|
export type { SignedMessage, AuthChallenge } from './crypto/signatureService';
|
|
66
67
|
export { RecoveryPhraseService } from './crypto/recoveryPhrase';
|
|
@@ -502,7 +502,17 @@ export declare function OxyServicesCivicMixin<T extends typeof OxyServicesBase>(
|
|
|
502
502
|
handleError(error: unknown): Error;
|
|
503
503
|
healthCheck(): Promise<{
|
|
504
504
|
status: string;
|
|
505
|
-
users
|
|
505
|
+
users
|
|
506
|
+
/**
|
|
507
|
+
* The VC type tags. `'VerifiableCredential'` is the required base type and is
|
|
508
|
+
* prepended automatically when the caller omits it; provide at least one
|
|
509
|
+
* specific type alongside (e.g. `'EmploymentCredential'`).
|
|
510
|
+
*/
|
|
511
|
+
? /**
|
|
512
|
+
* The VC type tags. `'VerifiableCredential'` is the required base type and is
|
|
513
|
+
* prepended automatically when the caller omits it; provide at least one
|
|
514
|
+
* specific type alongside (e.g. `'EmploymentCredential'`).
|
|
515
|
+
*/: number;
|
|
506
516
|
timestamp?: string;
|
|
507
517
|
[key: string]: any;
|
|
508
518
|
}>;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Linked Accounts Methods Mixin
|
|
3
|
+
*
|
|
4
|
+
* External accounts (any Mastodon-API server, Bluesky) the current user has
|
|
5
|
+
* PROVEN they own by completing an OAuth authorization there. Oxy uses that
|
|
6
|
+
* authorization only to learn which account granted it and then discards the
|
|
7
|
+
* token — nothing here ever carries a third-party token.
|
|
8
|
+
*
|
|
9
|
+
* The flow is a top-level browser navigation: {@link startLinkedAccount} returns
|
|
10
|
+
* the other network's `authorizeUrl`; open it (a new tab, or
|
|
11
|
+
* `expo-web-browser`'s auth session on native); the network sends the browser
|
|
12
|
+
* to Oxy's callback, and Oxy sends it to `returnTo` with `?link_code=<code>` or
|
|
13
|
+
* `?link_error=<code>`. Pass the code to {@link completeLinkedAccount} with the
|
|
14
|
+
* same user signed in: that call, not the callback, creates the link, so an
|
|
15
|
+
* authorization someone else started can never link to them. `returnTo` must
|
|
16
|
+
* be a redirect URI registered on the first-party application `clientId` names.
|
|
17
|
+
*
|
|
18
|
+
* Wire types live in `@oxy.so/contracts` (`linkedAccounts.ts`).
|
|
19
|
+
*/
|
|
20
|
+
import type { LinkedAccount, LinkedAccountNetwork, ServiceLinkedAccountListResponse, StartLinkedAccountRequest, StartLinkedAccountResponse } from '@oxy.so/contracts';
|
|
21
|
+
import type { OxyServicesBase } from '../OxyServices.base';
|
|
22
|
+
export declare function OxyServicesLinkedAccountsMixin<T extends typeof OxyServicesBase>(Base: T): {
|
|
23
|
+
new (...args: any[]): {
|
|
24
|
+
/** Inherited from the auth mixin at runtime. */
|
|
25
|
+
makeServiceRequest: <R = unknown>(method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE", url: string, data?: unknown, userId?: string) => Promise<R>;
|
|
26
|
+
/**
|
|
27
|
+
* Begin linking an external account for the current user.
|
|
28
|
+
*
|
|
29
|
+
* @param network - `'activitypub'` (any Mastodon-API server) or `'atproto'` (Bluesky).
|
|
30
|
+
* @param options - `instance` for ActivityPub (`mastodon.social`, or
|
|
31
|
+
* `@user@mastodon.social`), `handle` for atproto; `clientId` + `returnTo`
|
|
32
|
+
* to be sent back to your app afterwards.
|
|
33
|
+
* @returns The URL to open, and when the attempt expires (ten minutes).
|
|
34
|
+
*/
|
|
35
|
+
startLinkedAccount(network: LinkedAccountNetwork, options: StartLinkedAccountRequest): Promise<StartLinkedAccountResponse>;
|
|
36
|
+
/**
|
|
37
|
+
* Finish a link with the `link_code` the callback appended to `returnTo`.
|
|
38
|
+
* Must be the user who started the flow: for anyone else the API answers
|
|
39
|
+
* 403 and burns the code. 409 when the account is already someone else's
|
|
40
|
+
* link. Repeating it within the code's five minutes returns the same link.
|
|
41
|
+
*/
|
|
42
|
+
completeLinkedAccount(code: string): Promise<LinkedAccount>;
|
|
43
|
+
/** The current user's live linked accounts, oldest first. */
|
|
44
|
+
listLinkedAccounts(): Promise<LinkedAccount[]>;
|
|
45
|
+
/**
|
|
46
|
+
* Unlink one of the current user's accounts. An ActivityPub link stops being
|
|
47
|
+
* published as an `alsoKnownAs` alias, and the external account can then be
|
|
48
|
+
* linked to another Oxy account.
|
|
49
|
+
*/
|
|
50
|
+
revokeLinkedAccount(linkedAccountId: string): Promise<void>;
|
|
51
|
+
/**
|
|
52
|
+
* SERVICE: a user's live linked accounts, each with the federated shadow
|
|
53
|
+
* user Oxy already holds for it (`federatedUserId`). Requires a service
|
|
54
|
+
* credential whose application holds the privileged `linked-accounts:read`.
|
|
55
|
+
*/
|
|
56
|
+
getLinkedAccountsForUser(userId: string): Promise<ServiceLinkedAccountListResponse>;
|
|
57
|
+
httpService: import("../HttpService").HttpService;
|
|
58
|
+
cloudURL: string;
|
|
59
|
+
config: import("../OxyServices.base").OxyConfig;
|
|
60
|
+
__resetTokensForTests(): void;
|
|
61
|
+
makeRequest<T_1>(method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE", url: string, data?: any, options?: import("../HttpService").RequestOptions): Promise<T_1>;
|
|
62
|
+
getBaseURL(): string;
|
|
63
|
+
getClient(): import("../HttpService").HttpService;
|
|
64
|
+
createLinkedClient(config: import("../OxyServices.base").OxyConfig): import("..").LinkedHttpClient;
|
|
65
|
+
getMetrics(): {
|
|
66
|
+
totalRequests: number;
|
|
67
|
+
successfulRequests: number;
|
|
68
|
+
failedRequests: number;
|
|
69
|
+
cacheHits: number;
|
|
70
|
+
cacheMisses: number;
|
|
71
|
+
averageResponseTime: number;
|
|
72
|
+
};
|
|
73
|
+
clearCache(): void;
|
|
74
|
+
clearCacheEntry(key: string): void;
|
|
75
|
+
clearCacheByPrefix(prefix: string): number;
|
|
76
|
+
getCacheStats(): {
|
|
77
|
+
size: number;
|
|
78
|
+
hits: number;
|
|
79
|
+
misses: number;
|
|
80
|
+
hitRate: number;
|
|
81
|
+
};
|
|
82
|
+
getCloudURL(): string;
|
|
83
|
+
setTokens(accessToken: string): void;
|
|
84
|
+
clearTokens(): void;
|
|
85
|
+
onTokensChanged(listener: (accessToken: string | null) => void): () => void;
|
|
86
|
+
_cachedUserId: string | null | undefined;
|
|
87
|
+
_cachedAccessToken: string | null;
|
|
88
|
+
getCurrentUserId(): string | null;
|
|
89
|
+
hasValidToken(): boolean;
|
|
90
|
+
getAccessToken(): string | null;
|
|
91
|
+
getAccessTokenExpiry(): number | null;
|
|
92
|
+
waitForAuth(timeoutMs?: number): Promise<boolean>;
|
|
93
|
+
withAuthRetry<T_1>(operation: () => Promise<T_1>, operationName: string, options?: {
|
|
94
|
+
maxRetries?: number;
|
|
95
|
+
retryDelay?: number;
|
|
96
|
+
authTimeoutMs?: number;
|
|
97
|
+
}): Promise<T_1>;
|
|
98
|
+
validate(): Promise<boolean>;
|
|
99
|
+
handleError(error: unknown): Error;
|
|
100
|
+
healthCheck(): Promise<{
|
|
101
|
+
status: string;
|
|
102
|
+
users?: number;
|
|
103
|
+
timestamp?: string;
|
|
104
|
+
[key: string]: any;
|
|
105
|
+
}>;
|
|
106
|
+
};
|
|
107
|
+
} & T;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* User Management Methods Mixin
|
|
3
3
|
*/
|
|
4
|
+
import type { CreateOxyNotificationRequest } from '@oxy.so/contracts';
|
|
4
5
|
import type { User, Notification, NotificationPreferences, UserPreferences, SearchProfilesResponse, PrivacySettings } from '../models/interfaces';
|
|
5
6
|
import type { UserNameResponse, UserProfileUpdate, RecommendationRequest, RecommendationItem, ThemePreference } from '@oxy.so/contracts';
|
|
6
7
|
import type { OxyServicesBase } from '../OxyServices.base';
|
|
@@ -235,10 +236,7 @@ export declare function OxyServicesUserMixin<T extends typeof OxyServicesBase>(B
|
|
|
235
236
|
* `PostHydrationService`) and is unchanged.
|
|
236
237
|
* - **Plain client (browser / React Native with a user session):** when no
|
|
237
238
|
* service credentials are configured, the chunk is fetched via
|
|
238
|
-
* `makeRequest`, which attaches the configured user bearer.
|
|
239
|
-
* middleware skips bearer-authenticated writes, and `makeRequest` only
|
|
240
|
-
* fetches a CSRF token for cookie-only (no-bearer) state-changing requests,
|
|
241
|
-
* so the user-bearer POST is sent without CSRF and succeeds. Previously
|
|
239
|
+
* `makeRequest`, which attaches the configured user bearer. Previously
|
|
242
240
|
* this method always used the service path, so every client-side caller
|
|
243
241
|
* silently received `[]` because `getServiceToken()` had no credentials.
|
|
244
242
|
*
|
|
@@ -498,9 +496,13 @@ export declare function OxyServicesUserMixin<T extends typeof OxyServicesBase>(B
|
|
|
498
496
|
*/
|
|
499
497
|
getUnreadCount(): Promise<number>;
|
|
500
498
|
/**
|
|
501
|
-
* Create notification
|
|
499
|
+
* Create a notification (`POST /notifications`; the API requires a service
|
|
500
|
+
* token whose application holds `notifications:write`). `type` is one of
|
|
501
|
+
* `OXY_NOTIFICATION_TYPES` from `@oxy.so/contracts` — `system` for a message
|
|
502
|
+
* from an Oxy service about the recipient's own account, with the recipient
|
|
503
|
+
* as actor and their profile as the entity.
|
|
502
504
|
*/
|
|
503
|
-
createNotification(data: Partial<Notification>): Promise<Notification>;
|
|
505
|
+
createNotification(data: CreateOxyNotificationRequest | Partial<Notification>): Promise<Notification>;
|
|
504
506
|
/**
|
|
505
507
|
* Mark notification as read
|
|
506
508
|
*/
|
|
@@ -34,6 +34,7 @@ import { OxyServicesNodesMixin } from './OxyServices.nodes';
|
|
|
34
34
|
import { OxyServicesFollowGraphMixin } from './OxyServices.followGraph';
|
|
35
35
|
import { OxyServicesInferenceMixin } from './OxyServices.inference';
|
|
36
36
|
import { OxyServicesDeviceBootMixin } from './OxyServices.deviceBoot';
|
|
37
|
+
import { OxyServicesLinkedAccountsMixin } from './OxyServices.linkedAccounts';
|
|
37
38
|
/**
|
|
38
39
|
* Instance shape of every mixin in the pipeline, intersected. The runtime
|
|
39
40
|
* `composeOxyServices()` produces a class whose instances expose all of
|
|
@@ -43,7 +44,7 @@ import { OxyServicesDeviceBootMixin } from './OxyServices.deviceBoot';
|
|
|
43
44
|
* If you add a new mixin to `MIXIN_PIPELINE`, add it here too so its methods
|
|
44
45
|
* are visible without a cast.
|
|
45
46
|
*/
|
|
46
|
-
type AllMixinInstances = InstanceType<ReturnType<typeof OxyServicesAuthMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesUserMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesIdentityMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesIdentityBackupMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesPrivacyMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesLanguageMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesPaymentMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesReputationMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesAssetsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesAccountsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesConnectedAppsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesAgencyMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesStoreMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesLocationMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesAnalyticsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesDevicesMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesSecurityMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesFeaturesMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesTopicsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesContactsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesNotificationsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesAppDataMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesCivicMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesChainsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesNodesMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesFollowGraphMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesInferenceMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesDeviceBootMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesUtilityMixin<typeof OxyServicesBase>>>;
|
|
47
|
+
type AllMixinInstances = InstanceType<ReturnType<typeof OxyServicesAuthMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesUserMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesIdentityMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesIdentityBackupMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesPrivacyMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesLanguageMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesPaymentMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesReputationMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesAssetsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesAccountsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesConnectedAppsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesAgencyMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesStoreMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesLocationMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesAnalyticsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesDevicesMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesSecurityMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesFeaturesMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesTopicsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesContactsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesNotificationsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesAppDataMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesCivicMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesChainsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesNodesMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesFollowGraphMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesInferenceMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesDeviceBootMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesLinkedAccountsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesUtilityMixin<typeof OxyServicesBase>>>;
|
|
47
48
|
/**
|
|
48
49
|
* Constructor type for the fully composed mixin pipeline. Each mixin returns
|
|
49
50
|
* a new constructor that augments its input; reducing across the pipeline
|
|
@@ -99,6 +99,13 @@ export interface User {
|
|
|
99
99
|
* lists, profile search) — the two are separate fields on the User document.
|
|
100
100
|
*/
|
|
101
101
|
description?: string;
|
|
102
|
+
/**
|
|
103
|
+
* ActivityPub actor URIs this account is also known as — its live,
|
|
104
|
+
* OAuth-proven linked Mastodon-API accounts. Emitted by
|
|
105
|
+
* `GET /profiles/username/:username`; a relying app passes it to the shared
|
|
106
|
+
* actor builder (`@oxy.so/federation`) so its actor publishes it.
|
|
107
|
+
*/
|
|
108
|
+
alsoKnownAs?: string[];
|
|
102
109
|
phone?: string;
|
|
103
110
|
address?: string;
|
|
104
111
|
/** Legacy free-text birthday, kept for backward compatibility. Prefer `dateOfBirth`. */
|
|
@@ -237,6 +244,22 @@ export interface LoginResponse {
|
|
|
237
244
|
export interface Notification {
|
|
238
245
|
id: string;
|
|
239
246
|
message: string;
|
|
247
|
+
/** One of `OXY_NOTIFICATION_TYPES` (`@oxy.so/contracts`). */
|
|
248
|
+
type?: string;
|
|
249
|
+
/**
|
|
250
|
+
* `system` notifications only (with `message`): the text an Oxy service sent
|
|
251
|
+
* about the recipient's own account. Render these two instead of composing a
|
|
252
|
+
* sentence from actor + entity.
|
|
253
|
+
*/
|
|
254
|
+
title?: string;
|
|
255
|
+
/** `system` notifications only: an optional deep link (https or the sending app's scheme). */
|
|
256
|
+
url?: string;
|
|
257
|
+
/**
|
|
258
|
+
* What `entityId` names: `post` | `reply` | `profile`, or `app` — an opaque id
|
|
259
|
+
* in the notifying application's namespace (`system` notifications only).
|
|
260
|
+
*/
|
|
261
|
+
entityType?: string;
|
|
262
|
+
entityId?: string;
|
|
240
263
|
}
|
|
241
264
|
export interface Wallet {
|
|
242
265
|
id: string;
|
|
@@ -594,6 +617,13 @@ export interface ServiceAssetMetadata {
|
|
|
594
617
|
* play. Treat absence as "no adaptive stream, play the progressive original".
|
|
595
618
|
*/
|
|
596
619
|
hlsReadyAt?: string;
|
|
620
|
+
/**
|
|
621
|
+
* The Oxy user who owns the file (`null` for a system-owned file such as the
|
|
622
|
+
* federated media cache). PRESENT ONLY for Oxy's own applications (service
|
|
623
|
+
* tokens with `tier: 'internal'`), so a first-party service can check that an
|
|
624
|
+
* asset a user attached is really that user's. Absent for every other caller.
|
|
625
|
+
*/
|
|
626
|
+
ownerUserId?: string | null;
|
|
597
627
|
}
|
|
598
628
|
/**
|
|
599
629
|
* Reverse-lookup asset metadata returned by `POST /assets/service/by-sha256`.
|
|
@@ -161,6 +161,11 @@ export interface OxyPagesHeadersOptions {
|
|
|
161
161
|
* the app — is REMOVED from `script-src` and `connect-src`, so third-party
|
|
162
162
|
* measurement cannot run there even when the zone has Web Analytics enabled.
|
|
163
163
|
* A structural block in the policy the page is served with, not a build flag.
|
|
164
|
+
*
|
|
165
|
+
* It also sends `Cache-Control: no-transform`, which stops the edge from
|
|
166
|
+
* injecting the beacon at all — otherwise every page load logs a CSP
|
|
167
|
+
* violation for a script we never asked for. The rest of the value is Pages'
|
|
168
|
+
* own default, so caching is unchanged.
|
|
164
169
|
*/
|
|
165
170
|
sensitive?: boolean;
|
|
166
171
|
/**
|
|
@@ -82,6 +82,11 @@ export interface RefreshDeps {
|
|
|
82
82
|
* The device secret is FINE — it is the identity binding that went stale, so
|
|
83
83
|
* the caller must re-establish from the local key, never drop the credential.
|
|
84
84
|
* - `transient` — network / 5xx; keep the secret, a later attempt can succeed.
|
|
85
|
+
* - `session-ended` — the mint succeeded, but the local session was ended
|
|
86
|
+
* (`HttpService.endSession`, i.e. a sign-out) while it was in flight. Nothing
|
|
87
|
+
* is planted. The rotated secret is persisted only if the store still holds
|
|
88
|
+
* the credential that was presented — a store the sign-out cleared stays
|
|
89
|
+
* clear.
|
|
85
90
|
* - `persist-failed` — the mint succeeded but `nextDeviceSecret` could NOT be
|
|
86
91
|
* durably persisted. The token is deliberately NOT planted: advertising a
|
|
87
92
|
* healthy session on a secret that will not survive a reload is exactly the
|
|
@@ -103,6 +108,8 @@ export type DeviceSecretMintOutcome = {
|
|
|
103
108
|
status: 'account-not-on-device';
|
|
104
109
|
} | {
|
|
105
110
|
status: 'transient';
|
|
111
|
+
} | {
|
|
112
|
+
status: 'session-ended';
|
|
106
113
|
} | {
|
|
107
114
|
status: 'persist-failed';
|
|
108
115
|
};
|
|
@@ -93,7 +93,7 @@ export interface ParsedHttpErrorBody {
|
|
|
93
93
|
* - `{ error: '<CODE>', error_description }` — RFC 6749 §5.2 / RFC 6750 §3, the
|
|
94
94
|
* OAuth token and userinfo endpoints. `error_description` is the human text
|
|
95
95
|
* and `error` is the machine code, so both survive.
|
|
96
|
-
* - `{ message, code }` — e.g. the API's
|
|
96
|
+
* - `{ message, code }` — e.g. the API's middleware rejections.
|
|
97
97
|
* - `{ error: '<human message>' }` — legacy hand-rolled routes. With no sibling
|
|
98
98
|
* `message`/`error_description` the string is the message, not a code: a bare
|
|
99
99
|
* `error` string is not machine-readable enough to promote to `code`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oxy.so/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.0",
|
|
4
4
|
"description": "OxyHQ SDK Foundation — API client, authentication, cryptographic identity, and shared utilities",
|
|
5
5
|
"main": "dist/cjs/index.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
@@ -118,7 +118,7 @@
|
|
|
118
118
|
"@oxy.so/telemetry": "^1.1.1",
|
|
119
119
|
"@noble/ciphers": "^1.3.0",
|
|
120
120
|
"@noble/hashes": "^1.8.0",
|
|
121
|
-
"@oxy.so/contracts": "^1.
|
|
121
|
+
"@oxy.so/contracts": "^1.5.0",
|
|
122
122
|
"@oxy.so/protocol": "^1.1.1",
|
|
123
123
|
"@scure/bip39": "^1.6.0",
|
|
124
124
|
"buffer": "^6.0.3",
|