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:
146
cli/README.md
Normal file
146
cli/README.md
Normal file
@@ -0,0 +1,146 @@
|
||||
# hemmelig
|
||||
|
||||
CLI and library for creating encrypted, self-destructing secrets via [Hemmelig](https://hemmelig.app).
|
||||
|
||||
```
|
||||
_ _ _ _
|
||||
| | | | ___ _ __ ___ _ __ ___ ___| (_) __ _
|
||||
| |_| |/ _ \ '_ ` _ \| '_ ` _ \ / _ \ | |/ _` |
|
||||
| _ | __/ | | | | | | | | | | __/ | | (_| |
|
||||
|_| |_|\___|_| |_| |_|_| |_| |_|\___|_|_|\__, |
|
||||
|___/
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- **Client-side AES-256-GCM encryption** - Your secrets are encrypted before leaving your machine
|
||||
- **Zero-knowledge** - The server never sees your plaintext secrets
|
||||
- **Self-destructing** - Secrets auto-delete after views or expiration
|
||||
- **Password protection** - Optional additional security layer
|
||||
- **Works with any Hemmelig instance** - Use hemmelig.app or self-hosted
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install -g hemmelig
|
||||
```
|
||||
|
||||
Or use with npx:
|
||||
|
||||
```bash
|
||||
npx hemmelig "my secret"
|
||||
```
|
||||
|
||||
## CLI Usage
|
||||
|
||||
```bash
|
||||
# Create a simple secret
|
||||
hemmelig "my secret message"
|
||||
|
||||
# With a title and custom expiration
|
||||
hemmelig "API key: sk-1234" -t "Production API Key" -e 7d
|
||||
|
||||
# Password protected
|
||||
hemmelig "sensitive data" -p "mypassword"
|
||||
|
||||
# Multiple views allowed
|
||||
hemmelig "shared config" -v 5
|
||||
|
||||
# Pipe from stdin
|
||||
cat config.json | hemmelig -t "Config file"
|
||||
echo "my secret" | hemmelig
|
||||
|
||||
# Use a self-hosted instance
|
||||
hemmelig "internal secret" -u https://secrets.company.com
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
| Option | Description |
|
||||
| ----------------------- | ------------------------------------------------------ |
|
||||
| `-t, --title <title>` | Set a title for the secret |
|
||||
| `-p, --password <pass>` | Protect with a password |
|
||||
| `-e, --expires <time>` | Expiration: 5m, 30m, 1h, 4h, 12h, 1d, 3d, 7d, 14d, 28d |
|
||||
| `-v, --views <number>` | Max views (1-9999, default: 1) |
|
||||
| `-b, --burnable` | Burn after first view (default) |
|
||||
| `--no-burnable` | Don't burn until all views used |
|
||||
| `-u, --url <url>` | Base URL (default: https://hemmelig.app) |
|
||||
| `-h, --help` | Show help |
|
||||
| `--version` | Show version |
|
||||
|
||||
## Library Usage
|
||||
|
||||
```typescript
|
||||
import { createSecret } from 'hemmelig';
|
||||
|
||||
const result = await createSecret({
|
||||
secret: 'my secret message',
|
||||
title: 'API Key',
|
||||
expiresIn: '1h',
|
||||
views: 1,
|
||||
burnable: true,
|
||||
baseUrl: 'https://hemmelig.app', // optional
|
||||
});
|
||||
|
||||
console.log(result.url); // https://hemmelig.app/secret/abc123#decryptionKey=...
|
||||
console.log(result.id); // abc123
|
||||
```
|
||||
|
||||
### API
|
||||
|
||||
#### `createSecret(options: SecretOptions): Promise<CreateSecretResult>`
|
||||
|
||||
Creates an encrypted secret on a Hemmelig server.
|
||||
|
||||
**Options:**
|
||||
|
||||
| Property | Type | Default | Description |
|
||||
| ----------- | --------------- | ------------------------ | ----------------------------- |
|
||||
| `secret` | `string` | required | The secret content to encrypt |
|
||||
| `title` | `string` | - | Optional title |
|
||||
| `password` | `string` | - | Password protection |
|
||||
| `expiresIn` | `ExpirationKey` | `'1d'` | Expiration time |
|
||||
| `views` | `number` | `1` | Max views (1-9999) |
|
||||
| `burnable` | `boolean` | `true` | Burn on first view |
|
||||
| `baseUrl` | `string` | `'https://hemmelig.app'` | Server URL |
|
||||
|
||||
**Returns:**
|
||||
|
||||
| Property | Type | Description |
|
||||
| ----------- | -------- | ----------------------------- |
|
||||
| `url` | `string` | Full URL to access the secret |
|
||||
| `id` | `string` | The secret ID |
|
||||
| `expiresIn` | `string` | The expiration time set |
|
||||
|
||||
## CI/CD Integration
|
||||
|
||||
### GitHub Actions
|
||||
|
||||
```yaml
|
||||
- name: Share deployment credentials
|
||||
run: |
|
||||
SECRET_URL=$(npx hemmelig "${{ secrets.DEPLOY_KEY }}" \
|
||||
-t "Deployment Key" \
|
||||
-e 1h)
|
||||
echo "Secret URL: $SECRET_URL"
|
||||
```
|
||||
|
||||
### GitLab CI
|
||||
|
||||
```yaml
|
||||
share-secret:
|
||||
script:
|
||||
- SECRET_URL=$(npx hemmelig "$DB_PASSWORD" -e 4h)
|
||||
- echo "Secret URL: $SECRET_URL"
|
||||
```
|
||||
|
||||
## Security
|
||||
|
||||
- All encryption happens locally using AES-256-GCM
|
||||
- Keys are derived using PBKDF2 with 600,000 iterations
|
||||
- The decryption key is in the URL fragment (`#decryptionKey=...`), which is never sent to the server
|
||||
- The server only stores encrypted data
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
54
cli/package-lock.json
generated
Normal file
54
cli/package-lock.json
generated
Normal file
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"name": "hemmelig",
|
||||
"version": "7.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "hemmelig",
|
||||
"version": "7.0.0",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"hemmelig": "dist/bin.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.19.26",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.26.tgz",
|
||||
"integrity": "sha512-0l6cjgF0XnihUpndDhk+nyD3exio3iKaYROSgvh/qSevPXax3L8p5DBRFjbvalnwatGgHEQn2R88y2fA3g4irg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
54
cli/package.json
Normal file
54
cli/package.json
Normal file
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"name": "hemmelig",
|
||||
"version": "7.0.0",
|
||||
"description": "CLI for creating encrypted, self-destructing secrets via Hemmelig",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"bin": {
|
||||
"hemmelig": "dist/bin.js"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"keywords": [
|
||||
"hemmelig",
|
||||
"secret",
|
||||
"secrets",
|
||||
"encryption",
|
||||
"cli",
|
||||
"security",
|
||||
"privacy",
|
||||
"self-destruct",
|
||||
"one-time",
|
||||
"share"
|
||||
],
|
||||
"author": "Hemmelig",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/HemmeligOrg/Hemmelig.app.git",
|
||||
"directory": "cli"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/HemmeligOrg/Hemmelig.app/issues"
|
||||
},
|
||||
"homepage": "https://hemmelig.app",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
197
cli/src/bin.ts
Normal file
197
cli/src/bin.ts
Normal 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
210
cli/src/index.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
19
cli/tsconfig.json
Normal file
19
cli/tsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"lib": ["ES2022"],
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user