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
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,21 @@ impl VertexAuth {
}
};

log::debug!("Attempting to auth using JsonString strategy");
// Check if the string is too short to be valid JSON
if str.len() < 50 {
anyhow::bail!(
"Invalid GCP service account credentials: string is too short ({}). \
Expected a JSON object with fields like 'private_key', 'client_email', etc. \
Got: {}",
str.len(),
debug_str
);
}

Self(Some(serde_json::from_str(str).context(format!("Failed to parse 'credentials' as GCP service account creds (are you using JSON format creds?); credentials={debug_str}"))?))
}
ResolvedGcpAuthStrategy::JsonObject(json) => {
// NB: this should never happen in WASM, there's no way to pass a JSON object in
log::debug!("Attempting to auth using JsonObject strategy");
Self(Some(serde_json::from_value(
serde_json::to_value(json).context("Failed to parse service account credentials as GCP service account creds (issue during serialization)")?).context("Failed to parse service account credentials as GCP service account creds (are you using JSON format creds?)")?))
}
Expand Down Expand Up @@ -144,9 +153,12 @@ impl ServiceAccount {
async fn get_oauth2_token(&self) -> Result<Token> {
let claims = Claims::from_service_account(self);

let jwt = encode_jwt(&serde_json::to_value(claims)?, &self.private_key)
.await
.map_err(|e| anyhow::anyhow!(format!("{e:?}")))?;
let jwt = encode_jwt(
&serde_json::to_value(claims).context("Failed to serialize claims as JSON")?,
&self.private_key,
)
.await
.map_err(|e| anyhow::anyhow!(format!("JWT encoding error: {e:?}")))?;

// Make the token request
let client = reqwest::Client::new();
Expand Down
28 changes: 21 additions & 7 deletions engine/baml-runtime/src/internal/wasm_jwt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,18 @@ use web_sys::{window, CryptoKey, SubtleCrypto};

#[derive(Error, Debug)]
pub enum JwtError {
#[error("JavaScript error: {0:?}")]
#[error("Failed to import private key using WebCrypto API. This typically indicates:\n 1. The private key format is invalid (expected PKCS#8, got something else)\n 2. The private key data is corrupted or incomplete\n 3. The key contains embedded newline characters that need escaping in JSON\n\nTroubleshooting:\n - Verify your GCP service account JSON has a valid 'private_key' field\n - Ensure the private key starts with '-----BEGIN PRIVATE KEY-----' (not 'RSA PRIVATE KEY')\n - Check that newlines in the JSON are properly escaped as \\n\n Original error: {0:?}")]
JsError(JsValue),
#[error("Base64 decode error: {0}")]
#[error("Failed to decode private key from base64. The private key in your service account credentials appears to be malformed. Error: {0}")]
Base64Error(#[from] base64::DecodeError),
#[error("JSON error: {0}")]
#[error("Failed to serialize JWT claims as JSON: {0}")]
JsonError(#[from] serde_json::Error),
#[error("Missing window object")]
#[error("WebCrypto API is not available (missing window object). This code must run in a browser or WASM environment.")]
NoWindow,
#[error("Missing crypto API")]
#[error("WebCrypto API is not available (missing crypto API). Your browser may not support the required cryptographic operations.")]
NoCrypto,
#[error("Private key is too short ({0} bytes). Expected at least 100 bytes for a valid RSA private key. This likely indicates the key is invalid, corrupted, or just test data.")]
KeyTooShort(usize),
}

impl From<JsValue> for JwtError {
Expand Down Expand Up @@ -62,12 +64,24 @@ pub async fn encode_jwt(
let signing_input = format!("{header_segment}.{claims_segment}");

// Convert PEM to importable key format
// Remove PEM headers/footers and whitespace (newlines, carriage returns, tabs)
// Note: Do NOT remove spaces as they may be part of valid base64 content
let pem = private_key_pem
.trim()
.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replace('\n', "");
let key_data = STANDARD.decode(pem)?;
.replace("-----BEGIN RSA PRIVATE KEY-----", "")
.replace("-----END RSA PRIVATE KEY-----", "")
// Handle both literal \n strings (from JSON) and actual newline characters
.replace("\\n", "")
.replace(['\n', '\r', '\t'], "");

let key_data = STANDARD.decode(&pem)?;

// Validate key length before attempting to import
if key_data.len() < 100 {
return Err(JwtError::KeyTooShort(key_data.len()));
}

// Import the key
let import_params = Object::new();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,10 @@ export const envKeyValuesAtom = atom(
| { itemIndex: number; remove: true }
// Insert key
| {
itemIndex: null;
key: string;
value?: string;
},
itemIndex: null;
key: string;
value?: string;
},
) => {
if (update.itemIndex !== null) {
const keyValues = [...get(envKeyValueStorage)];
Expand Down Expand Up @@ -365,13 +365,16 @@ export const deleteApiKeyAtom = atom(
delete newVars[key];
return newVars;
});
set(hasLocalChangesAtom, true);
// Remove from recently added keys if it was there
set(recentlyAddedKeysAtom, (prev: Set<string>) => {
const newSet = new Set(prev);
newSet.delete(key);
return newSet;
});
// Auto-save the deletion immediately
const localApiKeys = get(localApiKeysAtom);
set(userApiKeysAtom, localApiKeys);
set(hasLocalChangesAtom, false);
}
);

Expand Down
Loading