Skip to content
Open
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
4 changes: 2 additions & 2 deletions packages/cre-rust-inject-alpha/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ use javy_plugin_api::javy::quickjs::prelude::*;
use javy_plugin_api::javy::quickjs::{Ctx, Object};

pub fn register(ctx: &Ctx<'_>) {
let obj = Object::new(ctx.clone()).unwrap();
let obj = Object::new(ctx.clone()).expect("failed to create rustAlpha export object");
obj.set(
"greet",
Func::from(|| -> String { "Hello from alpha".to_string() }),
)
.unwrap();
.expect("failed to set rustAlpha.greet export");
extend_wasm_exports(ctx, "rustAlpha", obj);
}
4 changes: 2 additions & 2 deletions packages/cre-rust-inject-beta/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ use javy_plugin_api::javy::quickjs::prelude::*;
use javy_plugin_api::javy::quickjs::{Ctx, Object};

pub fn register(ctx: &Ctx<'_>) {
let obj = Object::new(ctx.clone()).unwrap();
let obj = Object::new(ctx.clone()).expect("failed to create rustBeta export object");
obj.set(
"greet",
Func::from(|| -> String { "Hello from beta".to_string() }),
)
.unwrap();
.expect("failed to set rustBeta.greet export");
extend_wasm_exports(ctx, "rustBeta", obj);
}
1 change: 1 addition & 0 deletions packages/cre-sdk-javy-plugin/.gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
node_modules
src/javy_chainlink_sdk/target/
src/cre_wasm_exports/target/
.cargo-target/
.turbo

Expand Down
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ pub unsafe extern "C" fn initialize_runtime() {
runtime
},
)
.unwrap();
.expect("failed to initialize CRE Javy runtime");
}
`

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ pub fn extend_wasm_exports<'js, V: IntoJs<'js>>(ctx: &Ctx<'js>, name: &'static s
panic!("Duplicate WASM export: '{name}' is already registered");
}
});
ctx.globals().set(name, value).unwrap();
ctx.globals()
.set(name, value)
.unwrap_or_else(|err| panic!("failed to register WASM export '{name}': {err:?}"));
}

/// Resets the export registry for a new initialization cycle.
Expand Down Expand Up @@ -53,7 +55,8 @@ mod tests {
#[should_panic(expected = "Duplicate WASM export")]
fn rejects_duplicate_export_name() {
REGISTERED.with(|cell| cell.borrow_mut().clear());
let runtime = Runtime::new(Config::default()).unwrap();
let runtime =
Runtime::new(Config::default()).expect("failed to create QuickJS runtime for test");
runtime.context().with(|ctx| {
extend_wasm_exports(&ctx, "duplicatedName", true);
extend_wasm_exports(&ctx, "duplicatedName", true);
Expand Down
223 changes: 115 additions & 108 deletions packages/cre-sdk-javy-plugin/src/javy_chainlink_sdk/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
use cre_wasm_exports::{__clear_registry, extend_wasm_exports};
use javy_plugin_api::{
import_namespace,
Config, import_namespace,
javy::{Runtime, quickjs::prelude::*},
Config,
};

use base64::Engine;
use javy_plugin_api::javy::quickjs::{
ArrayBuffer, Ctx, Error, Exception, FromJs, TypedArray, Value,
};
use rand::{Rng, SeedableRng};
use rand_chacha::ChaCha8Rng;
use std::collections::HashMap;
use std::env;
use std::sync::{Mutex, OnceLock};
use base64::Engine;
use rand::{Rng, SeedableRng};
use rand_chacha::ChaCha8Rng;

static CURRENT_MODE: Mutex<i32> = Mutex::new(0);
static RANDOM_GENERATORS: OnceLock<Mutex<HashMap<i32, ChaCha8Rng>>> = OnceLock::new();
const MAX_RESPONSE_LEN_BYTES: i32 = 64 * 1024 * 1024;

// ✅ Host imports: implemented in Go
#[link(wasm_import_module = "env")]
Expand Down Expand Up @@ -96,8 +96,63 @@ pub fn config() -> Config {
config
}

/// Applies CRE plugin globals and host bindings. Used by the default plugin build and by generated host crates that add `--cre-exports` extensions.
///
fn validate_max_response_len(max_len: i32) -> Result<usize, &'static str> {
if max_len < 0 {
return Err("maxLen < 0");
}

if max_len > MAX_RESPONSE_LEN_BYTES {
return Err("maxLen exceeds maximum allowed response size");
}

Ok(max_len as usize)
}

fn checked_response_buffer_len(ctx: &Ctx<'_>, max_len: i32) -> Result<usize, Error> {
validate_max_response_len(max_len).map_err(|message| Exception::throw_range(ctx, message))
}

/// Invokes a host fn with shape `(req_ptr, req_len, buf_ptr, max_len) -> i64`,
/// validates `max_len`, returns the populated response slice or maps host errors
/// to JS exceptions. `op_name` appears in the empty-error fallback; `capacity_msg`
/// is static because `Error::new_into_js` requires `&'static str`.
fn dispatch_host_call(
ctx: &Ctx<'_>,
req: ArgBytes,
max_len: i32,
op_name: &str,
capacity_msg: &'static str,
host_fn: unsafe extern "C" fn(*const u8, i32, *mut u8, i32) -> i64,
) -> Result<Vec<u8>, Error> {
let max_len_usize = checked_response_buffer_len(ctx, max_len)?;
let req_bytes = req.0;
let mut buf = vec![0u8; max_len_usize];

let n = unsafe {
host_fn(
req_bytes.as_ptr(),
req_bytes.len() as i32,
buf.as_mut_ptr(),
max_len,
)
};
if n < 0 {
let error_len = (-n) as usize;
let error_msg =
String::from_utf8_lossy(&buf[..error_len.min(max_len_usize)]).into_owned();
let error_msg = if error_msg.is_empty() {
format!("{op_name} failed")
} else {
error_msg
};
return Err(Exception::throw_message(ctx, &error_msg));
}
if n > max_len_usize as i64 {
return Err(Error::new_into_js("Error", capacity_msg));
}
Ok(buf[..n as usize].to_vec())
}

