diff --git a/src/index.ts b/src/index.ts index f01ace5..e3409c4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,49 +1,58 @@ -import { marked } from "marked"; -import hljs from "highlight.js/lib/core"; +import { marked } from 'marked'; +import hljs from 'highlight.js/lib/core'; // Import only the languages we want to support to keep bundle size down -import javascript from "highlight.js/lib/languages/javascript"; -import typescript from "highlight.js/lib/languages/typescript"; -import python from "highlight.js/lib/languages/python"; -import json from "highlight.js/lib/languages/json"; -import xml from "highlight.js/lib/languages/xml"; // HTML, XML -import css from "highlight.js/lib/languages/css"; -import markdown from "highlight.js/lib/languages/markdown"; -import bash from "highlight.js/lib/languages/bash"; -import yaml from "highlight.js/lib/languages/yaml"; -import sql from "highlight.js/lib/languages/sql"; -import java from "highlight.js/lib/languages/java"; -import cpp from "highlight.js/lib/languages/cpp"; -import go from "highlight.js/lib/languages/go"; -import rust from "highlight.js/lib/languages/rust"; -import ruby from "highlight.js/lib/languages/ruby"; -import php from "highlight.js/lib/languages/php"; +import javascript from 'highlight.js/lib/languages/javascript'; +import typescript from 'highlight.js/lib/languages/typescript'; +import python from 'highlight.js/lib/languages/python'; +import json from 'highlight.js/lib/languages/json'; +import xml from 'highlight.js/lib/languages/xml'; // HTML, XML +import css from 'highlight.js/lib/languages/css'; +import markdown from 'highlight.js/lib/languages/markdown'; +import bash from 'highlight.js/lib/languages/bash'; +import yaml from 'highlight.js/lib/languages/yaml'; +import sql from 'highlight.js/lib/languages/sql'; +import java from 'highlight.js/lib/languages/java'; +import cpp from 'highlight.js/lib/languages/cpp'; +import go from 'highlight.js/lib/languages/go'; +import rust from 'highlight.js/lib/languages/rust'; +import ruby from 'highlight.js/lib/languages/ruby'; +import php from 'highlight.js/lib/languages/php'; // Import MUI styles -import { getHomepageHTML, getMuiCSS, getMarkdownStyles } from "./muiStyles"; +import { getHomepageHTML, getMuiCSS, getMarkdownStyles } from './muiStyles'; + +// Import browser viewer templates +import { + renderTextViewerPage, + renderEncryptedLandingPage, + renderOneTimeInterstitialPage, + renderErrorPage, + getHljsThemeStyles, +} from './viewerTemplates'; // Import analytics -import { createAnalytics, WorkerAnalytics } from "./analytics"; +import { createAnalytics, WorkerAnalytics } from './analytics'; // Register languages with highlight.js -hljs.registerLanguage("javascript", javascript); -hljs.registerLanguage("typescript", typescript); -hljs.registerLanguage("python", python); -hljs.registerLanguage("json", json); -hljs.registerLanguage("xml", xml); -hljs.registerLanguage("html", xml); // HTML is handled by XML -hljs.registerLanguage("css", css); -hljs.registerLanguage("markdown", markdown); -hljs.registerLanguage("bash", bash); -hljs.registerLanguage("sh", bash); // Shell alias -hljs.registerLanguage("yaml", yaml); -hljs.registerLanguage("sql", sql); -hljs.registerLanguage("java", java); -hljs.registerLanguage("cpp", cpp); -hljs.registerLanguage("c++", cpp); // C++ alias -hljs.registerLanguage("go", go); -hljs.registerLanguage("rust", rust); -hljs.registerLanguage("ruby", ruby); -hljs.registerLanguage("php", php); +hljs.registerLanguage('javascript', javascript); +hljs.registerLanguage('typescript', typescript); +hljs.registerLanguage('python', python); +hljs.registerLanguage('json', json); +hljs.registerLanguage('xml', xml); +hljs.registerLanguage('html', xml); // HTML is handled by XML +hljs.registerLanguage('css', css); +hljs.registerLanguage('markdown', markdown); +hljs.registerLanguage('bash', bash); +hljs.registerLanguage('sh', bash); // Shell alias +hljs.registerLanguage('yaml', yaml); +hljs.registerLanguage('sql', sql); +hljs.registerLanguage('java', java); +hljs.registerLanguage('cpp', cpp); +hljs.registerLanguage('c++', cpp); // C++ alias +hljs.registerLanguage('go', go); +hljs.registerLanguage('rust', rust); +hljs.registerLanguage('ruby', ruby); +hljs.registerLanguage('php', php); // Import multipart upload types import type { @@ -55,11 +64,11 @@ import type { MultipartCompleteRequest, MultipartCompleteResponse, UploadStatusResponse, -} from "./types"; -import { LARGE_FILE_CONSTANTS } from "./types"; +} from './types'; +import { LARGE_FILE_CONSTANTS } from './types'; // Import service layer -import { createServices, parseDuration } from "./services"; +import { createServices, parseDuration } from './services'; // Import validation import { @@ -67,10 +76,10 @@ import { validateWebUpload, checkValidation, parseJsonBody, -} from "./validation"; +} from './validation'; // Import API v1 router -import { routeApiV1 } from "./api/v1"; +import { routeApiV1 } from './api/v1'; // Define the environment interface for Cloudflare Workers type Env = { @@ -94,33 +103,239 @@ type PasteMetadata = { // For one-time pastes, we'll use a completely different key format // with a prefix to make identifying them clear -const ONE_TIME_PREFIX = "onetime-"; +const ONE_TIME_PREFIX = 'onetime-'; // Maximum upload size for standard (non-multipart) uploads: 25MB const MAX_STANDARD_UPLOAD_SIZE = 25 * 1024 * 1024; +// Pastes larger than this skip server-side syntax highlighting (escaped
 only)
