mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-21 09:22:41 -04:00
Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1c22c7c47 | ||
|
|
2275526684 | ||
|
|
a7db67900d | ||
|
|
75be0c17a2 | ||
|
|
62ad9c484a | ||
|
|
48f9cd405b | ||
|
|
7febc6a1d8 | ||
|
|
30daca8462 |
Generated
+40
@@ -6496,6 +6496,16 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jwt_light"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
"serde_json",
|
||||
"workspace-build-acceleration",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "k256"
|
||||
version = "0.13.3"
|
||||
@@ -10301,6 +10311,36 @@ dependencies = [
|
||||
"realfft",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "runwayml_client"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
"cookie 0.18.1",
|
||||
"crockford",
|
||||
"errors",
|
||||
"filesys",
|
||||
"hex-literal",
|
||||
"idempotency",
|
||||
"jwt_light",
|
||||
"log",
|
||||
"once_cell",
|
||||
"rand 0.9.1",
|
||||
"reqwest 0.12.15",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"sha3",
|
||||
"subprocess_common",
|
||||
"testing",
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
"toml 0.8.22",
|
||||
"workspace-build-acceleration",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed"
|
||||
version = "8.0.0"
|
||||
|
||||
@@ -34,6 +34,7 @@ members = [
|
||||
"crates/lib/api_clients/google_drive_common",
|
||||
"crates/lib/api_clients/google_sign_in",
|
||||
"crates/lib/api_clients/openai_sora_client",
|
||||
"crates/lib/api_clients/runwayml_client",
|
||||
"crates/lib/api_clients/storyteller_client",
|
||||
"crates/lib/api_clients/thumbnail_generator",
|
||||
"crates/lib/bootstrap",
|
||||
@@ -65,6 +66,7 @@ members = [
|
||||
"crates/lib/idempotency",
|
||||
"crates/lib/image_algorithms",
|
||||
"crates/lib/jobs_common",
|
||||
"crates/lib/jwt_light",
|
||||
"crates/lib/logging",
|
||||
"crates/lib/machine_learning/ml_models",
|
||||
"crates/lib/machine_learning/ml_weights_registry",
|
||||
@@ -191,6 +193,7 @@ fal_client = { path = "crates/lib/api_clients/fal_client" }
|
||||
filesys = { path = "crates/lib/files/filesys" }
|
||||
idempotency = { path = "crates/lib/idempotency" }
|
||||
images = { path = "crates/lib/files/images" }
|
||||
jwt_light = { path = "crates/lib/jwt_light" }
|
||||
memory_caching = { path = "crates/lib/caching/memory_caching" }
|
||||
memory_store = { path = "crates/lib/caching/memory_store" }
|
||||
mimetypes = { path = "crates/lib/files/mimetypes" }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "openai_sora_client"
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
version = "0.0.1"
|
||||
publish = false
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
[package]
|
||||
name = "runwayml_client"
|
||||
edition = "2021"
|
||||
version = "0.0.1"
|
||||
publish = false
|
||||
|
||||
[package.metadata.cargo-udeps.ignore]
|
||||
normal = ["workspace-build-acceleration"]
|
||||
|
||||
[lib]
|
||||
name = "runwayml_client"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
|
||||
# Workspace hack (faster compile times)
|
||||
workspace-build-acceleration = { workspace = true }
|
||||
|
||||
# Internal
|
||||
crockford = { path = "../../data/crockford" }
|
||||
errors = { path = "../../errors" }
|
||||
idempotency = { path = "../../idempotency" }
|
||||
jwt_light = { workspace = true }
|
||||
subprocess_common = { path = "../../subprocess_common" }
|
||||
|
||||
# External
|
||||
anyhow = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
chrono = { workspace = true}
|
||||
cookie = { workspace = true }
|
||||
hex-literal = { workspace = true }
|
||||
log = { workspace = true }
|
||||
once_cell= { workspace = true }
|
||||
rand = { workspace = true }
|
||||
reqwest = { workspace = true, features = ["multipart", "gzip"] }
|
||||
serde = { workspace = true }
|
||||
serde_derive = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
sha3 = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { version = "1.38.0", features = ["macros"] }
|
||||
toml = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
testing = { path = "../../testing" }
|
||||
filesys = { path = "../../files/filesys" }
|
||||
@@ -0,0 +1,16 @@
|
||||
use crate::creds::jwt_bearer_token::JwtBearerToken;
|
||||
use crate::error::api_error::ApiError;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Credentials {
|
||||
pub jwt_bearer_token: Option<JwtBearerToken>,
|
||||
}
|
||||
|
||||
impl Credentials {
|
||||
pub fn from_jwt_str(jwt_str: &str) -> Result<Self, ApiError> {
|
||||
let jwt_bearer_token = JwtBearerToken::new(jwt_str.to_string())?;
|
||||
Ok(Self {
|
||||
jwt_bearer_token: Some(jwt_bearer_token),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
use crate::creds::jwt_claims::JwtClaims;
|
||||
use crate::error::api_error::ApiError;
|
||||
use jwt_light::parse_jwt_claims_trait::ParseJwtClaims;
|
||||
|
||||
/// Sora bearer tokens are JWT tokens that can be minted with a valid cookie/session.
|
||||
#[derive(Clone)]
|
||||
pub struct JwtBearerToken {
|
||||
raw_jwt: String,
|
||||
jwt_claims: JwtClaims,
|
||||
}
|
||||
|
||||
impl JwtBearerToken {
|
||||
pub fn new(raw_jwt: String) -> Result<Self, ApiError> {
|
||||
let jwt_claims = JwtClaims::parse_claims(&raw_jwt)?;
|
||||
Ok(Self {
|
||||
raw_jwt,
|
||||
jwt_claims,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn raw_jwt_str(&self) -> &str {
|
||||
&self.raw_jwt
|
||||
}
|
||||
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
self.raw_jwt.as_bytes()
|
||||
}
|
||||
|
||||
pub fn jwt_claims(&self) -> &JwtClaims {
|
||||
&self.jwt_claims
|
||||
}
|
||||
|
||||
pub fn to_authorization_header_value(&self) -> String {
|
||||
match self.raw_jwt.get(0..6) {
|
||||
Some("bearer") | Some("Bearer") => self.raw_jwt.clone(),
|
||||
_ => "Bearer ".to_owned() + &self.raw_jwt,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
use base64::Engine;
|
||||
use chrono::{DateTime, Utc};
|
||||
use jwt_light::common_claims::CommonClaims;
|
||||
use jwt_light::error::JwtError;
|
||||
use jwt_light::parse_jwt_claims_trait::ParseJwtClaims;
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct JwtClaims {
|
||||
/// From the "iat" field
|
||||
pub created: DateTime<Utc>,
|
||||
/// From the "exp" field
|
||||
pub expiration: DateTime<Utc>,
|
||||
/// From the "id" field
|
||||
pub id: String,
|
||||
/// From the "email" field
|
||||
pub email: String,
|
||||
/// From the "sso" field
|
||||
pub sso: bool,
|
||||
}
|
||||
|
||||
impl ParseJwtClaims for JwtClaims {
|
||||
fn extract_claims(common_claims: CommonClaims, extra_claims: Map<String, Value>) -> Result<Self, JwtError> {
|
||||
let id = extra_claims.get("id")
|
||||
.ok_or_else(|| JwtError::CustomClaimsFieldError("no id claim".to_string()))?
|
||||
.as_i64()
|
||||
.ok_or_else(|| JwtError::CustomClaimsFieldError("id is not a string".to_string()))?
|
||||
.to_string();
|
||||
|
||||
let email = extra_claims.get("email")
|
||||
.ok_or_else(|| JwtError::CustomClaimsFieldError("no email claim".to_string()))?
|
||||
.as_str()
|
||||
.ok_or_else(|| JwtError::CustomClaimsFieldError("email is not a string".to_string()))?
|
||||
.to_string();
|
||||
|
||||
let sso = extra_claims.get("sso")
|
||||
.map(|val| val.as_bool())
|
||||
.flatten()
|
||||
.unwrap_or(false);
|
||||
|
||||
Ok(Self {
|
||||
created: common_claims.created,
|
||||
expiration: common_claims.expiration,
|
||||
id,
|
||||
email,
|
||||
sso,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::creds::jwt_claims::JwtClaims;
|
||||
use jwt_light::parse_jwt_claims_trait::ParseJwtClaims;
|
||||
|
||||
#[test]
|
||||
fn test_token() {
|
||||
/*{
|
||||
"id": 23129405,
|
||||
"email": "echelon@gmail.com",
|
||||
"exp": 1750201256.49,
|
||||
"iat": 1747609256.49,
|
||||
"sso": false
|
||||
}*/
|
||||
let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MjMxMjk0MDUsImVtYWlsIjoiZWNoZWxvbkBnbWFpbC5jb20iLCJleHAiOjE3NTAyMDEyNTYuNDksImlhdCI6MTc0NzYwOTI1Ni40OSwic3NvIjpmYWxzZX0.UxbJozIiHSApqI8_Vl7o2d7q7CzqpXIzsZoazCtY75s";
|
||||
let claims = JwtClaims::parse_claims(jwt).unwrap();
|
||||
|
||||
assert_eq!(claims.id, "23129405");
|
||||
assert_eq!(claims.email, "echelon@gmail.com");
|
||||
assert_eq!(claims.sso, false);
|
||||
assert_eq!(claims.created.timestamp(), 1747609256);
|
||||
assert_eq!(claims.expiration.timestamp(), 1750201256);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod credentials;
|
||||
pub mod jwt_bearer_token;
|
||||
pub mod jwt_claims;
|
||||
@@ -0,0 +1,39 @@
|
||||
use jwt_light::error::JwtError;
|
||||
use reqwest::StatusCode;
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ApiError {
|
||||
/// Inability to parse JWT.
|
||||
JwtError(JwtError),
|
||||
|
||||
/// An error that occurred as or after the request was sent.
|
||||
ReqwestError(reqwest::Error),
|
||||
|
||||
/// Some other failure response.
|
||||
UncategorizedBadResponse{
|
||||
status_code: StatusCode,
|
||||
message : String,
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ApiError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ApiError::JwtError(err) => write!(f, "JwtError: {}", err),
|
||||
ApiError::ReqwestError(err) => write!(f, "ReqwestError: {}", err),
|
||||
ApiError::UncategorizedBadResponse { status_code, message } => {
|
||||
write!(f, "UncategorizedBadResponse: {:?} : {}", status_code, message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for ApiError {}
|
||||
|
||||
impl From<JwtError> for ApiError {
|
||||
fn from(err: JwtError) -> Self {
|
||||
ApiError::JwtError(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
use crate::error::api_error::ApiError;
|
||||
use crate::error::runwayml_error::RunwayMlError;
|
||||
|
||||
/// This assumes the request failed and returned a non-200.
|
||||
/// The caller should check.
|
||||
pub async fn classify_http_error_response(response: reqwest::Response) -> RunwayMlError {
|
||||
let status = response.status();
|
||||
let message = match response.text().await {
|
||||
Ok(text) => text,
|
||||
Err(err) => return RunwayMlError::Api(ApiError::ReqwestError(err)),
|
||||
};
|
||||
|
||||
// TODO: Handle various error messages from the API.
|
||||
|
||||
//anyhow::anyhow!("Upload failed with status {}: {}", status, message))
|
||||
RunwayMlError::Api(ApiError::UncategorizedBadResponse { status_code: status, message })
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ClientError {
|
||||
/// No JWT bearer token in the credentials
|
||||
NoJwtBearerToken,
|
||||
|
||||
/// An error that occurred clientside before the request was sent.
|
||||
ReqwestError(reqwest::Error),
|
||||
}
|
||||
|
||||
impl Display for ClientError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ClientError::NoJwtBearerToken => write!(f, "No JWT bearer token in the credentials"),
|
||||
ClientError::ReqwestError(err) => write!(f, "Reqwest error: {}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for ClientError {}
|
||||
@@ -0,0 +1,4 @@
|
||||
pub mod api_error;
|
||||
pub mod classify_http_error_response;
|
||||
pub mod client_error;
|
||||
pub mod runwayml_error;
|
||||
@@ -0,0 +1,33 @@
|
||||
use crate::error::api_error::ApiError;
|
||||
use crate::error::client_error::ClientError;
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum RunwayMlError {
|
||||
Api(ApiError),
|
||||
Client(ClientError),
|
||||
}
|
||||
|
||||
impl Display for RunwayMlError{
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Api(err) => write!(f, "ApiError: {}", err),
|
||||
Self::Client(err) => write!(f, "ClientError: {}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for RunwayMlError {}
|
||||
|
||||
impl From<ApiError> for RunwayMlError {
|
||||
fn from(err: ApiError) -> Self {
|
||||
RunwayMlError::Api(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ClientError> for RunwayMlError {
|
||||
fn from(err: ClientError) -> Self {
|
||||
RunwayMlError::Client(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
/// TODO DOCS
|
||||
|
||||
pub mod creds;
|
||||
pub mod error;
|
||||
pub mod requests;
|
||||
@@ -0,0 +1,158 @@
|
||||
use crate::creds::credentials::Credentials;
|
||||
use crate::error::api_error::ApiError;
|
||||
use crate::error::classify_http_error_response::classify_http_error_response;
|
||||
use crate::error::client_error::ClientError;
|
||||
use crate::error::runwayml_error::RunwayMlError;
|
||||
use log::{debug, error, warn};
|
||||
use reqwest::Client;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
const GENERATE_URL: &str = "https://api.runwayml.com/v1/tasks";
|
||||
|
||||
// TODO: This endpoint is not implemented yet.
|
||||
// TODO: This endpoint is not implemented yet.
|
||||
// TODO: This endpoint is not implemented yet.
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct TaskRequest {
|
||||
/// eg. "gen4"
|
||||
#[serde(rename = "taskType")]
|
||||
task_type: String,
|
||||
/// eg. false
|
||||
internal: bool,
|
||||
/// inner request payload
|
||||
options: TaskRequestOptions,
|
||||
/// eg. 23129405
|
||||
#[serde(rename = "asTeamId")]
|
||||
as_team_id: u64,
|
||||
/// eg. fc66957b-2651-4362-aca0-5468e4ee61b0
|
||||
#[serde(rename = "sessionId")]
|
||||
session_id: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct TaskRequestOptions {
|
||||
/// eg. Gen-4 3438043024
|
||||
name: String,
|
||||
/// eg. 3438043024
|
||||
seed: u64,
|
||||
/// Probably whether the request is free (exploreMode = unlimited queue)
|
||||
#[serde(rename = "exploreMode")]
|
||||
explore_mode: bool,
|
||||
/// eg. 5
|
||||
seconds: u8,
|
||||
/// eg. false
|
||||
watermark: bool,
|
||||
/// This can be blank / empty string!
|
||||
text_prompt: String,
|
||||
/// eg. "i2v"
|
||||
route: String,
|
||||
/// A URL, eg. https://d2jqrm6oza8nb6.cloudfront.net/datasets/586d77ff-b33b-4100-b4d3-c04f8b046b00.jpg?_jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXlIYXNoIjoiNTJhMTY0N2RkZjFjMGU3NCIsImJ1Y2tldCI6InJ1bndheS1kYXRhc2V0cyIsInN0YWdlIjoicHJvZCIsImV4cCI6MTc0Nzc4NTYwMH0.N_TztqlU2pzPegOq7e8tn_6zb16OLmDmerlTtfr_-FI
|
||||
init_image: String,
|
||||
/// eg. 1280
|
||||
width: u16,
|
||||
/// eg. 720
|
||||
height: u16,
|
||||
/// eg. b5c42f29-d63d-4124-88c0-019ab1299829
|
||||
#[serde(rename = "assetGroupId")]
|
||||
asset_group_id: String,
|
||||
}
|
||||
|
||||
pub async fn gen4_image_to_video(
|
||||
credentials: &Credentials,
|
||||
) -> Result<(), RunwayMlError> {
|
||||
|
||||
let jwt = match credentials.jwt_bearer_token.as_ref() {
|
||||
Some(token) => token,
|
||||
None => {
|
||||
error!("Failed to generate bearer token. No JWT bearer token found.");
|
||||
return Err(ClientError::NoJwtBearerToken.into());
|
||||
}
|
||||
};
|
||||
|
||||
let endpoint_request = TaskRequest {
|
||||
task_type: "gen4".to_string(),
|
||||
internal: false,
|
||||
options: TaskRequestOptions {
|
||||
name: "Gen-4 3438043025".to_string(),
|
||||
seed: 3438043025,
|
||||
explore_mode: true,
|
||||
seconds: 5,
|
||||
watermark: false,
|
||||
text_prompt: "".to_string(),
|
||||
route: "i2v".to_string(),
|
||||
init_image: "https://d2jqrm6oza8nb6.cloudfront.net/datasets/586d77ff-b33b-4100-b4d3-c04f8b046b00.jpg?_jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXlIYXNoIjoiNTJhMTY0N2RkZjFjMGU3NCIsImJ1Y2tldCI6InJ1bndheS1kYXRhc2V0cyIsInN0YWdlIjoicHJvZCIsImV4cCI6MTc0Nzc4NTYwMH0.N_TztqlU2pzPegOq7e8tn_6zb16OLmDmerlTtfr_-FI".to_string(),
|
||||
width: 1280,
|
||||
height: 720,
|
||||
asset_group_id: "b5c42f29-d63d-4124-88c0-019ab1299829".to_string(),
|
||||
},
|
||||
as_team_id: 23129405,
|
||||
session_id: "fc66957b-2651-4362-aca0-5468e4ee61b0".to_string(),
|
||||
};
|
||||
|
||||
let client = Client::builder()
|
||||
.gzip(true)
|
||||
.build()
|
||||
.map_err(|err| {
|
||||
error!("Failed to create HTTP client: {}", err);
|
||||
RunwayMlError::Client(ClientError::ReqwestError(err))
|
||||
})?;
|
||||
|
||||
let http_request = client.post(GENERATE_URL)
|
||||
.header("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:137.0) Gecko/20100101 Firefox/137.0")
|
||||
.header("Accept", "application/json")
|
||||
.header("Accept-Encoding", "gzip, deflate, br")
|
||||
.header("Accept-Language", "en-US,en;q=0.5")
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Authorization", jwt.to_authorization_header_value());
|
||||
|
||||
let http_request = http_request.json(&endpoint_request)
|
||||
.build()
|
||||
.map_err(|err| RunwayMlError::Client(ClientError::ReqwestError(err)))?;
|
||||
|
||||
let response = client.execute(http_request)
|
||||
.await
|
||||
.map_err(|err| RunwayMlError::Api(ApiError::ReqwestError(err)))?;
|
||||
|
||||
let status = response.status();
|
||||
|
||||
if !status.is_success() {
|
||||
warn!("Gen4 image to video failure. Status: {:?}", status);
|
||||
|
||||
let error = classify_http_error_response(response).await;
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
debug!("Gen4 I2V returned a 200.");
|
||||
|
||||
let response_body = &response
|
||||
.text()
|
||||
.await
|
||||
.map_err(|err| RunwayMlError::Api(ApiError::ReqwestError(err)))?;
|
||||
|
||||
debug!("Response: {}", response_body);
|
||||
|
||||
//if &response_body == "null" {
|
||||
// error!("Failed to generate bearer token. Response was `null`.");
|
||||
// return Err(SoraError::FailedToGenerateBearer)
|
||||
//}
|
||||
|
||||
//let auth_response: SoraAuthResponse = serde_json::from_str(&response_body)?;
|
||||
|
||||
// TODO: This endpoint is not implemented yet.
|
||||
// TODO: This endpoint is not implemented yet.
|
||||
// TODO: This endpoint is not implemented yet.
|
||||
|
||||
todo!("This is not yet implemented")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use errors::AnyhowResult;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn send_test_request() -> AnyhowResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod gen4_image_to_video;
|
||||
@@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "jwt_light"
|
||||
edition = "2024"
|
||||
version = "0.0.1"
|
||||
publish = false
|
||||
|
||||
[package.metadata.cargo-udeps.ignore]
|
||||
normal = ["workspace-build-acceleration"]
|
||||
|
||||
[lib]
|
||||
name = "jwt_light"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
|
||||
# Workspace hack (faster compile times)
|
||||
workspace-build-acceleration = { workspace = true }
|
||||
|
||||
# Internal
|
||||
# (None)
|
||||
|
||||
# External
|
||||
base64 = { workspace = true }
|
||||
chrono = { workspace = true}
|
||||
serde_json = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
# (None)
|
||||
@@ -0,0 +1,109 @@
|
||||
use crate::error::JwtError;
|
||||
use crate::utils::json_number_to_datetime::json_number_to_datetime;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde_json::Value;
|
||||
|
||||
pub struct CommonClaims {
|
||||
/// From the "iat" field
|
||||
pub created: DateTime<Utc>,
|
||||
|
||||
/// From the "exp" field
|
||||
pub expiration: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl CommonClaims {
|
||||
pub fn from_json(raw_json_claims: &str) -> Result<Self, JwtError> {
|
||||
let claims : serde_json::Map<String, Value> = serde_json::from_str(&raw_json_claims)
|
||||
.map_err(|err| JwtError::CommonFieldError(
|
||||
format!("failure to parse claims json: {:?}", err)))?;
|
||||
|
||||
// NB: Some JWTs have "iat" and "exp" as integer numbers, some have them as floats
|
||||
let iat = {
|
||||
let iat = claims.get("iat")
|
||||
.map(|val| val.as_number())
|
||||
.flatten()
|
||||
.ok_or_else(|| JwtError::CommonFieldError("no iat claim".to_string()))?;
|
||||
|
||||
json_number_to_datetime(iat, "iat")?
|
||||
};
|
||||
|
||||
let exp = {
|
||||
let exp = claims.get("exp")
|
||||
.map(|val| val.as_number())
|
||||
.flatten()
|
||||
.ok_or_else(|| JwtError::CommonFieldError("no exp claim".to_string()))?;
|
||||
|
||||
json_number_to_datetime(exp, "exp")?
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
created: iat,
|
||||
expiration: exp,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::common_claims::CommonClaims;
|
||||
use crate::utils::raw_jwt_to_raw_json::raw_jwt_to_raw_json;
|
||||
use chrono::DateTime;
|
||||
|
||||
#[test]
|
||||
fn test_runwayml() {
|
||||
let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MjMxMjk0MDUsImVtYWlsIjoiZWNoZWxvbkBnbWFpbC5jb20iLCJleHAiOjE3NTAyMDEyNTYuNDksImlhdCI6MTc0NzYwOTI1Ni40OSwic3NvIjpmYWxzZX0.UxbJozIiHSApqI8_Vl7o2d7q7CzqpXIzsZoazCtY75s";
|
||||
|
||||
let json = raw_jwt_to_raw_json(jwt).unwrap();
|
||||
|
||||
let claims = CommonClaims::from_json(&json).unwrap();
|
||||
|
||||
// jwt payload:
|
||||
// {
|
||||
// "id": 23129405,
|
||||
// "email": "echelon@gmail.com",
|
||||
// "exp": 1750201256.49,
|
||||
// "iat": 1747609256.49,
|
||||
// "sso": false
|
||||
// }
|
||||
|
||||
assert_eq!(claims.created, DateTime::from_timestamp(1747609256, 0).unwrap());
|
||||
assert_eq!(claims.expiration, DateTime::from_timestamp(1750201256, 0).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sora() {
|
||||
let token =
|
||||
"eyJhbGciOiJSUzI1NiIsImtpZCI6IjE5MzQ0ZTY1LWJiYzktNDRkMS1hOWQwLWY5NTdiMDc5YmQwZSIsInR5cCI6Ik\
|
||||
pXVCJ9.eyJhdWQiOlsiaHR0cHM6Ly9hcGkub3BlbmFpLmNvbS92MSJdLCJjbGllbnRfaWQiOiJhcHBfTTFuUTN0UjV2\
|
||||
VzdYOWpMMnBFNmdIOGRLNCIsImV4cCI6MTc0NTgwMDQ3MiwiaHR0cHM6Ly9hcGkub3BlbmFpLmNvbS9hdXRoIjp7InV\
|
||||
zZXJfaWQiOiJ1c2VyLTZOeEpmbEFIb0VCREp6Wmw5aVhocWJERyJ9LCJodHRwczovL2FwaS5vcGVuYWkuY29tL3Byb2\
|
||||
ZpbGUiOnsiZW1haWwiOiJ2b2NvZGVzMjAyMEBnbWFpbC5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZX0sImlhdCI6M\
|
||||
Tc0NDkzNjQ3MSwiaXNzIjoiaHR0cHM6Ly9hdXRoLm9wZW5haS5jb20iLCJqdGkiOiI0NTZmZjY5Yi0xZTZhLTQzOWYt\
|
||||
YTYzMC1jNmFjYzI3NzU1YjAiLCJuYmYiOjE3NDQ5MzY0NzEsInB3ZF9hdXRoX3RpbWUiOjE3NDQwNTkzMTc3NTAsInN\
|
||||
jcCI6WyJvcGVuaWQiLCJlbWFpbCIsInByb2ZpbGUiLCJvZmZsaW5lX2FjY2VzcyJdLCJzZXNzaW9uX2lkIjoiYXV0aH\
|
||||
Nlc3NfcEE4UGl3V0trNmJsSE1qQnVCQnVSRmh2Iiwic3ViIjoiZ29vZ2xlLW9hdXRoMnwxMTMxMDE5Njc2MTIzOTY3O\
|
||||
TM3NzcifQ.JOxYfrQSIsHdf-VN1zcOqDOS3tF1HpUYnRVfPKgi65am3sNiniOM1kWTNrKE5-sh4DaZ33G_XniGkhg-9\
|
||||
QKF8tspXpZ6BnTNkf-QdGqR_v89AIO8pZgQ7oECLXhnmg-31G-LAHlGfwWedJ6qPVy4dL5zoR0wVyoQ8gYv5QzQC-zd\
|
||||
IF7uY-umKKloAAYP_tkKLlQHackjTynN-bpu2mKv55-h7a3hSEPfxvgX0SsvL69xtNsZkwb43bqyP1x_c2ErfYoPyzg\
|
||||
rqgMIRZjCw9TI33vISKM_BfQXBiZT_BHkX4s0Jng3ph4vrwvnBsa0HpNxBhQYKb1u9gZXs7XjLZhjZJZsDTQI1V4Wov\
|
||||
iAL7ihvZkLxJRtDCOmX7-5BTEsMInFgXUAhCDhQUMwSfLLJuinBS96NGSX5_TfIVMkbCl-HWzdQx7KZAsahNe_CmuYT\
|
||||
lZH32NuRn78ohY4eViRyfC8OXVLwoLxxpen2CWlV2dxV5wwXeij_kHt5wOXSGYpSfxz37cK0CaK8K2pwudasxQ0lv2V\
|
||||
_Xx_z4UaEOr5wqT_BK1A1HO4HRjvFX2soF-qm4GCSWAbmFZs7DVEClrotxRx2romifRVb8XRia2M6YTmDyIT0YphFgG\
|
||||
nnrDIZNKO23uDYT60LVMVYpCCY2RiwxCR9bn7AeGbmi7cpL9aYZ5uJRM";
|
||||
|
||||
let json = raw_jwt_to_raw_json(token).unwrap();
|
||||
|
||||
// jwt payload :
|
||||
// - "https://api.openai.com/auth".user_id = "user-6NxJflAHoEBDJzZl9iXhqbDG"
|
||||
// - "https://api.openai.com/profile".email = "vocodes2020@gmail.com"
|
||||
// - "https://api.openai.com/profile".email_verified = true
|
||||
// - "iat": 1744936471, - issued at claim (5 days ago)
|
||||
// - "nbf": 1744936471, - not before, don't use before date
|
||||
// - "exp": 1745800472, - in 5 days, expiry
|
||||
|
||||
let claims = CommonClaims::from_json(&json).unwrap();
|
||||
|
||||
assert_eq!(claims.created, DateTime::from_timestamp(1744936471, 0).unwrap());
|
||||
assert_eq!(claims.expiration, DateTime::from_timestamp(1745800472, 0).unwrap());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum JwtError {
|
||||
/// Generic error parsing the JWT
|
||||
ParseError(String),
|
||||
/// Error parsing the JWT JSON payload specifically
|
||||
JsonParseError(serde_json::Error),
|
||||
/// Error decoding the base64 payload
|
||||
Base64DecodeError(base64::DecodeError),
|
||||
/// Error decoding from UTF-8 (should be exceedingly rare)
|
||||
Utf8Error(std::string::FromUtf8Error),
|
||||
/// Error parsing a common field (iat, exp, etc.)
|
||||
CommonFieldError(String),
|
||||
/// Error parsing a custom claims field.
|
||||
CustomClaimsFieldError(String),
|
||||
}
|
||||
|
||||
impl Error for JwtError {}
|
||||
|
||||
impl Display for JwtError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::ParseError(reason) => { write!(f, "JwtError::ParseError : {}", reason) }
|
||||
Self::JsonParseError(err) => { write!(f, "JwtError::JsonParseError : {:?}", err) }
|
||||
Self::Base64DecodeError(err) => { write!(f, "JwtError::Base64DecodeError : {:?}", err) }
|
||||
Self::Utf8Error(err) => { write!(f, "JwtError::Utf8Error : {:?}", err) }
|
||||
Self::CommonFieldError(reason) => { write!(f, "JwtError::CommonFieldError : {}", reason) }
|
||||
Self::CustomClaimsFieldError(reason) => { write!(f, "JwtError::CustomClaimsFieldError : {}", reason) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for JwtError {
|
||||
fn from(err: serde_json::Error) -> Self {
|
||||
JwtError::JsonParseError(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<base64::DecodeError> for JwtError {
|
||||
fn from(err: base64::DecodeError) -> Self {
|
||||
JwtError::Base64DecodeError(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::string::FromUtf8Error> for JwtError {
|
||||
fn from(err: std::string::FromUtf8Error) -> Self {
|
||||
JwtError::Utf8Error(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
//! Lightweight JWT library for Rust.
|
||||
//! No cryptographic verification, no OpenSSL, etc.
|
||||
//!
|
||||
//! DO NOT USE THIS FOR OUR OWN JWT VERIFICATION !!!
|
||||
//! It's okay to parse external vendors since we're not validating, but
|
||||
//! do not ever use this to secure our own server.
|
||||
|
||||
pub (crate) mod utils;
|
||||
|
||||
pub mod common_claims;
|
||||
pub mod error;
|
||||
pub mod parse_jwt_claims_trait;
|
||||
@@ -0,0 +1,27 @@
|
||||
use crate::common_claims::CommonClaims;
|
||||
use crate::error::JwtError;
|
||||
use crate::utils::decode_base64::decode_base64;
|
||||
use crate::utils::split_components::split_components;
|
||||
use serde_json::Value;
|
||||
|
||||
/// Trait for simple JWT claims parsing
|
||||
pub trait ParseJwtClaims : Sized {
|
||||
/// Parse any special fields relevant to the struct
|
||||
/// Implement this method to complete the parsing.
|
||||
fn extract_claims(
|
||||
common_claims: CommonClaims,
|
||||
extra_claims: serde_json::Map<String, Value>,
|
||||
) -> Result<Self, JwtError>;
|
||||
|
||||
fn parse_claims(raw_jwt: &str) -> Result<Self, JwtError> {
|
||||
let [_header_str, claims_str, _signature_str] = split_components(raw_jwt)?;
|
||||
|
||||
let raw_json = decode_base64(claims_str)?;
|
||||
|
||||
let common_claims = CommonClaims::from_json(&raw_json)?;
|
||||
|
||||
let extra_claims : serde_json::Map<String, Value> = serde_json::from_str(&raw_json)?;
|
||||
|
||||
Self::extract_claims(common_claims, extra_claims)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
use crate::error::JwtError;
|
||||
use base64::prelude::BASE64_URL_SAFE_NO_PAD;
|
||||
use base64::Engine;
|
||||
|
||||
pub fn decode_base64(raw: &str) -> Result<String, JwtError> {
|
||||
let json_bytes = BASE64_URL_SAFE_NO_PAD.decode(raw)?;
|
||||
let json_str = String::from_utf8(json_bytes.clone())?;
|
||||
Ok(json_str)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
use crate::error::JwtError;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde_json::Number;
|
||||
|
||||
// NB: Some JWTs have "iat" and "exp" as integer numbers, some have them as floats
|
||||
pub (crate) fn json_number_to_datetime(timestamp: &Number, field_name: &str) -> Result<DateTime<Utc>, JwtError> {
|
||||
let result;
|
||||
|
||||
if timestamp.is_i64() {
|
||||
// Integer case
|
||||
let timestamp = timestamp.as_i64()
|
||||
.ok_or_else(|| JwtError::CommonFieldError(format!("{} is not an integer", field_name)))?;
|
||||
|
||||
result = DateTime::from_timestamp(timestamp, 0)
|
||||
.ok_or_else(|| JwtError::CommonFieldError(format!("{} is not a valid timestamp", field_name)))?;
|
||||
} else if timestamp.is_f64() {
|
||||
// Float case
|
||||
let timestamp = timestamp.as_f64()
|
||||
.ok_or_else(|| JwtError::CommonFieldError(format!("{} is not an integer", field_name)))?;
|
||||
|
||||
let timestamp = timestamp.round() as i64;
|
||||
|
||||
result = DateTime::from_timestamp(timestamp, 0)
|
||||
.ok_or_else(|| JwtError::CommonFieldError(format!("{} is not a valid timestamp", field_name)))?;
|
||||
} else {
|
||||
return Err(JwtError::CommonFieldError(format!("{} is not a number", field_name)));
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
pub (crate) mod decode_base64;
|
||||
pub (crate) mod json_number_to_datetime;
|
||||
pub (crate) mod raw_jwt_to_raw_json;
|
||||
pub (crate) mod split_components;
|
||||
@@ -0,0 +1,9 @@
|
||||
use crate::error::JwtError;
|
||||
use crate::utils::decode_base64::decode_base64;
|
||||
use crate::utils::split_components::split_components;
|
||||
|
||||
pub (crate) fn raw_jwt_to_raw_json(raw_jwt: &str) -> Result<String, JwtError> {
|
||||
let [_header_str, claims_str, _signature_str] = split_components(raw_jwt)?;
|
||||
let raw_json = decode_base64(claims_str)?;
|
||||
Ok(raw_json)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
use crate::error::JwtError;
|
||||
|
||||
const SEPARATOR: &str = ".";
|
||||
|
||||
pub fn split_components(token: &str) -> Result<[&str; 3], JwtError> {
|
||||
let mut components = token.split(SEPARATOR);
|
||||
let header = components.next()
|
||||
.ok_or_else(|| jwt_error("no header component"))?;
|
||||
|
||||
let claims = components.next()
|
||||
.ok_or_else(|| jwt_error("no claims component"))?;
|
||||
|
||||
let signature = components.next()
|
||||
.ok_or_else(|| jwt_error("no signature component"))?;
|
||||
|
||||
if components.next().is_some() {
|
||||
return Err(jwt_error("too many components"));
|
||||
}
|
||||
|
||||
Ok([header, claims, signature])
|
||||
}
|
||||
|
||||
fn jwt_error(reason: &str) -> JwtError {
|
||||
JwtError::ParseError(reason.to_string())
|
||||
}
|
||||
Reference in New Issue
Block a user