dove-core 0.1.1

The shared library behind dove — client-side-encrypted, expiring file sharing from a cloud you own.
Documentation
//! The access-gate Lambda: its embedded source and a helper to package it for
//! deployment. The gate enforces the download policy (DynamoDB) and redirects to
//! a short-lived presigned S3 URL — it never sees the decryption key.
//!
//! Moved from the `dove` CLI's `src/gate.rs` verbatim (assets included) as part
//! of the provisioning extraction — no logic changes.

use anyhow::{Context, Result};
use std::io::Write;
use std::path::Path;
use zip::write::SimpleFileOptions;
use zip::ZipWriter;

/// The gate handler source (Python), embedded so provisioning deploys it with
/// no external files or build step.
pub const SOURCE: &str = include_str!("../../assets/gate.py");

/// The self-contained share/decryptor page (built from dove-site), served by
/// the gate to browsers. Regenerate with: build dove-site, then copy
/// `dist/share/index.html` to `assets/share.html`.
pub const PAGE: &str = include_str!("../../assets/share.html");

/// The self-contained request/upload page (built from dove-site), served by the
/// gate at `/r`. It encrypts the chosen file in the browser (WebCrypto) into the
/// same container `crypto::decrypt` reads, then uploads. Regenerate with: build
/// dove-site, then copy `dist/share/request/index.html` to `assets/request.html`.
pub const REQUEST_PAGE: &str = include_str!("../../assets/request.html");

/// The link-preview image, shipped in the Lambda zip and served at `/og.png`.
pub const OG_PNG: &[u8] = include_bytes!("../../assets/og.png");

/// The request-link preview image, served at `/og-request.png` — a distinct
/// "a file was requested" card so a request unfurl differs from a share's.
pub const OG_REQUEST_PNG: &[u8] = include_bytes!("../../assets/og-request.png");

/// The Lambda handler entrypoint: file `lambda_function.py`, function `handler`.
pub const HANDLER: &str = "lambda_function.handler";
/// The Lambda runtime the gate targets.
pub const RUNTIME: &str = "python3.12";

/// Write a Lambda deployment zip (containing `lambda_function.py`) to `dest`.
pub fn write_deployment_zip(dest: &Path) -> Result<()> {
    let file =
        std::fs::File::create(dest).with_context(|| format!("creating {}", dest.display()))?;
    let mut zip = ZipWriter::new(file);
    zip.start_file("lambda_function.py", SimpleFileOptions::default())?;
    zip.write_all(SOURCE.as_bytes())?;
    zip.start_file("share.html", SimpleFileOptions::default())?;
    zip.write_all(PAGE.as_bytes())?;
    zip.start_file("request.html", SimpleFileOptions::default())?;
    zip.write_all(REQUEST_PAGE.as_bytes())?;
    zip.start_file("og.png", SimpleFileOptions::default())?;
    zip.write_all(OG_PNG)?;
    zip.start_file("og-request.png", SimpleFileOptions::default())?;
    zip.write_all(OG_REQUEST_PNG)?;
    zip.finish()?;
    Ok(())
}

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

    #[test]
    fn embedded_source_is_the_handler() {
        assert!(SOURCE.contains("def handler(event, _context):"));
        assert!(SOURCE.contains("ConditionExpression"));
        assert!(SOURCE.contains("generate_presigned_url"));
    }

    #[test]
    fn request_page_is_self_contained_with_ogbase() {
        // The gate serves one HTML string, so the page must inline its own JS/CSS
        // and only reference the same font @import share.html uses.
        assert!(REQUEST_PAGE.contains("__OGBASE__/og-request.png"));
        assert!(REQUEST_PAGE.contains("async function encrypt")); // real crypto is inlined
        assert!(!REQUEST_PAGE.contains("<script type=\"module\"")); // no bundled/external JS
        assert!(!REQUEST_PAGE.contains("rel=\"stylesheet\"")); // CSS inlined
        assert!(!REQUEST_PAGE.contains("Phil")); // no real name in the served page
    }

    #[test]
    fn deployment_zip_contains_the_handler_file() {
        let mut b = [0u8; 6];
        getrandom::getrandom(&mut b).unwrap();
        let tag: String = b.iter().map(|x| format!("{x:02x}")).collect();
        let dest = std::env::temp_dir().join(format!("dove-gate-{tag}.zip"));
        write_deployment_zip(&dest).unwrap();

        let f = std::fs::File::open(&dest).unwrap();
        let mut archive = zip::ZipArchive::new(f).unwrap();
        assert!(archive.by_name("lambda_function.py").is_ok());
        assert!(archive.by_name("share.html").is_ok());
        assert!(archive.by_name("request.html").is_ok());
        std::fs::remove_file(&dest).ok();
    }
}