cred: add pure scripts, signing, and OAuth refresh; validation blocked

This commit is contained in:
2026-11-06 19:05:07 +00:00
parent 1dc09bc418
commit 9e7e5d7b2a
10 changed files with 1888 additions and 44 deletions
+305
View File
@@ -0,0 +1,305 @@
//! Key readers and signature/public-key codecs. No grant or storage policy here.
use super::sign::{Result, SignResult, error, invalid_key};
use base64::{
Engine,
engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD},
};
use ed25519_dalek::{Signer, SigningKey};
use hmac::{Hmac, Mac};
use rsa::{
pkcs1::DecodeRsaPrivateKey,
pkcs8::{DecodePrivateKey, EncodePublicKey},
traits::PublicKeyParts,
};
use serde_json::json;
use sha2::{Digest, Sha256};
use sha2::{Sha384, Sha512};
use zeroize::Zeroizing;
pub(super) enum Key {
Ed(SigningKey),
Ec(p256::ecdsa::SigningKey),
Rsa(rsa::RsaPrivateKey),
Mac(Zeroizing<Vec<u8>>),
}
fn decode(s: &str) -> Result<Zeroizing<Vec<u8>>> {
Ok(Zeroizing::new(
STANDARD
.decode(s)
.or_else(|_| URL_SAFE_NO_PAD.decode(s))
.map_err(|_| invalid_key())?,
))
}
fn family(format: &str) -> &str {
match format {
"ed25519-raw" => "ed25519",
"rs256" | "rsa-sha2-256" | "rsa-sha2-512" => "rsa",
s if s.starts_with("hmac-") => "hmac",
s => s,
}
}
pub(super) fn read(value: &str, format: &str, passphrase: Option<&str>) -> Result<Key> {
let value = value.trim();
let family = family(format);
let key = if value.starts_with("-----BEGIN OPENSSH PRIVATE KEY-----") {
let key = ssh_key::PrivateKey::from_openssh(value).map_err(|_| invalid_key())?;
let key = if key.is_encrypted() {
key.decrypt(passphrase.ok_or_else(invalid_key)?)
.map_err(|_| invalid_key())?
} else {
key
};
match key.key_data() {
ssh_key::private::KeypairData::Ed25519(k) => {
Key::Ed(SigningKey::from_bytes(&k.private.to_bytes()))
}
ssh_key::private::KeypairData::Ecdsa(
k @ ssh_key::private::EcdsaKeypair::NistP256 { .. },
) => Key::Ec(
p256::ecdsa::SigningKey::from_slice(k.private_key_bytes())
.map_err(|_| invalid_key())?,
),
ssh_key::private::KeypairData::Rsa(k) => {
// ssh-key 0.6.7's TryFrom<&RsaKeypair> accidentally passes p twice.
// Convert the parsed components explicitly, preserving p and q.
let int = |n: &ssh_key::Mpint| rsa::BigUint::try_from(n).map_err(|_| invalid_key());
let key = rsa::RsaPrivateKey::from_components(
int(&k.public.n)?,
int(&k.public.e)?,
int(&k.private.d)?,
vec![int(&k.private.p)?, int(&k.private.q)?],
)
.map_err(|_| invalid_key())?;
key.validate().map_err(|_| invalid_key())?;
Key::Rsa(key)
}
_ => return Err(invalid_key()),
}
} else if value.starts_with("-----BEGIN ") {
match family {
"ed25519" => Key::Ed(SigningKey::from_pkcs8_pem(value).map_err(|_| invalid_key())?),
"es256" => {
let key = if value.starts_with("-----BEGIN EC PRIVATE KEY-----") {
p256::SecretKey::from_sec1_pem(value).map_err(|_| invalid_key())?
} else {
p256::SecretKey::from_pkcs8_pem(value).map_err(|_| invalid_key())?
};
Key::Ec(key.into())
}
"rsa" => Key::Rsa(if value.starts_with("-----BEGIN RSA PRIVATE KEY-----") {
rsa::RsaPrivateKey::from_pkcs1_pem(value).map_err(|_| invalid_key())?
} else {
rsa::RsaPrivateKey::from_pkcs8_pem(value).map_err(|_| invalid_key())?
}),
_ => return Err(invalid_key()),
}
} else if value.starts_with('{') {
let j: serde_json::Value = serde_json::from_str(value).map_err(|_| invalid_key())?;
let bytes = |name: &str| -> Result<Zeroizing<Vec<u8>>> {
decode(j[name].as_str().ok_or_else(invalid_key)?)
};
match j["kty"].as_str() {
Some("oct") => Key::Mac(bytes("k")?),
Some("OKP") if j["crv"] == "Ed25519" => {
let d = bytes("d")?;
let k = SigningKey::from_bytes(d.as_slice().try_into().map_err(|_| invalid_key())?);
if bytes("x")?.as_slice() != k.verifying_key().as_bytes() {
return Err(invalid_key());
}
Key::Ed(k)
}
Some("EC") if j["crv"] == "P-256" => {
let k =
p256::ecdsa::SigningKey::from_slice(&bytes("d")?).map_err(|_| invalid_key())?;
let point = k.verifying_key().to_encoded_point(false);
if bytes("x")?.as_slice() != point.x().ok_or_else(invalid_key)?.as_slice()
|| bytes("y")?.as_slice() != point.y().ok_or_else(invalid_key)?.as_slice()
{
return Err(invalid_key());
}
Key::Ec(k)
}
Some("RSA") => {
let int = |name| -> Result<rsa::BigUint> {
Ok(rsa::BigUint::from_bytes_be(&bytes(name)?))
};
let k = rsa::RsaPrivateKey::from_components(
int("n")?,
int("e")?,
int("d")?,
vec![int("p")?, int("q")?],
)
.map_err(|_| invalid_key())?;
k.validate().map_err(|_| invalid_key())?;
Key::Rsa(k)
}
_ => return Err(invalid_key()),
}
} else {
let bytes = decode(value)?;
match family {
"ed25519" => Key::Ed(SigningKey::from_bytes(
bytes.as_slice().try_into().map_err(|_| invalid_key())?,
)),
"es256" => {
Key::Ec(p256::ecdsa::SigningKey::from_slice(&bytes).map_err(|_| invalid_key())?)
}
"hmac" => Key::Mac(bytes),
_ => return Err(invalid_key()),
}
};
if !matches!(
(&key, family),
(Key::Ed(_), "ed25519")
| (Key::Ec(_), "es256")
| (Key::Rsa(_), "rsa")
| (Key::Mac(_), "hmac")
) {
return Err(invalid_key());
}
Ok(key)
}
pub(super) fn check(format: &str, padding: Option<&str>, hash: Option<&str>) -> Result<()> {
match format {
"ed25519" | "ed25519-raw" | "es256" | "hmac-sha256" | "hmac-sha384" | "hmac-sha512"
if padding.is_none() && hash.is_none() =>
{
Ok(())
}
"rs256" | "rsa-sha2-256" | "rsa-sha2-512"
if padding == Some("pkcs1v15")
&& hash
== Some(if format == "rsa-sha2-512" {
"sha512"
} else {
"sha256"
}) =>
{
Ok(())
}
_ => Err(error(
"invalid_params",
"unknown algorithm or inconsistent padding/hash; RSA requires padding=pkcs1v15 and explicit sha256/sha512 matching the algorithm",
)),
}
}
pub(super) fn sign_key(key: &Key, format: &str, payload: &[u8]) -> Result<Vec<u8>> {
Ok(match key {
Key::Ed(k) => k.sign(payload).to_bytes().to_vec(),
Key::Ec(k) => {
let s: p256::ecdsa::Signature = k.sign(payload);
s.to_bytes().to_vec()
}
Key::Rsa(k) => if format == "rsa-sha2-512" {
k.sign(rsa::Pkcs1v15Sign::new::<Sha512>(), &Sha512::digest(payload))
} else {
k.sign(rsa::Pkcs1v15Sign::new::<Sha256>(), &Sha256::digest(payload))
}
.map_err(|_| invalid_key())?,
Key::Mac(k) => {
macro_rules! mac {
($h:ty) => {{
let mut mac = Hmac::<$h>::new_from_slice(k).map_err(|_| invalid_key())?;
mac.update(payload);
mac.finalize().into_bytes().to_vec()
}};
}
match format {
"hmac-sha384" => mac!(Sha384),
"hmac-sha512" => mac!(Sha512),
_ => mac!(Sha256),
}
}
})
}
fn string(out: &mut Vec<u8>, bytes: &[u8]) {
out.extend_from_slice(&(bytes.len() as u32).to_be_bytes());
out.extend_from_slice(bytes);
}
fn mpint(out: &mut Vec<u8>, bytes: &[u8]) {
let bytes = &bytes[bytes.iter().position(|b| *b != 0).unwrap_or(bytes.len())..];
let mut positive = Vec::new();
if bytes.first().is_some_and(|b| b & 128 != 0) {
positive.push(0);
}
positive.extend_from_slice(bytes);
string(out, &positive);
}
fn wire_signature(format: &str, signature: &[u8]) -> Result<Vec<u8>> {
let (algorithm, signature) = match format {
"ed25519" | "ed25519-raw" => ("ssh-ed25519", signature.to_vec()),
"rs256" | "rsa-sha2-256" => ("rsa-sha2-256", signature.to_vec()),
"rsa-sha2-512" => ("rsa-sha2-512", signature.to_vec()),
"es256" => {
let mut s = Vec::new();
mpint(&mut s, &signature[..32]);
mpint(&mut s, &signature[32..]);
("ecdsa-sha2-nistp256", s)
}
_ => {
return Err(error(
"invalid_params",
"SSH wire output requires an asymmetric key",
));
}
};
let mut out = Vec::new();
string(&mut out, algorithm.as_bytes());
string(&mut out, &signature);
Ok(out)
}
pub(super) fn output(format: &str, output: &str, signature: Vec<u8>) -> Result<SignResult> {
let (signature, bytes) = match output {
"raw" => (String::new(), Some(signature)),
"hex" => (hex::encode(signature), None),
"jws" => (URL_SAFE_NO_PAD.encode(signature), None),
"ssh" => (STANDARD.encode(wire_signature(format, &signature)?), None),
"base64" => (STANDARD.encode(signature), None),
_ => return Err(error("invalid_params", "unknown output format")),
};
Ok(SignResult { signature, bytes })
}
pub(super) fn public(key: &Key, output: &str) -> Result<String> {
let raw = match key {
Key::Ed(k) => k.verifying_key().to_bytes().to_vec(),
Key::Ec(k) => k
.verifying_key()
.to_encoded_point(false)
.as_bytes()
.to_vec(),
Key::Rsa(k) => k
.to_public_key()
.to_public_key_der()
.map_err(|_| invalid_key())?
.as_bytes()
.to_vec(),
Key::Mac(_) => return Err(error("invalid_params", "symmetric keys have no public key")),
};
Ok(match output {
"base64" => STANDARD.encode(&raw),
"hex" => hex::encode(&raw),
"pem" => match key {
Key::Ed(k) => k.verifying_key().to_public_key_pem(Default::default()),
Key::Ec(k) => k.verifying_key().to_public_key_pem(Default::default()),
Key::Rsa(k) => k.to_public_key().to_public_key_pem(Default::default()),
_ => unreachable!(),
}.map_err(|_| invalid_key())?,
"jwk" => match key {
Key::Ed(_) => json!({"kty":"OKP","crv":"Ed25519","x":URL_SAFE_NO_PAD.encode(&raw)}),
Key::Ec(_) => json!({"kty":"EC","crv":"P-256","x":URL_SAFE_NO_PAD.encode(&raw[1..33]),"y":URL_SAFE_NO_PAD.encode(&raw[33..])}),
Key::Rsa(k) => json!({"kty":"RSA","n":URL_SAFE_NO_PAD.encode(k.n().to_bytes_be()),"e":URL_SAFE_NO_PAD.encode(k.e().to_bytes_be())}),
_ => unreachable!(),
}.to_string(),
"ssh" => {
let mut out = Vec::new();
match key {
Key::Ed(_) => { string(&mut out, b"ssh-ed25519"); string(&mut out, &raw); },
Key::Ec(_) => { string(&mut out, b"ecdsa-sha2-nistp256"); string(&mut out, b"nistp256"); string(&mut out, &raw); },
Key::Rsa(k) => { string(&mut out, b"ssh-rsa"); mpint(&mut out, &k.e().to_bytes_be()); mpint(&mut out, &k.n().to_bytes_be()); },
_ => unreachable!(),
}
STANDARD.encode(out)
}
_ => return Err(error("invalid_params", "unknown public key output")),
})
}
+33
View File
@@ -1,3 +1,36 @@
//! The credentials manager (P2, plan §4.4): items, grants, requests and audit in the host,
//! outside the world. Programs in the world never see secret values.
//!
//! `Cred::open` loads `cred.sqlite` and the host master key. Read methods return public snapshots.
//! `Caller` carries a binary path and the nearest-first conversation ancestry.
//! `types::Subject` binds grants to a conversation subtree or a binary path.
//! `file_request` normalizes item identities and replays keyed requests. Edit requests carry public diffs.
//! `approve` and `deny` trust the route layer's verified approver identity.
//! That identity can be `None` during bootstrap, before any passkey registration.
//! Each decision commits one SQLite transaction. `subscribe` reports committed decisions across threads.
//! Subscribers must read SQLite after a lag or restart. The durable task layer calls `expire_requests`.
//! `inject` transforms one request's URL and headers. The body stays outside this library.
//! An unmatched request forwards unchanged. An unauthorized match names the item and its filed request.
//! `audit_sign` records signature metadata without the signed payload.
pub mod entry;
pub mod webauthn;
pub mod store;
pub mod schema;
pub mod seal;
pub mod items;
pub mod types;
pub mod grants;
pub mod requests;
pub mod edits;
pub mod summary;
pub mod audit;
pub mod inject;
pub mod error;
pub use store::Cred;
pub use error::{Error, Result};
pub use types::Caller;
pub mod script;
pub mod keys;
pub mod sign;
pub mod oauth2;
pub mod v8;
+119
View File
@@ -0,0 +1,119 @@
//! OAuth2 refresh exchange and a process-local cache keyed by item and version.
use std::{
collections::BTreeMap,
time::{Duration, Instant},
};
use zeroize::Zeroizing;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Error;
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("OAuth refresh failed or returned an invalid response")
}
}
impl std::error::Error for Error {}
/// HTTP adapter. Implementations must reject unsuccessful HTTP status codes.
pub trait Http: Sync {
fn send(
&self,
request: &Request,
deadline: Instant,
) -> impl std::future::Future<Output = Result<Zeroizing<Vec<u8>>, Error>> + Send;
}
/// Contains credentials. Deliberately does not implement Debug or Serialize.
pub struct Request {
pub url: String,
pub method: &'static str,
pub headers: Vec<(&'static str, String)>,
pub body: Zeroizing<String>,
}
pub fn request(
token_url: &str,
refresh_token: &str,
client_id: &str,
client_secret: &str,
) -> Result<Request, Error> {
let target = url::Url::parse(token_url).map_err(|_| Error)?;
if target.scheme() != "https"
|| !target.username().is_empty()
|| target.password().is_some()
|| target.fragment().is_some()
|| [refresh_token, client_id, client_secret].contains(&"")
{
return Err(Error);
}
let body = url::form_urlencoded::Serializer::new(String::new())
.append_pair("grant_type", "refresh_token")
.append_pair("refresh_token", refresh_token)
.append_pair("client_id", client_id)
.append_pair("client_secret", client_secret)
.finish();
Ok(Request {
url: token_url.into(),
method: "POST",
headers: vec![
("Content-Type", "application/x-www-form-urlencoded".into()),
("Content-Length", body.len().to_string()),
],
body: Zeroizing::new(body),
})
}
#[derive(Clone)]
pub struct Token {
pub value: Zeroizing<String>,
pub scopes: Option<Vec<String>>,
pub expires: Instant,
}
pub fn decode(bytes: &[u8]) -> Result<Token, Error> {
#[derive(serde::Deserialize)]
struct Response {
access_token: String,
token_type: String,
expires_in: u64,
scope: Option<String>,
}
let r: Response = serde_json::from_slice(bytes).map_err(|_| Error)?;
if r.access_token.is_empty()
|| !r.token_type.eq_ignore_ascii_case("bearer")
|| r.expires_in == 0
|| http::header::HeaderValue::from_str(&format!("Bearer {}", r.access_token)).is_err()
{
return Err(Error);
}
Ok(Token {
value: Zeroizing::new(r.access_token),
scopes: r
.scope
.map(|s| s.split_whitespace().map(str::to_owned).collect()),
expires: Instant::now() + Duration::from_secs(r.expires_in.min(86400).saturating_sub(30)),
})
}
/// The caller owns synchronization and endpoint policy. No lock crosses an HTTP call here.
#[derive(Default)]
pub struct Cache {
tokens: BTreeMap<(String, i64), Token>,
}
impl Cache {
pub fn clear(&mut self) {
self.tokens.clear();
}
pub async fn refresh(
&mut self,
item: &str,
version: i64,
request: &Request,
http: &impl Http,
deadline: Instant,
) -> Result<Token, Error> {
let key = (item.to_owned(), version);
if let Some(t) = self.tokens.get(&key).filter(|t| t.expires > Instant::now()) {
return Ok(t.clone());
}
self.tokens.retain(|(id, _), _| id != item);
let token = decode(&http.send(request, deadline).await?)?;
self.tokens.insert(key, token.clone());
Ok(token)
}
}
+61
View File
@@ -0,0 +1,61 @@
//! Pure signing and public-key export over decrypted field values. No storage or grant policy.
use super::keys;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Error {
pub kind: &'static str,
pub message: &'static str,
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.message)
}
}
impl std::error::Error for Error {}
pub type Result<T> = std::result::Result<T, Error>;
pub(super) fn error(kind: &'static str, message: &'static str) -> Error {
Error { kind, message }
}
pub(super) fn invalid_key() -> Error {
error("invalid_key", "invalid signing key encoding or length")
}
#[derive(Debug, serde::Serialize)]
pub struct SignResult {
pub signature: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub bytes: Option<Vec<u8>>,
}
/// Sign bytes. RSA requires explicit, matching padding and digest parameters.
pub fn sign(
value: &str,
passphrase: Option<&str>,
format: &str,
payload: &[u8],
output: &str,
padding: Option<&str>,
hash: Option<&str>,
) -> Result<SignResult> {
keys::check(format, padding, hash)?;
let key = keys::read(value, format, passphrase)?;
keys::output(format, output, keys::sign_key(&key, format, payload)?)
}
/// Export only the public part of an asymmetric key.
pub fn public_key(
value: &str,
passphrase: Option<&str>,
format: &str,
output: &str,
) -> Result<String> {
keys::public(&keys::read(value, format, passphrase)?, output)
}
/// Generate a P-256 private key in PKCS#8 PEM format for ES256.
pub fn generate_es256() -> Result<zeroize::Zeroizing<String>> {
use p256::pkcs8::EncodePrivateKey;
p256::SecretKey::random(&mut rsa::rand_core::OsRng)
.to_pkcs8_pem(Default::default())
.map_err(|_| invalid_key())
}
+196
View File
@@ -0,0 +1,196 @@
//! Fresh, bounded V8 isolates for credential scripts. Each call owns a dedicated worker thread.
use super::script::{Error, ErrorKind, Limits, Script};
use deno_core::{JsRuntime, RuntimeOptions, v8};
use std::{
sync::{
Arc,
atomic::{AtomicBool, Ordering},
mpsc,
},
time::Duration,
};
#[derive(Default)]
pub struct V8;
fn failure(kind: ErrorKind) -> Error {
Error {
kind,
message: match kind {
ErrorKind::Limit => "script time, memory or stack limit exceeded",
ErrorKind::Syntax => "invalid script or JSON",
ErrorKind::Throw => "script threw or rejected",
ErrorKind::NonJson => "result is not JSON",
ErrorKind::Engine => "script engine failure",
}
.into(),
}
}
impl Script for V8 {
fn evaluate(
&self,
source: &str,
input: &str,
export: Option<&str>,
limits: Limits,
) -> Result<String, Error> {
let source = source.to_owned();
let input = input.to_owned();
let export = export.unwrap_or("default").to_owned();
std::thread::spawn(move || evaluate(source, input, export, limits))
.join()
.map_err(|_| failure(ErrorKind::Engine))?
}
}
fn evaluate(
source: String,
input: String,
export: String,
limits: Limits,
) -> Result<String, Error> {
let heap = limits
.max_memory_bytes
.unwrap_or(8 * 1024 * 1024)
.clamp(256 * 1024, 32 * 1024 * 1024);
let mut runtime = JsRuntime::new(RuntimeOptions {
// Bootstrap before imposing the caller's budget: V8 cannot recover from
// bootstrap OOM because the runtime cannot install its callback yet.
create_params: Some(v8::CreateParams::default().heap_limits(0, 32 * 1024 * 1024)),
..Default::default()
});
if runtime.v8_isolate().get_heap_statistics().total_heap_size() > heap {
return Err(failure(ErrorKind::Limit));
}
// Removing a temporary callback sets the heap limit after bootstrap.
runtime.add_near_heap_limit_callback(|current, _| current);
runtime.remove_near_heap_limit_callback(heap);
let handle = runtime.v8_isolate().thread_safe_handle();
let limited = Arc::new(AtomicBool::new(false));
let flag = limited.clone();
let heap_handle = handle.clone();
runtime.add_near_heap_limit_callback(move |current, _| {
flag.store(true, Ordering::SeqCst);
heap_handle.terminate_execution();
// V8 needs emergency headroom to unwind after termination.
current + 16 * 1024 * 1024
});
let (done, wait) = mpsc::channel();
let flag = limited.clone();
let watchdog = std::thread::spawn(move || {
if wait
.recv_timeout(Duration::from_millis(
limits.max_time_ms.unwrap_or(100).clamp(1, 1000),
))
.is_err()
{
flag.store(true, Ordering::SeqCst);
handle.terminate_execution();
}
});
let task = async {
runtime
.execute_script("sandbox.js", BOOTSTRAP)
.map_err(|_| failure(ErrorKind::Engine))?;
let input = runtime
.execute_script(
"input.js",
format!("JSON.parse({})", serde_json::to_string(&input).unwrap()),
)
.map_err(|_| failure(ErrorKind::Syntax))?;
let spec = deno_core::ModuleSpecifier::parse("file:///credential.js").unwrap();
let module = runtime
.load_main_es_module_from_code(&spec, source)
.await
.map_err(|_| failure(ErrorKind::Syntax))?;
let evaluated = runtime.mod_evaluate(module);
runtime
.with_event_loop_promise(evaluated, Default::default())
.await
.map_err(|e| {
if e.to_string().contains("call stack") {
failure(ErrorKind::Limit)
} else {
failure(ErrorKind::Throw)
}
})?;
let ns = runtime
.get_module_namespace(module)
.map_err(|_| failure(ErrorKind::Engine))?;
{
deno_core::scope!(scope, &mut runtime);
let ns = v8::Local::new(scope, ns);
let global = scope.get_current_context().global(scope);
let key =
v8::String::new(scope, "__module").ok_or_else(|| failure(ErrorKind::Limit))?;
global.set(scope, key.into(), ns.into());
let input = v8::Local::new(scope, input);
let key = v8::String::new(scope, "__input").ok_or_else(|| failure(ErrorKind::Limit))?;
global.set(scope, key.into(), input);
}
let code = format!(
r#"(async()=>{{
const f=__module[{}];
if(typeof f!=='function') return JSON.stringify({{kind:'Engine'}});
let value;
try {{ value=await f(__input); }} catch(e) {{ const message=String(e?.message); return JSON.stringify({{kind: e instanceof RangeError && message.includes('call stack') ? 'Limit':'Throw'}}); }}
try {{ const text=JSON.stringify(value); return JSON.stringify(text===undefined?{{kind:'NonJson'}}:{{text}}); }} catch(e) {{ return JSON.stringify({{kind:e instanceof RangeError && String(e.message).includes('call stack') ? 'Limit':'NonJson'}}); }}
}})()"#,
serde_json::to_string(&export).unwrap()
);
let value = runtime
.execute_script("call.js", code)
.map_err(|_| failure(ErrorKind::Throw))?;
let resolve = runtime.resolve(value);
let value = runtime
.with_event_loop_promise(resolve, Default::default())
.await
.map_err(|_| failure(ErrorKind::Engine))?;
let text = {
deno_core::scope!(scope, &mut runtime);
v8::Local::new(scope, value).to_rust_string_lossy(scope)
};
let result: serde_json::Value =
serde_json::from_str(&text).map_err(|_| failure(ErrorKind::Engine))?;
match result["text"].as_str() {
Some(s) => Ok(s.to_owned()),
None => Err(failure(match result["kind"].as_str() {
Some("NonJson") => ErrorKind::NonJson,
Some("Throw") => ErrorKind::Throw,
Some("Limit") => ErrorKind::Limit,
_ => ErrorKind::Engine,
})),
}
};
let result = tokio::runtime::Builder::new_current_thread()
.build()
.map_err(|_| failure(ErrorKind::Engine))?
.block_on(task);
let _ = done.send(());
let _ = watchdog.join();
if limited.load(Ordering::SeqCst) {
Err(failure(ErrorKind::Limit))
} else {
result
}
}
const BOOTSTRAP: &str = r#"
delete globalThis.Date; delete Math.random;
for (const name of ['Deno','__bootstrap','console','performance']) delete globalThis[name];
const alphabet='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
globalThis.btoa = value => {
const s=String(value); let out='';
for(let i=0;i<s.length;i+=3){
const a=s.charCodeAt(i), b=s.charCodeAt(i+1), c=s.charCodeAt(i+2);
if(a>255||b>255||c>255) throw Error('invalid base64 input');
const n=(a<<16)|((b||0)<<8)|(c||0);
out+=alphabet[(n>>>18)&63]+alphabet[(n>>>12)&63]+(i+1<s.length?alphabet[(n>>>6)&63]:'=')+(i+2<s.length?alphabet[n&63]:'=');
} return out;
};
globalThis.atob = value => {
let s=String(value).replace(/[\t\n\f\r ]/g,'');
if(s.length%4===0) s=s.replace(/==?$/,'');
if(s.length%4===1||/[^A-Za-z0-9+/]/.test(s)) throw Error('invalid base64 input');
let out='', bits=0, n=0;
for(const c of s){n=(n<<6)|alphabet.indexOf(c);bits+=6;if(bits>=8){bits-=8;out+=String.fromCharCode((n>>>bits)&255);}}
return out;
};
"#;