feat: rebrand Hemmelig to paste.es for cloudhost.es

- Set Spanish as default language with ephemeral/encrypted privacy focus
- Translate all user-facing strings and legal pages to Spanish
- Replace Norwegian flag with Spanish flag in footer
- Remove Hemmelig/terces.cloud links, add cloudhost.es sponsorship
- Rewrite PrivacyPage: zero data collection, ephemeral design emphasis
- Rewrite TermsPage: Spanish law, RGPD, paste.es/CloudHost.es references
- Update PWA manifest, HTML meta tags, package.json branding
- Rename webhook headers to X-Paste-Event / X-Paste-Signature
- Update API docs title and contact to paste.es / cloudhost.es

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-24 09:30:19 +01:00
commit bc9f96cbd4
268 changed files with 45773 additions and 0 deletions

197
cli/src/bin.ts Normal file
View File

@@ -0,0 +1,197 @@
#!/usr/bin/env node
import { createSecret, EXPIRATION_TIMES, type ExpirationKey, type SecretOptions } from './index.js';
const VERSION = '1.0.0';
/**
* Prints help message
*/
function printHelp(): void {
console.log(`
_ _ _ _
| | | | ___ _ __ ___ _ __ ___ ___| (_) __ _
| |_| |/ _ \\ '_ \` _ \\| '_ \` _ \\ / _ \\ | |/ _\` |
| _ | __/ | | | | | | | | | | __/ | | (_| |
|_| |_|\\___|_| |_| |_|_| |_| |_|\\___|_|_|\\__, |
|___/
Create encrypted secrets from the command line
Usage:
hemmelig <secret> [options]
echo "secret" | hemmelig [options]
hemmelig --help
Options:
-t, --title <title> Set a title for the secret
-p, --password <pass> Protect with a password (if not set, key is in URL)
-e, --expires <time> Expiration time (default: 1d)
Valid: 5m, 30m, 1h, 4h, 12h, 1d, 3d, 7d, 14d, 28d
-v, --views <number> Max views before deletion (default: 1, max: 9999)
-b, --burnable Burn after first view (default: true)
--no-burnable Don't burn after first view
-u, --url <url> Base URL (default: https://hemmelig.app)
-h, --help Show this help message
--version Show version number
Examples:
# Create a simple secret
hemmelig "my secret message"
# Create a secret with a title and 7-day expiration
hemmelig "my secret" -t "API Key" -e 7d
# Create a password-protected secret
hemmelig "my secret" -p "mypassword123"
# Create a secret with 5 views allowed
hemmelig "my secret" -v 5
# Pipe content from a file
cat ~/.ssh/id_rsa.pub | hemmelig -t "SSH Public Key"
# Use a self-hosted instance
hemmelig "my secret" -u https://secrets.mycompany.com
`);
}
/**
* Parses command line arguments
*/
function parseArgs(args: string[]): SecretOptions & { help?: boolean; version?: boolean } {
const options: SecretOptions & { help?: boolean; version?: boolean } = {
secret: '',
};
let i = 0;
while (i < args.length) {
const arg = args[i];
switch (arg) {
case '-h':
case '--help':
options.help = true;
break;
case '--version':
options.version = true;
break;
case '-t':
case '--title':
options.title = args[++i];
break;
case '-p':
case '--password':
options.password = args[++i];
break;
case '-e':
case '--expires':
options.expiresIn = args[++i] as ExpirationKey;
break;
case '-v':
case '--views':
options.views = parseInt(args[++i], 10);
break;
case '-b':
case '--burnable':
options.burnable = true;
break;
case '--no-burnable':
options.burnable = false;
break;
case '-u':
case '--url':
options.baseUrl = args[++i];
break;
default:
// If it doesn't start with -, it's the secret
if (!arg.startsWith('-') && !options.secret) {
options.secret = arg;
}
break;
}
i++;
}
return options;
}
/**
* Reads from stdin if available
*/
async function readStdin(): Promise<string> {
return new Promise((resolve) => {
// Check if stdin is a TTY (interactive terminal)
if (process.stdin.isTTY) {
resolve('');
return;
}
let data = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', (chunk) => {
data += chunk;
});
process.stdin.on('end', () => {
// Only trim trailing whitespace to preserve internal formatting
resolve(data.trimEnd());
});
// Timeout after 100ms if no data
setTimeout(() => {
if (!data) {
resolve('');
}
}, 100);
});
}
/**
* Main entry point
*/
async function main(): Promise<void> {
const args = process.argv.slice(2);
const options = parseArgs(args);
if (options.version) {
console.log(VERSION);
process.exit(0);
}
if (options.help) {
printHelp();
process.exit(0);
}
// Try to read from stdin if no secret provided
if (!options.secret) {
options.secret = await readStdin();
}
if (!options.secret) {
console.error('Error: No secret provided. Use --help for usage information.');
process.exit(1);
}
// Validate expiration time
if (options.expiresIn && !(options.expiresIn in EXPIRATION_TIMES)) {
console.error(`Error: Invalid expiration time "${options.expiresIn}".`);
console.error('Valid options: 5m, 30m, 1h, 4h, 12h, 1d, 3d, 7d, 14d, 28d');
process.exit(1);
}
// Validate views
if (options.views !== undefined && (options.views < 1 || options.views > 9999)) {
console.error('Error: Views must be between 1 and 9999.');
process.exit(1);
}
try {
const result = await createSecret(options);
console.log(result.url);
} catch (error) {
console.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
process.exit(1);
}
}
main();

210
cli/src/index.ts Normal file
View File

