Bump Peer.paste

This commit is contained in:
Raven Scott
2026-05-27 22:59:49 -04:00
parent c7ecffd3e4
commit 1974145b79
4 changed files with 97 additions and 21 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "Peer Paste",
"version": "1.0.2",
"version": "1.0.3",
"domain": "peer.paste",
"enabled": true,
"description": "Temporary P2P text snippets with expiration and burn-after-read options",
+64 -15
View File
@@ -61,6 +61,7 @@ function normalizePasteInput(body = {}, existing = null) {
const language = sanitizeText(body.language, existing ? existing.language : '').slice(0, MAX_LANGUAGE_CHARS);
const content = body.content !== undefined ? sanitizeText(body.content) : (existing ? existing.content : '');
const encrypted = body.encrypted && typeof body.encrypted === 'object' ? body.encrypted : null;
const isPublic = body.public === true;
const destroyOnRead = body.destroyOnRead !== undefined ? !!body.destroyOnRead : (existing ? !!existing.destroyOnRead : false);
const maxReadsRaw = body.maxReads !== undefined ? body.maxReads : (existing ? existing.maxReads : 0);
const maxReads = Math.max(0, Math.min(MAX_MAXREADS, Math.floor(Number(maxReadsRaw) || 0)));
@@ -70,6 +71,7 @@ function normalizePasteInput(body = {}, existing = null) {
language,
content,
encrypted,
isPublic,
destroyOnRead,
maxReads,
expiresHours
@@ -202,10 +204,21 @@ async function createPaste(req, res) {
if (!body) return true;
const input = normalizePasteInput(body);
if (!input.encrypted) {
return sdk.router.badRequest(res, 'encrypted payload is required');
let storedContent = '';
if (input.isPublic) {
if (!input.content || !input.content.trim()) {
return sdk.router.badRequest(res, 'content is required for public paste');
}
if (input.content.length > MAX_CONTENT_CHARS) {
return sdk.router.badRequest(res, `content exceeds ${MAX_CONTENT_CHARS} characters`);
}
storedContent = input.content;
} else {
if (!input.encrypted) {
return sdk.router.badRequest(res, 'encrypted payload is required');
}
storedContent = encodeEncryptedContent(input.encrypted);
}
const encryptedContent = encodeEncryptedContent(input.encrypted);
const createdAt = now();
const expiresAt = createdAt + (input.expiresHours || DEFAULT_EXPIRES_HOURS) * 60 * 60 * 1000;
@@ -214,7 +227,7 @@ async function createPaste(req, res) {
const paste = {
id: generatePasteId(),
title: '',
content: encryptedContent,
content: storedContent,
language: input.language,
ownerPeerId,
destroyOnRead: input.destroyOnRead,
@@ -417,12 +430,19 @@ function renderPastePage(paste) {
a{color:#60a5fa}
.meta{font-size:.9rem;color:#94a3b8;margin:.5rem 0 1rem}
pre{white-space:pre-wrap;word-break:break-word;background:#020617;border:1px solid #1e293b;border-radius:8px;padding:1rem}
.key-row{display:flex;gap:.5rem;margin:.75rem 0}
input{flex:1;background:#020617;color:#e2e8f0;border:1px solid #334155;border-radius:8px;padding:.5rem}
button{background:#2563eb;color:#fff;border:0;border-radius:8px;padding:.5rem .85rem;cursor:pointer}
</style>
</head>
<body>
<div class="wrap">
<h1>paste ${paste.id}</h1>
<div class="meta">Encrypted paste. Key must be present in URL fragment.</div>
<div class="key-row">
<input id="manual-key" placeholder="Paste key (base64url) if missing from URL" />
<button id="decrypt-btn" type="button">Decrypt</button>
</div>
<div class="card"><pre id="out">Decrypting...</pre></div>
<p><a href="/">Create another paste</a></p>
</div>
@@ -436,16 +456,20 @@ function renderPastePage(paste) {
for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);
return arr;
}
async function decryptAndRender() {
const out = document.getElementById('out');
function readKeyFromHash() {
const hash = location.hash || '';
const m = hash.match(/k=([^&]+)/);
if (!m) {
out.textContent = 'Missing decryption key in URL fragment (#k=...)';
return m ? decodeURIComponent(m[1]) : '';
}
async function decryptAndRender(providedKey) {
const out = document.getElementById('out');
const keyString = (providedKey || readKeyFromHash() || '').trim();
if (!keyString) {
out.textContent = 'Missing decryption key. Add #k=... to URL or paste key below.';
return;
}
try {
const keyBytes = b64uToBytes(decodeURIComponent(m[1]));
const keyBytes = b64uToBytes(keyString);
const iv = b64uToBytes(payload.iv);
const ciphertext = b64uToBytes(payload.ciphertext);
const key = await crypto.subtle.importKey('raw', keyBytes, { name: 'AES-GCM' }, false, ['decrypt']);
@@ -455,7 +479,19 @@ function renderPastePage(paste) {
out.textContent = 'Failed to decrypt paste: ' + (err && err.message ? err.message : 'unknown error');
}
}
decryptAndRender();
const btn = document.getElementById('decrypt-btn');
const input = document.getElementById('manual-key');
btn.addEventListener('click', () => decryptAndRender(input.value));
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') decryptAndRender(input.value);
});
const initial = readKeyFromHash();
if (initial) {
input.value = initial;
decryptAndRender(initial);
} else {
decryptAndRender('');
}
</script>
</body>
</html>`;
@@ -789,18 +825,31 @@ function setupWebSocketHandlers() {
if (message.type === 'create-paste') {
const input = normalizePasteInput(message.payload || {});
if (!input.encrypted) {
sdk.websocket.send(ws, { type: 'error', error: 'encrypted payload is required', requestId: message.requestId || null });
return;
let storedContent = '';
if (input.isPublic) {
if (!input.content || !input.content.trim()) {
sdk.websocket.send(ws, { type: 'error', error: 'content is required for public paste', requestId: message.requestId || null });
return;
}
if (input.content.length > MAX_CONTENT_CHARS) {
sdk.websocket.send(ws, { type: 'error', error: `content exceeds ${MAX_CONTENT_CHARS} characters`, requestId: message.requestId || null });
return;
}
storedContent = input.content;
} else {
if (!input.encrypted) {
sdk.websocket.send(ws, { type: 'error', error: 'encrypted payload is required', requestId: message.requestId || null });
return;
}
storedContent = encodeEncryptedContent(input.encrypted);
}
const encryptedContent = encodeEncryptedContent(input.encrypted);
const createdAt = now();
const expiresAt = createdAt + (input.expiresHours || DEFAULT_EXPIRES_HOURS) * 60 * 60 * 1000;
const paste = {
id: generatePasteId(),
title: '',
content: encryptedContent,
content: storedContent,
language: input.language,
ownerPeerId: sdk.state.localPeerId || '',
destroyOnRead: input.destroyOnRead,
+4
View File
@@ -34,6 +34,10 @@
<input type="checkbox" id="destroyOnRead" />
Burn after first read
</label>
<label class="check-row">
<input type="checkbox" id="publicPaste" />
Public paste (disable encryption)
</label>
</div>
</details>
+28 -5
View File
@@ -59,6 +59,12 @@ let visibleCount = 0;
let hasMoreServer = true;
let isLoadingPage = false;
function updateCreateButtonLabel() {
const publicPaste = document.getElementById('publicPaste').checked;
const createBtn = document.getElementById('create-btn');
createBtn.textContent = publicPaste ? 'Create public link' : 'Create secure link';
}
function nextRequestId() {
requestCounter += 1;
return `req-${Date.now()}-${requestCounter}`;
@@ -167,23 +173,35 @@ document.getElementById('create-form').addEventListener('submit', async (e) => {
createBtn.textContent = 'Creating...';
setCreateState('Creating secure link...');
try {
const publicPaste = document.getElementById('publicPaste').checked;
const payload = {
language: document.getElementById('language').value,
expiresHours: Number(document.getElementById('expiresHours').value),
maxReads: Number(document.getElementById('maxReads').value),
destroyOnRead: document.getElementById('destroyOnRead').checked
destroyOnRead: document.getElementById('destroyOnRead').checked,
public: publicPaste
};
const plain = document.getElementById('content').value;
if (!plain || !plain.trim()) {
throw new Error('content is required');
}
const encrypted = await encryptContent(plain);
payload.encrypted = encrypted.encrypted;
let linkSuffix = '';
if (publicPaste) {
payload.content = plain;
} else {
const encrypted = await encryptContent(plain);
payload.encrypted = encrypted.encrypted;
linkSuffix = `#k=${encodeURIComponent(encrypted.key)}`;
}
const msg = await sendWs({ type: 'create-paste', payload });
const data = msg.data;
shareLink.value = `${window.location.origin}${data.viewUrl}#k=${encodeURIComponent(encrypted.key)}`;
shareLink.value = `${window.location.origin}${data.viewUrl}${linkSuffix}`;
openBtn.href = data.viewUrl;
setCreateState(`Created encrypted paste. Expires in about ${msToHuman(data.paste.expiresInMs)}.`);
if (publicPaste) {
setCreateState(`Created public (unencrypted) paste. Expires in about ${msToHuman(data.paste.expiresInMs)}.`, true);
} else {
setCreateState(`Created encrypted paste. Expires in about ${msToHuman(data.paste.expiresInMs)}.`);
}
content.value = '';
content.focus();
} catch (err) {
@@ -210,11 +228,15 @@ document.getElementById('clear-btn').addEventListener('click', () => {
document.getElementById('expiresHours').value = '24';
document.getElementById('maxReads').value = '0';
document.getElementById('destroyOnRead').checked = false;
document.getElementById('publicPaste').checked = false;
updateCreateButtonLabel();
document.getElementById('content').value = '';
document.getElementById('share-link').value = '';
setCreateState('');
});
document.getElementById('publicPaste').addEventListener('change', updateCreateButtonLabel);
document.getElementById('list').addEventListener('click', async (e) => {
const link = e.target.closest('[data-copy-url]');
const destroy = e.target.closest('[data-destroy-id]');
@@ -289,3 +311,4 @@ window.addEventListener('scroll', () => {
});
connectWebSocket();
updateCreateButtonLabel();