Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 7 additions & 19 deletions crates/perry-codegen/src/lower_call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8531,32 +8531,20 @@ const NATIVE_MODULE_TABLE: &[NativeModSig] = &[
ret: NR_F64,
},
// ========== jsonwebtoken ==========
// `sign` is intentionally handled in lower_call/native.rs. It needs
// option-dependent runtime selection plus an already-NaN-boxed string
// return, so the generic table must not grow a second path for it.
NativeModSig {
module: "jsonwebtoken",
has_receiver: false,
method: "verify",
class_filter: None,
runtime: "js_jwt_verify",
// js_jwt_verify(token_ptr: *const StringHeader, secret_ptr: *const StringHeader)
// -> *mut StringHeader (JSON of claims). NR_OBJ_FROM_JSON_STR pipes
// the returned JSON through js_json_parse so the value visible to
// user code is a real object (decoded.sub works), not the JSON
// text. Per the jsonwebtoken README, `jwt.verify` returns the
// payload as an object. Issue #927.
args: &[NA_STR, NA_STR],
ret: NR_OBJ_FROM_JSON_STR,
},
// `sign` and `verify` are intentionally handled in
// lower_call/native.rs — both need option-dependent runtime
// selection (HS256 / ES256 / RS256) that the generic table can't
// express. `decode` stays here because it has no algorithm options.
NativeModSig {
module: "jsonwebtoken",
has_receiver: false,
method: "decode",
class_filter: None,
runtime: "js_jwt_decode",
// js_jwt_decode(token_ptr) -> *mut StringHeader (JSON of payload).
// Mirror `verify` — returns an object to user code. Issue #927.
// NR_OBJ_FROM_JSON_STR pipes the returned JSON through
// js_json_parse_or_null so user code sees an object (mirrors
// `verify`'s post-#927 contract). Issue #927.
args: &[NA_STR],
ret: NR_OBJ_FROM_JSON_STR,
},
Expand Down
90 changes: 90 additions & 0 deletions crates/perry-codegen/src/lower_call/native.rs
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,93 @@ fn lower_jsonwebtoken_sign(ctx: &mut FnCtx<'_>, args: &[Expr]) -> Result<String>
Ok(ctx.block().bitcast_i64_to_double(&raw))
}

/// Dispatch `jsonwebtoken.verify(token, secret_or_pem, options?)` to
/// the right runtime (HS256 / ES256 / RS256) based on the
/// `algorithms: ['…']` (or singular `algorithm: '…'`) option.
/// Mirrors `lower_jsonwebtoken_sign`.
///
/// perry#927 follow-up: the generic NativeModSig table picked
/// `js_jwt_verify` (HS256-only) for every algorithm, so ES256 / RS256
/// tokens silently failed verification (returning `null` to user
/// code, breaking the shop-admin auth middleware after a successful
/// signup). Verify needs the same option-aware routing that `sign`
/// already has.
///
/// Return shape matches the old `NR_OBJ_FROM_JSON_STR`: the runtime
/// hands back a JSON-text `*mut StringHeader` (or null), which we
/// pipe through `js_json_parse_or_null` so user code sees a real
/// object on success and `null` on failure (no throw).
fn lower_jsonwebtoken_verify(ctx: &mut FnCtx<'_>, args: &[Expr]) -> Result<String> {
if args.len() < 2 {
bail!(
"jsonwebtoken.verify(token, secret, options?) expects at least 2 args, got {}",
args.len()
);
}

let token_ptr = get_raw_string_ptr(ctx, &args[0])?;
let secret_ptr = get_raw_string_ptr(ctx, &args[1])?;
let mut runtime = "js_jwt_verify";

if let Some(options) = args.get(2) {
if let Some(props) = extract_options_fields(ctx, options) {
for (key, val) in &props {
match key.as_str() {
// `algorithm: 'ES256'` (singular) — accepted for
// symmetry with `sign`'s option name.
"algorithm" => {
if let Expr::String(algorithm) = val {
runtime = match algorithm.as_str() {
"ES256" => "js_jwt_verify_es256",
"RS256" => "js_jwt_verify_rs256",
_ => "js_jwt_verify",
};
} else {
let _ = lower_expr(ctx, val)?;
}
}
// `algorithms: ['ES256']` (plural array) — the
// canonical Node `jsonwebtoken.verify` shape.
// First entry decides routing; the underlying Rust
// jsonwebtoken crate's verify is single-algorithm,
// so multi-algorithm fallback isn't honored.
"algorithms" => {
if let Expr::Array(elems) = val {
if let Some(Expr::String(algorithm)) = elems.first() {
runtime = match algorithm.as_str() {
"ES256" => "js_jwt_verify_es256",
"RS256" => "js_jwt_verify_rs256",
_ => "js_jwt_verify",
};
}
} else {
let _ = lower_expr(ctx, val)?;
}
}
_ => {
let _ = lower_expr(ctx, val)?;
}
}
}
} else {
let _ = lower_expr(ctx, options)?;
}
}

for extra in args.iter().skip(3) {
let _ = lower_expr(ctx, extra)?;
}

ctx.pending_declares
.push((runtime.to_string(), I64, vec![I64, I64]));
ctx.pending_declares
.push(("js_json_parse_or_null".to_string(), I64, vec![I64]));
let blk = ctx.block();
let raw = blk.call(I64, runtime, &[(I64, &token_ptr), (I64, &secret_ptr)]);
let parsed_bits = blk.call(I64, "js_json_parse_or_null", &[(I64, &raw)]);
Ok(blk.bitcast_i64_to_double(&parsed_bits))
}

