@oxy.so/protocol 1.2.2 → 1.3.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.
Files changed (58) hide show
  1. package/dist/cjs/chain/engine.js +1 -1
  2. package/dist/cjs/chain/recordStore.js +3 -3
  3. package/dist/cjs/chain/verify.js +8 -4
  4. package/dist/cjs/envelope/signingInput.js +12 -1
  5. package/dist/cjs/identity/resolver.js +2 -1
  6. package/dist/cjs/node/didWebResolver.js +1 -3
  7. package/dist/cjs/node/nodeApp.js +9 -3
  8. package/dist/cjs/node/nodeClient.js +7 -2
  9. package/dist/cjs/platform/platform.js +1 -1
  10. package/dist/cjs/secp256k1.js +25 -26
  11. package/dist/cjs/sign.js +17 -0
  12. package/dist/esm/chain/engine.js +1 -1
  13. package/dist/esm/chain/recordStore.js +3 -3
  14. package/dist/esm/chain/verify.js +8 -4
  15. package/dist/esm/envelope/sign.js +1 -1
  16. package/dist/esm/envelope/signingInput.js +12 -1
  17. package/dist/esm/identity/resolver.js +2 -1
  18. package/dist/esm/node/didWebResolver.js +1 -3
  19. package/dist/esm/node/nodeApp.js +9 -3
  20. package/dist/esm/node/nodeClient.js +8 -3
  21. package/dist/esm/platform/platform.js +1 -1
  22. package/dist/esm/secp256k1.js +26 -27
  23. package/dist/esm/sign.js +10 -0
  24. package/dist/types/chain/engine.d.ts +1 -1
  25. package/dist/types/chain/recordStore.d.ts +3 -3
  26. package/dist/types/chain/verify.d.ts +3 -2
  27. package/dist/types/sign.d.ts +10 -0
  28. package/package.json +21 -22
  29. package/src/__tests__/canonicalJson.test.ts +2 -6
  30. package/src/__tests__/chain.test.ts +42 -11
  31. package/src/__tests__/commonsIdentityBridge.test.ts +7 -2
  32. package/src/__tests__/didWebResolver.test.ts +36 -7
  33. package/src/__tests__/nodeApp.test.ts +76 -21
  34. package/src/__tests__/nodeClient.test.ts +30 -7
  35. package/src/__tests__/nodeHarness.ts +7 -3
  36. package/src/__tests__/optionalNativePeers.test.ts +8 -6
  37. package/src/__tests__/rateLimit.test.ts +2 -1
  38. package/src/__tests__/registrationPow.test.ts +35 -37
  39. package/src/__tests__/secp256k1.test.ts +41 -70
  40. package/src/__tests__/transparency.test.ts +4 -1
  41. package/src/auth/registrationPow.ts +5 -1
  42. package/src/chain/engine.ts +1 -1
  43. package/src/chain/recordStore.ts +3 -3
  44. package/src/chain/verify.ts +11 -4
  45. package/src/envelope/recordId.ts +1 -4
  46. package/src/envelope/sign.ts +6 -6
  47. package/src/envelope/signingInput.ts +12 -1
  48. package/src/identity/resolver.ts +9 -2
  49. package/src/node/didWebResolver.ts +2 -7
  50. package/src/node/nodeApp.ts +85 -67
  51. package/src/node/nodeClient.ts +35 -16
  52. package/src/platform/crypto.native.ts +9 -2
  53. package/src/platform/platform.ts +3 -1
  54. package/src/secp256k1.ts +33 -58
  55. package/src/sign.ts +16 -0
  56. package/dist/cjs/.tsbuildinfo +0 -1
  57. package/dist/esm/.tsbuildinfo +0 -1
  58. package/dist/types/.tsbuildinfo +0 -1
@@ -10,7 +10,8 @@
10
10
  * - the {@link RecordStore} owns the monotonicity frontier + the chain head.
11
11
  *
12
12
  * The ordered checks (first failure wins):
