@oxyhq/core 12.4.1 → 12.5.1
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/polyfill.js +85 -17
- package/dist/cjs/mixins/OxyServices.user.js +11 -0
- package/dist/cjs/server/safeFetch.js +4 -4
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/crypto/polyfill.js +86 -18
- package/dist/esm/mixins/OxyServices.user.js +11 -0
- package/dist/esm/server/safeFetch.js +4 -4
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/crypto/polyfill.d.ts +16 -4
- package/dist/types/mixins/OxyServices.user.d.ts +10 -1
- package/dist/types/models/interfaces.d.ts +15 -1
- package/dist/types/server/safeFetch.d.ts +2 -0
- package/package.json +2 -2
- package/src/crypto/__tests__/polyfill.test.ts +109 -0
- package/src/crypto/polyfill.ts +94 -18
- package/src/mixins/OxyServices.user.ts +15 -0
- package/src/models/interfaces.ts +20 -1
- package/src/server/safeFetch.ts +6 -1
|
@@ -4,10 +4,22 @@
|
|
|
4
4
|
* Ensures Buffer and crypto.getRandomValues are available
|
|
5
5
|
* across all platforms (Node.js, Browser, React Native).
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* Guard order when installing a `getRandomValues` shim (see bottom of file and
|
|
8
|
+
* {@link cryptoPolyfill}):
|
|
9
|
+
*
|
|
10
|
+
* 1. A REAL `globalThis.crypto.getRandomValues` — used as-is (browser, Node
|
|
11
|
+
* >= 20, modern Hermes). The shim below is only installed when the host is
|
|
12
|
+
* missing it, so this branch is the install-time gate.
|
|
13
|
+
* 2. Node — backed by the built-in `node:crypto` module (`webcrypto`, else
|
|
14
|
+
* `randomFillSync`). This is what a Node runtime WITHOUT a global WebCrypto
|
|
15
|
+
* (Node 18 script entrypoints, some embedded hosts) falls back to.
|
|
16
|
+
* 3. React Native — `expo-crypto.getRandomBytes` (statically imported via the
|
|
17
|
+
* per-platform `platform/crypto` module in `@oxyhq/protocol`).
|
|
18
|
+
*
|
|
19
|
+
* Historically step (2) delegated to `@oxyhq/protocol`'s RN-only
|
|
20
|
+
* `getRandomBytesRN`, which THROWS on Node — so any Node host lacking a global
|
|
21
|
+
* WebCrypto crashed here instead of getting randomness. It is now a proper
|
|
22
|
+
* Node-backed implementation.
|
|
11
23
|
*/
|
|
12
24
|
import { Buffer } from 'buffer';
|
|
13
25
|
export { Buffer };
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* User Management Methods Mixin
|
|
3
3
|
*/
|
|
4
4
|
import type { User, Notification, NotificationPreferences, UserPreferences, SearchProfilesResponse, PrivacySettings } from '../models/interfaces';
|
|
5
|
-
import type { UserNameResponse, UserProfileUpdate, RecommendationRequest, RecommendationItem } from '@oxyhq/contracts';
|
|
5
|
+
import type { UserNameResponse, UserProfileUpdate, RecommendationRequest, RecommendationItem, ThemePreference } from '@oxyhq/contracts';
|
|
6
6
|
import type { OxyServicesBase } from '../OxyServices.base';
|
|
7
7
|
import { type PaginationParams } from '../utils/apiUtils';
|
|
8
8
|
/**
|
|
@@ -292,6 +292,15 @@ export declare function OxyServicesUserMixin<T extends typeof OxyServicesBase>(B
|
|
|
292
292
|
* `PUT /users/me` — same cache-invalidation behaviour as `updateProfile`.
|
|
293
293
|
*/
|
|
294
294
|
updateUserPreferences(preferences: Partial<UserPreferences>): Promise<User>;
|
|
295
|
+
/**
|
|
296
|
+
* Update the authenticated user's portable theme preference (light/dark/
|
|
297
|
+
* system + Bloom color-preset key). Persisted on the User document via the
|
|
298
|
+
* SAME `PUT /users/me` settings path as the other preferences — same cache
|
|
299
|
+
* invalidation — so the next cold boot serves it on the self/session payload
|
|
300
|
+
* with no extra network call. The full object is written (both `mode` and
|
|
301
|
+
* `colorPreset` are required by the API).
|
|
302
|
+
*/
|
|
303
|
+
updateThemePreference(themePreference: ThemePreference): Promise<User>;
|
|
295
304
|
/**
|
|
296
305
|
* Request account verification
|
|
297
306
|
*/
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { OrganizationCategory, UserNameResponse } from '@oxyhq/contracts';
|
|
1
|
+
import type { OrganizationCategory, UserNameResponse, UserRelationship, ThemePreference } from '@oxyhq/contracts';
|
|
2
2
|
export interface OxyConfig {
|
|
3
3
|
baseURL: string;
|
|
4
4
|
cloudURL?: string;
|
|
@@ -147,6 +147,20 @@ export interface User {
|
|
|
147
147
|
languages?: string[];
|
|
148
148
|
notificationPreferences?: NotificationPreferences;
|
|
149
149
|
userPreferences?: UserPreferences;
|
|
150
|
+
/**
|
|
151
|
+
* Portable theme preference (mode + Bloom color-preset key). Rides the
|
|
152
|
+
* self/session payload so it is present on the current-user DTO at cold boot
|
|
153
|
+
* (`useAuth().user.themePreference`) with no extra fetch. Absent until the
|
|
154
|
+
* user sets it. Updated via `updateThemePreference` / `updateProfile`.
|
|
155
|
+
*/
|
|
156
|
+
themePreference?: ThemePreference;
|
|
157
|
+
/**
|
|
158
|
+
* The authenticated viewer's relationship to THIS profile. Populated ONLY on
|
|
159
|
+
* single-profile fetches (`getProfileByUsername` / `getUserById`) when the
|
|
160
|
+
* request is authenticated; absent for anonymous requests, self-views, and
|
|
161
|
+
* bulk fetches — so `undefined` means "unknown", not "not following".
|
|
162
|
+
*/
|
|
163
|
+
relationship?: UserRelationship;
|
|
150
164
|
[key: string]: unknown;
|
|
151
165
|
}
|
|
152
166
|
/**
|
|
@@ -91,6 +91,8 @@ export interface SafeFetchOptions {
|
|
|
91
91
|
method?: string;
|
|
92
92
|
/** Extra request headers. A `User-Agent` is added if none is provided. */
|
|
93
93
|
headers?: Record<string, string>;
|
|
94
|
+
/** Request body for POST/PUT/PATCH. */
|
|
95
|
+
body?: string | Buffer;
|
|
94
96
|
/**
|
|
95
97
|
* Maximum number of redirects to follow (each re-validated). Defaults to
|
|
96
98
|
* {@link MAX_REDIRECTS}. Set to `0` to disallow redirects.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oxyhq/core",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.5.1",
|
|
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",
|
|
@@ -111,7 +111,7 @@
|
|
|
111
111
|
"dependencies": {
|
|
112
112
|
"@noble/ciphers": "^1.3.0",
|
|
113
113
|
"@noble/hashes": "^1.8.0",
|
|
114
|
-
"@oxyhq/contracts": "^0.
|
|
114
|
+
"@oxyhq/contracts": "^0.17.0",
|
|
115
115
|
"@oxyhq/protocol": "^0.1.5",
|
|
116
116
|
"bip39": "^3.1.0",
|
|
117
117
|
"buffer": "^6.0.3",
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Crypto polyfill — `getRandomValues` install + platform routing.
|
|
3
|
+
*
|
|
4
|
+
* Regression coverage for the latent Node crash: when a Node runtime ships
|
|
5
|
+
* WITHOUT a global WebCrypto (Node 18 script entrypoints, some embedded hosts),
|
|
6
|
+
* the installed `getRandomValues` shim must be backed by `node:crypto` and MUST
|
|
7
|
+
* NOT fall through to `@oxyhq/protocol`'s RN-only `getRandomBytesRN` stub (which
|
|
8
|
+
* throws `Tried to load 'expo-crypto...' outside React Native`).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// Controllable stand-ins for `@oxyhq/protocol`'s platform predicates. Names are
|
|
12
|
+
// `mock`-prefixed so the (hoisted) `jest.mock` factory may reference them.
|
|
13
|
+
const mockGetRandomBytesRN = jest.fn<Uint8Array, [number]>();
|
|
14
|
+
const mockState = { isNodeJS: true };
|
|
15
|
+
|
|
16
|
+
jest.mock('@oxyhq/protocol', () => ({
|
|
17
|
+
isNodeJS: () => mockState.isNodeJS,
|
|
18
|
+
getRandomBytesRN: (byteCount: number) => mockGetRandomBytesRN(byteCount),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
type CryptoLike = {
|
|
22
|
+
getRandomValues: <T extends ArrayBufferView>(array: T) => T;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const ORIGINAL_CRYPTO_DESCRIPTOR = Object.getOwnPropertyDescriptor(globalThis, 'crypto');
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Re-run the polyfill module with NO host `globalThis.crypto`, returning the
|
|
29
|
+
* `getRandomValues` shim it installs. Restores the real global afterwards so
|
|
30
|
+
* the manipulation never leaks into other tests.
|
|
31
|
+
*/
|
|
32
|
+
function installShimWithoutHostCrypto(): CryptoLike {
|
|
33
|
+
Object.defineProperty(globalThis, 'crypto', {
|
|
34
|
+
value: undefined,
|
|
35
|
+
configurable: true,
|
|
36
|
+
writable: true,
|
|
37
|
+
});
|
|
38
|
+
try {
|
|
39
|
+
jest.isolateModules(() => {
|
|
40
|
+
require('../polyfill');
|
|
41
|
+
});
|
|
42
|
+
const installed = (globalThis as { crypto?: CryptoLike }).crypto;
|
|
43
|
+
if (!installed || typeof installed.getRandomValues !== 'function') {
|
|
44
|
+
throw new Error('polyfill did not install a getRandomValues shim');
|
|
45
|
+
}
|
|
46
|
+
return installed;
|
|
47
|
+
} finally {
|
|
48
|
+
Object.defineProperty(
|
|
49
|
+
globalThis,
|
|
50
|
+
'crypto',
|
|
51
|
+
ORIGINAL_CRYPTO_DESCRIPTOR ?? { value: undefined, configurable: true, writable: true },
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
beforeEach(() => {
|
|
57
|
+
mockGetRandomBytesRN.mockReset();
|
|
58
|
+
mockState.isNodeJS = true;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('crypto polyfill getRandomValues', () => {
|
|
62
|
+
it('on Node without global WebCrypto, fills from node:crypto and never calls the RN stub', () => {
|
|
63
|
+
mockState.isNodeJS = true;
|
|
64
|
+
// If the shim ever fell through to the RN path on Node, this would throw —
|
|
65
|
+
// exactly the latent crash we are guarding against.
|
|
66
|
+
mockGetRandomBytesRN.mockImplementation(() => {
|
|
67
|
+
throw new Error('RN getRandomBytesRN must not be called on Node');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const shim = installShimWithoutHostCrypto();
|
|
71
|
+
const array = new Uint8Array(16);
|
|
72
|
+
|
|
73
|
+
expect(() => shim.getRandomValues(array)).not.toThrow();
|
|
74
|
+
// Backed by a real CSPRNG: all-zero output is cryptographically impossible.
|
|
75
|
+
expect(array.some((byte) => byte !== 0)).toBe(true);
|
|
76
|
+
expect(mockGetRandomBytesRN).not.toHaveBeenCalled();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('on Node, routes a non-integer view (DataView) through randomFillSync', () => {
|
|
80
|
+
mockState.isNodeJS = true;
|
|
81
|
+
mockGetRandomBytesRN.mockImplementation(() => {
|
|
82
|
+
throw new Error('RN getRandomBytesRN must not be called on Node');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const shim = installShimWithoutHostCrypto();
|
|
86
|
+
const view = new DataView(new ArrayBuffer(16));
|
|
87
|
+
|
|
88
|
+
expect(() => shim.getRandomValues(view)).not.toThrow();
|
|
89
|
+
let anyNonZero = false;
|
|
90
|
+
for (let i = 0; i < view.byteLength; i += 1) {
|
|
91
|
+
if (view.getUint8(i) !== 0) anyNonZero = true;
|
|
92
|
+
}
|
|
93
|
+
expect(anyNonZero).toBe(true);
|
|
94
|
+
expect(mockGetRandomBytesRN).not.toHaveBeenCalled();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('on React Native, delegates to expo-crypto via getRandomBytesRN (path unchanged)', () => {
|
|
98
|
+
mockState.isNodeJS = false;
|
|
99
|
+
const rnBytes = Uint8Array.from([1, 2, 3, 4, 5, 6, 7, 8]);
|
|
100
|
+
mockGetRandomBytesRN.mockReturnValue(rnBytes);
|
|
101
|
+
|
|
102
|
+
const shim = installShimWithoutHostCrypto();
|
|
103
|
+
const array = new Uint8Array(8);
|
|
104
|
+
const result = shim.getRandomValues(array);
|
|
105
|
+
|
|
106
|
+
expect(mockGetRandomBytesRN).toHaveBeenCalledWith(8);
|
|
107
|
+
expect(Array.from(result)).toEqual([1, 2, 3, 4, 5, 6, 7, 8]);
|
|
108
|
+
});
|
|
109
|
+
});
|
package/src/crypto/polyfill.ts
CHANGED
|
@@ -4,14 +4,26 @@
|
|
|
4
4
|
* Ensures Buffer and crypto.getRandomValues are available
|
|
5
5
|
* across all platforms (Node.js, Browser, React Native).
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* Guard order when installing a `getRandomValues` shim (see bottom of file and
|
|
8
|
+
* {@link cryptoPolyfill}):
|
|
9
|
+
*
|
|
10
|
+
* 1. A REAL `globalThis.crypto.getRandomValues` — used as-is (browser, Node
|
|
11
|
+
* >= 20, modern Hermes). The shim below is only installed when the host is
|
|
12
|
+
* missing it, so this branch is the install-time gate.
|
|
13
|
+
* 2. Node — backed by the built-in `node:crypto` module (`webcrypto`, else
|
|
14
|
+
* `randomFillSync`). This is what a Node runtime WITHOUT a global WebCrypto
|
|
15
|
+
* (Node 18 script entrypoints, some embedded hosts) falls back to.
|
|
16
|
+
* 3. React Native — `expo-crypto.getRandomBytes` (statically imported via the
|
|
17
|
+
* per-platform `platform/crypto` module in `@oxyhq/protocol`).
|
|
18
|
+
*
|
|
19
|
+
* Historically step (2) delegated to `@oxyhq/protocol`'s RN-only
|
|
20
|
+
* `getRandomBytesRN`, which THROWS on Node — so any Node host lacking a global
|
|
21
|
+
* WebCrypto crashed here instead of getting randomness. It is now a proper
|
|
22
|
+
* Node-backed implementation.
|
|
11
23
|
*/
|
|
12
24
|
|
|
13
25
|
import { Buffer } from 'buffer';
|
|
14
|
-
import { getRandomBytesRN } from '@oxyhq/protocol';
|
|
26
|
+
import { getRandomBytesRN, isNodeJS } from '@oxyhq/protocol';
|
|
15
27
|
|
|
16
28
|
const getGlobalObject = (): typeof globalThis => {
|
|
17
29
|
if (typeof globalThis !== 'undefined') return globalThis;
|
|
@@ -32,25 +44,89 @@ type CryptoLike = {
|
|
|
32
44
|
getRandomValues: <T extends ArrayBufferView>(array: T) => T;
|
|
33
45
|
};
|
|
34
46
|
|
|
47
|
+
/** Minimal structural shape of the parts of `node:crypto` this polyfill uses. */
|
|
48
|
+
interface NodeCryptoLike {
|
|
49
|
+
webcrypto?: {
|
|
50
|
+
getRandomValues?: <T extends ArrayBufferView>(array: T) => T;
|
|
51
|
+
};
|
|
52
|
+
randomFillSync?: <T extends ArrayBufferView>(buffer: T) => T;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Lazily-resolved `node:crypto` module, cached after the first attempt.
|
|
57
|
+
* `undefined` = not tried yet; `null` = tried and unavailable (non-Node host).
|
|
58
|
+
*/
|
|
59
|
+
let cachedNodeCrypto: NodeCryptoLike | null | undefined;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Synchronously load `node:crypto` on a Node runtime, or `null` elsewhere.
|
|
63
|
+
*
|
|
64
|
+
* Uses a guarded, Node-only `require`. Every runtime that actually reaches this
|
|
65
|
+
* branch has a working CommonJS `require`: `@oxyhq/core` publishes no
|
|
66
|
+
* `"type": "module"`, so Node loads it as CommonJS and the `require` free
|
|
67
|
+
* variable is present. Browsers never reach here (they own `globalThis.crypto`,
|
|
68
|
+
* so this polyfill is never installed) and React Native takes the
|
|
69
|
+
* `getRandomBytesRN` branch — so the `node:crypto` reference is dead code in
|
|
70
|
+
* those bundles, and Expo's Metro resolver shims `node:*` builtins, keeping
|
|
71
|
+
* web/native bundles green.
|
|
72
|
+
*/
|
|
73
|
+
function loadNodeCryptoSync(): NodeCryptoLike | null {
|
|
74
|
+
if (cachedNodeCrypto !== undefined) {
|
|
75
|
+
return cachedNodeCrypto;
|
|
76
|
+
}
|
|
77
|
+
if (typeof require !== 'function') {
|
|
78
|
+
cachedNodeCrypto = null;
|
|
79
|
+
return cachedNodeCrypto;
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
cachedNodeCrypto = require('node:crypto') as NodeCryptoLike;
|
|
83
|
+
} catch {
|
|
84
|
+
// No Node crypto (unexpected on a real Node host) — degrade to the next
|
|
85
|
+
// mechanism rather than crash.
|
|
86
|
+
cachedNodeCrypto = null;
|
|
87
|
+
}
|
|
88
|
+
return cachedNodeCrypto;
|
|
89
|
+
}
|
|
90
|
+
|
|
35
91
|
/**
|
|
36
|
-
*
|
|
37
|
-
* `
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* browser, this throws — but is never called there because both platforms
|
|
41
|
-
* already provide `globalThis.crypto.getRandomValues` natively.
|
|
92
|
+
* Fill `array` with cryptographically-secure random bytes from `node:crypto`.
|
|
93
|
+
* Prefers `webcrypto.getRandomValues`; falls back to `randomFillSync`. Returns
|
|
94
|
+
* `false` when Node crypto is unavailable so the caller can try the next
|
|
95
|
+
* mechanism.
|
|
42
96
|
*/
|
|
43
|
-
function
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
97
|
+
function fillFromNodeCrypto(array: ArrayBufferView): boolean {
|
|
98
|
+
const nodeCrypto = loadNodeCryptoSync();
|
|
99
|
+
if (!nodeCrypto) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
const webcrypto = nodeCrypto.webcrypto;
|
|
103
|
+
if (webcrypto && typeof webcrypto.getRandomValues === 'function') {
|
|
104
|
+
try {
|
|
105
|
+
webcrypto.getRandomValues(array);
|
|
106
|
+
return true;
|
|
107
|
+
} catch {
|
|
108
|
+
// `webcrypto.getRandomValues` rejects non-integer views (Float*Array,
|
|
109
|
+
// DataView); fall through to `randomFillSync`, which accepts any view.
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (typeof nodeCrypto.randomFillSync === 'function') {
|
|
113
|
+
nodeCrypto.randomFillSync(array);
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
return false;
|
|
49
117
|
}
|
|
50
118
|
|
|
51
119
|
const cryptoPolyfill: CryptoLike = {
|
|
52
120
|
getRandomValues<T extends ArrayBufferView>(array: T): T {
|
|
53
|
-
|
|
121
|
+
// Node: back the CSPRNG with `node:crypto`. This is the path that matters on
|
|
122
|
+
// Node runtimes shipping WITHOUT a global WebCrypto — where delegating to
|
|
123
|
+
// the RN-only expo-crypto stub would throw.
|
|
124
|
+
if (isNodeJS() && fillFromNodeCrypto(array)) {
|
|
125
|
+
return array;
|
|
126
|
+
}
|
|
127
|
+
// React Native (and any non-Node host without WebCrypto): synchronous
|
|
128
|
+
// expo-crypto via @oxyhq/protocol's RN `platform/crypto` variant.
|
|
129
|
+
const bytes = getRandomBytesRN(array.byteLength);
|
|
54
130
|
const uint8View = new Uint8Array(array.buffer, array.byteOffset, array.byteLength);
|
|
55
131
|
uint8View.set(bytes);
|
|
56
132
|
return array;
|
|
@@ -15,6 +15,7 @@ import type {
|
|
|
15
15
|
UserProfileUpdate,
|
|
16
16
|
RecommendationRequest,
|
|
17
17
|
RecommendationItem,
|
|
18
|
+
ThemePreference,
|
|
18
19
|
} from '@oxyhq/contracts';
|
|
19
20
|
import { recommendationRequestSchema } from '@oxyhq/contracts';
|
|
20
21
|
import type { OxyServicesBase } from '../OxyServices.base';
|
|
@@ -628,6 +629,20 @@ export function OxyServicesUserMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
628
629
|
return this.updateProfile({ userPreferences: preferences });
|
|
629
630
|
}
|
|
630
631
|
|
|
632
|
+
/**
|
|
633
|
+
* Update the authenticated user's portable theme preference (light/dark/
|
|
634
|
+
* system + Bloom color-preset key). Persisted on the User document via the
|
|
635
|
+
* SAME `PUT /users/me` settings path as the other preferences — same cache
|
|
636
|
+
* invalidation — so the next cold boot serves it on the self/session payload
|
|
637
|
+
* with no extra network call. The full object is written (both `mode` and
|
|
638
|
+
* `colorPreset` are required by the API).
|
|
639
|
+
*/
|
|
640
|
+
async updateThemePreference(
|
|
641
|
+
themePreference: ThemePreference
|
|
642
|
+
): Promise<User> {
|
|
643
|
+
return this.updateProfile({ themePreference });
|
|
644
|
+
}
|
|
645
|
+
|
|
631
646
|
/**
|
|
632
647
|
* Request account verification
|
|
633
648
|
*/
|
package/src/models/interfaces.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
OrganizationCategory,
|
|
3
|
+
UserNameResponse,
|
|
4
|
+
UserRelationship,
|
|
5
|
+
ThemePreference,
|
|
6
|
+
} from '@oxyhq/contracts';
|
|
2
7
|
|
|
3
8
|
export interface OxyConfig {
|
|
4
9
|
baseURL: string;
|
|
@@ -163,6 +168,20 @@ export interface User {
|
|
|
163
168
|
notificationPreferences?: NotificationPreferences;
|
|
164
169
|
// General app-wide user preferences. Updated via `PUT /users/me`.
|
|
165
170
|
userPreferences?: UserPreferences;
|
|
171
|
+
/**
|
|
172
|
+
* Portable theme preference (mode + Bloom color-preset key). Rides the
|
|
173
|
+
* self/session payload so it is present on the current-user DTO at cold boot
|
|
174
|
+
* (`useAuth().user.themePreference`) with no extra fetch. Absent until the
|
|
175
|
+
* user sets it. Updated via `updateThemePreference` / `updateProfile`.
|
|
176
|
+
*/
|
|
177
|
+
themePreference?: ThemePreference;
|
|
178
|
+
/**
|
|
179
|
+
* The authenticated viewer's relationship to THIS profile. Populated ONLY on
|
|
180
|
+
* single-profile fetches (`getProfileByUsername` / `getUserById`) when the
|
|
181
|
+
* request is authenticated; absent for anonymous requests, self-views, and
|
|
182
|
+
* bulk fetches — so `undefined` means "unknown", not "not following".
|
|
183
|
+
*/
|
|
184
|
+
relationship?: UserRelationship;
|
|
166
185
|
[key: string]: unknown;
|
|
167
186
|
}
|
|
168
187
|
|
package/src/server/safeFetch.ts
CHANGED
|
@@ -390,6 +390,8 @@ export interface SafeFetchOptions {
|
|
|
390
390
|
method?: string;
|
|
391
391
|
/** Extra request headers. A `User-Agent` is added if none is provided. */
|
|
392
392
|
headers?: Record<string, string>;
|
|
393
|
+
/** Request body for POST/PUT/PATCH. */
|
|
394
|
+
body?: string | Buffer;
|
|
393
395
|
/**
|
|
394
396
|
* Maximum number of redirects to follow (each re-validated). Defaults to
|
|
395
397
|
* {@link MAX_REDIRECTS}. Set to `0` to disallow redirects.
|
|
@@ -475,6 +477,7 @@ function fetchOnce(
|
|
|
475
477
|
options: https.RequestOptions,
|
|
476
478
|
isHttps: boolean,
|
|
477
479
|
headersTimeoutMs: number,
|
|
480
|
+
body?: string | Buffer,
|
|
478
481
|
): Promise<IncomingMessage> {
|
|
479
482
|
return new Promise<IncomingMessage>((resolve, reject) => {
|
|
480
483
|
const transport = isHttps ? https : http;
|
|
@@ -484,7 +487,7 @@ function fetchOnce(
|
|
|
484
487
|
req.destroy(new UpstreamError('upstream headers timeout'));
|
|
485
488
|
});
|
|
486
489
|
req.on('error', (err) => reject(err));
|
|
487
|
-
req.end();
|
|
490
|
+
req.end(body);
|
|
488
491
|
});
|
|
489
492
|
}
|
|
490
493
|
|
|
@@ -506,6 +509,7 @@ export async function safeFetch(
|
|
|
506
509
|
const {
|
|
507
510
|
method = 'GET',
|
|
508
511
|
headers: callerHeaders,
|
|
512
|
+
body,
|
|
509
513
|
maxRedirects = MAX_REDIRECTS,
|
|
510
514
|
headersTimeoutMs = UPSTREAM_HEADERS_TIMEOUT_MS,
|
|
511
515
|
signal,
|
|
@@ -548,6 +552,7 @@ export async function safeFetch(
|
|
|
548
552
|
requestOptions,
|
|
549
553
|
target.protocol === 'https:',
|
|
550
554
|
headersTimeoutMs,
|
|
555
|
+
body,
|
|
551
556
|
);
|
|
552
557
|
|
|
553
558
|
const status = response.statusCode ?? 0;
|