pub(crate) fn lower_native_method_call(
ctx: &mut FnCtx<'_>,
module: &str,
Expand Down Expand Up @@ -348,6 +435,9 @@ pub(crate) fn lower_native_method_call(
if module == "jsonwebtoken" && method == "sign" && object.is_none() {
return lower_jsonwebtoken_sign(ctx, args);
}
if module == "jsonwebtoken" && method == "verify" && object.is_none() {
return lower_jsonwebtoken_verify(ctx, args);
}

// `perry/ui.App({ title, width, height, body, icon? })` — minimum-viable
// dispatch so a perry/ui app actually launches an NSApplication and
Expand Down
2 changes: 2 additions & 0 deletions crates/perry-codegen/src/runtime_decls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1942,6 +1942,8 @@ pub fn declare_stdlib_ffi(module: &mut LlModule) {
module.declare_function("js_jwt_sign_es256", I64, &[I64, I64, DOUBLE, I64]);
module.declare_function("js_jwt_sign_rs256", I64, &[I64, I64, DOUBLE, I64]);
module.declare_function("js_jwt_verify", I64, &[I64, I64]);
module.declare_function("js_jwt_verify_es256", I64, &[I64, I64]);
module.declare_function("js_jwt_verify_rs256", I64, &[I64, I64]);

// ========== axios / node-fetch ==========
module.declare_function("js_axios_create", DOUBLE, &[I64]);
Expand Down
5 changes: 4 additions & 1 deletion crates/perry-stdlib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ bundled-mongodb = ["dep:mongodb", "dep:bson", "dep:futures-util", "async-runtime
crypto = ["dep:sha2", "dep:sha1", "dep:md-5", "dep:hex", "dep:hmac", "dep:aes", "dep:cbc", "dep:scrypt", "dep:pbkdf2", "dep:base64", "dep:x25519-dalek", "dep:ed25519-dalek", "dep:aes-gcm", "dep:aes-kw", "dep:hkdf", "async-runtime", "ids", "bundled-bcrypt", "bundled-argon2", "bundled-jsonwebtoken", "bundled-ethers"]
bundled-bcrypt = ["dep:bcrypt", "async-runtime"]
bundled-argon2 = ["dep:argon2", "async-runtime"]
bundled-jsonwebtoken = ["dep:jsonwebtoken"]
bundled-jsonwebtoken = ["dep:jsonwebtoken", "dep:p256", "dep:rsa", "dep:spki"]
# ethers blockchain utilities — pure Rust, no extra deps. Default-on
# through `crypto` umbrella; the well-known flip strips this and
# routes to perry-ext-ethers when `import 'ethers'` is detected.
Expand Down Expand Up @@ -309,6 +309,9 @@ hex = { version = "0.4", optional = true }
hmac = { version = "0.12", optional = true }
bcrypt = { version = "0.17", optional = true }
jsonwebtoken = { version = "10.4", optional = true, default-features = false, features = ["rust_crypto", "use_pem"] }
p256 = { version = "0.13", optional = true, default-features = false, features = ["pkcs8", "pem", "ecdsa"] }
rsa = { version = "0.9", optional = true, default-features = false, features = ["pem"] }
spki = { version = "0.7", optional = true, default-features = false, features = ["pem", "alloc"] }
aes = { version = "0.8", optional = true }
cbc = { version = "0.1", optional = true }
scrypt = { version = "0.11", optional = true }
Expand Down
Loading
Loading