Skip to content

refactor(stdlib): remove jsonwebtoken native binding - #10687

Closed
proggeramlug wants to merge 2 commits into
mainfrom
wip/10683-remove-jwt-binding
Closed

proggeramlug wants to merge 2 commits into
mainfrom
wip/10683-remove-jwt-binding

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Sep 19, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #10683 — the removal is the fix.

Removes the native jsonwebtoken binding so import jwt from "jsonwebtoken"
(no perry.compilePackages entry) resolves to the real npm package,
compiled from source, per the owner's decision to stop shipping
hand-written Rust reimplementations of npm packages.

The security defect this closes

Native verify() returned null instead of throwing on every forgery
case: tampered payload, wrong secret, alg:none, garbage token, tampered
signature, expired token. Any app that wrote the idiomatic
try { jwt.verify(token, secret) } catch { reject() } never rejected a
forged token — it silently treated the forgery as valid-but-unauthenticated.

Native sign(payload, secret, { expiresIn: "1h" }) also silently dropped
the expiry
: the string "1h" coerced to NaN via a numeric cast, and the
runtime only wrote the exp claim when the value was > 0.0, so the token
never expired.

The real jsonwebtoken source (compiled from npm, no wrapper) matches Node
exactly, including all six thrown error names/messages.

What was found and removed

