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