@oxy.so/protocol 1.5.1 → 1.6.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.
@@ -25,7 +25,7 @@
25
25
  * platform hashing primitive and is trivially unit-testable with fixed
26
26
  * digests. Each side hashes with whatever SHA-256 it already has — Node's
27
27
  * `crypto` server-side (`packages/api/src/controllers/session.controller.ts`),
28
- * `@noble/hashes/sha256` client-side (`packages/core/src/crypto/registrationPow.ts`,
28
+ * `@noble/hashes/sha2.js`'s `sha256` client-side (`packages/core/src/crypto/registrationPow.ts`,
29
29
  * already a `@oxy.so/core` dependency — see `crypto/kdf.ts`,
30
30
  * `crypto/identityProof.ts`) — and both produce byte-identical digests over
31
31
  * the same message, because SHA-256 has exactly one correct output for a
@@ -101,7 +101,7 @@ function registrationPowMessage(publicKey, timestamp, nonce) {
101
101
  * requires of a solved nonce's digest.
102
102
  *
103
103
  * 16 bits ⇒ 65,536 SHA-256 attempts on average to solve. The client grinds
104
- * with `@noble/hashes/sha256` — a pure-JS, SYNCHRONOUS implementation with
104
+ * with `@noble/hashes/sha2.js`'s `sha256` — a pure-JS, SYNCHRONOUS implementation with
105
105
  * "identical behaviour on web, Node, and React Native with zero WebCrypto /
106
106
  * native-module dependency" (its own doc comment in `crypto/kdf.ts`) — so,
107
107
  * unlike a solve loop built on a platform hashing primitive that crosses a
@@ -7,11 +7,10 @@
7
7
  *
8
8
  * This module is deliberately tiny and imports NOTHING but `expo-crypto`: it
9
9
  * backs `@oxy.so/core`'s `globalThis.crypto.getRandomValues` polyfill, which
10
- * must be fully installed before any module that captures `globalThis.crypto`
11
- * at evaluation time (`@noble/hashes` 1.x's `crypto.js` does exactly that) is
12
- * evaluated. Reaching a crypto library from here would let that library
13
- * capture the missing global first and throw
14
- * `crypto.getRandomValues must be defined` for the lifetime of the app.
10
+ * must be fully installed before any module that reads `globalThis.crypto`
11
+ * at evaluation time is evaluated (`@noble/hashes` 1.x's `crypto.js` did, and
12
+ * threw `crypto.getRandomValues must be defined` for the lifetime of the app).
13
+ * Reaching a crypto library from here would evaluate it before the shim.
15
14
  *
16
15
  * `expo-crypto` is an OPTIONAL peer, so it is resolved with a string-literal
17
16
  * `require` inside a `try` (Metro's optional-dependency form; see
@@ -3,13 +3,12 @@
3
3
  * `@oxy.so/protocol/random` — the platform randomness source, and nothing else.
4
4
  *
5
5
  * `@oxy.so/core`'s crypto polyfill installs `globalThis.crypto.getRandomValues`
6
- * on hosts that lack it (React Native / Hermes). `@noble/hashes` 1.x captures
7
- * `globalThis.crypto` ONCE, when its `crypto.js` is evaluated, so the polyfill
8
- * has to be installed before any `@noble/*` module is evaluated. ES imports are
6
+ * on hosts that lack it (React Native / Hermes), and has to be installed before
7
+ * any module that reads `globalThis.crypto` is evaluated. ES imports are
9
8
  * evaluated before the importing module's body, so whatever the polyfill
10
9
  * imports is evaluated first: importing the ROOT entry (which reaches
11
- * `@noble/curves` through the envelope signer) let noble capture `undefined`
12
- * and broke identity creation on Android.
10
+ * `@noble/curves` through the envelope signer) let `@noble/hashes` 1.x capture
11
+ * `undefined` at evaluation and broke identity creation on Android.
13
12
  *
14
13
  * This entry therefore reaches no crypto library, no `@oxy.so/*` package and no
15
14
  * third-party module other than the optional `expo-crypto` peer (RN variant
@@ -121,10 +121,13 @@ function normalizeSecp256k1PublicKey(publicKeyHex, compressed = false) {
121
121
  * accepts both forms; callers that require low-S normalization opt in explicitly.
122
122
  */
123
123
  function signSecp256k1Digest(privateKeyHex, digestHex, options = {}) {
124
+ // The input is already a digest: `prehash: false` keeps noble from hashing it again.
124
125
  const signature = secp256k1_js_1.secp256k1.sign(parseDigest(digestHex), parsePrivateKey(privateKeyHex), {
126
+ prehash: false,
125
127
  lowS: options.lowS ?? false,
128
+ format: 'der',
126
129
  });
127
- return signature.toHex('der');
130
+ return bytesToHex(signature);
128
131
  }
129
132
  /** Verify a DER-encoded ECDSA signature, accepting historical high-S signatures. */
130
133
  function verifySecp256k1Digest(publicKeyHex, digestHex, signatureDerHex) {
@@ -134,6 +137,7 @@ function verifySecp256k1Digest(publicKeyHex, digestHex, signatureDerHex) {
134
137
  // Parse once up front so malformed or non-DER input is rejected explicitly.
135
138
  secp256k1_js_1.secp256k1.Signature.fromBytes(signature, 'der');
136
139
  return secp256k1_js_1.secp256k1.verify(signature, digest, publicKey, {
140
+ prehash: false,
137
141
  format: 'der',
138
142
  lowS: false,
139
143
  });
@@ -24,7 +24,7 @@
24
24
  * platform hashing primitive and is trivially unit-testable with fixed
25
25
  * digests. Each side hashes with whatever SHA-256 it already has — Node's
26
26
  * `crypto` server-side (`packages/api/src/controllers/session.controller.ts`),
27
- * `@noble/hashes/sha256` client-side (`packages/core/src/crypto/registrationPow.ts`,
27
+ * `@noble/hashes/sha2.js`'s `sha256` client-side (`packages/core/src/crypto/registrationPow.ts`,
28
28
  * already a `@oxy.so/core` dependency — see `crypto/kdf.ts`,
29
29
  * `crypto/identityProof.ts`) — and both produce byte-identical digests over
30
30
  * the same message, because SHA-256 has exactly one correct output for a
@@ -95,7 +95,7 @@ export function registrationPowMessage(publicKey, timestamp, nonce) {
95
95
  * requires of a solved nonce's digest.
96
96
  *
97
97
  * 16 bits ⇒ 65,536 SHA-256 attempts on average to solve. The client grinds
98
- * with `@noble/hashes/sha256` — a pure-JS, SYNCHRONOUS implementation with
98
+ * with `@noble/hashes/sha2.js`'s `sha256` — a pure-JS, SYNCHRONOUS implementation with
99
99
  * "identical behaviour on web, Node, and React Native with zero WebCrypto /
100
100
  * native-module dependency" (its own doc comment in `crypto/kdf.ts`) — so,
101
101
  * unlike a solve loop built on a platform hashing primitive that crosses a
@@ -6,11 +6,10 @@
6
6
  *
7
7
  * This module is deliberately tiny and imports NOTHING but `expo-crypto`: it
8
8
  * backs `@oxy.so/core`'s `globalThis.crypto.getRandomValues` polyfill, which
9
- * must be fully installed before any module that captures `globalThis.crypto`
10
- * at evaluation time (`@noble/hashes` 1.x's `crypto.js` does exactly that) is
11
- * evaluated. Reaching a crypto library from here would let that library
12
- * capture the missing global first and throw
13
- * `crypto.getRandomValues must be defined` for the lifetime of the app.
9
+ * must be fully installed before any module that reads `globalThis.crypto`
10
+ * at evaluation time is evaluated (`@noble/hashes` 1.x's `crypto.js` did, and
11
+ * threw `crypto.getRandomValues must be defined` for the lifetime of the app).
12
+ * Reaching a crypto library from here would evaluate it before the shim.
14
13
  *
15
14
  * `expo-crypto` is an OPTIONAL peer, so it is resolved with a string-literal
16
15
  * `require` inside a `try` (Metro's optional-dependency form; see
@@ -2,13 +2,12 @@
2
2
  * `@oxy.so/protocol/random` — the platform randomness source, and nothing else.
3
3
  *
4
4
  * `@oxy.so/core`'s crypto polyfill installs `globalThis.crypto.getRandomValues`
5
- * on hosts that lack it (React Native / Hermes). `@noble/hashes` 1.x captures
6
- * `globalThis.crypto` ONCE, when its `crypto.js` is evaluated, so the polyfill
7
- * has to be installed before any `@noble/*` module is evaluated. ES imports are
5
+ * on hosts that lack it (React Native / Hermes), and has to be installed before
6
+ * any module that reads `globalThis.crypto` is evaluated. ES imports are
8
7
  * evaluated before the importing module's body, so whatever the polyfill
9
8
  * imports is evaluated first: importing the ROOT entry (which reaches
10
- * `@noble/curves` through the envelope signer) let noble capture `undefined`
11
- * and broke identity creation on Android.
9
+ * `@noble/curves` through the envelope signer) let `@noble/hashes` 1.x capture
10
+ * `undefined` at evaluation and broke identity creation on Android.
12
11
  *
13
12
  * This entry therefore reaches no crypto library, no `@oxy.so/*` package and no
14
13
  * third-party module other than the optional `expo-crypto` peer (RN variant
@@ -109,10 +109,13 @@ export function normalizeSecp256k1PublicKey(publicKeyHex, compressed = false) {
109
109
  * accepts both forms; callers that require low-S normalization opt in explicitly.
110
110
  */
111
111
  export function signSecp256k1Digest(privateKeyHex, digestHex, options = {}) {
112
+ // The input is already a digest: `prehash: false` keeps noble from hashing it again.
112
113
  const signature = secp256k1.sign(parseDigest(digestHex), parsePrivateKey(privateKeyHex), {
114
+ prehash: false,
113
115
  lowS: options.lowS ?? false,
116
+ format: 'der',
114
117
  });
115
- return signature.toHex('der');
118
+ return bytesToHex(signature);
116
119
  }
117
120
  /** Verify a DER-encoded ECDSA signature, accepting historical high-S signatures. */
118
121
  export function verifySecp256k1Digest(publicKeyHex, digestHex, signatureDerHex) {
@@ -122,6 +125,7 @@ export function verifySecp256k1Digest(publicKeyHex, digestHex, signatureDerHex)
122
125
  // Parse once up front so malformed or non-DER input is rejected explicitly.
123
126
  secp256k1.Signature.fromBytes(signature, 'der');
124
127
  return secp256k1.verify(signature, digest, publicKey, {
128
+ prehash: false,
125
129
  format: 'der',
126
130
  lowS: false,
127
131
  });
@@ -24,7 +24,7 @@
24
24
  * platform hashing primitive and is trivially unit-testable with fixed
25
25
  * digests. Each side hashes with whatever SHA-256 it already has — Node's
26
26
  * `crypto` server-side (`packages/api/src/controllers/session.controller.ts`),
27
- * `@noble/hashes/sha256` client-side (`packages/core/src/crypto/registrationPow.ts`,
27
+ * `@noble/hashes/sha2.js`'s `sha256` client-side (`packages/core/src/crypto/registrationPow.ts`,
28
28
  * already a `@oxy.so/core` dependency — see `crypto/kdf.ts`,
29
29
  * `crypto/identityProof.ts`) — and both produce byte-identical digests over
30
30
  * the same message, because SHA-256 has exactly one correct output for a
@@ -67,7 +67,7 @@ export declare function registrationPowMessage(publicKey: string, timestamp: num
67
67
  * requires of a solved nonce's digest.
68
68
  *
69
69
  * 16 bits ⇒ 65,536 SHA-256 attempts on average to solve. The client grinds
70
- * with `@noble/hashes/sha256` — a pure-JS, SYNCHRONOUS implementation with
70
+ * with `@noble/hashes/sha2.js`'s `sha256` — a pure-JS, SYNCHRONOUS implementation with
71
71
  * "identical behaviour on web, Node, and React Native with zero WebCrypto /
72
72
  * native-module dependency" (its own doc comment in `crypto/kdf.ts`) — so,
73
73
  * unlike a solve loop built on a platform hashing primitive that crosses a
@@ -6,11 +6,10 @@
6
6
  *
7
7
  * This module is deliberately tiny and imports NOTHING but `expo-crypto`: it
8
8
  * backs `@oxy.so/core`'s `globalThis.crypto.getRandomValues` polyfill, which
9
- * must be fully installed before any module that captures `globalThis.crypto`
10
- * at evaluation time (`@noble/hashes` 1.x's `crypto.js` does exactly that) is
11
- * evaluated. Reaching a crypto library from here would let that library
12
- * capture the missing global first and throw
13
- * `crypto.getRandomValues must be defined` for the lifetime of the app.
9
+ * must be fully installed before any module that reads `globalThis.crypto`
10
+ * at evaluation time is evaluated (`@noble/hashes` 1.x's `crypto.js` did, and
11
+ * threw `crypto.getRandomValues must be defined` for the lifetime of the app).
12
+ * Reaching a crypto library from here would evaluate it before the shim.
14
13
  *
15
14
  * `expo-crypto` is an OPTIONAL peer, so it is resolved with a string-literal
16
15
  * `require` inside a `try` (Metro's optional-dependency form; see
@@ -2,13 +2,12 @@
2
2
  * `@oxy.so/protocol/random` — the platform randomness source, and nothing else.
3
3
  *
4
4
  * `@oxy.so/core`'s crypto polyfill installs `globalThis.crypto.getRandomValues`
5
- * on hosts that lack it (React Native / Hermes). `@noble/hashes` 1.x captures
6
- * `globalThis.crypto` ONCE, when its `crypto.js` is evaluated, so the polyfill
7
- * has to be installed before any `@noble/*` module is evaluated. ES imports are
5
+ * on hosts that lack it (React Native / Hermes), and has to be installed before
6
+ * any module that reads `globalThis.crypto` is evaluated. ES imports are
8
7
  * evaluated before the importing module's body, so whatever the polyfill
9
8
  * imports is evaluated first: importing the ROOT entry (which reaches
10
- * `@noble/curves` through the envelope signer) let noble capture `undefined`
11
- * and broke identity creation on Android.
9
+ * `@noble/curves` through the envelope signer) let `@noble/hashes` 1.x capture
10
+ * `undefined` at evaluation and broke identity creation on Android.
12
11
  *
13
12
  * This entry therefore reaches no crypto library, no `@oxy.so/*` package and no
14
13
  * third-party module other than the optional `expo-crypto` peer (RN variant
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxy.so/protocol",
3
- "version": "1.5.1",
3
+ "version": "1.6.0",
4
4
  "description": "Oxy Protocol — the app-agnostic base substrate: signed-record envelope, canonical JSON, signature/verification, and platform crypto. Reused by any Oxy app to decentralize its own content.",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -98,8 +98,10 @@
98
98
  "NOTICE",
99
99
  "dist",
100
100
  "!dist/**/*.tsbuildinfo",
101
- "src"
101
+ "src",
102
+ "!dist/.oxy-source-digest"
102
103
  ],
104
+ "oxySourceStamp": "dist",
103
105
  "keywords": [
104
106
  "oxyhq",
105
107
  "protocol",
@@ -117,10 +119,10 @@
117
119
  "license": "SEE LICENSE IN LICENSE",
118
120
  "homepage": "https://oxy.so",
119
121
  "engines": {
120
- "node": ">=18.0.0"
122
+ "node": "^20.19.0 || >=22.12.0"
121
123
  },
122
124
  "scripts": {
123
- "build": "bun run build:cjs && bun run build:esm && bun run build:types",
125
+ "build": "bun run build:cjs && bun run build:esm && bun run build:types && node -e \"const p='../app-preset/metro/source-stamp.js';require('node:fs').existsSync(p)&&require(p).writeStamp(process.cwd())\"",
124
126
  "build:cjs": "tsc -p tsconfig.cjs.json",
125
127
  "build:esm": "tsc -p tsconfig.esm.json && node scripts/fix-esm-imports.mjs",
126
128
  "build:types": "tsc -p tsconfig.types.json",
@@ -130,11 +132,11 @@
130
132
  "lint": "biome lint --error-on-warnings ./src"
131
133
  },
132
134
  "dependencies": {
133
- "@noble/curves": "^1.9.7",
134
- "@oxy.so/contracts": "^6.0.0"
135
+ "@noble/curves": "^2.4.0",
136
+ "@oxy.so/contracts": "^6.5.0"
135
137
  },
136
138
  "peerDependencies": {
137
- "@react-native-async-storage/async-storage": "<3.0.0",
139
+ "@react-native-async-storage/async-storage": "<4.0.0",
138
140
  "expo-crypto": "<59.0.0",
139
141
  "expo-secure-store": "<59.0.0",
140
142
  "express": "^4.18.0 || ^5.0.0",
@@ -155,12 +157,12 @@
155
157
  }
156
158
  },
157
159
  "devDependencies": {
158
- "@react-native-async-storage/async-storage": "^2.2.0",
160
+ "@react-native-async-storage/async-storage": "^3.1.1",
159
161
  "@types/express": "^4.17.25",
160
162
  "@types/node": "^22.20.1",
161
163
  "@types/supertest": "^6.0.2",
162
164
  "expo-crypto": "~56.0.3",
163
- "expo-modules-core": "~58.0.22",
165
+ "expo-modules-core": "~58.0.26",
164
166
  "expo-secure-store": "~57.0.4",
165
167
  "express": "^4.22.2",
166
168
  "jest": "^30.5.1",
@@ -4,11 +4,10 @@
4
4
  *
5
5
  * `@oxy.so/core`'s crypto polyfill imports this entry to install
6
6
  * `globalThis.crypto.getRandomValues` on React Native. Everything the polyfill
7
- * imports is evaluated BEFORE the polyfill body, and `@noble/hashes` 1.x
8
- * captures `globalThis.crypto` once, at evaluation. When the polyfill imported
9
- * the root entry instead, noble was reached through the envelope signer,
10
- * captured `undefined`, and every Android identity creation failed with
11
- * `crypto.getRandomValues must be defined`. This guard keeps the entry's graph
7
+ * imports is evaluated BEFORE the polyfill body. When the polyfill imported
8
+ * the root entry instead, `@noble/hashes` 1.x was reached through the envelope
9
+ * signer, captured an absent `globalThis.crypto` at evaluation, and every
10
+ * Android identity creation failed with `crypto.getRandomValues must be defined`. This guard keeps the entry's graph
12
11
  * dependency-free, including the `.native` siblings Metro substitutes.
13
12
  */
14
13
 
@@ -24,7 +24,7 @@
24
24
  * platform hashing primitive and is trivially unit-testable with fixed
25
25
  * digests. Each side hashes with whatever SHA-256 it already has — Node's
26
26
  * `crypto` server-side (`packages/api/src/controllers/session.controller.ts`),
27
- * `@noble/hashes/sha256` client-side (`packages/core/src/crypto/registrationPow.ts`,
27
+ * `@noble/hashes/sha2.js`'s `sha256` client-side (`packages/core/src/crypto/registrationPow.ts`,
28
28
  * already a `@oxy.so/core` dependency — see `crypto/kdf.ts`,
29
29
  * `crypto/identityProof.ts`) — and both produce byte-identical digests over
30
30
  * the same message, because SHA-256 has exactly one correct output for a
@@ -102,7 +102,7 @@ export function registrationPowMessage(
102
102
  * requires of a solved nonce's digest.
103
103
  *
104
104
  * 16 bits ⇒ 65,536 SHA-256 attempts on average to solve. The client grinds
105
- * with `@noble/hashes/sha256` — a pure-JS, SYNCHRONOUS implementation with
105
+ * with `@noble/hashes/sha2.js`'s `sha256` — a pure-JS, SYNCHRONOUS implementation with
106
106
  * "identical behaviour on web, Node, and React Native with zero WebCrypto /
107
107
  * native-module dependency" (its own doc comment in `crypto/kdf.ts`) — so,
108
108
  * unlike a solve loop built on a platform hashing primitive that crosses a
@@ -6,11 +6,10 @@
6
6
  *
7
7
  * This module is deliberately tiny and imports NOTHING but `expo-crypto`: it
8
8
  * backs `@oxy.so/core`'s `globalThis.crypto.getRandomValues` polyfill, which
9
- * must be fully installed before any module that captures `globalThis.crypto`
10
- * at evaluation time (`@noble/hashes` 1.x's `crypto.js` does exactly that) is
11
- * evaluated. Reaching a crypto library from here would let that library
12
- * capture the missing global first and throw
13
- * `crypto.getRandomValues must be defined` for the lifetime of the app.
9
+ * must be fully installed before any module that reads `globalThis.crypto`
10
+ * at evaluation time is evaluated (`@noble/hashes` 1.x's `crypto.js` did, and
11
+ * threw `crypto.getRandomValues must be defined` for the lifetime of the app).
12
+ * Reaching a crypto library from here would evaluate it before the shim.
14
13
  *
15
14
  * `expo-crypto` is an OPTIONAL peer, so it is resolved with a string-literal
16
15
  * `require` inside a `try` (Metro's optional-dependency form; see
package/src/random.ts CHANGED
@@ -2,13 +2,12 @@
2
2
  * `@oxy.so/protocol/random` — the platform randomness source, and nothing else.
3
3
  *
4
4
  * `@oxy.so/core`'s crypto polyfill installs `globalThis.crypto.getRandomValues`
5
- * on hosts that lack it (React Native / Hermes). `@noble/hashes` 1.x captures
6
- * `globalThis.crypto` ONCE, when its `crypto.js` is evaluated, so the polyfill
7
- * has to be installed before any `@noble/*` module is evaluated. ES imports are
5
+ * on hosts that lack it (React Native / Hermes), and has to be installed before
6
+ * any module that reads `globalThis.crypto` is evaluated. ES imports are
8
7
  * evaluated before the importing module's body, so whatever the polyfill
9
8
  * imports is evaluated first: importing the ROOT entry (which reaches
10
- * `@noble/curves` through the envelope signer) let noble capture `undefined`
11
- * and broke identity creation on Android.
9
+ * `@noble/curves` through the envelope signer) let `@noble/hashes` 1.x capture
10
+ * `undefined` at evaluation and broke identity creation on Android.
12
11
  *
13
12
  * This entry therefore reaches no crypto library, no `@oxy.so/*` package and no
14
13
  * third-party module other than the optional `expo-crypto` peer (RN variant
package/src/secp256k1.ts CHANGED
@@ -144,10 +144,13 @@ export function signSecp256k1Digest(
144
144
  digestHex: string,
145
145
  options: Secp256k1SignatureOptions = {},
146
146
  ): string {
147
+ // The input is already a digest: `prehash: false` keeps noble from hashing it again.
147
148
  const signature = secp256k1.sign(parseDigest(digestHex), parsePrivateKey(privateKeyHex), {
149
+ prehash: false,
148
150
  lowS: options.lowS ?? false,
151
+ format: 'der',
149
152
  });
150
- return signature.toHex('der');
153
+ return bytesToHex(signature);
151
154
  }
152
155
 
153
156
  /** Verify a DER-encoded ECDSA signature, accepting historical high-S signatures. */
@@ -163,6 +166,7 @@ export function verifySecp256k1Digest(
163
166
  // Parse once up front so malformed or non-DER input is rejected explicitly.
164
167
  secp256k1.Signature.fromBytes(signature, 'der');
165
168
  return secp256k1.verify(signature, digest, publicKey, {
169
+ prehash: false,
166
170
  format: 'der',
167
171
  lowS: false,
168
172
  });