+const VIEWER_HIGHLIGHT_MAX_SIZE = 500 * 1024;
+
+// Pastes larger than this skip the HTML viewer entirely and stream raw bytes
+// (kept small because the viewer must buffer the full body in Worker memory)
+const VIEWER_MAX_SIZE = 1024 * 1024;
+
+// Map of filename extensions to registered highlight.js language names
+const EXTENSION_LANGUAGE_MAP: Record = {
+  js: 'javascript',
+  mjs: 'javascript',
+  cjs: 'javascript',
+  jsx: 'javascript',
+  ts: 'typescript',
+  tsx: 'typescript',
+  py: 'python',
+  json: 'json',
+  xml: 'xml',
+  html: 'html',
+  htm: 'html',
+  svg: 'xml',
+  css: 'css',
+  md: 'markdown',
+  markdown: 'markdown',
+  sh: 'bash',
+  bash: 'bash',
+  zsh: 'bash',
+  yml: 'yaml',
+  yaml: 'yaml',
+  sql: 'sql',
+  java: 'java',
+  c: 'cpp',
+  h: 'cpp',
+  cc: 'cpp',
+  cpp: 'cpp',
+  cxx: 'cpp',
+  hpp: 'cpp',
+  go: 'go',
+  rs: 'rust',
+  rb: 'ruby',
+  php: 'php',
+};
+
+// Map of content types to registered highlight.js language names
+const CONTENT_TYPE_LANGUAGE_MAP: Record = {
+  'application/json': 'json',
+  'application/javascript': 'javascript',
+  'application/xml': 'xml',
+  'text/javascript': 'javascript',
+  'text/html': 'html',
+  'text/css': 'css',
+  'text/xml': 'xml',
+  'text/x-python': 'python',
+  'text/x-shellscript': 'bash',
+  'text/yaml': 'yaml',
+  'text/x-yaml': 'yaml',
+};
+
+/**
+ * Normalizes a string read from R2 custom metadata. Undefined values can be
+ * stringified to "undefined" on write, so treat those as empty.
+ */
+function cleanMetadataString(value: string | undefined): string {
+  if (!value || value === 'undefined' || value === 'null') {
+    return '';
+  }
+  return value;
+}
+
+/**
+ * Strips undefined values from a metadata object so they are not stringified
+ * to the literal "undefined" when stored as R2 custom metadata.
+ */
+function toCustomMetadata(metadata: PasteMetadata): Record {
+  const result: Record = {};
+  for (const [key, value] of Object.entries(metadata)) {
+    if (value !== undefined && value !== null) {
+      result[key] = String(value);
+    }
+  }
+  return result;
+}
+
+/**
+ * Determines whether the client is a browser expecting an HTML page.
+ * Browsers send Accept headers containing "text/html"; curl and node fetch
+ * default to "*\/*", so CLI/programmatic clients keep receiving raw content.
+ */
+function requestAcceptsHtml(request: Request): boolean {
+  return (request.headers.get('Accept') || '').includes('text/html');
+}
+
+/**
+ * Determines whether a content type is text-like and eligible for the
+ * universal browser text viewer.
+ */
+function isTextualContentType(contentType: string): boolean {
+  const ct = contentType.toLowerCase().split(';')[0].trim();
+  return (
+    ct.startsWith('text/') ||
+    ct === 'application/json' ||
+    ct === 'application/xml' ||
+    ct === 'application/javascript'
+  );
+}
+
+/**
+ * Picks a highlight.js language for a paste based on its filename extension,
+ * falling back to its content type. Returns null when unknown (auto-detect).
+ */
+function detectViewerLanguage(filename: string, contentType: string): string | null {
+  const extMatch = filename.toLowerCase().match(/\.([a-z0-9]+)$/);
+  if (extMatch && EXTENSION_LANGUAGE_MAP[extMatch[1]]) {
+    return EXTENSION_LANGUAGE_MAP[extMatch[1]];
+  }
+  const ct = contentType.toLowerCase().split(';')[0].trim();
+  return CONTENT_TYPE_LANGUAGE_MAP[ct] || null;
+}
+
+/**
+ * Builds the universal text viewer HTML for a text-like paste, applying
+ * server-side syntax highlighting with safe fallbacks.
+ * @param textContent Decoded paste text
+ * @param pasteId Paste ID
+ * @param filename Filename for display/language detection (may be empty)
+ * @param contentType Stored content type
+ * @param sizeBytes Paste size in bytes
+ * @param isOneTime Whether the paste is one-time
+ */
+function buildTextViewerHtml(
+  textContent: string,
+  pasteId: string,
+  filename: string,
+  contentType: string,
+  sizeBytes: number,
+  isOneTime: boolean
+): string {
+  let highlightedHtml: string;
+  let language = 'plaintext';
+  const highlightingSkipped = sizeBytes > VIEWER_HIGHLIGHT_MAX_SIZE;
+
+  if (highlightingSkipped) {
+    highlightedHtml = escapeHtml(textContent);
+  } else {
+    try {
+      const knownLanguage = detectViewerLanguage(filename, contentType);
+      if (knownLanguage && hljs.getLanguage(knownLanguage)) {
+        highlightedHtml = hljs.highlight(textContent, {
+          language: knownLanguage,
+        }).value;
+        language = knownLanguage;
+      } else {
+        const result = hljs.highlightAuto(textContent);
+        highlightedHtml = result.value;
+        language = result.language || 'plaintext';
+      }
+    } catch (err) {
+      console.error(`[VIEWER] Syntax highlighting failed for ${pasteId}:`, err);
+      highlightedHtml = escapeHtml(textContent);
+      language = 'plaintext';
+    }
+  }
+
+  return renderTextViewerPage({
+    pasteId,
+    filename,
+    contentType,
+    sizeBytes,
+    highlightedHtml,
+    lineCount: textContent.split('\n').length,
+    language,
+    highlightingSkipped,
+    isOneTime,
+  });
+}
+
+/** Standard headers for browser-facing viewer HTML responses */
+function htmlViewerHeaders(extra: Record = {}): Record {
+  return {
+    'Content-Type': 'text/html; charset=utf-8',
+    'Cache-Control': 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0',
+    'CDN-Cache-Control': 'no-store',
+    'Surrogate-Control': 'no-store',
+    Pragma: 'no-cache',
+    Expires: '0',
+    ...extra,
+  };
+}
+
+/**
+ * Builds a "paste not found" response: a styled HTML page for browsers,
+ * plain text for CLI/programmatic clients.
+ */
+function notFoundResponse(
+  request: Request,
+  message = 'Paste not found - it may have expired or was a one-time paste that has already been viewed.'
+): Response {
+  if (requestAcceptsHtml(request)) {
+    return new Response(renderErrorPage({ title: 'Paste not found', message }), {
+      status: 404,
+      headers: htmlViewerHeaders(),
+    });
+  }
+  return new Response('Paste not found', { status: 404 });
+}
+
 // Allowed CORS origin (restrict from wildcard)
