Skip to content

feat(node): migrate @noble/curves from v1 to v2#1111

Open
thinktanktom wants to merge 2 commits intohyperledger:mainfrom
thinktanktom:feat/noble-curves-v2
Open

feat(node): migrate @noble/curves from v1 to v2#1111
thinktanktom wants to merge 2 commits intohyperledger:mainfrom
thinktanktom:feat/noble-curves-v2

Conversation

@thinktanktom
Copy link
Copy Markdown

Summary

Upgrades @noble/curves from ^1.9.4 to ^2.0.0 in the Node.js SDK.
Resolves #1109.

@noble/curves v2 is ESM-only, which required migrating the node/
package to ESM output and updating all affected crypto API call sites.


Changes

Package

  • Bumped @noble/curves to ^2.0.0
  • Added "type": "module" to package.json
  • Added exports field for ESM consumers

TypeScript

  • Overrode module and moduleResolution in tsconfig.json to
    esnext/bundler — see notes below
  • Added .js extensions to all relative imports (ESM requirement)

Crypto API (v1 → v2 breaking changes)

  • sign() now returns Uint8Array directly instead of a Signature
    object — updated ecdsa.ts with { format: 'der', prehash: false }
  • normalizeS() removed — replaced with hasHighS() + manual S
    normalization in hsmsigner.ts
  • fromCompact()/fromDER() replaced with fromBytes(data, 'compact')
    and fromBytes(data, 'der')
  • randomPrivateKey() renamed to randomSecretKey() in test

Jest

  • Converted jest.config.js to ESM export default syntax
  • Added extensionsToTreatAsEsm, moduleNameMapper, and dual
    transform rules to support ESM test execution
  • Added transformIgnorePatterns to allow Jest to transform
    @noble/* packages which are ESM-only

Known tradeoffs — maintainer input welcome

1. moduleResolution: "bundler" in tsconfig.json
google-protobuf has no exports field, which causes a type error
under strict node16 resolution on the deep import path
google-protobuf/google/protobuf/timestamp_pb. Overriding to bundler
resolution fixes this but is not strictly correct for a Node.js library.
Alternatives considered: ambient module declaration (rejected by ESLint
rules), local shim (same issue). Happy to explore other approaches if
the maintainer has a preference.

2. CJS consumers
The exports field currently only exposes an ESM path. Any consumer
using require('@hyperledger/fabric-gateway') will break. If CJS
support needs to be maintained, a dual build step producing
dist/index.cjs would be required. Happy to add this if needed, or
this could be treated as a conscious breaking change.

3. Dynamic require() in signers.ts
The optional HSM dependency is lazy-loaded via require(), which Jest
polyfills in ESM mode but will throw in a native ESM runtime. Converting
to await import() is the correct long-term fix but is out of scope for
this PR. Flagging for awareness.


Test results

Test Suites: 15 passed, 15 total
Tests:       226 passed, 226 total

@thinktanktom thinktanktom requested a review from a team as a code owner February 17, 2026 09:17
Copy link
Copy Markdown
Member

@bestbeforetoday bestbeforetoday left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you very much for this contribution.

For some future v2 release, I am open to breaking backwards compatibility with CommonJS, but there are quite a few other API changes I would want to include in any v2 release. For now we need to maintain backwards compatibility with CommonJS consumers.

Comment on lines -102 to -103
// EC signatures have length of 2n according to the PKCS11 spec:
// https://docs.oasis-open.org/pkcs11/pkcs11-spec/v3.1/pkcs11-spec-v3.1.html
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be great to retain the explanatory comment.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Restored — sorry for the accidental removal.

Comment on lines +104 to +106
const sig = p256.Signature.fromBytes(compactSignature, 'compact');
const normalizedSig = sig.hasHighS() ? new p256.Signature(sig.r, p256.Point.CURVE().n - sig.s) : sig;
return normalizedSig.toBytes('der');
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer to avoid abbreviation where variable names a reasonably short anyway: signature instead of sig. I think I find this slightly more verbose structure more readable too, but the code is good so just a suggestion:

Suggested change
const sig = p256.Signature.fromBytes(compactSignature, 'compact');
const normalizedSig = sig.hasHighS() ? new p256.Signature(sig.r, p256.Point.CURVE().n - sig.s) : sig;
return normalizedSig.toBytes('der');
let signature = p256.Signature.fromBytes(compactSignature, 'compact');
if (signature.hasHighS()) {
signature = new p256.Signature(signature.r, p256.Point.CURVE().n - signature.s);
}
return signature.toBytes('der');

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call, updated to use signature throughout and expanded the ternary to an if block for readability.

"extends": "@tsconfig/node20/tsconfig.json",
"compilerOptions": {
"module": "esnext",
"moduleResolution": "bundler",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The TypeScript documentation advises against using "moduleResolution": "bundler".

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replaced with "module": "node18" as recommended by the TypeScript library authoring docs. The google-protobuf deep import path is unresolvable under strict node18 resolution since it has no exports field — addressed this with targeted eslint-disable-next-line comments on the two affected import lines, with an explanatory comment so the workaround is visible and self-documenting.

@thinktanktom
Copy link
Copy Markdown
Author

thinktanktom commented Feb 17, 2026

Thanks for the context on the v2 plans. I'll leave the CJS compatibility question for that future work. For now the exports field in this PR is ESM-only.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Use @noble/curves v2 in Node API

2 participants