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
+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>