@@ -0,0 +1,210 @@
import { createCipheriv, pbkdf2Sync, randomBytes } from 'node:crypto';
/**
* Valid expiration times in seconds
*/
export const EXPIRATION_TIMES = {
'5m': 300,
'30m': 1800,
'1h': 3600,
'4h': 14400,
'12h': 43200,
'1d': 86400,
'3d': 259200,
'7d': 604800,
'14d': 1209600,
'28d': 2419200,
} as const;
export type ExpirationKey = keyof typeof EXPIRATION_TIMES;
/**
* Options for creating a secret
*/
export interface SecretOptions {
/** The secret content to encrypt */
secret: string;
/** Optional title for the secret */
title?: string;
/** Optional password protection */
password?: string;
/** Expiration time (default: '1d') */
expiresIn?: ExpirationKey;
/** Maximum number of views (default: 1, max: 9999) */
views?: number;
/** Whether to burn after first view (default: true) */
burnable?: boolean;
/** Base URL of the Hemmelig instance (default: 'https://hemmelig.app') */
baseUrl?: string;
}
/**
* Result from creating a secret
*/
export interface CreateSecretResult {
/** The full URL to access the secret */
url: string;
/** The secret ID */
id: string;
/** The expiration time that was set */
expiresIn: string;
}
/**
* Generates a random 32-character string using URL-safe base64 encoding
*/
function generateKey(): string {
return randomBytes(24).toString('base64url').slice(0, 32);
}
/**
* Generates a random 32-character salt
*/
function generateSalt(): string {
return randomBytes(24).toString('base64url').slice(0, 32);
}
/**
* Derives a 256-bit AES key using PBKDF2-SHA256
*/
function deriveKey(password: string, salt: string): Buffer {
return pbkdf2Sync(password, salt, 1300000, 32, 'sha256');
}
/**
* Encrypts data using AES-256-GCM
* Returns IV (12 bytes) + ciphertext + auth tag (16 bytes)
*/
function encrypt(data: Buffer, encryptionKey: string, salt: string): Uint8Array {
const key = deriveKey(encryptionKey, salt);
const iv = randomBytes(12);
const cipher = createCipheriv('aes-256-gcm', key, iv);
const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
const authTag = cipher.getAuthTag();
// Format: IV (12 bytes) + ciphertext + authTag (16 bytes)
const fullMessage = new Uint8Array(iv.length + encrypted.length + authTag.length);
fullMessage.set(iv, 0);
fullMessage.set(encrypted, iv.length);
fullMessage.set(authTag, iv.length + encrypted.length);
return fullMessage;
}
/**
* Encrypts text using AES-256-GCM
*/
function encryptText(text: string, encryptionKey: string, salt: string): Uint8Array {
return encrypt(Buffer.from(text, 'utf8'), encryptionKey, salt);
}
/**
* Converts Uint8Array to a JSON-serializable object format
* This matches the format expected by the API's jsonToUint8ArraySchema
*/
function uint8ArrayToObject(arr: Uint8Array): Record<string, number> {
const obj: Record<string, number> = {};
for (let i = 0; i < arr.length; i++) {
obj[i.toString()] = arr[i];
}
return obj;
}
/**
* Creates an encrypted secret on a Hemmelig server
*
* @example
* ```typescript
* import { createSecret } from 'hemmelig';
*
* const result = await createSecret({
* secret: 'my secret message',
* title: 'API Key',
* expiresIn: '1h',
* views: 1
* });
*
* console.log(result.url); // https://hemmelig.app/secret/abc123#decryptionKey=...
* ```
*/
export async function createSecret(options: SecretOptions): Promise<CreateSecretResult> {
const {
secret,
title,
password,
expiresIn = '1d',
views = 1,
burnable = true,
baseUrl = 'https://hemmelig.app',
} = options;
// Validate expiration time
if (!(expiresIn in EXPIRATION_TIMES)) {
throw new Error(
`Invalid expiration time "${expiresIn}". Valid options: ${Object.keys(EXPIRATION_TIMES).join(', ')}`
);
}
// Validate views
if (views < 1 || views > 9999) {
throw new Error('Views must be between 1 and 9999');
}
// Generate encryption key and salt
const encryptionKey = password || generateKey();
const salt = generateSalt();
// Encrypt the secret (and title if provided)
const encryptedSecret = encryptText(secret, encryptionKey, salt);
const encryptedTitle = title ? encryptText(title, encryptionKey, salt) : null;
// Prepare the request payload
const payload: Record<string, unknown> = {
secret: uint8ArrayToObject(encryptedSecret),
salt,
expiresAt: EXPIRATION_TIMES[expiresIn],
views,
isBurnable: burnable,
};
if (encryptedTitle) {
payload.title = uint8ArrayToObject(encryptedTitle);
}
// If password is provided, send it for server-side hashing
// Otherwise, leave it empty (key will be in URL fragment)
if (password) {
payload.password = password;
}
// Make the API request
const response = await fetch(`${baseUrl}/api/secrets`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
});
if (!response.ok) {
const errorData = (await response.json().catch(() => ({ error: 'Unknown error' }))) as {
error?: string;
};
throw new Error(`Failed to create secret: ${errorData.error || response.statusText}`);
}
const data = (await response.json()) as { id: string };
// Construct the URL
// If no password was provided, include the decryption key in the URL fragment
const url = password
? `${baseUrl}/secret/${data.id}`
: `${baseUrl}/secret/${data.id}#decryptionKey=${encryptionKey}`;
return {
url,
id: data.id,
expiresIn,
};
}