feat: add PGP public key page with multiple access endpoints

- Add dedicated /pgp page with key viewer and download options
- Support CLI-friendly endpoints (/publickey.asc, /pgp.asc, /.well-known/pgpkey.asc)
- Detect user-agent to serve raw key to curl/wget or HTML to browsers
- Add modal component for quick key access from homepage
- Embed static key file in Rust assets for efficient serving
This commit is contained in:
2026-01-06 21:35:41 -06:00
parent 5c4d3b6efa
commit 80061aad7a
9 changed files with 505 additions and 5 deletions
+14
View File
@@ -55,6 +55,20 @@ fn serve_asset_by_path(path: &str) -> Response {
}
}
/// Get a static file from the embedded CLIENT_ASSETS.
///
/// Static files are served from web/static/ and embedded at compile time.
///
/// # Arguments
/// * `path` - Path to the file (e.g., "publickey.asc")
///
/// # Returns
/// * `Some(&[u8])` - File content if file exists
/// * `None` - If file not found
pub fn get_static_file(path: &str) -> Option<&'static [u8]> {
CLIENT_ASSETS.get_file(path).map(|f| f.contents())
}
/// Get prerendered error page HTML for a given status code.
///
/// Error pages are prerendered by SvelteKit and embedded at compile time.
+82
View File
@@ -211,6 +211,11 @@ async fn main() {
"/_app/{*path}",
axum::routing::get(serve_embedded_asset).head(serve_embedded_asset),
)
.route("/pgp", axum::routing::get(handle_pgp_route))
.route("/publickey.asc", axum::routing::get(serve_pgp_key))
.route("/pgp.asc", axum::routing::get(serve_pgp_key))
.route("/.well-known/pgpkey.asc", axum::routing::get(serve_pgp_key))
.route("/keys", axum::routing::get(redirect_to_pgp))
}
fn apply_middleware(
@@ -409,6 +414,44 @@ fn accepts_html(headers: &HeaderMap) -> bool {
true
}
/// Determines if request prefers raw content (CLI tools) over HTML
fn prefers_raw_content(headers: &HeaderMap) -> bool {
// Check User-Agent for known CLI tools first (most reliable)
if let Some(ua) = headers.get(axum::http::header::USER_AGENT) {
if let Ok(ua_str) = ua.to_str() {
let ua_lower = ua_str.to_lowercase();
if ua_lower.starts_with("curl/")
|| ua_lower.starts_with("wget/")
|| ua_lower.starts_with("httpie/")
|| ua_lower.contains("curlie")
{
return true;
}
}
}
// Check Accept header - if it explicitly prefers text/html, serve HTML
if let Some(accept) = headers.get(axum::http::header::ACCEPT) {
if let Ok(accept_str) = accept.to_str() {
// If text/html appears before */* in the list, they prefer HTML
if let Some(html_pos) = accept_str.find("text/html") {
if let Some(wildcard_pos) = accept_str.find("*/*") {
return html_pos > wildcard_pos;
}
// Has text/html but no */* → prefers HTML
return false;
}
// Has */* but no text/html → probably a CLI tool
if accept_str.contains("*/*") && !accept_str.contains("text/html") {
return true;
}
}
}
// No Accept header → assume browser (safer default)
false
}
fn serve_error_page(status: StatusCode) -> Response {
let status_code = status.as_u16();
@@ -460,6 +503,45 @@ async fn health_handler(State(state): State<Arc<AppState>>) -> impl IntoResponse
}
}
async fn serve_pgp_key() -> impl IntoResponse {
if let Some(content) = assets::get_static_file("publickey.asc") {
let mut headers = HeaderMap::new();
headers.insert(
axum::http::header::CONTENT_TYPE,
axum::http::HeaderValue::from_static("application/pgp-keys"),
);
headers.insert(
axum::http::header::CONTENT_DISPOSITION,
axum::http::HeaderValue::from_static("attachment; filename=\"publickey.asc\""),
);
headers.insert(
axum::http::header::CACHE_CONTROL,
axum::http::HeaderValue::from_static("public, max-age=86400"),
);
(StatusCode::OK, headers, content).into_response()
} else {
(StatusCode::NOT_FOUND, "PGP key not found").into_response()
}
}
async fn redirect_to_pgp() -> impl IntoResponse {
axum::response::Redirect::permanent("/pgp")
}
async fn handle_pgp_route(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
req: Request,
) -> Response {
if prefers_raw_content(&headers) {
// Serve raw .asc file for CLI tools
serve_pgp_key().await.into_response()
} else {
// Proxy to Bun for HTML page
isr_handler(State(state), req).await
}
}
async fn api_404_and_method_handler(req: Request) -> impl IntoResponse {
let method = req.method();
let uri = req.uri();
+5 -5
View File
@@ -21,12 +21,12 @@
<div class={cn("pointer-events-none fixed inset-0 -z-20 bg-white dark:bg-black transition-colors duration-300", bgColor)}></div>
<Dots class={[backgroundClass]} />
{#if showThemeToggle}
<div class="fixed top-5 right-6 z-50">
<ThemeToggle />
</div>
{/if}
<main class={cn("relative min-h-screen text-zinc-900 dark:text-zinc-50 transition-colors duration-300", className)}>
{#if showThemeToggle}
<div class="absolute top-5 right-6 z-50">
<ThemeToggle />
</div>
{/if}
{#if children}
{@render children()}
{/if}
+164
View File
@@ -0,0 +1,164 @@
<script lang="ts">
import { PGP_KEY_METADATA } from "$lib/pgp/key-info";
import { OverlayScrollbarsComponent } from "overlayscrollbars-svelte";
import "overlayscrollbars/overlayscrollbars.css";
import IconDownload from "~icons/material-symbols/download-rounded";
import IconCopy from "~icons/material-symbols/content-copy-rounded";
import { fade, scale } from "svelte/transition";
interface Props {
open: boolean;
}
let { open = $bindable(false) }: Props = $props();
let copySuccess = $state(false);
let keyContent = $state<string>("");
let loading = $state(false);
// Fetch key content when modal opens
$effect(() => {
if (open && !keyContent && !loading) {
loading = true;
fetch("/publickey.asc")
.then((res) => res.text())
.then((text) => {
keyContent = text;
loading = false;
})
.catch((err) => {
console.error("Failed to fetch PGP key:", err);
loading = false;
});
}
});
function handleBackdropClick(e: MouseEvent) {
if (e.target === e.currentTarget) {
open = false;
}
}
function handleClose() {
open = false;
}
async function copyToClipboard() {
if (!keyContent) return;
try {
await navigator.clipboard.writeText(keyContent);
copySuccess = true;
setTimeout(() => {
copySuccess = false;
}, 2000);
} catch (err) {
console.error("Failed to copy:", err);
}
}
function downloadKey() {
const a = document.createElement("a");
a.href = "/publickey.asc";
a.download = "publickey.asc";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
</script>
{#if open}
<div
class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-[2px] p-4"
onclick={handleBackdropClick}
onkeydown={(e) => e.key === "Escape" && handleClose()}
role="presentation"
tabindex="-1"
transition:fade={{ duration: 200 }}
>
<div
class="relative w-full max-w-2xl rounded-lg bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 p-6 shadow-xl"
role="dialog"
aria-modal="true"
aria-labelledby="pgp-modal-title"
transition:scale={{ duration: 200, start: 0.95 }}
>
<div class="flex items-start justify-between mb-4">
<h2 id="pgp-modal-title" class="text-xl font-semibold text-zinc-900 dark:text-white">
PGP Public Key
</h2>
<button
onclick={handleClose}
class="text-zinc-500 hover:text-zinc-700 dark:text-zinc-400 dark:hover:text-zinc-200 transition-colors"
aria-label="Close modal"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
<!-- Fingerprint -->
<div class="mb-4 p-3 bg-zinc-100 dark:bg-zinc-800 rounded border border-zinc-200 dark:border-zinc-700">
<div class="text-xs font-medium text-zinc-600 dark:text-zinc-400 mb-1">
Fingerprint
</div>
<div class="font-mono text-sm text-zinc-900 dark:text-zinc-100 break-all">
{PGP_KEY_METADATA.fingerprint}
</div>
</div>
<!-- Key Content -->
<div class="mb-4 border border-zinc-200 dark:border-zinc-700 rounded overflow-hidden">
{#if loading}
<div class="p-4 text-center text-zinc-600 dark:text-zinc-400">
Loading key...
</div>
{:else if keyContent}
<OverlayScrollbarsComponent
options={{
scrollbars: { autoHide: "leave", autoHideDelay: 800 },
}}
defer
style="max-height: 400px"
>
<pre
class="p-4 text-xs font-mono text-zinc-800 dark:text-zinc-200 bg-zinc-50 dark:bg-zinc-900/50 overflow-x-auto">{keyContent}</pre>
</OverlayScrollbarsComponent>
{:else}
<div class="p-4 text-center text-zinc-600 dark:text-zinc-400">
Failed to load key
</div>
{/if}
</div>
<!-- Action Buttons -->
<div class="flex gap-3 justify-end">
<button
onclick={downloadKey}
class="flex items-center gap-2 px-4 py-2 rounded-sm bg-zinc-100 dark:bg-zinc-800 text-zinc-800 dark:text-zinc-100 hover:bg-zinc-200 dark:hover:bg-zinc-700 transition-colors"
>
<IconDownload class="size-4" />
<span class="text-sm font-medium">Download</span>
</button>
<button
onclick={copyToClipboard}
class="flex items-center gap-2 px-4 py-2 rounded-sm bg-zinc-900 dark:bg-zinc-100 text-white dark:text-zinc-900 hover:bg-zinc-800 dark:hover:bg-zinc-200 transition-colors"
>
<IconCopy class="size-4" />
<span class="text-sm font-medium">{copySuccess ? "Copied!" : "Copy to Clipboard"}</span>
</button>
</div>
</div>
</div>
{/if}
+6
View File
@@ -0,0 +1,6 @@
export const PGP_KEY_METADATA = {
fingerprint: '211D 7157 249B F07D 81C8 B9DE C217 005C F3C0 0672',
keyId: 'C217005CF3C00672',
email: 'xevion@xevion.dev',
name: 'Ryan Walters',
} as const;
+5
View File
@@ -1,6 +1,7 @@
<script lang="ts">
import AppWrapper from "$lib/components/AppWrapper.svelte";
import ProjectCard from "$lib/components/ProjectCard.svelte";
import PgpKeyModal from "$lib/components/PgpKeyModal.svelte";
import type { PageData } from "./$types";
import IconSimpleIconsGithub from "~icons/simple-icons/github";
import IconSimpleIconsLinkedin from "~icons/simple-icons/linkedin";
@@ -10,6 +11,7 @@
let { data }: { data: PageData } = $props();
const projects = data.projects;
let pgpModalOpen = $state(false);
</script>
<AppWrapper class="overflow-x-hidden font-schibsted">
@@ -70,6 +72,7 @@
<button
type="button"
class="flex items-center gap-x-1.5 px-1.5 py-1 rounded-sm bg-zinc-100 dark:bg-zinc-900 shadow-sm hover:bg-zinc-200 dark:hover:bg-zinc-800 transition-colors"
onclick={() => (pgpModalOpen = true)}
>
<MaterialSymbolsVpnKey class="size-4.5 text-zinc-600 dark:text-zinc-300" />
<span class="whitespace-nowrap text-sm text-zinc-800 dark:text-zinc-100">PGP Key</span>
@@ -87,3 +90,5 @@
</div>
</div>
</AppWrapper>
<PgpKeyModal bind:open={pgpModalOpen} />
+18
View File
@@ -0,0 +1,18 @@
import { PGP_KEY_METADATA } from '$lib/pgp/key-info';
import { readFileSync } from 'fs';
import { join } from 'path';
export const prerender = true;
export const load = () => {
// Read the PGP key from static directory at build time
const keyPath = join(process.cwd(), 'static', 'publickey.asc');
const content = readFileSync(keyPath, 'utf-8');
return {
key: {
...PGP_KEY_METADATA,
content,
},
};
};
+159
View File
@@ -0,0 +1,159 @@
<script lang="ts">
import AppWrapper from "$lib/components/AppWrapper.svelte";
import { OverlayScrollbarsComponent } from "overlayscrollbars-svelte";
import "overlayscrollbars/overlayscrollbars.css";
import IconDownload from "~icons/material-symbols/download-rounded";
import IconCopy from "~icons/material-symbols/content-copy-rounded";
import IconCheck from "~icons/material-symbols/check-rounded";
import type { PageData } from "./$types";
let { data }: { data: PageData } = $props();
let copySuccess = $state(false);
let copyCommandSuccess = $state(false);
async function copyToClipboard() {
try {
await navigator.clipboard.writeText(data.key.content);
copySuccess = true;
setTimeout(() => {
copySuccess = false;
}, 2000);
} catch (err) {
console.error("Failed to copy:", err);
}
}
async function copyCommand() {
try {
await navigator.clipboard.writeText("curl https://xevion.dev/pgp | gpg --import");
copyCommandSuccess = true;
setTimeout(() => {
copyCommandSuccess = false;
}, 2000);
} catch (err) {
console.error("Failed to copy command:", err);
}
}
function downloadKey() {
const a = document.createElement("a");
a.href = "/publickey.asc";
a.download = "publickey.asc";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
</script>
<svelte:head>
<title>PGP Public Key - Ryan Walters</title>
<meta name="description" content="Download or copy Ryan Walters' PGP public key" />
</svelte:head>
<AppWrapper class="overflow-x-hidden font-schibsted">
<div class="flex items-center flex-col pt-14 pb-20 px-4 sm:px-6">
<div class="max-w-2xl w-full">
<!-- Header -->
<div class="mb-6">
<h1 class="text-2xl sm:text-3xl font-bold text-zinc-900 dark:text-white mb-2">
PGP Public Key
</h1>
<p class="text-sm sm:text-base text-zinc-600 dark:text-zinc-400">
Use this key to send me encrypted messages or verify my signed content.
</p>
</div>
<!-- Fingerprint -->
<div
class="mb-6 p-3 sm:p-4 bg-zinc-100 dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700"
>
<div class="text-xs sm:text-sm font-semibold text-zinc-700 dark:text-zinc-300 mb-2">
Key Fingerprint
</div>
<div class="font-mono text-sm sm:text-base text-zinc-900 dark:text-zinc-100 break-all">
{data.key.fingerprint}
</div>
<div class="mt-3 pt-3 border-t border-zinc-200 dark:border-zinc-700 space-y-1">
<div class="text-xs sm:text-sm text-zinc-600 dark:text-zinc-400">
<span class="font-medium">Key ID:</span>
<span class="font-mono ml-2">{data.key.keyId}</span>
</div>
<div class="text-xs sm:text-sm text-zinc-600 dark:text-zinc-400">
<span class="font-medium">Email:</span>
<span class="ml-2">{data.key.email}</span>
</div>
</div>
</div>
<!-- Key Content Card -->
<div
class="mb-6 border border-zinc-200 dark:border-zinc-700 rounded-lg overflow-hidden bg-white dark:bg-zinc-900"
>
<div class="px-3 sm:px-4 py-2 sm:py-3 bg-zinc-50 dark:bg-zinc-800 border-b border-zinc-200 dark:border-zinc-700">
<div class="text-xs sm:text-sm font-semibold text-zinc-700 dark:text-zinc-300">
Public Key
</div>
</div>
<OverlayScrollbarsComponent
options={{
scrollbars: { autoHide: "leave", autoHideDelay: 800 },
}}
defer
style="max-height: 400px"
>
<pre
class="p-3 sm:p-4 text-xs font-mono text-zinc-800 dark:text-zinc-200 bg-zinc-50 dark:bg-zinc-900/50 overflow-x-auto">{data.key.content}</pre>
</OverlayScrollbarsComponent>
</div>
<!-- Action Buttons -->
<div class="flex flex-col sm:flex-row gap-2 sm:gap-3">
<button
onclick={copyToClipboard}
class="flex items-center justify-center gap-2 px-3 sm:px-4 py-2 sm:py-2.5 rounded-sm bg-zinc-900 dark:bg-zinc-100 text-white dark:text-zinc-900 hover:bg-zinc-800 dark:hover:bg-zinc-200 transition-colors shadow-sm"
>
<IconCopy class="size-4 sm:size-5" />
<span class="text-sm sm:text-base font-medium">{copySuccess ? "Copied!" : "Copy to Clipboard"}</span>
</button>
<button
onclick={downloadKey}
class="flex items-center justify-center gap-2 px-3 sm:px-4 py-2 sm:py-2.5 rounded-sm bg-zinc-100 dark:bg-zinc-800 text-zinc-800 dark:text-zinc-100 hover:bg-zinc-200 dark:hover:bg-zinc-700 transition-colors"
>
<IconDownload class="size-4 sm:size-5" />
<span class="text-sm sm:text-base font-medium">Download</span>
</button>
</div>
<!-- Additional Info -->
<div class="mt-8 p-3 sm:p-4 bg-zinc-50 dark:bg-zinc-800/50 rounded-lg border border-zinc-200 dark:border-zinc-700">
<h2 class="text-xs sm:text-sm font-semibold text-zinc-700 dark:text-zinc-300 mb-2">
How to use this key
</h2>
<div class="text-xs sm:text-sm text-zinc-600 dark:text-zinc-400 space-y-2">
<p>
Import this key into your GPG keyring to encrypt messages for me or verify my signatures:
</p>
<div class="relative">
<pre class="p-2 sm:p-3 pr-12 bg-white dark:bg-zinc-900 rounded border border-zinc-200 dark:border-zinc-700 font-mono text-xs overflow-x-auto">curl https://xevion.dev/pgp | gpg --import</pre>
<button
onclick={copyCommand}
disabled={copyCommandSuccess}
class="absolute top-1/2 -translate-y-1/2 right-2 p-1 rounded border border-zinc-300 dark:border-zinc-600 bg-zinc-50 dark:bg-zinc-800 hover:bg-zinc-100 dark:hover:bg-zinc-700 hover:border-zinc-400 dark:hover:border-zinc-500 transition-all {copyCommandSuccess ? 'cursor-default' : 'cursor-pointer'}"
title={copyCommandSuccess ? "Copied!" : "Copy command"}
>
{#if copyCommandSuccess}
<IconCheck class="size-3.5 text-green-600 dark:text-green-500" />
{:else}
<IconCopy class="size-3.5 text-zinc-600 dark:text-zinc-400" />
{/if}
</button>
</div>
<p class="text-xs text-zinc-500 dark:text-zinc-500">
You can also find this key on public keyservers by searching for the fingerprint above.
</p>
</div>
</div>
</div>
</div>
</AppWrapper>
+52
View File
@@ -0,0 +1,52 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
JUK4SSLSxYQ=
=yHBH
-----END PGP PUBLIC KEY BLOCK-----