/// Duplicate export names are caught eagerly by `extend_wasm_exports`.
pub fn modify_runtime(runtime: Runtime) -> Runtime {
__clear_registry();
Expand All @@ -118,118 +173,44 @@ pub fn modify_runtime(runtime: Runtime) -> Runtime {
&ctx,
"awaitCapabilities",
Func::from(|ctx: Ctx<'_>, req: ArgBytes, max_len: i32| {
if max_len < 0 {
return Err(Exception::throw_range(&ctx, "maxLen < 0"));
}
let req_bytes = req.0;
let mut buf = vec![0u8; max_len as usize];

let n = unsafe {
await_capabilities(
req_bytes.as_ptr(),
req_bytes.len() as i32,
buf.as_mut_ptr(),
max_len,
)
};
if n < 0 {
let error_len = (-n) as usize;
let error_msg =
String::from_utf8_lossy(&buf[..error_len.min(max_len as usize)]).into_owned();
let error_msg_static: &'static str = Box::leak(error_msg.into_boxed_str());
return Err(Error::new_into_js("Error", error_msg_static));
}
if n > max_len as i64 {
return Err(Error::new_into_js(
"Error",
"await_capabilities: host returned length exceeding buffer capacity",
));
}

let out = &buf[..n as usize];
Ok::<Vec<u8>, Error>(out.to_vec())
dispatch_host_call(
&ctx,
req,
max_len,
"await_capabilities",
"await_capabilities: host returned length exceeding buffer capacity",
await_capabilities,
)
}),
);

extend_wasm_exports(
&ctx,
"getSecrets",
Func::from(|ctx: Ctx<'_>, req: ArgBytes, max_len: i32| {
if max_len < 0 {
return Err(Exception::throw_range(&ctx, "maxLen < 0"));
}
let req_bytes = req.0;
let mut buf = vec![0u8; max_len as usize];

let n = unsafe {
get_secrets(
req_bytes.as_ptr(),
req_bytes.len() as i32,
buf.as_mut_ptr(),
max_len,
)
};
if n < 0 {
let error_len = (-n) as usize;
let error_msg =
String::from_utf8_lossy(&buf[..error_len.min(max_len as usize)]).into_owned();
let error_msg = if error_msg.is_empty() {
"get_secrets failed".to_string()
} else {
error_msg
};
return Err(Exception::throw_message(&ctx, &error_msg));
}
if n > max_len as i64 {
return Err(Error::new_into_js(
"Error",
"get_secrets: host returned length exceeding buffer capacity",
));
}

let out = &buf[..n as usize];
Ok::<Vec<u8>, Error>(out.to_vec())
dispatch_host_call(
&ctx,
req,
max_len,
"get_secrets",
"get_secrets: host returned length exceeding buffer capacity",
get_secrets,
)
}),
);