-const ALLOWED_ORIGIN = "https://paste.d3d.dev";
+const ALLOWED_ORIGIN = 'https://paste.d3d.dev';
 
 /** Security headers applied to all responses */
 const SECURITY_HEADERS: Record = {
-  "X-Content-Type-Options": "nosniff",
-  "X-Frame-Options": "DENY",
-  "Strict-Transport-Security": "max-age=31536000; includeSubDomains",
-  "Referrer-Policy": "no-referrer",
-  "Permissions-Policy": "camera=(), microphone=(), geolocation=()",
-  "X-XSS-Protection": "1; mode=block",
+  'X-Content-Type-Options': 'nosniff',
+  'X-Frame-Options': 'DENY',
+  'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
+  'Referrer-Policy': 'no-referrer',
+  'Permissions-Policy': 'camera=(), microphone=(), geolocation=()',
+  'X-XSS-Protection': '1; mode=block',
 };
 
 /** Get CORS origin header based on request origin */
 function getCorsOrigin(request?: Request): string {
   if (!request) return ALLOWED_ORIGIN;
-  const origin = request.headers.get("Origin") || "";
+  const origin = request.headers.get('Origin') || '';
   // Allow the primary domain and localhost for development
   if (
     origin === ALLOWED_ORIGIN ||
-    origin.startsWith("http://localhost:") ||
-    origin.startsWith("http://127.0.0.1:")
+    origin.startsWith('http://localhost:') ||
+    origin.startsWith('http://127.0.0.1:')
   ) {
     return origin;
   }
@@ -134,16 +349,16 @@ function withSecurityHeaders(response: Response, request?: Request): Response {
     newHeaders.set(key, value);
   }
   // Set CSP for HTML responses
-  const contentType = newHeaders.get("Content-Type") || "";
-  if (contentType.includes("text/html")) {
+  const contentType = newHeaders.get('Content-Type') || '';
+  if (contentType.includes('text/html')) {
     newHeaders.set(
-      "Content-Security-Policy",
-      "default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src https://fonts.gstatic.com; img-src 'self' data:; connect-src 'self';",
+      'Content-Security-Policy',
+      "default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src https://fonts.gstatic.com; img-src 'self' data:; connect-src 'self';"
     );
   }
   // Restrict CORS
-  if (newHeaders.has("Access-Control-Allow-Origin")) {
-    newHeaders.set("Access-Control-Allow-Origin", getCorsOrigin(request));
+  if (newHeaders.has('Access-Control-Allow-Origin')) {
+    newHeaders.set('Access-Control-Allow-Origin', getCorsOrigin(request));
   }
   return new Response(response.body, {
     status: response.status,
@@ -154,11 +369,10 @@ function withSecurityHeaders(response: Response, request?: Request): Response {
 
 // Generate a cryptographically secure random ID for the paste
 function generateId(length = 16): string {
-  const chars =
-    "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
+  const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
   const randomValues = new Uint32Array(length);
   crypto.getRandomValues(randomValues);
-  let result = "";
+  let result = '';
   for (let i = 0; i < length; i++) {
     result += chars.charAt(randomValues[i] % chars.length);
   }
@@ -179,7 +393,7 @@ interface ViewedPasteTracker {
 const viewedPastes: ViewedPasteTracker = {};
 
 // KV namespace key for tracking viewed pastes (to persist across worker restarts/instances)
-const VIEWED_PASTES_KEY = "viewed_pastes_registry";
+const VIEWED_PASTES_KEY = 'viewed_pastes_registry';
 
 // Helper function to safely parse JSON with a default value
 function safeJsonParse(jsonString: string | null, defaultValue: T): T {
@@ -187,27 +401,19 @@ function safeJsonParse(jsonString: string | null, defaultValue: T): T {
   try {
     return JSON.parse(jsonString) as T;
   } catch (e) {
-    console.error("Error parsing JSON:", e);
+    console.error('Error parsing JSON:', e);
     return defaultValue;
   }
 }
 
 export default {
-  async fetch(
-    request: Request,
-    env: Env,
-    ctx: ExecutionContext,
-  ): Promise {
+  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise {
     const response = await handleRequest(request, env, ctx);
     return withSecurityHeaders(response, request);
   },
 };
 
-async function handleRequest(
-  request: Request,
-  env: Env,
-  ctx: ExecutionContext,
-): Promise {
+async function handleRequest(request: Request, env: Env, ctx: ExecutionContext): Promise {
   // Initialize analytics
   const analytics = createAnalytics(env);
 
@@ -218,32 +424,28 @@ async function handleRequest(
   // Initialize viewedPastes from KV store if available (completely optional enhancement)
   try {
     if (env.PASTE_METADATA) {
-      const storedViewedPastes =
-        await env.PASTE_METADATA.get(VIEWED_PASTES_KEY);
+      const storedViewedPastes = await env.PASTE_METADATA.get(VIEWED_PASTES_KEY);
       if (storedViewedPastes) {
-        const parsedPastes = safeJsonParse(
-          storedViewedPastes,
-          {},
-        );
+        const parsedPastes = safeJsonParse(storedViewedPastes, {});
         // Merge with any in-memory pastes (newer ones take precedence)
         Object.assign(viewedPastes, parsedPastes);
       }
     }
   } catch (error) {
     // Log but continue without error - KV is an enhancement, not a requirement
-    console.log("[KV] Optional paste metadata storage not available:", error);
+    console.log('[KV] Optional paste metadata storage not available:', error);
   }
   const url = new URL(request.url);
   const path = url.pathname;
 
   // Handle OPTIONS requests for CORS
-  if (request.method === "OPTIONS") {
+  if (request.method === 'OPTIONS') {
     return new Response(null, {
       headers: {
-        "Access-Control-Allow-Origin": getCorsOrigin(request),
-        "Access-Control-Allow-Methods": "GET, POST, PUT, OPTIONS",
-        "Access-Control-Allow-Headers":
-          "Content-Type, X-Filename, X-Content-Type, X-One-Time, X-Expire, X-Burn-Reads, Authorization",
+        'Access-Control-Allow-Origin': getCorsOrigin(request),
+        'Access-Control-Allow-Methods': 'GET, POST, PUT, OPTIONS',
+        'Access-Control-Allow-Headers':
+          'Content-Type, X-Filename, X-Content-Type, X-One-Time, X-Expire, X-Burn-Reads, Authorization',
       },
     });
   }
@@ -257,25 +459,25 @@ async function handleRequest(
   }
 
   // Upload a new paste
-  if (request.method === "POST") {
+  if (request.method === 'POST') {
     // Handle API uploads for web interface
-    if (path === "/api/upload") {
+    if (path === '/api/upload') {
       return await handleWebUpload(request, env);
     }
 
-    if (path === "/api/text") {
+    if (path === '/api/text') {
       return await handleTextUpload(request, env);
     }
 
     // Handle regular uploads
-    if (path === "/upload" || path === "/temp") {
-      const isOneTime = path === "/temp";
+    if (path === '/upload' || path === '/temp') {
+      const isOneTime = path === '/temp';
       return await handleUpload(request, env, isOneTime, false, analytics);
     }
 
     // Handle encrypted uploads
-    if (path === "/e/upload" || path === "/e/temp") {
-      const isOneTime = path === "/e/temp";
+    if (path === '/e/upload' || path === '/e/temp') {
+      const isOneTime = path === '/e/temp';
       return await handleUpload(request, env, isOneTime, true, analytics);
     }
 
@@ -284,24 +486,17 @@ async function handleRequest(
     // ============================================
 
     // Initialize multipart upload
-    if (path === "/upload/init" || path === "/e/upload/init") {
-      const isEncrypted = path.startsWith("/e/");
+    if (path === '/upload/init' || path === '/e/upload/init') {
+      const isEncrypted = path.startsWith('/e/');
       return await handleMultipartInit(request, env, isEncrypted);
     }
 
     // Complete multipart upload
-    const completeMatch = path.match(
-      /^(\/e)?\/upload\/([a-zA-Z0-9_-]+)\/complete$/,
-    );
+    const completeMatch = path.match(/^(\/e)?\/upload\/([a-zA-Z0-9_-]+)\/complete$/);
     if (completeMatch) {
       const isEncrypted = !!completeMatch[1];
       const sessionId = completeMatch[2];
-      return await handleMultipartComplete(
-        request,
-        env,
-        sessionId,
-        isEncrypted,
-      );
+      return await handleMultipartComplete(request, env, sessionId, isEncrypted);
     }
 
     // Abort multipart upload
@@ -311,35 +506,26 @@ async function handleRequest(
       return await handleMultipartAbort(request, env, sessionId);
     }
 
-    return new Response("Not found", { status: 404 });
+    return new Response('Not found', { status: 404 });
   }
 
   // Handle PUT requests for multipart upload parts
-  if (request.method === "PUT") {
-    const partMatch = path.match(
-      /^(\/e)?\/upload\/([a-zA-Z0-9_-]+)\/part\/(\d+)$/,
-    );
+  if (request.method === 'PUT') {
+    const partMatch = path.match(/^(\/e)?\/upload\/([a-zA-Z0-9_-]+)\/part\/(\d+)$/);
     if (partMatch) {
       const isEncrypted = !!partMatch[1];
       const sessionId = partMatch[2];
       const partNumber = parseInt(partMatch[3], 10);
-      return await handleMultipartPartUpload(
-        request,
-        env,
-        sessionId,
-        partNumber,
-      );
+      return await handleMultipartPartUpload(request, env, sessionId, partNumber);
     }
 
-    return new Response("Not found", { status: 404 });
+    return new Response('Not found', { status: 404 });
   }
 
   // Get a paste
-  if (request.method === "GET") {
+  if (request.method === 'GET') {
     // Handle multipart upload status (for resume)
-    const statusMatch = path.match(
-      /^(\/e)?\/upload\/([a-zA-Z0-9_-]+)\/status$/,
-    );
+    const statusMatch = path.match(/^(\/e)?\/upload\/([a-zA-Z0-9_-]+)\/status$/);
     if (statusMatch) {
       const sessionId = statusMatch[2];
       return await handleMultipartStatus(env, sessionId);
@@ -369,331 +555,53 @@ async function handleRequest(
     }
 
     // Try to serve styles.css directly if [site] configuration doesn't work
-    if (path === "/styles.css") {
+    if (path === '/styles.css') {
       // Serve MUI CSS instead of Tailwind
       const cssContent = getMuiCSS();
 
-      console.log("Serving MUI CSS file");
+      console.log('Serving MUI CSS file');
       return new Response(cssContent, {
         headers: {
-          "Content-Type": "text/css",
-          "Cache-Control": "public, max-age=86400",
+          'Content-Type': 'text/css',
+          'Cache-Control': 'public, max-age=86400',
         },
       });
     }
 
     // Serve the HTML homepage
-    if (path === "/") {
+    if (path === '/') {
       // Track homepage view
       await analytics.trackHomepageView(request);
 
-      return new Response(generateHomepage(url.origin), {
+      return new Response(generateHomepage(), {
         headers: {
-          "Content-Type": "text/html",
-          "Access-Control-Allow-Origin": "*",
+          'Content-Type': 'text/html',
         },
       });
     }
 
-    return new Response("Not found", { status: 404 });
+    if (requestAcceptsHtml(request)) {
+      return new Response(
+        renderErrorPage({
+          title: 'Page not found',
+          message:
+            'The page you requested does not exist. If you followed a paste link, it may have expired or was a one-time paste that has already been viewed.',
+        }),
+        { status: 404, headers: htmlViewerHeaders() }
+      );
+    }
+
+    return new Response('Not found', { status: 404 });
   }
 
-  return new Response("Method not allowed", { status: 405 });
+  return new Response('Method not allowed', { status: 405 });
 }
 
-function generateHomepage(origin: string): string {
-  // Use the MUI styled homepage HTML
+/**
+ * Generates the homepage HTML using the MUI styled template.
+ */
+function generateHomepage(): string {
   return getHomepageHTML();
-  /* REMOVED TAILWIND HTML - Replaced with MUI version from muiStyles.ts
-
-
-  DedPaste - Secure Pastebin Service
-  
-  
-  
-  
-  
-  
-
-
-  
-
- -
-
- -
-
-
-

Secure Pastebin with Advanced Encryption

-

A powerful CLI tool for sharing text and files with end-to-end encryption, PGP support, and one-time pastes.

-
- -
-
-
- End-to-End Encryption -
-

Keep Your Content Private

-

All encryption happens client-side. The server never sees your unencrypted content or keys.

-
- -
-
- PGP Integration -
-

Use Your Existing Keys

-

Leverage PGP keys from keyservers, GPG keyring, or Keybase for trusted communications.

-
- -
-
- One-Time Pastes -
-

Self-Destructing Content

-

Create pastes that automatically delete after being viewed once.

-
- -
-
- Binary Support -
-

Beyond Just Text

-

Upload and share binary files with proper content type detection.

-
- -
-
- Friend-to-Friend -
-

Secure Sharing

-

Easily manage keys for your friends and encrypt content specifically for them.

-
- -
-
- CLI Power -
-

Advanced Scripting

-

Command-line interface for easy integration with your existing scripts and workflows.

-
-
-
- -
-
-

Installation

- -
-

Using npm (recommended)

-
npm install -g dedpaste
-
- -
-

From source

-
git clone https://github.com/anoncam/dedpaste.git
-cd dedpaste
-npm install
-npm link
-
-
-
- -
-
-

Quick Start Examples

- -
-
-

Basic Usage

-
# Create a paste from stdin
-echo "Hello, World!" | dedpaste
-
-# Create a paste from a file
-dedpaste < file.txt
-
-# Create a one-time paste
-echo "Secret content" | dedpaste --temp
-
-# Specify file explicitly
-dedpaste --file path/to/file.txt
-
- -
-

Encryption

-
# Generate your key pair first
-dedpaste keys --gen-key
-
-# Create encrypted paste
-echo "Secret data" | dedpaste --encrypt
-
-# Encrypt for a friend
-echo "For Alice" | dedpaste send --encrypt --for alice
-
-# Use PGP encryption
-echo "PGP Secret" | dedpaste send --encrypt --for user@example.com --pgp
-
-
- -
-
-

Key Management

-
# Enhanced interactive key management (recommended)
-dedpaste keys:enhanced
-
-# List all your keys
-dedpaste keys --list
-
-# Add a friend's public key
-dedpaste keys --add-friend alice --key-file alice.pem
-
-# Add a PGP key from keyservers
-dedpaste keys --pgp-key user@example.com
-
-# Add a Keybase user's key
-dedpaste keys --keybase username
-
- -
-

Retrieving Pastes

-
# Get and display a paste
-dedpaste get https://paste.d3d.dev/AbCdEfGh
-
-# Get and decrypt an encrypted paste
-dedpaste get https://paste.d3d.dev/e/AbCdEfGh
-
-# Use a specific key file
-dedpaste get https://paste.d3d.dev/e/AbCdEfGh --key-file private.pem
-
-
-
-
- -
-
-

Troubleshooting

- -
-
-

Common PGP Errors

-
-
-

Error: PGP encryption requires a recipient

-

Always specify a recipient when using PGP encryption:

-
echo "secret" | dedpaste send --encrypt --for user@example.com --pgp
-
- -
-

Error: Failed to find PGP key for recipient

-

Make sure you've added the recipient's PGP key first:

-
dedpaste keys --pgp-key user@example.com
-
-
-
- -
-

Key Management Issues

-
-
-

Error: No personal key found

-

Generate your key pair first:

-
dedpaste keys --gen-key
-
- -
-

Error: Friend not found in key database

-

Add the friend's key before encrypting for them:

-
dedpaste keys --add-friend name --key-file path/to/key.pem
-
-
-
- -
-

CLI Parameter Issues

-
-
-

Error: File not found with --file flag

-

Double-check the file path and use quotes for paths with spaces:

-
dedpaste --file "path/to/my file.txt"
-
- -
-

Error: --for is required when using --pgp

-

PGP encryption always requires specifying a recipient:

-
dedpaste send --encrypt --for recipient@example.com --pgp
-
-
-
-
-
-
- -
-
-

API Usage

- -
# Post content
-curl -X POST -H "Content-Type: text/plain" --data "Your content here" ${origin}/upload
-
-# Post one-time content
-curl -X POST -H "Content-Type: text/plain" --data "Your content here" ${origin}/temp
-
-# Post encrypted content (client-side encryption)
-curl -X POST -H "Content-Type: text/plain" --data "Your encrypted content" ${origin}/e/upload
-
-# Post encrypted one-time content
-curl -X POST -H "Content-Type: text/plain" --data "Your encrypted content" ${origin}/e/temp
-
-# Get content
-curl ${origin}/{paste-id}
-
-# Get encrypted content (requires client-side decryption)
-curl ${origin}/e/{paste-id}
-
-
-
- -