Per #10678 (duplicate extern "C" exports across perry-ext-*/perry-stdlib
pairs), this binding existed twice, plus a dedicated codegen fast path:

  • crates/perry-ext-jsonwebtoken/ — the governance-tracked binding crate
    wired into well_known_bindings.toml's [bindings.jsonwebtoken].
  • crates/perry-stdlib/src/jsonwebtoken.rs (904 lines) — a second,
    independent implementation behind perry-stdlib's bundled-jsonwebtoken
    feature (default-on via the crypto umbrella), exporting the exact same
    js_jwt_sign/js_jwt_verify/js_jwt_decode/*_es256/*_rs256/*_dyn/
    *_dyn_opts symbols as perry-ext-jsonwebtoken. This is the copy the
    compiler actually calls
    : sign/verify don't go through the generic
    well-known-binding FFI dispatch at all — crates/perry-codegen/src/lower_call/native/jsonwebtoken.rs
    (lower_jsonwebtoken_sign/_verify, algorithm-aware routing added in
    jwt.sign algorithm option only matches inline string literals — const refs fall back to HS256 #1074) special-cases module == "jsonwebtoken" directly in
    native_runtime_branch.rs and always lowers to perry-stdlib's symbols,
    bypassing whichever crate well_known_bindings.toml names. decode (no
    algorithm options) stayed in the generic NativeModSig table
    (native_table/utils_crypto.rs) pointing at js_jwt_decode.

All three removed together — the dedicated codegen lowering path, both
duplicate FFI implementations, and every registry row:

  • crates/perry-ext-jsonwebtoken/ (crate deleted)
  • crates/perry-stdlib/src/jsonwebtoken.rs (deleted) + its
    bundled-jsonwebtoken feature and #[cfg(feature = "bundled-jsonwebtoken")]
    module wiring in perry-stdlib/src/lib.rs
  • crates/perry-codegen/src/lower_call/native/jsonwebtoken.rs (deleted) +
    its mod/use in native/mod.rs and the two dispatch branches in
    native_runtime_branch.rs
  • The jsonwebtoken NativeModSig row in native_table/utils_crypto.rs
  • The js_jwt_* FFI declarations in runtime_decls/stdlib_ffi/third_party.rs
  • [bindings.jsonwebtoken] + .upstream in well_known_bindings.toml
  • The "jsonwebtoken" NATIVE_MODULES entry and its three manifest rows
    (sign/verify/decode) in perry-api-manifest
  • The "jsonwebtoken" => &["bundled-jsonwebtoken"] line in stdlib_features.rs
  • 5 Android stub exports (js_jwt_decode/sign/sign_es256/sign_rs256/verify)
    in perry-ui-android/src/stdlib_stubs.rs
  • The perry-ext-jsonwebtoken workspace member + path dependency in the root
    Cargo.toml
  • workspace-architecture.json's perry-ext-jsonwebtoken decision entry
    (workspace_members 83→82, externalize 33→32)
  • The stale entry in scripts/unrooted_local_shape_baseline.json

One subtlety, fixed in the same PR: perry-stdlib's bundled-jsonwebtoken
feature was ["dep:jsonwebtoken", "dep:p256", "dep:rsa", "dep:spki"]. Deleting
it outright broke the build — crates/perry-stdlib/src/webcrypto/key_object.rs
and keys.rs need dep:rsa/dep:spki unconditionally for WebCrypto
(to_pkcs8_pem/to_public_key_pem/to_pkcs8_der), unrelated to jsonwebtoken;
they were only reachable through this feature by historical accident. Re-wired
dep:rsa/dep:spki directly onto perry-stdlib's crypto feature so removing
bundled-jsonwebtoken doesn't take WebCrypto down with it. Confirmed
dep:jsonwebtoken (the crates.io jsonwebtoken Rust crate) has no other
callers in perry-stdlib.

Left alone, deliberately: the real jsonwebtoken crates.io dependency in
the root Cargo.toml / crates/perry/Cargo.toml — this is unrelated Rust
tooling used by Perry's own Apple code-signing
(crates/perry/src/commands/run/resign.rs,
crates/perry/src/commands/setup/common_apple.rs), nothing to do with the
npm binding. Also left alone: prose comments across perry-runtime/perry-codegen
that cite jsonwebtoken as a motivating example for unrelated compiler/runtime
behavior (e.g. null_stub.rs's js_unresolved_default_call comment,
callable_exports.rs's safe-buffer name-descriptor comment) — those explain
why the compiler behaves a certain way, not the binding being removed.
test-files/test_issue_6675_createprivatekey_throws.ts mirrors jsonwebtoken's
normalizeSecret pattern in its comments but imports only crypto, not
jsonwebtoken — untouched.

Not touched, and why it matters: five standalone test-files/*.ts
regression fixtures (test_issue_915_jwt_sign.ts,
test_issue_915_jwt_sign_after_async_route.ts,
test_issue_915_native_module_after_async_resume.ts,
test_issue_927_jwt_verify_returns_object.ts, test_jwt_sign_dynamic_alg.ts)
import jwt from "jsonwebtoken" and exercise the now-deleted dedicated
codegen dispatch mechanism directly (dispatch-table row order across async
resume, algorithm-aware routing, etc.). Four of the five are already tracked
untriaged in test-parity/known_failures.json under the unrelated #8271 audit
(dated 2026-08-17, long before this campaign); none has a package.json/
node_modules of its own, so with the native module gone they now fail with
an unresolved-import compile error instead of a parity mismatch —
verified directly (test_jwt_sign_dynamic_alg.ts now reports "no stdlib
binding / package not found" rather than a byte diff). Left as-is rather than
deleted or hand-patched: their entire subject (the deleted native dispatch
internals) no longer exists, updating them to install a real jsonwebtoken
would test something they weren't written to test, and deleting fixtures is
out of scope for a removal PR per this campaign's own convention. Flagging
explicitly rather than leaving it for CI to discover silently.

Acceptance test: real sign/verify + 6 forgery cases, no compilePackages entry

Built on perrymaster (--profile perry-dev, -p perry -p perry-runtime-static -p perry-stdlib-static), confirmed .a mtimes moved. Test project:

{ "dependencies": { "jsonwebtoken": "^9.0.2" } }
import jwt from "jsonwebtoken";
// sign/verify roundtrip, decode(), expiresIn, then 6 forgery cases:
// wrong secret, tampered payload, tampered signature, alg:none forgery,
// garbage token, expired token — each must THROW, not return null.

No perry.compilePackages entry at all. Compile log: Compile package wildcard: expanded to 15 installed package(s) (jsonwebtoken + jws, jwa, ms,
lodash.*, semver, etc.), 77 native modules, real AOT compile from source.

Ran the binary and diffed against node --experimental-strip-types (Node
26.5.1, the pinned oracle): byte-for-byte identical except one unrelated,
pre-existing cosmetic difference — Perry's JSON.parse error message text
for malformed JSON doesn't match V8's exact wording ("JSON parse error: malformed input" vs "Expected ',' or '}' after property value in JSON at position 44…"); the throw itself is identical on both sides, and this is a
general JSON.parse gap unrelated to jsonwebtoken. All 6 forgery cases threw
with matching error names/messages on both sides:

wrong-secret THREW JsonWebTokenError invalid signature
tampered-payload THREW SyntaxError <message text differs, both throw>
tampered-signature THREW JsonWebTokenError invalid signature
alg-none-forgery THREW JsonWebTokenError jwt signature is required
garbage-token THREW JsonWebTokenError invalid token
expired-token THREW TokenExpiredError jwt expired

expiresIn: "1h" now correctly encodes a real exp claim (exp present: true), fixing the second reported defect.

Verification

  • cargo check --workspace --all-targets (default dev profile, not
    perry-dev — see note below) under -D warnings: clean, excluding the
    cross-host UI crates per this repo's own convention.
  • cargo build --profile perry-dev -p perry -p perry-runtime-static -p perry-stdlib-static: clean; confirmed the re-wired dep:rsa/dep:spki
    fix (without it, perry-stdlib fails with 48 errors — multiple-spki-
    version trait resolution breaking p256's EncodePrivateKey/
    EncodePublicKey impls in webcrypto/key_object.rs/keys.rs, unrelated to
    jsonwebtoken but exposed by deleting bundled-jsonwebtoken naively).
  • cargo test -p perry-api-manifest: 39+4 passing.
  • cargo test -p perry-codegen --test manifest_consistency: 5/5 passing
    (every_native_module_has_at_least_one_manifest_entry,
    every_well_known_binding_has_manifest_entry, etc.).
  • python3 scripts/binding_governance.py --check: OK (39 extension crates).
  • node scripts/binding_pins.mjs --check: OK (37 pinned, lock-step holds).
  • python3 scripts/workspace_architecture.py --check: OK.
  • cargo fmt --all -- --check: clean.
  • scripts/run_lint_gates.sh (SKIP_COMPILE_GATES=1): 77 of 79 passed
    (compile tier skipped). Two reds: "Public benchmark evidence freshness" —
    pre-existing on every PR in this repo, not touched here; "String
    payload-access inventory" — genuinely caused by this PR (perry-stdlib's
    inline-offset count dropped 40→39 with jsonwebtoken.rs deleted), fixed in
    this PR via --write-baseline.
  • Real jsonwebtoken round-trip + 6 forgery cases: see above.

Not run / out of scope

  • Compile tier of run_lint_gates.sh (known-red on Linux per this campaign's
    contract).
  • Full gap suite (host stalls under auto-optimize per contract); ran the
    targeted registry/consistency tests plus the direct acceptance test instead.
  • No version bump / CLAUDE.md edit — per this campaign's convention, the
    maintainer bumps at merge time.

Summary by CodeRabbit

  • Breaking Changes
    • Removed the bundled native jsonwebtoken binding.
    • jsonwebtoken imports now resolve to the package from node_modules; otherwise, resolution may fail.
    • Removed built-in sign, verify, and decode API support and their type declarations.
  • Documentation
    • Updated API references and native-library documentation to remove jsonwebtoken.
  • Chores
    • Removed related workspace configuration and platform-specific stubs.

Ralph Küpper added 2 commits September 19, 2026 04:46
Fixes #10683.

The hand-written native jsonwebtoken binding (crates/perry-ext-jsonwebtoken,
plus a second duplicate implementation in crates/perry-stdlib/src/jsonwebtoken.rs
exporting the same js_jwt_* symbols per #10678) has a live security defect:
verify() returns null instead of throwing on every forgery case (tampered
payload, wrong secret, alg:none, garbage token, tampered signature, expired
token), so `try { jwt.verify(...) } catch { reject() }` never rejects a
forgery. sign(..., { expiresIn: "1h" }) also silently drops the expiry
(string coerces to NaN, and the runtime only writes `exp` when > 0.0).

Removes both copies plus the dedicated codegen lowering path
(lower_call/native/jsonwebtoken.rs's lower_jsonwebtoken_sign/_verify and its
native_runtime_branch.rs dispatch), the decode-only NativeModSig row in
native_table/utils_crypto.rs, the js_jwt_* FFI declarations in
runtime_decls/stdlib_ffi/third_party.rs, the well_known_bindings.toml entry,
the NATIVE_MODULES/manifest rows, the bundled-jsonwebtoken stdlib feature
(re-wiring dep:rsa/dep:spki directly onto perry-stdlib's `crypto` feature,
since webcrypto/key_object.rs and keys.rs need them independently of
jsonwebtoken), and the Android stub exports. The real `jsonwebtoken` crates.io
dependency stays — it is unrelated Rust tooling used by perry's own Apple
code-signing (commands/run/resign.rs, commands/setup/common_apple.rs).

Regenerated docs/api/perry.d.ts, docs/src/api/reference.md (--print-api-manifest)
and docs/src/native-libraries/governance.md (binding_governance.py --table).
Updated workspace-architecture.json (workspace_members 83->82, externalize
33->32) and scripts/string_payload_access_baseline.txt (perry-stdlib
inline-offset sites 40->39, from the deleted stdlib file).
@coderabbitai

coderabbitai Bot commented Sep 19, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 14e2f985-5b8a-41ee-b6bd-0cbbb55b9202

📥 Commits

Reviewing files that changed from the base of the PR and between 8df83f8 and f62ebcf.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (23)
  • Cargo.toml
  • changelog.d/10687-jsonwebtoken-native-binding-removal.md
  • crates/perry-api-manifest/src/entries.rs
  • crates/perry-api-manifest/src/entries/part_1.rs
  • crates/perry-codegen/src/lower_call/native/jsonwebtoken.rs
  • crates/perry-codegen/src/lower_call/native/mod.rs
  • crates/perry-codegen/src/lower_call/native/native_runtime_branch.rs
  • crates/perry-codegen/src/lower_call/native_table/utils_crypto.rs
  • crates/perry-codegen/src/runtime_decls/stdlib_ffi/third_party.rs
  • crates/perry-ext-jsonwebtoken/Cargo.toml
  • crates/perry-ext-jsonwebtoken/src/lib.rs
  • crates/perry-stdlib/Cargo.toml
  • crates/perry-stdlib/src/jsonwebtoken.rs
  • crates/perry-stdlib/src/lib.rs
  • crates/perry-ui-android/src/stdlib_stubs.rs
  • crates/perry/src/commands/stdlib_features.rs
  • crates/perry/well_known_bindings.toml
  • docs/api/perry.d.ts
  • docs/src/api/reference.md
  • docs/src/native-libraries/governance.md
  • scripts/string_payload_access_baseline.txt
  • scripts/unrooted_local_shape_baseline.json
  • workspace-architecture.json
💤 Files with no reviewable changes (17)
  • crates/perry/src/commands/stdlib_features.rs
  • crates/perry-ext-jsonwebtoken/Cargo.toml
  • docs/src/native-libraries/governance.md
  • crates/perry-codegen/src/lower_call/native/native_runtime_branch.rs
  • crates/perry-codegen/src/lower_call/native/jsonwebtoken.rs
  • crates/perry-api-manifest/src/entries/part_1.rs
  • crates/perry-api-manifest/src/entries.rs
  • crates/perry/well_known_bindings.toml
  • crates/perry-codegen/src/lower_call/native_table/utils_crypto.rs
  • crates/perry-ext-jsonwebtoken/src/lib.rs
  • crates/perry-codegen/src/runtime_decls/stdlib_ffi/third_party.rs
  • crates/perry-stdlib/src/lib.rs
  • crates/perry-ui-android/src/stdlib_stubs.rs
  • scripts/unrooted_local_shape_baseline.json
  • crates/perry-stdlib/src/jsonwebtoken.rs
  • Cargo.toml
  • crates/perry-codegen/src/lower_call/native/mod.rs

Included review availability: Your plan provides up to 8 included reviews per hour; 5 remain after this review.


📝 Walkthrough

Walkthrough

The native jsonwebtoken binding was removed from the workspace, compiler, runtime, manifests, documentation, and generated metadata. Imports now resolve to the real npm package or produce a resolution error.

Changes

Native runtime removal

Layer / File(s) Summary
Remove JWT runtime implementations
Cargo.toml, crates/perry-ext-jsonwebtoken/*, crates/perry-stdlib/*
The native crate, stdlib JWT implementation, FFI exports, feature, and optional dependency were removed.
Remove compiler and binding resolution paths
crates/perry-api-manifest/*, crates/perry-codegen/*, crates/perry/src/commands/stdlib_features.rs, crates/perry/well_known_bindings.toml
JWT manifest entries, lowering, dispatch, FFI declarations, feature mapping, and bundled-binding resolution were removed.
Update generated references and baselines
docs/*, scripts/*, workspace-architecture.json
JWT API documentation, governance data, architecture data, and analysis baselines were updated.

Priority: ⬆️ High

Estimated code review effort: 2 (Simple) | ~15 minutes

Change: Bug fix · Severity of issue fixed: High

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely identifies the primary change: removal of the native jsonwebtoken binding.
Description check ✅ Passed The description is complete and relevant. It covers the summary, concrete changes, linked issue, extensive test plan and results, scope decisions, and checklist-related constraints. The optional scree…
Linked Issues check ✅ Passed The changes satisfy issue #10683. They delete the native jsonwebtoken implementation, codegen dispatch, FFI declarations, registry entries, feature wiring, Android stubs, and workspace crate metadat…
Out of Scope Changes check ✅ Passed The changes remain within issue #10683. The changelog, API declarations, documentation, governance inventory, workspace metadata, architecture data, and baselines support removal of the native binding…
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 1…
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Commit to this branch
  • Create a new PR
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@proggeramlug

Copy link
Copy Markdown
Contributor Author

Landed via merge train #10716 (v0.5.1598). All source commits preserve authorship; merged main matches the validated train exactly.

proggeramlug pushed a commit that referenced this pull request Sep 19, 2026
The rebase over origin/main (which already applied #10687's jsonwebtoken
removal) resolved the manifest header-count conflict with placeholder
values from before the rebase. Recompute them from the actual resolved
tree via scripts/regen_api_docs.sh's two perry --print-api-manifest
invocations: 2085 entries across 134 modules (perry.d.ts), 3027 entries
across 136 modules (reference.md).
proggeramlug pushed a commit that referenced this pull request Sep 19, 2026
The rebase over origin/main (which already applied #10687's jsonwebtoken
removal) resolved the manifest header-count conflict with placeholder
values from before the rebase. Recompute them from the actual resolved
tree via scripts/regen_api_docs.sh's two perry --print-api-manifest
invocations: 2085 entries across 134 modules (perry.d.ts), 3027 entries
across 136 modules (reference.md).
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.

SECURITY: native jsonwebtoken verify() returns null instead of throwing on forged/tampered/expired tokens, and sign() silently drops expiresIn

1 participant