Skip to content

Commit 2916086

Browse files
aarlintclaude
andcommitted
fix: support file paths in paste URLs and fix CLI hanging
This commit fixes two issues with the CLI's get command: 1. URL parsing now supports optional file paths (e.g., /e/{id}/filename.txt) - Updated regex patterns to match URLs with optional filenames - Preserves full pathname when constructing fetch URLs - Enables retrieval of pastes with filenames like https://paste.d3d.dev/e/jbcpXnCw/README.md 2. Fixed CLI process hanging after successful retrieval - Added process.exit(0) after content output - Prevents analytics setInterval timer from keeping process alive - Commands now exit cleanly after completion 3. Fixed decryption output property access - Changed from result.content to result.decryptedContent || result.content - Handles both property naming conventions from different decryption methods Tested with: - Encrypted pastes with filenames - GitHub user encryption (gh: prefix) - Full encryption/decryption workflow - All commands now exit properly 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 33c8825 commit 2916086

2 files changed

Lines changed: 187 additions & 35 deletions

File tree

‎cli/index.js‎

Lines changed: 160 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,12 @@ program
159159
.option('--github <username>', 'Fetch and add a GitHub user\'s GPG public key')
160160
.option('--github-name <name>', 'Custom name for the GitHub user\'s key (optional)')
161161
.option('--refresh-github-keys', 'Force refresh of cached GitHub keys (re-fetch from GitHub)')
162+
// Group management options
163+
.option('--group-create <name> <members...>', 'Create a new recipient group with members')
164+
.option('--group-add <name> <members...>', 'Add members to an existing group')
165+
.option('--group-remove <name> <members...>', 'Remove members from a group')
166+
.option('--group-delete <name>', 'Delete a group entirely')
167+
.option('--group-list', 'List all groups and their members')
162168
// Debugging and logging options
163169
.option('--verbose', 'Enable verbose logging (same as --log-level debug)')
164170
.option('--debug', 'Enable debug mode with extensive logging (same as --log-level trace)')
@@ -188,6 +194,13 @@ GitHub Integration:
188194
$ dedpaste keys --github username # Add a GitHub user's GPG key
189195
$ dedpaste keys --github username --github-name bob # Add with custom name
190196
197+
Group Management (NEW):
198+
$ dedpaste keys --group-create team gh:alice kb:bob # Create a group named 'team'
199+
$ dedpaste keys --group-add team gh:charlie # Add member to group
200+
$ dedpaste keys --group-remove team gh:bob # Remove member from group
201+
$ dedpaste keys --group-list # List all groups
202+
$ dedpaste keys --group-delete team # Delete a group
203+
191204
Key Storage:
192205
- Your keys are stored in ~/.dedpaste/keys/
193206
- Friend keys are stored in ~/.dedpaste/friends/
@@ -686,6 +699,106 @@ Key Storage:
686699
}
687700
return;
688701
}
702+
// Group management commands
703+
if (options.groupCreate) {
704+
const [groupName, ...members] = options.groupCreate;
705+
if (!groupName || members.length === 0) {
706+
console.error('Error: Group name and at least one member required');
707+
console.error('Usage: dedpaste keys --group-create <name> <member1> <member2> ...');
708+
process.exit(1);
709+
}
710+
try {
711+
const { createGroup } = await import('./groupManager.js');
712+
await createGroup(groupName, members);
713+
console.log(`\n✓ Created group "${groupName}" with ${members.length} member(s):`);
714+
for (const member of members) {
715+
console.log(` - ${member}`);
716+
}
717+
}
718+
catch (error) {
719+
console.error(`Error creating group: ${error.message}`);
720+
process.exit(1);
721+
}
722+
return;
723+
}
724+
if (options.groupAdd) {
725+
const [groupName, ...members] = options.groupAdd;
726+
if (!groupName || members.length === 0) {
727+
console.error('Error: Group name and at least one member required');
728+
console.error('Usage: dedpaste keys --group-add <name> <member1> <member2> ...');
729+
process.exit(1);
730+
}
731+
try {
732+
const { addToGroup } = await import('./groupManager.js');
733+
await addToGroup(groupName, members);
734+
console.log(`\n✓ Added ${members.length} member(s) to group "${groupName}":`);
735+
for (const member of members) {
736+
console.log(` - ${member}`);
737+
}
738+
}
739+
catch (error) {
740+
console.error(`Error adding to group: ${error.message}`);
741+
process.exit(1);
742+
}
743+
return;
744+
}
745+
if (options.groupRemove) {
746+
const [groupName, ...members] = options.groupRemove;
747+
if (!groupName || members.length === 0) {
748+
console.error('Error: Group name and at least one member required');
749+
console.error('Usage: dedpaste keys --group-remove <name> <member1> <member2> ...');
750+
process.exit(1);
751+
}
752+
try {
753+
const { removeFromGroup } = await import('./groupManager.js');
754+
await removeFromGroup(groupName, members);
755+
console.log(`\n✓ Removed ${members.length} member(s) from group "${groupName}":`);
756+
for (const member of members) {
757+
console.log(` - ${member}`);
758+
}
759+
}
760+
catch (error) {
761+
console.error(`Error removing from group: ${error.message}`);
762+
process.exit(1);
763+
}
764+
return;
765+
}
766+
if (options.groupDelete) {
767+
try {
768+
const { deleteGroup } = await import('./groupManager.js');
769+
await deleteGroup(options.groupDelete);
770+
console.log(`\n✓ Deleted group "${options.groupDelete}"`);
771+
}
772+
catch (error) {
773+
console.error(`Error deleting group: ${error.message}`);
774+
process.exit(1);
775+
}
776+
return;
777+
}
778+
if (options.groupList) {
779+
try {
780+
const { listGroups } = await import('./groupManager.js');
781+
const groups = await listGroups();
782+
if (Object.keys(groups).length === 0) {
783+
console.log('\nNo groups found. Create one with:');
784+
console.log(' dedpaste keys --group-create <name> <member1> <member2> ...');
785+
return;
786+
}
787+
console.log('\n📋 Recipient Groups:\n');
788+
for (const [name, members] of Object.entries(groups)) {
789+
console.log(` ${name} (${members.length} member${members.length !== 1 ? 's' : ''}):`);
790+
for (const member of members) {
791+
console.log(` - ${member}`);
792+
}
793+
console.log('');
794+
}
795+
}
796+
catch (error) {
797+
console.error(`Error listing groups: ${error.message}`);
798+
process.exit(1);
799+
}
800+
return;
801+
}
689802
// Generate a new key pair
690803
if (options.genKey) {
691804
logger.debug('Generating new key pair');
@@ -739,7 +852,7 @@ program
739852
.option('-f, --file <path>', 'Upload a file from the specified path instead of stdin')
740853
.option('-o, --output', 'Print only the URL (without any additional text, useful for scripts)')
741854
.option('-e, --encrypt', 'Encrypt the content before uploading (requires key setup)')
742-
.option('--for <friend>', 'Encrypt for a specific friend (requires adding their key first)')
855+
.option('--for <recipients...>', 'Encrypt for recipient(s): supports gh:user, kb:user, email@domain, or group names')
743856
.option('--list-friends', 'List available friends you can encrypt messages for')
744857
.option('--key-file <path>', 'Path to public key for encryption (alternative to stored keys)')
745858
.option('--gen-key', 'Generate a new key pair for encryption if you don\'t have one')
@@ -761,18 +874,24 @@ Examples:
761874
$ dedpaste send --enhanced --encrypt # Enhanced interactive mode with full key support
762875
$ dedpaste send --list-friends # List available recipients
763876
877+
Short Prefix Support (NEW):
878+
$ dedpaste send --encrypt --for gh:torvalds # GitHub user (short prefix)
879+
$ dedpaste send --encrypt --for kb:username # Keybase user (short prefix)
880+
$ dedpaste send --encrypt --for github:torvalds # GitHub user (full prefix - still works)
881+
882+
Multiple Recipients (NEW):
883+
$ dedpaste send --encrypt --for gh:alice kb:bob charlie@example.com # Multiple recipients (space-separated)
884+
885+
Groups (NEW):
886+
$ dedpaste keys --group-create team gh:alice kb:bob # Create a group
887+
$ dedpaste send --encrypt --for team # Encrypt for all group members
888+
764889
PGP Options:
765-
$ dedpaste send --encrypt --for alice@example.com --pgp # Encrypt for PGP key (REQUIRED: specify recipient)
890+
$ dedpaste send --encrypt --for alice@example.com # Encrypt for PGP key (auto-detects PGP)
766891
$ dedpaste send --encrypt --for alice --pgp --pgp-key-file friend.asc # Use specific PGP key file
767892
$ dedpaste send --enhanced --encrypt --pgp # Use enhanced mode with GPG keyring support
768893
769-
Keybase Integration:
770-
$ dedpaste send --encrypt --for keybase:username --pgp # Encrypt for Keybase user
771-
772-
GitHub Integration:
773-
$ dedpaste send --encrypt --for github:username --pgp # Encrypt for GitHub user (auto-fetches GPG key)
774-
775-
IMPORTANT: PGP encryption always requires specifying a recipient with --for
894+
NOTE: PGP encryption is now auto-detected based on recipient type. The --pgp flag is optional.
776895
777896
Encryption:
778897
- Standard encryption uses RSA for key exchange and AES-256-GCM for content
@@ -818,7 +937,8 @@ Encryption:
818937
process.exit(1);
819938
}
820939
content = Buffer.from(result.content || '');
821-
recipientName = result.recipient ?? undefined;
940+
// Interactive mode returns a single string, wrap in array if provided
941+
recipientName = result.recipient ? [result.recipient] : undefined;
822942
options.temp = result.temp;
823943
options.pgp = result.pgp; // Use PGP flag from enhanced mode
824944
contentType = 'text/plain';
@@ -827,7 +947,8 @@ Encryption:
827947
// Use standard interactive mode
828948
const result = await interactiveSend();
829949
content = Buffer.from(result.content || '');
830-
recipientName = result.recipient ?? undefined;
950+
// Interactive mode returns a single string, wrap in array if provided
951+
recipientName = result.recipient ? [result.recipient] : undefined;
831952
options.temp = result.temp;
832953
contentType = 'text/plain';
833954
}
@@ -896,17 +1017,19 @@ Encryption:
8961017
}
8971018
// If PGP key file is provided, read it and use it directly
8981019
if (options.pgpKeyFile) {
899-
if (!recipientName) {
1020+
if (!recipientName || (Array.isArray(recipientName) && recipientName.length === 0)) {
9001021
console.error(`Error: When using --pgp-key-file, you must specify a recipient with --for`);
9011022
process.exit(1);
9021023
}
1024+
// For pgpKeyFile, use the first recipient
1025+
const firstRecipient = Array.isArray(recipientName) ? recipientName[0] : recipientName;
9031026
const pgpKeyContent = await fsPromises.readFile(options.pgpKeyFile, 'utf8');
904-
const pgpResult = await createPgpEncryptedMessage(content, pgpKeyContent, recipientName);
1027+
const pgpResult = await createPgpEncryptedMessage(content, pgpKeyContent, firstRecipient);
9051028
content = typeof pgpResult === 'string' ? Buffer.from(pgpResult) : pgpResult;
9061029
}
9071030
else {
9081031
// Use the standard encryption flow with PGP option
909-
const encryptResult = await encryptContent(content.toString('utf8'), recipientName, usePgp, options.refreshGithubKeys);
1032+
const encryptResult = await encryptContent(content.toString('utf8'), recipientName ?? null, usePgp, options.refreshGithubKeys);
9101033
content = typeof encryptResult === 'string' ? Buffer.from(encryptResult) : encryptResult;
9111034
}
9121035
// Log PGP mode if used
@@ -1086,14 +1209,15 @@ GPG Keyring Integration:
10861209
// It's a URL
10871210
const url = new URL(urlOrId);
10881211
const path = url.pathname;
1089-
// Check if it's an encrypted paste
1090-
const encryptedMatch = path.match(/^\/e\/([a-zA-Z0-9]{8})$/);
1212+
// Check if it's an encrypted paste (with optional filename)
1213+
const encryptedMatch = path.match(/^\/e\/([a-zA-Z0-9]{8})(?:\/(.+))?$/);
10911214
if (encryptedMatch) {
10921215
id = encryptedMatch[1];
10931216
isEncrypted = true;
10941217
}
10951218
else {
1096-
const regularMatch = path.match(/^\/([a-zA-Z0-9]{8})$/);
1219+
// Check if it's a regular paste (with optional filename)
1220+
const regularMatch = path.match(/^\/([a-zA-Z0-9]{8})(?:\/(.+))?$/);
10971221
if (regularMatch) {
10981222
id = regularMatch[1];
10991223
}
@@ -1118,9 +1242,17 @@ GPG Keyring Integration:
11181242
process.exit(1);
11191243
}
11201244
// Determine the URL to fetch
1121-
const fetchUrl = isEncrypted
1122-
? `${API_URL}/e/${id}`
1123-
: `${API_URL}/${id}`;
1245+
// If the original URL had a full path with filename, use it
1246+
let fetchUrl;
1247+
if (urlOrId.startsWith('http')) {
1248+
const url = new URL(urlOrId);
1249+
fetchUrl = `${API_URL}${url.pathname}`;
1250+
}
1251+
else {
1252+
fetchUrl = isEncrypted
1253+
? `${API_URL}/e/${id}`
1254+
: `${API_URL}/${id}`;
1255+
}
11241256
console.log(`Fetching paste from ${fetchUrl}...`);
11251257
// Fetch the paste
11261258
const response = await fetch(fetchUrl);
@@ -1306,11 +1438,14 @@ GPG Keyring Integration:
13061438
}
13071439
}
13081440
console.log('\n✓ Paste decrypted successfully:\n');
1309-
process.stdout.write(result.content);
1441+
const contentToWrite = result.decryptedContent || result.content;
1442+
process.stdout.write(contentToWrite);
13101443
// Add a newline at the end if the content doesn't end with one
1311-
if (result.content.length > 0 && result.content[result.content.length - 1] !== 10) { // ASCII 10 is newline
1312-
process.stdout.write(Buffer.from('\n'));
1444+
if (contentToWrite.length > 0 && contentToWrite[contentToWrite.length - 1] !== '\n') {
1445+
process.stdout.write('\n');
13131446
}
1447+
// Exit successfully
1448+
process.exit(0);
13141449
}
13151450
catch (error) {
13161451
console.error(`\n❌ Decryption error: ${error.message}`);
@@ -1346,6 +1481,8 @@ GPG Keyring Integration:
13461481
if (contentBuffer.length > 0 && contentBuffer[contentBuffer.length - 1] !== 10) { // ASCII 10 is newline
13471482
process.stdout.write(Buffer.from('\n'));
13481483
}
1484+
// Exit successfully
1485+
process.exit(0);
13491486
}
13501487
}
13511488
catch (error) {

‎cli/index.ts‎

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1485,14 +1485,15 @@ GPG Keyring Integration:
14851485
// It's a URL
14861486
const url = new URL(urlOrId);
14871487
const path = url.pathname;
1488-
1489-
// Check if it's an encrypted paste
1490-
const encryptedMatch = path.match(/^\/e\/([a-zA-Z0-9]{8})$/);
1488+
1489+
// Check if it's an encrypted paste (with optional filename)
1490+
const encryptedMatch = path.match(/^\/e\/([a-zA-Z0-9]{8})(?:\/(.+))?$/);
14911491
if (encryptedMatch) {
14921492
id = encryptedMatch[1];
14931493
isEncrypted = true;
14941494
} else {
1495-
const regularMatch = path.match(/^\/([a-zA-Z0-9]{8})$/);
1495+
// Check if it's a regular paste (with optional filename)
1496+
const regularMatch = path.match(/^\/([a-zA-Z0-9]{8})(?:\/(.+))?$/);
14961497
if (regularMatch) {
14971498
id = regularMatch[1];
14981499
} else {
@@ -1514,9 +1515,16 @@ GPG Keyring Integration:
15141515
}
15151516

15161517
// Determine the URL to fetch
1517-
const fetchUrl = isEncrypted
1518-
? `${API_URL}/e/${id}`
1519-
: `${API_URL}/${id}`;
1518+
// If the original URL had a full path with filename, use it
1519+
let fetchUrl: string;
1520+
if (urlOrId.startsWith('http')) {
1521+
const url = new URL(urlOrId);
1522+
fetchUrl = `${API_URL}${url.pathname}`;
1523+
} else {
1524+
fetchUrl = isEncrypted
1525+
? `${API_URL}/e/${id}`
1526+
: `${API_URL}/${id}`;
1527+
}
15201528

15211529
console.log(`Fetching paste from ${fetchUrl}...`);
15221530

@@ -1727,12 +1735,16 @@ GPG Keyring Integration:
17271735
}
17281736

17291737
console.log('\n✓ Paste decrypted successfully:\n');
1730-
process.stdout.write(result.content);
1731-
1738+
const contentToWrite = result.decryptedContent || result.content;
1739+
process.stdout.write(contentToWrite);
1740+
17321741
// Add a newline at the end if the content doesn't end with one
1733-
if (result.content.length > 0 && result.content[result.content.length - 1] !== 10) { // ASCII 10 is newline
1734-
process.stdout.write(Buffer.from('\n'));
1742+
if (contentToWrite.length > 0 && contentToWrite[contentToWrite.length - 1] !== '\n') {
1743+
process.stdout.write('\n');
17351744
}
1745+
1746+
// Exit successfully
1747+
process.exit(0);
17361748
} catch (error: any) {
17371749
console.error(`\n❌ Decryption error: ${error.message}`);
17381750

@@ -1761,11 +1773,14 @@ GPG Keyring Integration:
17611773
} else {
17621774
// Just output the content
17631775
process.stdout.write(contentBuffer);
1764-
1776+
17651777
// Add a newline at the end if the content doesn't end with one
17661778
if (contentBuffer.length > 0 && contentBuffer[contentBuffer.length - 1] !== 10) { // ASCII 10 is newline
17671779
process.stdout.write(Buffer.from('\n'));
17681780
}
1781+
1782+
// Exit successfully
1783+
process.exit(0);
17691784
}
17701785
} catch (error: any) {
17711786
console.error('Error:', error.message);

0 commit comments

Comments
 (0)