@oxy.so/protocol 1.1.4 → 1.2.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.
package/dist/esm/index.js CHANGED
@@ -37,4 +37,4 @@ export { isAuthorizedKey } from './identity/resolver.js';
37
37
  // Platform — runtime predicates + lazy crypto/storage loaders
38
38
  // ---------------------------------------------------------------------------
39
39
  export { isReactNative, isNodeJS } from './platform/platform.js';
40
- export { loadNodeCrypto, loadExpoCrypto, loadSecureStore, loadAsyncStorage, getRandomBytesRN, loadSharedIdentityBridge, } from './platform/crypto.js';
40
+ export { loadNodeCrypto, loadExpoCrypto, loadSecureStore, loadAsyncStorage, getRandomBytesRN, loadCommonsIdentityBridge, } from './platform/crypto.js';
@@ -128,13 +128,10 @@ export async function loadAsyncStorage() {
128
128
  // `@oxy.so/protocol/random` entry without evaluating any crypto library first.
129
129
  export { getRandomBytesRN } from './random.js';
130
130
  // ---------------------------------------------------------------------------
131
- // Shared identity bridge — `@oxy.so/expo-oxy-identity` (native-only).
131
+ // Commons identity bridge — the `OxyIdentity` native module (native-only).
132
132
  //
133
- // The default (web / Node) variant has no cross-app identity channel, so this
134
- // always resolves to `null`. `@oxy.so/core`'s `KeyManager` treats `null` as "no
135
- // bridge" and falls back to its package-private store — which is correct on web
136
- // (there is no shared identity there).
133
+ // Web and Node have no Commons to ask, so this always resolves to `null`.
137
134
  // ---------------------------------------------------------------------------
138
- export function loadSharedIdentityBridge() {
135
+ export function loadCommonsIdentityBridge() {
139
136
  return Promise.resolve(null);
140
137
  }
@@ -123,41 +123,71 @@ export async function loadAsyncStorage() {
123
123
  // any crypto library first. The explicit `.native` specifier keeps tsc and
124
124
  // Metro pointed at the same file.
125
125
  export { getRandomBytesRN } from './random.native.js';
126
- // ---------------------------------------------------------------------------
127
- // Shared identity bridge — `@oxy.so/expo-oxy-identity` (native-only, OPTIONAL).
128
- //
129
- // `@oxy.so/expo-oxy-identity` is the in-repo Expo module autolinked into the
130
- // identity apps (Commons + the reader RPs). We resolve its NATIVE module
131
- // directly via expo-modules-core's `requireOptionalNativeModule('OxyIdentity')`
132
- // — a static import Metro always resolves — instead of dynamically importing the
133
- // module's JS wrapper. A runtime-computed `import(moduleName)` compiled to a
134
- // `require(variable)` in the CJS build, which Metro cannot resolve in a consuming
135
- // repo (the bridge silently resolved `null` there — the cross-app SSO bug). Since
136
- // the native module is what actually holds the shared identity, going through the
137
- // native registry is both correct and Metro-safe. `requireOptionalNativeModule`
138
- // returns `null` (never throws) when the module is not autolinked (web, or apps
139
- // that don't ship it), so `@oxy.so/core`'s `KeyManager` cleanly falls back to its
140
- // package-private store.
141
- // ---------------------------------------------------------------------------
142
- let sharedIdentityBridgePromise = null;
143
- export function loadSharedIdentityBridge() {
144
- if (!sharedIdentityBridgePromise) {
145
- sharedIdentityBridgePromise = Promise.resolve().then(() => {
126
+ const LOWER_HEX = /^[0-9a-f]+$/;
127
+ function record(value) {
128
+ return typeof value === 'object' && value !== null ? value : null;
129
+ }
130
+ function hexField(value, length) {
131
+ if (typeof value !== 'string' || !LOWER_HEX.test(value))
132
+ return null;
133
+ if (length !== undefined && value.length !== length)
134
+ return null;
135
+ return value;
136
+ }
137
+ function narrowDescription(value) {
138
+ const r = record(value);
139
+ const publicKey = hexField(r?.publicKey, 130);
140
+ const v = typeof r?.v === 'number' ? r.v : Number(r?.v);
141
+ if (!publicKey || !Number.isInteger(v))
142
+ return null;
143
+ return { v, publicKey };
144
+ }
145
+ function narrowProof(value) {
146
+ const r = record(value);
147
+ const publicKey = hexField(r?.publicKey, 130);
148
+ const signature = hexField(r?.signature);
149
+ const timestamp = typeof r?.timestamp === 'number' ? r.timestamp : Number(r?.timestamp);
150
+ if (!publicKey || !signature || !Number.isSafeInteger(timestamp) || timestamp <= 0)
151
+ return null;
152
+ return { publicKey, signature, timestamp };
153
+ }
154
+ function narrowSocialReceive(value) {
155
+ const r = record(value);
156
+ const signature = hexField(r?.signature);
157
+ const publicKey = hexField(r?.publicKey, 66);
158
+ if (!signature || !publicKey)
159
+ return null;
160
+ return { signature, publicKey };
161
+ }
162
+ /** Run one native call; a rejection or an unexpected shape is `null`. */
163
+ async function ask(call, narrow) {
164
+ try {
165
+ return narrow(await call());
166
+ }
167
+ catch {
168
+ return null;
169
+ }
170
+ }
171
+ let commonsIdentityBridgePromise = null;
172
+ export function loadCommonsIdentityBridge() {
173
+ if (!commonsIdentityBridgePromise) {
174
+ commonsIdentityBridgePromise = Promise.resolve().then(() => {
146
175
  const native = requireOptionalNativeModule('OxyIdentity');
147
- if (native &&
148
- typeof native.getShared === 'function' &&
149
- typeof native.putShared === 'function' &&
150
- typeof native.hasShared === 'function' &&
151
- typeof native.clearShared === 'function') {
152
- return {
153
- getShared: native.getShared.bind(native),
154
- putShared: native.putShared.bind(native),
155
- hasShared: native.hasShared.bind(native),
156
- clearShared: native.clearShared.bind(native),
157
- };
176
+ if (!native ||
177
+ typeof native.describe !== 'function' ||
178
+ typeof native.proveIdentity !== 'function' ||
179
+ typeof native.deriveScopedSeed !== 'function' ||
180
+ typeof native.signSocialReceive !== 'function') {
181
+ return null;
158
182
  }
159
- return null;
183
+ const m = native;
184
+ return {
185
+ describe: () => ask(() => m.describe(), narrowDescription),
186
+ proveIdentity: (challenge) => ask(() => m.proveIdentity(challenge), narrowProof),
187
+ deriveScopedSeed: (info) => ask(() => m.deriveScopedSeed(info), (v) => hexField(v, 64)),
188
+ signSocialReceive: (index, digest) => ask(() => m.signSocialReceive(index, digest), narrowSocialReceive),
189
+ };
160
190
  });
161
191
  }
162
- return sharedIdentityBridgePromise;
192
+ return commonsIdentityBridgePromise;
163
193
  }
@@ -127,6 +127,29 @@ export function verifySecp256k1Digest(publicKeyHex, digestHex, signatureDerHex)
127
127
  lowS: false,
128
128
  });
129
129
  }
130
+ /**
131
+ * `(privateKey + tweak) mod n`, the scalar step of BIP32 non-hardened child
132
+ * derivation (`CKDpriv`: the tweak is `IL`, the left half of the HMAC-SHA512).
133
+ *
134
+ * Throws, as BIP32 requires, when the tweak is not below the curve order or
135
+ * the sum is zero: that child is invalid and the caller must not use it.
136
+ */
137
+ export function tweakAddSecp256k1PrivateKey(privateKeyHex, tweakHex) {
138
+ const n = secp256k1.Point.Fn.ORDER;
139
+ const key = BigInt(`0x${normalizeSecp256k1PrivateKey(privateKeyHex)}`);
140
+ if (typeof tweakHex !== "string" || tweakHex.length !== 64 || !HEX.test(tweakHex)) {
141
+ throw new Error("secp256k1 tweak must be exactly 32 bytes of hexadecimal data");
142
+ }
143
+ const tweak = BigInt(`0x${tweakHex}`);
144
+ if (tweak >= n) {
145
+ throw new Error("secp256k1 tweak is not below the curve order");
146
+ }
147
+ const child = (key + tweak) % n;
148
+ if (child === 0n) {
149
+ throw new Error("secp256k1 tweaked private key is zero");
150
+ }
151
+ return child.toString(16).padStart(64, "0");
152
+ }
130
153
  /** Derive the fixed-width 32-byte ECDH x-coordinate shared secret. */
131
154
  export function deriveSecp256k1SharedSecret(privateKeyHex, publicKeyHex) {
132
155
  const encodedPoint = secp256k1.getSharedSecret(parsePrivateKey(privateKeyHex), parsePublicKey(publicKeyHex), true);