extend_wasm_exports(
&ctx,
"awaitSecrets",
Func::from(|ctx: Ctx<'_>, req: ArgBytes, max_len: i32| {
if max_len < 0 {
return Err(Exception::throw_range(&ctx, "maxLen < 0"));
}
let req_bytes = req.0;
let mut buf = vec![0u8; max_len as usize];

let n = unsafe {
await_secrets(
req_bytes.as_ptr(),
req_bytes.len() as i32,
buf.as_mut_ptr(),
max_len,
)
};
if n < 0 {
let error_len = (-n) as usize;
let error_msg =
String::from_utf8_lossy(&buf[..error_len.min(max_len as usize)]).into_owned();
let error_msg = if error_msg.is_empty() {
"await_secrets failed".to_string()
} else {
error_msg
};
return Err(Exception::throw_message(&ctx, &error_msg));
}
if n > max_len as i64 {
return Err(Error::new_into_js(
"Error",
"await_secrets: host returned length exceeding buffer capacity",
));
}

let out = &buf[..n as usize];
Ok::<Vec<u8>, Error>(out.to_vec())
dispatch_host_call(
&ctx,
req,
max_len,
"await_secrets",
"await_secrets: host returned length exceeding buffer capacity",
await_secrets,
)
}),
);

Expand Down Expand Up @@ -312,9 +293,8 @@ pub fn modify_runtime(runtime: Runtime) -> Runtime {
"getWasiArgs",
Func::from(|_ctx: Ctx<'_>| -> Result<String, Error> {
let args: Vec<String> = env::args().collect();
let args_json = serde_json::to_string(&args).map_err(|_| {
Error::new_into_js("Error", "Failed to serialize args to JSON")
})?;
let args_json = serde_json::to_string(&args)
.map_err(|_| Error::new_into_js("Error", "Failed to serialize args to JSON"))?;
Ok(args_json)
}),
);
Expand Down Expand Up @@ -346,3 +326,30 @@ pub fn modify_runtime(runtime: Runtime) -> Runtime {

runtime
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn validate_max_response_len_accepts_values_within_cap() {
assert_eq!(validate_max_response_len(0), Ok(0));
assert_eq!(
validate_max_response_len(MAX_RESPONSE_LEN_BYTES),
Ok(MAX_RESPONSE_LEN_BYTES as usize)
);
}

#[test]
fn validate_max_response_len_rejects_negative_values() {
assert_eq!(validate_max_response_len(-1), Err("maxLen < 0"));
}

#[test]
fn validate_max_response_len_rejects_values_above_cap() {
assert_eq!(
validate_max_response_len(MAX_RESPONSE_LEN_BYTES + 1),
Err("maxLen exceeds maximum allowed response size")
);
}
}
38 changes: 38 additions & 0 deletions packages/cre-sdk/src/sdk/impl/runtime-impl.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,44 @@ describe('test getSecret', () => {
expect(result.value).toEqual('value-456')
})

test('normalizes missing secret namespace to default for JSON and protobuf requests', () => {
const observedNamespaces: string[] = []
const helpers = createRuntimeHelpersMock({
getSecrets: mock((request) => {
expect(request.requests.length).toEqual(1)
observedNamespaces.push(request.requests[0].namespace)
}),
awaitSecrets: mock((request) => {
const id = request.ids[0]
return create(AwaitSecretsResponseSchema, {
responses: {
[id]: create(SecretResponsesSchema, {
responses: [
create(SecretResponseSchema, {
response: {
case: 'secret',
value: {
id: 'secret',
namespace: 'default',
owner: 'owner',
value: 'value',
},
},
}),
],
}),
},
})
}),
})

const runtime = new RuntimeImpl<unknown>({}, 1, helpers, anyMaxSize)
runtime.getSecret({ id: 'json-secret' }).result()
runtime.getSecret(create(SecretRequestSchema, { id: 'proto-secret' })).result()

expect(observedNamespaces).toEqual(['default', 'default'])
})

test('getSecrets throws → wrapped as SecretsError', () => {
const secretRequest = create(SecretRequestSchema, {
id: 'test-secret',
Expand Down
Loading
Loading