@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.
@@ -5,10 +5,22 @@
5
5
  * Ensures Buffer and crypto.getRandomValues are available
6
6
  * across all platforms (Node.js, Browser, React Native).
7
7
  *
8
- * - Browser/Node.js: Uses native crypto
9
- * - React Native: Uses expo-crypto (statically imported via the
10
- * per-platform `platform/crypto` module in `@oxyhq/protocol` — see that
11
- * file's doc-comment for how platform routing works).
8
+ * Guard order when installing a `getRandomValues` shim (see bottom of file and
9
+ * {@link cryptoPolyfill}):
10
+ *
11
+ * 1. A REAL `globalThis.crypto.getRandomValues` used as-is (browser, Node
12
+ * >= 20, modern Hermes). The shim below is only installed when the host is
13
+ * missing it, so this branch is the install-time gate.
14
+ * 2. Node — backed by the built-in `node:crypto` module (`webcrypto`, else
15
+ * `randomFillSync`). This is what a Node runtime WITHOUT a global WebCrypto
16
+ * (Node 18 script entrypoints, some embedded hosts) falls back to.
17
+ * 3. React Native — `expo-crypto.getRandomBytes` (statically imported via the
18
+ * per-platform `platform/crypto` module in `@oxyhq/protocol`).
19
+ *
20
+ * Historically step (2) delegated to `@oxyhq/protocol`'s RN-only
21
+ * `getRandomBytesRN`, which THROWS on Node — so any Node host lacking a global
22
+ * WebCrypto crashed here instead of getting randomness. It is now a proper
23
+ * Node-backed implementation.
12
24
  */
13
25
  Object.defineProperty(exports, "__esModule", { value: true });
14
26
  exports.Buffer = void 0;
@@ -32,23 +44,79 @@ if (!globalObject.Buffer) {
32
44
  globalObject.Buffer = buffer_1.Buffer;
33
45
  }
34
46
  /**
35
- * Synchronous random-bytes shim. On RN, this delegates to
36
- * `expo-crypto.getRandomBytes` (statically imported by the RN variant of
37
- * `@oxyhq/protocol`'s `platform/crypto`, so available without any async
38
- * warm-up). On Node /
39
- * browser, this throws — but is never called there because both platforms
40
- * already provide `globalThis.crypto.getRandomValues` natively.
47
+ * Lazily-resolved `node:crypto` module, cached after the first attempt.
48
+ * `undefined` = not tried yet; `null` = tried and unavailable (non-Node host).
49
+ */
50
+ let cachedNodeCrypto;
51
+ /**
52
+ * Synchronously load `node:crypto` on a Node runtime, or `null` elsewhere.
53
+ *
54
+ * Uses a guarded, Node-only `require`. Every runtime that actually reaches this
55
+ * branch has a working CommonJS `require`: `@oxyhq/core` publishes no
56
+ * `"type": "module"`, so Node loads it as CommonJS and the `require` free
57
+ * variable is present. Browsers never reach here (they own `globalThis.crypto`,
58
+ * so this polyfill is never installed) and React Native takes the
59
+ * `getRandomBytesRN` branch — so the `node:crypto` reference is dead code in
60
+ * those bundles, and Expo's Metro resolver shims `node:*` builtins, keeping
61
+ * web/native bundles green.
62
+ */
63
+ function loadNodeCryptoSync() {
64
+ if (cachedNodeCrypto !== undefined) {
65
+ return cachedNodeCrypto;
66
+ }
67
+ if (typeof require !== 'function') {
68
+ cachedNodeCrypto = null;
69
+ return cachedNodeCrypto;
70
+ }
71
+ try {
72
+ cachedNodeCrypto = require('node:crypto');
73
+ }
74
+ catch {
75
+ // No Node crypto (unexpected on a real Node host) — degrade to the next
76
+ // mechanism rather than crash.
77
+ cachedNodeCrypto = null;
78
+ }
79
+ return cachedNodeCrypto;
80
+ }
81
+ /**
82
+ * Fill `array` with cryptographically-secure random bytes from `node:crypto`.
83
+ * Prefers `webcrypto.getRandomValues`; falls back to `randomFillSync`. Returns
84
+ * `false` when Node crypto is unavailable so the caller can try the next
85
+ * mechanism.
41
86
  */
42
- function getRandomBytesSync(byteCount) {
43
- // `getRandomBytesRN` throws on non-RN platforms. That's fine: this
44
- // function is only ever called as a fallback when the native
45
- // `globalThis.crypto.getRandomValues` is missing, which on a normal
46
- // Node/browser host never happens.
47
- return (0, protocol_1.getRandomBytesRN)(byteCount);
87
+ function fillFromNodeCrypto(array) {
88
+ const nodeCrypto = loadNodeCryptoSync();
89
+ if (!nodeCrypto) {
90
+ return false;
91
+ }
92
+ const webcrypto = nodeCrypto.webcrypto;
93
+ if (webcrypto && typeof webcrypto.getRandomValues === 'function') {
94
+ try {
95
+ webcrypto.getRandomValues(array);
96
+ return true;
97
+ }
98
+ catch {
99
+ // `webcrypto.getRandomValues` rejects non-integer views (Float*Array,
100
+ // DataView); fall through to `randomFillSync`, which accepts any view.
101
+ }
102
+ }
103
+ if (typeof nodeCrypto.randomFillSync === 'function') {
104
+ nodeCrypto.randomFillSync(array);
105
+ return true;
106
+ }
107
+ return false;
48
108
  }
49
109
  const cryptoPolyfill = {
50
110
  getRandomValues(array) {
51
- const bytes = getRandomBytesSync(array.byteLength);
111
+ // Node: back the CSPRNG with `node:crypto`. This is the path that matters on
112
+ // Node runtimes shipping WITHOUT a global WebCrypto — where delegating to
113
+ // the RN-only expo-crypto stub would throw.
114
+ if ((0, protocol_1.isNodeJS)() && fillFromNodeCrypto(array)) {
115
+ return array;
116
+ }
117
+ // React Native (and any non-Node host without WebCrypto): synchronous
118
+ // expo-crypto via @oxyhq/protocol's RN `platform/crypto` variant.
119
+ const bytes = (0, protocol_1.getRandomBytesRN)(array.byteLength);
52
120
  const uint8View = new Uint8Array(array.buffer, array.byteOffset, array.byteLength);
53
121
  uint8View.set(bytes);
54
122
  return array;
@@ -440,6 +440,17 @@ function OxyServicesUserMixin(Base) {
440
440
  async updateUserPreferences(preferences) {
441
441
  return this.updateProfile({ userPreferences: preferences });
442
442
  }
443
+ /**
444
+ * Update the authenticated user's portable theme preference (light/dark/
445
+ * system + Bloom color-preset key). Persisted on the User document via the
446
+ * SAME `PUT /users/me` settings path as the other preferences — same cache
447
+ * invalidation — so the next cold boot serves it on the self/session payload
448
+ * with no extra network call. The full object is written (both `mode` and
449
+ * `colorPreset` are required by the API).
450
+ */
451
+ async updateThemePreference(themePreference) {
452
+ return this.updateProfile({ themePreference });
453
+ }
443
454
  /**
444
455
  * Request account verification
445
456
  */
@@ -383,7 +383,7 @@ function buildRequestOptions(target, pinnedIp, pinnedFamily, method, headers, si
383
383
  };
384
384
  }
385
385
  /** Perform a single upstream request (no auto-redirect). */
386
- function fetchOnce(options, isHttps, headersTimeoutMs) {
386
+ function fetchOnce(options, isHttps, headersTimeoutMs, body) {
387
387
  return new Promise((resolve, reject) => {
388
388
  const transport = isHttps ? node_https_1.default : node_http_1.default;
389
389
  const req = transport.request(options, (res) => resolve(res));
@@ -391,7 +391,7 @@ function fetchOnce(options, isHttps, headersTimeoutMs) {
391
391
  req.destroy(new UpstreamError('upstream headers timeout'));
392
392
  });
393
393
  req.on('error', (err) => reject(err));
394
- req.end();
394
+ req.end(body);
395
395
  });
396
396
  }
397
397
  /**
@@ -406,7 +406,7 @@ function fetchOnce(options, isHttps, headersTimeoutMs) {
406
406
  * @throws {UpstreamError} on redirect-loop / malformed-redirect / timeout.
407
407
  */
408
408
  async function safeFetch(rawUrl, options = {}) {
409
- const { method = 'GET', headers: callerHeaders, maxRedirects = exports.MAX_REDIRECTS, headersTimeoutMs = exports.UPSTREAM_HEADERS_TIMEOUT_MS, signal, } = options;
409
+ const { method = 'GET', headers: callerHeaders, body, maxRedirects = exports.MAX_REDIRECTS, headersTimeoutMs = exports.UPSTREAM_HEADERS_TIMEOUT_MS, signal, } = options;
410
410
  // Normalize a case-insensitive header map and ensure a User-Agent default.
411
411
  const baseHeaders = {};
412
412
  if (callerHeaders) {
@@ -429,7 +429,7 @@ async function safeFetch(rawUrl, options = {}) {
429
429
  }
430
430
  const target = new node_url_1.URL(currentUrl);
431
431
  const requestOptions = buildRequestOptions(target, guard.ip, guard.family, method, baseHeaders, signal);
432
- const response = await fetchOnce(requestOptions, target.protocol === 'https:', headersTimeoutMs);
432
+ const response = await fetchOnce(requestOptions, target.protocol === 'https:', headersTimeoutMs, body);
433
433
  const status = response.statusCode ?? 0;
434
434
  if (REDIRECT_STATUS_CODES.has(status)) {
435
435
  const location = response.headers.location;