13
- * 1. **shape** — the base `signedRecordEnvelopeSchema` (open `type`, opaque
13
+ * 1. **shape** — the base `signedRecordEnvelopeCheck`, the Zod-free twin of
14
+ * `signedRecordEnvelopeSchema` (open `type`, opaque
14
15
  * `record`). An app re-narrows `type` to its own set in its adapter.
15
16
  * 2. **signature** — `verifyEnvelopeSignature` recomputes the canonical signing
16
17
  * input and checks the secp256k1 signature against the embedded `publicKey`.
@@ -25,7 +26,8 @@
25
26
  * caller makes before invoking the engine, not a property of the envelope.
26
27
  */
27
28
 
28
- import { signedRecordEnvelopeSchema, type SignedRecordEnvelope } from '@oxy.so/contracts';
29
+ import type { SignedRecordEnvelope } from '@oxy.so/contracts';
30
+ import { checkContract, signedRecordEnvelopeCheck } from '@oxy.so/contracts/client';
29
31
  import { verifyEnvelopeSignature } from '../envelope/sign';
30
32
  import { isAuthorizedKey, type VerificationMethodResolver } from '../identity/resolver';
31
33
  import { checkContinuity } from './continuity';
@@ -56,7 +58,7 @@ export async function verifyEnvelope(
56
58
  ): Promise<VerifyOutcome> {
57
59
  // 1. Base envelope shape (open `type`, `record` opaque). An app's adapter
58
60
  // re-narrows `type` to its own accepted set before/around this call.
59
- if (!signedRecordEnvelopeSchema.safeParse(env).success) {
61
+ if (checkContract(signedRecordEnvelopeCheck, env) === null) {
60
62
  return { ok: false, reason: 'invalid_envelope' };
61
63
  }
62
64
 
@@ -67,7 +69,12 @@ export async function verifyEnvelope(
67
69
  }
68
70
 
69
71
  // Enforce that v2 envelopes strictly contain required chain fields.
70
- if (env.version === 2 && (typeof env.seq !== 'number' || typeof env.collection !== 'string' || typeof env.rkey !== 'string')) {
72
+ if (
73
+ env.version === 2 &&
74
+ (typeof env.seq !== 'number' ||
75
+ typeof env.collection !== 'string' ||
76
+ typeof env.rkey !== 'string')
77
+ ) {
71
78
  return { ok: false, reason: 'invalid_envelope' };
72
79
  }
73
80
 
@@ -23,10 +23,7 @@ export async function sha256(message: string): Promise<string> {
23
23
  // In React Native, use expo-crypto
24
24
  if (isReactNative()) {
25
25
  const Crypto = await loadExpoCrypto();
26
- return Crypto.digestStringAsync(
27
- Crypto.CryptoDigestAlgorithm.SHA256,
28
- message,
29
- );
26
+ return Crypto.digestStringAsync(Crypto.CryptoDigestAlgorithm.SHA256, message);
30
27
  }
31
28
 
32
29
  if (isNodeJS()) {
@@ -10,11 +10,7 @@
10
10
  import type { SignedRecordEnvelope } from '@oxy.so/contracts';
11
11
  import { signedRecordSigningInput, type SignedRecordSigningFields } from './signingInput';
12
12
  import { sha256 } from './recordId';
13
- import {
14
- deriveSecp256k1PublicKey,
15
- signSecp256k1Digest,
16
- verifySecp256k1Digest,
17
- } from '../secp256k1';
13
+ import { deriveSecp256k1PublicKey, signSecp256k1Digest, verifySecp256k1Digest } from '../secp256k1';
18
14
 
19
15
  /** The one signature algorithm identifier the protocol emits. */
20
16
  const ALG = 'ES256K-DER-SHA256' as const;
@@ -82,5 +78,9 @@ export async function signEnvelope(
82
78
  * server / node owner check.
83
79
  */
84
80
  export async function verifyEnvelopeSignature(envelope: SignedRecordEnvelope): Promise<boolean> {
85
- return verifySignature(signedRecordSigningInput(envelope), envelope.signature, envelope.publicKey);
81
+ return verifySignature(
82
+ signedRecordSigningInput(envelope),
83
+ envelope.signature,
84
+ envelope.publicKey,
85
+ );
86
86
  }
@@ -42,7 +42,18 @@ export function signedRecordSigningInput(fields: SignedRecordSigningFields): str
42
42
  const { version, type, subject, issuer, record, issuedAt } = fields;
43
43
  if (version === 2) {
44
44
  const { seq, prev, collection, rkey } = fields;
45
- return canonicalize({ version, type, subject, issuer, record, issuedAt, seq, prev, collection, rkey });
45
+ return canonicalize({
46
+ version,
47
+ type,
48
+ subject,
49
+ issuer,
50
+ record,
51
+ issuedAt,
52
+ seq,
53
+ prev,
54
+ collection,
55
+ rkey,
56
+ });
46
57
  }
47
58
  return canonicalize({ version, type, subject, issuer, record, issuedAt });
48
59
  }
@@ -50,7 +50,13 @@ export interface VerificationMethodResolver {
50
50
  /** Verdict of the key-authorization decision (a subset of {@link RejectionReason}). */
51
51
  export type KeyAuthorization =
52
52
  | { ok: true }
53
- | { ok: false; reason: Extract<RejectionReason, 'public_key_not_a_current_verification_method' | 'untrusted_issuer'> };
53
+ | {
54
+ ok: false;
55
+ reason: Extract<
56
+ RejectionReason,
57
+ 'public_key_not_a_current_verification_method' | 'untrusted_issuer'
58
+ >;
59
+ };
54
60
 
55
61
  /**
56
62
  * Decide whether `env`'s signing key (`env.publicKey`) is authorized for its
@@ -81,7 +87,8 @@ export function isAuthorizedKey(
81
87
  resolved.custodialIssuer !== undefined &&
82
88
  env.issuer === resolved.custodialIssuer
83
89
  ) {
84
- return resolved.custodialPublicKey !== undefined && env.publicKey === resolved.custodialPublicKey
90
+ return resolved.custodialPublicKey !== undefined &&
91
+ env.publicKey === resolved.custodialPublicKey
85
92
  ? { ok: true }
86
93
  : { ok: false, reason: 'public_key_not_a_current_verification_method' };
87
94
  }
@@ -22,10 +22,7 @@ import {
22
22
  type DidDocument,
23
23
  type Secp256k1VerificationMethod,
24
24
  } from '@oxy.so/contracts';
25
- import type {
26
- ResolvedVerificationMethods,
27
- VerificationMethodResolver,
28
- } from '../identity/resolver';
25
+ import type { ResolvedVerificationMethods, VerificationMethodResolver } from '../identity/resolver';
29
26
  import { type NodeFetch, readBoundedJson } from './httpFetch';
30
27
  import {
31
28
  DEFAULT_CLIENT_MAX_REDIRECTS,
@@ -95,9 +92,7 @@ function isSecp256k1Vm(
95
92
  */
96
93
  function collectCurrentPublicKeys(doc: DidDocument): string[] {
97
94
  const byId = new Map(
98
- doc.verificationMethod
99
- .filter(isSecp256k1Vm)
100
- .map((vm) => [vm.id, vm.publicKeyHex] as const),
95
+ doc.verificationMethod.filter(isSecp256k1Vm).map((vm) => [vm.id, vm.publicKeyHex] as const),
101
96
  );
102
97
  const keys: string[] = [];
103
98
  for (const id of doc.assertionMethod) {
@@ -261,7 +261,10 @@ export function createNodeApp(deps: NodeAppDependencies): NodeApp {
261
261
  mode: config.mode,
262
262
  version: config.protocolId,
263
263
  serviceType: config.serviceType,
264
- head: head && head.headRecordId !== null ? { seq: head.seq, headRecordId: head.headRecordId } : null,
264
+ head:
265
+ head && head.headRecordId !== null
266
+ ? { seq: head.seq, headRecordId: head.headRecordId }
267
+ : null,
265
268
  });
266
269
  } catch (error) {
267
270
  next(error);
@@ -288,7 +291,10 @@ export function createNodeApp(deps: NodeAppDependencies): NodeApp {
288
291
  const since = firstQueryValue(req.query.since);
289
292
  const limit = parseLimit(req.query.limit);
290
293
  const head = await store.getHead(subject);
291
- const headWire = head && head.headRecordId !== null ? { seq: head.seq, headRecordId: head.headRecordId } : null;
294
+ const headWire =
295
+ head && head.headRecordId !== null
296
+ ? { seq: head.seq, headRecordId: head.headRecordId }
297
+ : null;
292
298
 
293
299
  const sinceSeq = await resolveSinceSeq(store, subject, since);
294
300
  if (sinceSeq === null) {
@@ -305,80 +311,92 @@ export function createNodeApp(deps: NodeAppDependencies): NodeApp {
305
311
  });
306
312
 
307
313
  // ── Owner write: a single signed envelope ────────────────────────────────────
308
- app.post(NODE_RECORDS_PATH, writeRateLimit, jsonParser, async (req: Request, res: Response, next: NextFunction) => {
309
- try {
310
- const verification = await verifyNodeRecordEnvelope(req.body);
311
- if (!verification.ok) {
312
- res.status(400).json({ error: verification.reason });
313
- return;
314
- }
315
- if (!ownerAuth.isOwnerKey(verification.envelope.publicKey)) {
316
- res.status(403).json({ error: 'not_owner' });
317
- return;
318
- }
319
- if (!isCollectionAllowed(config, verification.envelope.collection ?? '')) {
320
- res.status(403).json({ error: 'foreign_collection' });
321
- return;
322
- }
323
- const outcome = await store.append(subject, verification.envelope, verification.recordId);
324
- if (!outcome.ok) {
325
- res.status(appendStatus(outcome.reason)).json({ error: outcome.reason });
326
- return;
327
- }
328
- res.status(201).json({ recordId: outcome.recordId, seq: outcome.seq });
329
- } catch (error) {
330
- next(error);
331
- }
332
- });
333
-
334
- // ── Owner write: a batch push (verified + appended in order) ──────────────────
335
- app.post(NODE_SYNC_PUSH_PATH, writeRateLimit, jsonParser, async (req: Request, res: Response, next: NextFunction) => {
336
- try {
337
- const body: unknown = req.body;
338
- const items =
339
- typeof body === 'object' && body !== null && Array.isArray((body as { records?: unknown }).records)
340
- ? (body as { records: unknown[] }).records
341
- : null;
342
- if (!items) {
343
- res.status(400).json({ error: 'invalid_batch' });
344
- return;
345
- }
346
- if (items.length > MAX_SYNC_BATCH) {
347
- res.status(400).json({ error: 'batch_too_large' });
348
- return;
349
- }
350
-
351
- const results: Array<
352
- { ok: true; recordId: string; seq: number } | { ok: false; reason: string }
353
- > = [];
354
- for (const item of items) {
355
- const verification = await verifyNodeRecordEnvelope(item);
314
+ app.post(
315
+ NODE_RECORDS_PATH,
316
+ writeRateLimit,
317
+ jsonParser,
318
+ async (req: Request, res: Response, next: NextFunction) => {
319
+ try {
320
+ const verification = await verifyNodeRecordEnvelope(req.body);
356
321
  if (!verification.ok) {
357
- results.push({ ok: false, reason: verification.reason });
358
- continue;
322
+ res.status(400).json({ error: verification.reason });
323
+ return;
359
324
  }
360
325
  if (!ownerAuth.isOwnerKey(verification.envelope.publicKey)) {
361
- results.push({ ok: false, reason: 'not_owner' });
362
- continue;
326
+ res.status(403).json({ error: 'not_owner' });
327
+ return;
363
328
  }
364
329
  if (!isCollectionAllowed(config, verification.envelope.collection ?? '')) {
365
- results.push({ ok: false, reason: 'foreign_collection' });
366
- continue;
330
+ res.status(403).json({ error: 'foreign_collection' });
331
+ return;
367
332
  }
368
333
  const outcome = await store.append(subject, verification.envelope, verification.recordId);
369
- results.push(
370
- outcome.ok
371
- ? { ok: true, recordId: outcome.recordId, seq: outcome.seq }
372
- : { ok: false, reason: outcome.reason },
373
- );
334
+ if (!outcome.ok) {
335
+ res.status(appendStatus(outcome.reason)).json({ error: outcome.reason });
336
+ return;
337
+ }
338
+ res.status(201).json({ recordId: outcome.recordId, seq: outcome.seq });
339
+ } catch (error) {
340
+ next(error);
374
341
  }
342
+ },
343
+ );
375
344
 
376
- const accepted = results.filter((result) => result.ok).length;
377
- res.json({ accepted, results });
378
- } catch (error) {
379
- next(error);
380
- }
381
- });
345
+ // ── Owner write: a batch push (verified + appended in order) ──────────────────
346
+ app.post(
347
+ NODE_SYNC_PUSH_PATH,
348
+ writeRateLimit,
349
+ jsonParser,
350
+ async (req: Request, res: Response, next: NextFunction) => {
351
+ try {
352
+ const body: unknown = req.body;
353
+ const items =
354
+ typeof body === 'object' &&
355
+ body !== null &&
356
+ Array.isArray((body as { records?: unknown }).records)
357
+ ? (body as { records: unknown[] }).records
358
+ : null;
359
+ if (!items) {
360
+ res.status(400).json({ error: 'invalid_batch' });
361
+ return;
362
+ }
363
+ if (items.length > MAX_SYNC_BATCH) {
364
+ res.status(400).json({ error: 'batch_too_large' });
365
+ return;
366
+ }
367
+
368
+ const results: Array<
369
+ { ok: true; recordId: string; seq: number } | { ok: false; reason: string }
370
+ > = [];
371
+ for (const item of items) {
372
+ const verification = await verifyNodeRecordEnvelope(item);
373
+ if (!verification.ok) {
374
+ results.push({ ok: false, reason: verification.reason });
375
+ continue;
376
+ }
377
+ if (!ownerAuth.isOwnerKey(verification.envelope.publicKey)) {
378
+ results.push({ ok: false, reason: 'not_owner' });
379
+ continue;
380
+ }
381
+ if (!isCollectionAllowed(config, verification.envelope.collection ?? '')) {
382
+ results.push({ ok: false, reason: 'foreign_collection' });
383
+ continue;
384
+ }
385
+ const outcome = await store.append(subject, verification.envelope, verification.recordId);
386
+ results.push(
387
+ outcome.ok
388
+ ? { ok: true, recordId: outcome.recordId, seq: outcome.seq }
389
+ : { ok: false, reason: outcome.reason },
390
+ );
391
+ }
392
+
393
+ const accepted = results.filter((result) => result.ok).length;
394
+ res.json({ accepted, results });
395
+ } catch (error) {
396
+ next(error);
397
+ }
398
+ },
399
+ );
382
400
 
383
401
  // ── Serve a content-addressed blob ───────────────────────────────────────────
384
402
  app.get(`${NODE_BLOBS_PATH}/:hash`, async (req: Request, res: Response, next: NextFunction) => {
@@ -13,12 +13,7 @@
13
13
  */
14
14
 
15
15
  import type { SignedRecordEnvelope } from '@oxy.so/contracts';
16
- import {
17
- type NodeFetch,
18
- type NodeFetchInit,
19
- readBoundedBytes,
20
- readBoundedJson,
21
- } from './httpFetch';
16
+ import { type NodeFetch, type NodeFetchInit, readBoundedBytes, readBoundedJson } from './httpFetch';
22
17
  import {
23
18
  DEFAULT_CLIENT_MAX_REDIRECTS,
24
19
  DEFAULT_CLIENT_TIMEOUT_MS,
@@ -105,7 +100,11 @@ export interface NodeClientOptions {
105
100
  }
106
101
 
107
102
  function readError(body: unknown): string | undefined {
108
- if (typeof body === 'object' && body !== null && typeof (body as { error?: unknown }).error === 'string') {
103
+ if (
104
+ typeof body === 'object' &&
105
+ body !== null &&
106
+ typeof (body as { error?: unknown }).error === 'string'
107
+ ) {
109
108
  return (body as { error: string }).error;
110
109
  }
111
110
  return undefined;
@@ -200,7 +199,10 @@ export class NodeClient {
200
199
  headRaw !== null &&
201
200
  typeof (headRaw as { seq?: unknown }).seq === 'number' &&
202
201
  typeof (headRaw as { headRecordId?: unknown }).headRecordId === 'string'
203
- ? { seq: (headRaw as { seq: number }).seq, headRecordId: (headRaw as { headRecordId: string }).headRecordId }
202
+ ? {
203
+ seq: (headRaw as { seq: number }).seq,
204
+ headRecordId: (headRaw as { headRecordId: string }).headRecordId,
205
+ }
204
206
  : null;
205
207
  return { records, count: records.length, head };
206
208
  }
@@ -231,7 +233,10 @@ export class NodeClient {
231
233
  }
232
234
  const obj = body as { recordId?: unknown; seq?: unknown };
233
235
  if (typeof obj.recordId !== 'string' || typeof obj.seq !== 'number') {
234
- throw new NodeClientError(`node ${NODE_RECORDS_PATH} returned a malformed write result`, res.status);
236
+ throw new NodeClientError(
237
+ `node ${NODE_RECORDS_PATH} returned a malformed write result`,
238
+ res.status,
239
+ );
235
240
  }
236
241
  return { recordId: obj.recordId, seq: obj.seq };
237
242
  }
@@ -241,9 +246,10 @@ export class NodeClient {
241
246
  * node's per-item results. Throws {@link NodeClientError} only on a non-2xx
242
247
  * batch-level failure (`invalid_batch` / `batch_too_large`).
243
248
  */
244
- async pushRecords(
245
- envelopes: SignedRecordEnvelope[],
246
- ): Promise<{ accepted: number; results: Array<{ ok: boolean; recordId?: string; seq?: number; reason?: string }> }> {
249
+ async pushRecords(envelopes: SignedRecordEnvelope[]): Promise<{
250
+ accepted: number;
251
+ results: Array<{ ok: boolean; recordId?: string; seq?: number; reason?: string }>;
252
+ }> {
247
253
  const res = await this.fetch(
248
254
  `${this.baseUrl}${NODE_SYNC_PUSH_PATH}`,
249
255
  this.init({
@@ -272,14 +278,20 @@ export class NodeClient {
272
278
 
273
279
  /** Fetch a content-addressed blob. Returns `null` on a 404; throws on other non-2xx. */
274
280
  async getBlob(hash: string): Promise<Buffer | null> {
275
- const res = await this.fetch(`${this.baseUrl}${NODE_BLOBS_PATH}/${encodeURIComponent(hash)}`, this.init({ method: 'GET' }));
281
+ const res = await this.fetch(
282
+ `${this.baseUrl}${NODE_BLOBS_PATH}/${encodeURIComponent(hash)}`,
283
+ this.init({ method: 'GET' }),
284
+ );
276
285
  if (res.status === 404) {
277
286
  res.destroy();
278
287
  return null;
279
288
  }
280
289
  if (res.status < 200 || res.status >= 300) {
281
290
  res.destroy();
282
- throw new NodeClientError(`node ${NODE_BLOBS_PATH}/:hash responded HTTP ${res.status}`, res.status);
291
+ throw new NodeClientError(
292
+ `node ${NODE_BLOBS_PATH}/:hash responded HTTP ${res.status}`,
293
+ res.status,
294
+ );
283
295
  }
284
296
  return readBoundedBytes(res, this.blobMaxBytes);
285
297
  }
@@ -290,7 +302,11 @@ export class NodeClient {
290
302
  * passes the resulting `{ publicKey, signature, timestamp }`; the client sets
291
303
  * the owner-auth headers. Throws {@link NodeClientError} on a non-2xx.
292
304
  */
293
- async putBlob(hash: string, bytes: Uint8Array, auth: NodeBlobPinAuth): Promise<NodeBlobPutResult> {
305
+ async putBlob(
306
+ hash: string,
307
+ bytes: Uint8Array,
308
+ auth: NodeBlobPinAuth,
309
+ ): Promise<NodeBlobPutResult> {
294
310
  const res = await this.fetch(
295
311
  `${this.baseUrl}${NODE_BLOBS_PATH}/${encodeURIComponent(hash)}`,
296
312
  this.init({
@@ -315,7 +331,10 @@ export class NodeClient {
315
331
  }
316
332
  const obj = body as { hash?: unknown; size?: unknown };
317
333
  if (typeof obj.hash !== 'string' || typeof obj.size !== 'number') {
318
- throw new NodeClientError(`node ${NODE_BLOBS_PATH}/:hash returned a malformed pin result`, res.status);
334
+ throw new NodeClientError(
335
+ `node ${NODE_BLOBS_PATH}/:hash returned a malformed pin result`,
336
+ res.status,
337
+ );
319
338
  }
320
339
  return { hash: obj.hash, size: obj.size };
321
340
  }
@@ -244,7 +244,10 @@ function narrowSocialReceive(value: unknown): CommonsSocialReceiveSignature | nu
244
244
  }
245
245
 
246
246
  /** Run one native call; a rejection or an unexpected shape is `null`. */
247
- async function ask<T>(call: () => Promise<unknown>, narrow: (value: unknown) => T | null): Promise<T | null> {
247
+ async function ask<T>(
248
+ call: () => Promise<unknown>,
249
+ narrow: (value: unknown) => T | null,
250
+ ): Promise<T | null> {
248
251
  try {
249
252
  return narrow(await call());
250
253
  } catch {
@@ -271,7 +274,11 @@ export function loadCommonsIdentityBridge(): Promise<CommonsIdentityBridge | nul
271
274
  return {
272
275
  describe: () => ask(() => m.describe(), narrowDescription),
273
276
  proveIdentity: (challenge: string) => ask(() => m.proveIdentity(challenge), narrowProof),
274
- deriveScopedSeed: (info: string) => ask(() => m.deriveScopedSeed(info), (v) => hexField(v, 64)),
277
+ deriveScopedSeed: (info: string) =>
278
+ ask(
279
+ () => m.deriveScopedSeed(info),
280
+ (v) => hexField(v, 64),
281
+ ),
275
282
  signSocialReceive: (index: number, digest: string) =>
276
283
  ask(() => m.signSocialReceive(index, digest), narrowSocialReceive),
277
284
  } satisfies CommonsIdentityBridge;
@@ -27,5 +27,7 @@ export function isReactNative(): boolean {
27
27
  * `randomBytes`) and the `await import('node:crypto')` loader.
28
28
  */
29
29
  export function isNodeJS(): boolean {
30
- return typeof process !== 'undefined' && process.versions != null && process.versions.node != null;
30
+ return (
31
+ typeof process !== 'undefined' && process.versions != null && process.versions.node != null
32
+ );
31
33
  }
package/src/secp256k1.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  * ECDH x-coordinate. Callers never receive a library-specific key object.
8
8
  */
9
9
 
10
- import { secp256k1 } from "@noble/curves/secp256k1.js";
10
+ import { secp256k1 } from '@noble/curves/secp256k1.js';
11
11
 
12
12
  const HEX = /^[0-9a-fA-F]+$/;
13
13
  const COMPRESSED_PUBLIC_KEY = /^(02|03)[0-9a-fA-F]{64}$/;
@@ -26,9 +26,9 @@ export interface Secp256k1SignatureOptions {
26
26
  }
27
27
 
28
28
  function bytesToHex(bytes: Uint8Array): string {
29
- let result = "";
29
+ let result = '';
30
30
  for (const byte of bytes) {
31
- result += byte.toString(16).padStart(2, "0");
31
+ result += byte.toString(16).padStart(2, '0');
32
32
  }
33
33
  return result;
34
34
  }
@@ -51,20 +51,18 @@ function hexToBytes(value: string, label: string): Uint8Array {
51
51
  */
52
52
  export function normalizeSecp256k1PrivateKey(privateKeyHex: string): string {
53
53
  if (
54
- typeof privateKeyHex !== "string" ||
54
+ typeof privateKeyHex !== 'string' ||
55
55
  privateKeyHex.length === 0 ||
56
56
  privateKeyHex.length > 64 ||
57
57
  !HEX.test(privateKeyHex)
58
58
  ) {
59
- throw new Error(
60
- "secp256k1 private key must contain 1 to 64 hexadecimal characters",
61
- );
59
+ throw new Error('secp256k1 private key must contain 1 to 64 hexadecimal characters');
62
60
  }
63
61
 
64
- const normalized = privateKeyHex.toLowerCase().padStart(64, "0");
65
- const privateKey = hexToBytes(normalized, "secp256k1 private key");
62
+ const normalized = privateKeyHex.toLowerCase().padStart(64, '0');
63
+ const privateKey = hexToBytes(normalized, 'secp256k1 private key');
66
64
  if (!secp256k1.utils.isValidSecretKey(privateKey)) {
67
- throw new Error("secp256k1 private key is outside the valid scalar range");
65
+ throw new Error('secp256k1 private key is outside the valid scalar range');
68
66
  }
69
67
  return normalized;
70
68
  }
@@ -80,36 +78,28 @@ export function isValidSecp256k1PrivateKey(privateKeyHex: string): boolean {
80
78
  }
81
79
 
82
80
  function parsePrivateKey(privateKeyHex: string): Uint8Array {
83
- return hexToBytes(
84
- normalizeSecp256k1PrivateKey(privateKeyHex),
85
- "secp256k1 private key",
86
- );
81
+ return hexToBytes(normalizeSecp256k1PrivateKey(privateKeyHex), 'secp256k1 private key');
87
82
  }
88
83
 
89
84
  function parsePublicKey(publicKeyHex: string): Uint8Array {
90
85
  if (
91
- typeof publicKeyHex !== "string" ||
92
- (!COMPRESSED_PUBLIC_KEY.test(publicKeyHex) &&
93
- !UNCOMPRESSED_PUBLIC_KEY.test(publicKeyHex))
86
+ typeof publicKeyHex !== 'string' ||
87
+ (!COMPRESSED_PUBLIC_KEY.test(publicKeyHex) && !UNCOMPRESSED_PUBLIC_KEY.test(publicKeyHex))
94
88
  ) {
95
- throw new Error(
96
- "secp256k1 public key must be a compressed or uncompressed SEC1 hex key",
97
- );
89
+ throw new Error('secp256k1 public key must be a compressed or uncompressed SEC1 hex key');
98
90
  }
99
91
 
100
- const publicKey = hexToBytes(publicKeyHex, "secp256k1 public key");
92
+ const publicKey = hexToBytes(publicKeyHex, 'secp256k1 public key');
101
93
  // Parsing validates the SEC1 prefix, coordinate range, and curve equation.
102
94
  secp256k1.Point.fromBytes(publicKey);
103
95
  return publicKey;
104
96
  }
105
97
 
106
98
  function parseDigest(digestHex: string): Uint8Array {
107
- if (typeof digestHex !== "string" || digestHex.length !== 64) {
108
- throw new Error(
109
- "secp256k1 digest must be exactly 32 bytes of hexadecimal data",
110
- );
99
+ if (typeof digestHex !== 'string' || digestHex.length !== 64) {
100
+ throw new Error('secp256k1 digest must be exactly 32 bytes of hexadecimal data');
111
101
  }
112
- return hexToBytes(digestHex, "secp256k1 digest");
102
+ return hexToBytes(digestHex, 'secp256k1 digest');
113
103
  }
114
104
 
115
105
  /** True for a valid compressed or uncompressed SEC1 secp256k1 public key. */
@@ -132,20 +122,12 @@ export function generateSecp256k1KeyPair(): Secp256k1KeyPair {
132
122
  }
133
123
 
134
124
  /** Derive a compressed or uncompressed SEC1 public key from a private scalar. */
135
- export function deriveSecp256k1PublicKey(
136
- privateKeyHex: string,
137
- compressed = false,
138
- ): string {
139
- return bytesToHex(
140
- secp256k1.getPublicKey(parsePrivateKey(privateKeyHex), compressed),
141
- );
125
+ export function deriveSecp256k1PublicKey(privateKeyHex: string, compressed = false): string {
126
+ return bytesToHex(secp256k1.getPublicKey(parsePrivateKey(privateKeyHex), compressed));
142
127
  }
143
128
 
144
129
  /** Parse and re-encode a public key in canonical lowercase SEC1 form. */
145
- export function normalizeSecp256k1PublicKey(
146
- publicKeyHex: string,
147
- compressed = false,
148
- ): string {
130
+ export function normalizeSecp256k1PublicKey(publicKeyHex: string, compressed = false): string {
149
131
  const point = secp256k1.Point.fromBytes(parsePublicKey(publicKeyHex));
150
132
  return point.toHex(compressed);
151
133
  }
@@ -162,14 +144,10 @@ export function signSecp256k1Digest(
162
144
  digestHex: string,
163
145
  options: Secp256k1SignatureOptions = {},
164
146
  ): string {
165
- const signature = secp256k1.sign(
166
- parseDigest(digestHex),
167
- parsePrivateKey(privateKeyHex),
168
- {
169
- lowS: options.lowS ?? false,
170
- },
171
- );
172
- return signature.toHex("der");
147
+ const signature = secp256k1.sign(parseDigest(digestHex), parsePrivateKey(privateKeyHex), {
148
+ lowS: options.lowS ?? false,
149
+ });
150
+ return signature.toHex('der');
173
151
  }
174
152
 
175
153
  /** Verify a DER-encoded ECDSA signature, accepting historical high-S signatures. */
@@ -180,12 +158,12 @@ export function verifySecp256k1Digest(
180
158
  ): boolean {
181
159
  const publicKey = parsePublicKey(publicKeyHex);
182
160
  const digest = parseDigest(digestHex);
183
- const signature = hexToBytes(signatureDerHex, "secp256k1 DER signature");
161
+ const signature = hexToBytes(signatureDerHex, 'secp256k1 DER signature');
184
162
 
185
163
  // Parse once up front so malformed or non-DER input is rejected explicitly.
186
- secp256k1.Signature.fromBytes(signature, "der");
164
+ secp256k1.Signature.fromBytes(signature, 'der');
187
165
  return secp256k1.verify(signature, digest, publicKey, {
188
- format: "der",
166
+ format: 'der',
189
167
  lowS: false,
190
168
  });
191
169
  }
@@ -197,24 +175,21 @@ export function verifySecp256k1Digest(
197
175
  * Throws, as BIP32 requires, when the tweak is not below the curve order or
198
176
  * the sum is zero: that child is invalid and the caller must not use it.
199
177
  */
200
- export function tweakAddSecp256k1PrivateKey(
201
- privateKeyHex: string,
202
- tweakHex: string,
203
- ): string {
178
+ export function tweakAddSecp256k1PrivateKey(privateKeyHex: string, tweakHex: string): string {
204
179
  const n = secp256k1.Point.Fn.ORDER;
205
180
  const key = BigInt(`0x${normalizeSecp256k1PrivateKey(privateKeyHex)}`);
206
- if (typeof tweakHex !== "string" || tweakHex.length !== 64 || !HEX.test(tweakHex)) {
207
- throw new Error("secp256k1 tweak must be exactly 32 bytes of hexadecimal data");
181
+ if (typeof tweakHex !== 'string' || tweakHex.length !== 64 || !HEX.test(tweakHex)) {
182
+ throw new Error('secp256k1 tweak must be exactly 32 bytes of hexadecimal data');
208
183
  }
209
184
  const tweak = BigInt(`0x${tweakHex}`);
210
185
  if (tweak >= n) {
211
- throw new Error("secp256k1 tweak is not below the curve order");
186
+ throw new Error('secp256k1 tweak is not below the curve order');
212
187
  }
213
188
  const child = (key + tweak) % n;
214
189
  if (child === 0n) {
215
- throw new Error("secp256k1 tweaked private key is zero");
190
+ throw new Error('secp256k1 tweaked private key is zero');
216
191
  }
217
- return child.toString(16).padStart(64, "0");
192
+ return child.toString(16).padStart(64, '0');
218
193
  }
219
194
 
220
195
  /** Derive the fixed-width 32-byte ECDH x-coordinate shared secret. */
@@ -228,7 +203,7 @@ export function deriveSecp256k1SharedSecret(
228
203
  true,
229
204
  );
230
205
  if (encodedPoint.length !== 33) {
231
- throw new Error("secp256k1 ECDH returned an unexpected point encoding");
206
+ throw new Error('secp256k1 ECDH returned an unexpected point encoding');
232
207
  }
233
208
  return encodedPoint.slice(1);
234
209
  }