@oxy.so/core 1.7.0 → 1.7.3

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.
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Registration proof-of-work — the CLIENT half of the anti-automation nonce
3
+ * `register()` (`mixins/OxyServices.auth.ts`) submits alongside the
4
+ * registration signature. The message format, difficulty and the full design
5
+ * rationale (why 16 bits, why sequential nonces are fine, what this is and
6
+ * isn't a defense against) live in `@oxy.so/protocol`'s
7
+ * `auth/registrationPow.ts` — this file is only the solve loop.
8
+ */
9
+ import './polyfill.js';
10
+ import { sha256 } from '@noble/hashes/sha256';
11
+ import { bytesToHex, utf8ToBytes } from '@noble/hashes/utils';
12
+ import { meetsRegistrationPowDifficulty, registrationPowMessage, REGISTRATION_POW_DIFFICULTY_BITS, } from '@oxy.so/protocol';
13
+ /**
14
+ * How many attempts the grind loop runs before yielding to the event loop.
15
+ *
16
+ * A synchronous hot loop long enough to matter (an average solve at 16 bits
17
+ * is 65,536 attempts) would otherwise block the JS thread for its entire
18
+ * duration — on a mobile app that means no touch response, no animation, for
19
+ * up to roughly a second. Yielding periodically keeps the UI responsive
20
+ * during the grind at the cost of a handful of event-loop round trips, which
21
+ * is imperceptible next to the grind itself.
22
+ */
23
+ const YIELD_EVERY_N_ATTEMPTS = 2000;
24
+ function yieldToEventLoop() {
25
+ return new Promise((resolve) => setTimeout(resolve, 0));
26
+ }
27
+ /**
28
+ * Grind a nonce for `publicKey`/`timestamp` that clears
29
+ * {@link REGISTRATION_POW_DIFFICULTY_BITS}, returned as a decimal counter.
30
+ *
31
+ * Hashes with `@noble/hashes/sha256` — pure JS, synchronous, identical output
32
+ * on web/Node/React Native with no native module and no per-attempt bridge
33
+ * crossing (see the difficulty constant's own doc for why that distinction is
34
+ * the whole point). Nonces are sequential (0, 1, 2, …) rather than random: the
35
+ * candidate space a hash function spreads solutions across is dense either
36
+ * way, so a counter finds one exactly as reliably as randomness would, without
37
+ * needing a randomness source at all.
38
+ */
39
+ export async function solveRegistrationPow(publicKey, timestamp) {
40
+ for (let nonce = 0;; nonce += 1) {
41
+ const candidate = String(nonce);
42
+ const digest = bytesToHex(sha256(utf8ToBytes(registrationPowMessage(publicKey, timestamp, candidate))));
43
+ if (meetsRegistrationPowDifficulty(digest, REGISTRATION_POW_DIFFICULTY_BITS)) {
44
+ return candidate;
45
+ }
46
+ if (nonce % YIELD_EVERY_N_ATTEMPTS === YIELD_EVERY_N_ATTEMPTS - 1) {
47
+ await yieldToEventLoop();
48
+ }
49
+ }
50
+ }
@@ -2,6 +2,7 @@ import { loginResultSchema, safeParseContract } from '@oxy.so/contracts';
2
2
  export { getCommonsApprovalBlockingReason, parseCommonsApprovalExpiresAt, } from '../utils/commonsApproval.js';
3
3
  import { OxyAuthenticationError } from '../OxyServices.errors.js';
4
4
  import { KeyManager } from '../crypto/keyManager.js';
5
+ import { solveRegistrationPow } from '../crypto/registrationPow.js';
5
6
  import { SignatureService } from '../crypto/signatureService.js';
6
7
  import { loadNodeCrypto } from '@oxy.so/protocol';
7
8
  import { logger } from '../logger/index.js';
@@ -258,10 +259,10 @@ export function OxyServicesAuthMixin(Base) {
258
259
  /**
259
260
  * Whether this process can prove what it is (ADR 0026).
260
261
  *
261
- * The check and the module that performs it are loaded LAZILY and only on a
262
- * Node host. `@oxy.so/core`'s root barrel reaches React Native and Expo, and
263
- * `server/workloadIdentity` imports `node:crypto` — a static import here
264
- * would pull it into every mobile bundle for a path a phone can never take.
262
+ * The internal package subpath selects a client implementation for native
263
+ * and browser bundlers, and the signer only for Node hosts. The runtime
264
+ * Node guard avoids calling attestation on clients; the module boundary
265
+ * keeps node:crypto out of their bundle graphs (a guard alone cannot).
265
266
  *
266
267
  * @internal
267
268
  */
@@ -269,7 +270,7 @@ export function OxyServicesAuthMixin(Base) {
269
270
  if (typeof process === 'undefined' || !process.versions?.node)
270
271
  return false;
271
272
  try {
272
- const { canAttestWorkloadIdentity } = await import('../server/workloadIdentity');
273
+ const { canAttestWorkloadIdentity } = await import('@oxy.so/core/internal/workload-identity');
273
274
  return canAttestWorkloadIdentity();
274
275
  }
275
276
  catch {
@@ -297,7 +298,7 @@ export function OxyServicesAuthMixin(Base) {
297
298
  return entry.token;
298
299
  if (entry?.pending)
299
300
  return entry.pending;
300
- const { requestWorkloadServiceToken } = await import('../server/workloadIdentity');
301
+ const { requestWorkloadServiceToken } = await import('@oxy.so/core/internal/workload-identity');
301
302
  const seeded = entry ?? {
302
303
  token: '',
303
304
  expiresAt: 0,
@@ -432,10 +433,15 @@ export function OxyServicesAuthMixin(Base) {
432
433
  */
433
434
  async register(publicKey, signature, timestamp) {
434
435
  try {
436
+ // Advisory for now (server soft-enforces — see
437
+ // `SessionController.register`), but solved unconditionally so every
438
+ // client is already sending it once the server starts requiring it.
439
+ const powNonce = await solveRegistrationPow(publicKey, timestamp);
435
440
  const res = await this.makeRequest('POST', '/auth/register', {
436
441
  publicKey,
437
442
  signature,
438
443
  timestamp,
444
+ powNonce,
439
445
  }, { cache: false, skipAuth: true });
440
446
  if (!res || (typeof res === 'object' && Object.keys(res).length === 0)) {
441
447
  throw new OxyAuthenticationError('Registration failed', 'REGISTER_FAILED', 400);
@@ -118,7 +118,7 @@ export function OxyServicesIdentityBackupMixin(Base) {
118
118
  *
119
119
  * NATIVE-ONLY persistence: `KeyManager.importKeyPair` throws on web. It also
120
120
  * refuses to clobber a DIFFERENT existing on-device identity unless
121
- * `overwrite: true` — the {@link import('../crypto/keyManager').IdentityAlreadyExistsError}
121
+ * `overwrite: true` — the {@link import('../crypto/keyManager.js').IdentityAlreadyExistsError}
122
122
  * propagates to the caller (never swallowed) so the UI can confirm before
123
123
  * overwriting.
124
124
  *
@@ -0,0 +1,7 @@
1
+ /** Workload attestation belongs to a Node host, never a browser or device. */
2
+ export function canAttestWorkloadIdentity() {
3
+ return false;
4
+ }
5
+ export async function requestWorkloadServiceToken(_options) {
6
+ throw new Error('Workload identity is only available on a Node host');
7
+ }