@oxy.so/core 1.7.0 → 1.7.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/LICENSE +675 -201
- package/NOTICE +7 -2
- package/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/crypto/registrationPow.js +53 -0
- package/dist/cjs/mixins/OxyServices.auth.js +6 -0
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/crypto/registrationPow.js +50 -0
- package/dist/esm/mixins/OxyServices.auth.js +8 -2
- package/dist/esm/mixins/OxyServices.identityBackup.js +1 -1
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/crypto/registrationPow.d.ts +22 -0
- package/package.json +2 -2
- package/src/__tests__/fixEsmImports.test.ts +132 -0
- package/src/crypto/__tests__/registrationPow.test.ts +61 -0
- package/src/crypto/registrationPow.ts +60 -0
- package/src/mixins/OxyServices.auth.ts +7 -0
|
@@ -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';
|
|
@@ -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('../server/workloadIdentity.js');
|
|
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('../server/workloadIdentity.js');
|
|
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
|
*
|