239 lines
5.9 KiB
Rust
239 lines
5.9 KiB
Rust
use std::collections::HashMap;
|
|
|
|
use blake3::Hash;
|
|
use chrono::{DateTime, Utc};
|
|
use ed25519_dalek::Signature;
|
|
use serde::{Deserialize, Serialize};
|
|
use url::Url;
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct BundleMetadata {
|
|
pub version: String,
|
|
pub created_at: DateTime<Utc>,
|
|
#[serde(with = "signature_serde")]
|
|
pub signature: Signature,
|
|
pub manifest: Manifest,
|
|
#[serde(default)]
|
|
pub properties: HashMap<String, String>,
|
|
}
|
|
|
|
mod signature_serde {
|
|
use super::*;
|
|
use base64::{engine::general_purpose::STANDARD, Engine};
|
|
use serde::{de::Error, Deserializer, Serializer};
|
|
|
|
pub fn serialize<S>(sig: &Signature, serializer: S) -> Result<S::Ok, S::Error>
|
|
where
|
|
S: Serializer,
|
|
{
|
|
serializer.serialize_str(&STANDARD.encode(sig.to_bytes()))
|
|
}
|
|
|
|
pub fn deserialize<'de, D>(deserializer: D) -> Result<Signature, D::Error>
|
|
where
|
|
D: Deserializer<'de>,
|
|
{
|
|
let s = String::deserialize(deserializer)?;
|
|
let bytes = STANDARD.decode(&s).map_err(D::Error::custom)?;
|
|
let bytes: [u8; 64] = bytes
|
|
.try_into()
|
|
.map_err(|_| D::Error::custom("invalid signature length"))?;
|
|
Ok(Signature::from_bytes(&bytes))
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct FileEntry {
|
|
pub path: String,
|
|
pub size: u64,
|
|
#[serde(with = "hash_serde")]
|
|
pub hash: Hash,
|
|
pub mime_type: Option<String>,
|
|
}
|
|
|
|
mod hash_serde {
|
|
use super::*;
|
|
use base64::{engine::general_purpose::STANDARD, Engine};
|
|
use blake3::Hash;
|
|
|
|
pub fn serialize<S>(sig: &Hash, serializer: S) -> Result<S::Ok, S::Error>
|
|
where
|
|
S: serde::Serializer,
|
|
{
|
|
serializer.serialize_str(&STANDARD.encode(sig.as_bytes()))
|
|
}
|
|
|
|
pub fn deserialize<'de, D>(deserializer: D) -> Result<Hash, D::Error>
|
|
where
|
|
D: serde::Deserializer<'de>,
|
|
{
|
|
use serde::de::Error;
|
|
let s = String::deserialize(deserializer)?;
|
|
let bytes = STANDARD.decode(&s).map_err(D::Error::custom)?;
|
|
let bytes: [u8; 32] = bytes
|
|
.try_into()
|
|
.map_err(|_| D::Error::custom("invalid signature length"))?;
|
|
Ok(Hash::from_bytes(bytes))
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Manifest {
|
|
pub files: Vec<FileEntry>,
|
|
#[serde(default)]
|
|
pub version: Option<String>,
|
|
}
|
|
|
|
impl Manifest {
|
|
pub fn total_size(&self) -> u64 {
|
|
self.files.iter().map(|f| f.size).sum()
|
|
}
|
|
|
|
pub fn get_file(&self, path: &str) -> Option<&FileEntry> {
|
|
self.files.iter().find(|f| f.path == path)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
pub struct ApiResponse<T> {
|
|
pub success: bool,
|
|
#[serde(default)]
|
|
pub error: Option<String>,
|
|
pub data: T,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
pub struct BundleList {
|
|
pub bundles: Vec<BundleSummary>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
pub struct BundleSummary {
|
|
pub name: String,
|
|
pub version: String,
|
|
pub created_at: DateTime<Utc>,
|
|
pub file_count: usize,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
pub struct VerificationResponse {
|
|
pub status: String,
|
|
pub created_at: DateTime<Utc>,
|
|
pub version: String,
|
|
pub file_count: usize,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
pub struct PublicKeyInfo {
|
|
pub key: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct DownloadOptions {
|
|
pub server_url: Url,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct DownloadResponse {
|
|
pub success: bool,
|
|
pub bundle_name: String,
|
|
pub server_url: Url,
|
|
pub version: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct LoadOptions {
|
|
pub bundle_name: String,
|
|
/// Optional host override for the webview URL.
|
|
///
|
|
/// When provided, the webview will be loaded with `app://{host}/` instead of
|
|
/// `app://{bundle_name}/`. This enables cloud-for-orgs support where the same
|
|
/// bundle serves multiple organization subdomains.
|
|
///
|
|
/// Example: `host: "acme.hoppscotch.io"` will:
|
|
/// - Sanitize to "acme_hoppscotch_io"
|
|
/// - Create webview at `app://acme_hoppscotch_io/`
|
|
/// - Register mapping so file requests resolve to the correct bundle
|
|
#[serde(default)]
|
|
pub host: Option<String>,
|
|
#[serde(default)]
|
|
pub inline: bool,
|
|
#[serde(default)]
|
|
pub window: WindowOptions,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct LoadResponse {
|
|
pub success: bool,
|
|
pub window_label: String,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct CloseOptions {
|
|
pub window_label: String,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct CloseResponse {
|
|
pub success: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct RemoveOptions {
|
|
pub bundle_name: String,
|
|
pub server_url: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct RemoveResponse {
|
|
pub success: bool,
|
|
pub bundle_name: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct WindowOptions {
|
|
#[serde(default = "default_window_title")]
|
|
pub title: String,
|
|
#[serde(default = "default_window_width")]
|
|
pub width: f64,
|
|
#[serde(default = "default_window_height")]
|
|
pub height: f64,
|
|
#[serde(default = "default_resizable")]
|
|
pub resizable: bool,
|
|
}
|
|
|
|
fn default_window_title() -> String {
|
|
"Appload".into()
|
|
}
|
|
|
|
fn default_window_width() -> f64 {
|
|
800.0
|
|
}
|
|
|
|
fn default_window_height() -> f64 {
|
|
600.0
|
|
}
|
|
|
|
fn default_resizable() -> bool {
|
|
true
|
|
}
|
|
|
|
impl Default for WindowOptions {
|
|
fn default() -> Self {
|
|
Self {
|
|
title: default_window_title(),
|
|
width: default_window_width(),
|
|
height: default_window_height(),
|
|
resizable: default_resizable(),
|
|
}
|
|
}
|
|
}
|