Bump Peer.paste
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "Peer Paste",
|
"name": "Peer Paste",
|
||||||
"version": "1.0.2",
|
"version": "1.0.3",
|
||||||
"domain": "peer.paste",
|
"domain": "peer.paste",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"description": "Temporary P2P text snippets with expiration and burn-after-read options",
|
"description": "Temporary P2P text snippets with expiration and burn-after-read options",
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ function normalizePasteInput(body = {}, existing = null) {
|
|||||||
const language = sanitizeText(body.language, existing ? existing.language : '').slice(0, MAX_LANGUAGE_CHARS);
|
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 content = body.content !== undefined ? sanitizeText(body.content) : (existing ? existing.content : '');
|
||||||
const encrypted = body.encrypted && typeof body.encrypted === 'object' ? body.encrypted : null;
|
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 destroyOnRead = body.destroyOnRead !== undefined ? !!body.destroyOnRead : (existing ? !!existing.destroyOnRead : false);
|
||||||
const maxReadsRaw = body.maxReads !== undefined ? body.maxReads : (existing ? existing.maxReads : 0);
|
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)));
|
const maxReads = Math.max(0, Math.min(MAX_MAXREADS, Math.floor(Number(maxReadsRaw) || 0)));
|
||||||
@@ -70,6 +71,7 @@ function normalizePasteInput(body = {}, existing = null) {
|
|||||||
language,
|
language,
|
||||||
content,
|
content,
|
||||||
encrypted,
|
encrypted,
|
||||||
|
isPublic,
|
||||||
destroyOnRead,
|
destroyOnRead,
|
||||||
maxReads,
|
maxReads,
|
||||||
expiresHours
|
expiresHours
|
||||||
@@ -202,10 +204,21 @@ async function createPaste(req, res) {
|
|||||||
if (!body) return true;
|
if (!body) return true;
|
||||||
|
|
||||||
const input = normalizePasteInput(body);
|
const input = normalizePasteInput(body);
|
||||||
if (!input.encrypted) {
|
let storedContent = '';
|
||||||
return sdk.router.badRequest(res, 'encrypted payload is required');
|
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 createdAt = now();
|
||||||
const expiresAt = createdAt + (input.expiresHours || DEFAULT_EXPIRES_HOURS) * 60 * 60 * 1000;
|
const expiresAt = createdAt + (input.expiresHours || DEFAULT_EXPIRES_HOURS) * 60 * 60 * 1000;
|
||||||
@@ -214,7 +227,7 @@ async function createPaste(req, res) {
|
|||||||
const paste = {
|
const paste = {
|
||||||
id: generatePasteId(),
|
id: generatePasteId(),
|
||||||
title: '',
|
title: '',
|
||||||
content: encryptedContent,
|
content: storedContent,
|
||||||
language: input.language,
|
language: input.language,
|
||||||
ownerPeerId,
|
ownerPeerId,
|
||||||
destroyOnRead: input.destroyOnRead,
|
destroyOnRead: input.destroyOnRead,
|
||||||
@@ -417,12 +430,19 @@ function renderPastePage(paste) {
|
|||||||
a{color:#60a5fa}
|
a{color:#60a5fa}
|
||||||
.meta{font-size:.9rem;color:#94a3b8;margin:.5rem 0 1rem}
|
.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}
|
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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="wrap">
|
<div class="wrap">
|
||||||
<h1>paste ${paste.id}</h1>
|
<h1>paste ${paste.id}</h1>
|
||||||
<div class="meta">Encrypted paste. Key must be present in URL fragment.</div>
|
<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>
|
<div class="card"><pre id="out">Decrypting...</pre></div>
|
||||||
<p><a href="/">Create another paste</a></p>
|
<p><a href="/">Create another paste</a></p>
|
||||||
</div>
|
</div>
|
||||||
@@ -436,16 +456,20 @@ function renderPastePage(paste) {
|
|||||||
for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);
|
for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);
|
||||||
return arr;
|
return arr;
|
||||||
}
|
}
|
||||||
async function decryptAndRender() {
|
function readKeyFromHash() {
|
||||||
const out = document.getElementById('out');
|
|
||||||
const hash = location.hash || '';
|
const hash = location.hash || '';
|
||||||
const m = hash.match(/k=([^&]+)/);
|
const m = hash.match(/k=([^&]+)/);
|
||||||
if (!m) {
|
return m ? decodeURIComponent(m[1]) : '';
|
||||||
out.textContent = 'Missing decryption key in URL fragment (#k=...)';
|
}
|
||||||
|
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;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const keyBytes = b64uToBytes(decodeURIComponent(m[1]));
|
const keyBytes = b64uToBytes(keyString);
|
||||||
const iv = b64uToBytes(payload.iv);
|
const iv = b64uToBytes(payload.iv);
|
||||||
const ciphertext = b64uToBytes(payload.ciphertext);
|
const ciphertext = b64uToBytes(payload.ciphertext);
|
||||||
const key = await crypto.subtle.importKey('raw', keyBytes, { name: 'AES-GCM' }, false, ['decrypt']);
|
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');
|
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>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>`;
|
</html>`;
|
||||||
@@ -789,18 +825,31 @@ function setupWebSocketHandlers() {
|
|||||||
|
|
||||||
if (message.type === 'create-paste') {
|
if (message.type === 'create-paste') {
|
||||||
const input = normalizePasteInput(message.payload || {});
|
const input = normalizePasteInput(message.payload || {});
|
||||||
if (!input.encrypted) {
|
let storedContent = '';
|
||||||
sdk.websocket.send(ws, { type: 'error', error: 'encrypted payload is required', requestId: message.requestId || null });
|
if (input.isPublic) {
|
||||||
return;
|
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 createdAt = now();
|
||||||
const expiresAt = createdAt + (input.expiresHours || DEFAULT_EXPIRES_HOURS) * 60 * 60 * 1000;
|
const expiresAt = createdAt + (input.expiresHours || DEFAULT_EXPIRES_HOURS) * 60 * 60 * 1000;
|
||||||
const paste = {
|
const paste = {
|
||||||
id: generatePasteId(),
|
id: generatePasteId(),
|
||||||
title: '',
|
title: '',
|
||||||
content: encryptedContent,
|
content: storedContent,
|
||||||
language: input.language,
|
language: input.language,
|
||||||
ownerPeerId: sdk.state.localPeerId || '',
|
ownerPeerId: sdk.state.localPeerId || '',
|
||||||
destroyOnRead: input.destroyOnRead,
|
destroyOnRead: input.destroyOnRead,
|
||||||
|
|||||||
@@ -34,6 +34,10 @@
|
|||||||
<input type="checkbox" id="destroyOnRead" />
|
<input type="checkbox" id="destroyOnRead" />
|
||||||
Burn after first read
|
Burn after first read
|
||||||
</label>
|
</label>
|
||||||
|
<label class="check-row">
|
||||||
|
<input type="checkbox" id="publicPaste" />
|
||||||
|
Public paste (disable encryption)
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,12 @@ let visibleCount = 0;
|
|||||||
let hasMoreServer = true;
|
let hasMoreServer = true;
|
||||||
let isLoadingPage = false;
|
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() {
|
function nextRequestId() {
|
||||||
requestCounter += 1;
|
requestCounter += 1;
|
||||||
return `req-${Date.now()}-${requestCounter}`;
|
return `req-${Date.now()}-${requestCounter}`;
|
||||||
@@ -167,23 +173,35 @@ document.getElementById('create-form').addEventListener('submit', async (e) => {
|
|||||||
createBtn.textContent = 'Creating...';
|
createBtn.textContent = 'Creating...';
|
||||||
setCreateState('Creating secure link...');
|
setCreateState('Creating secure link...');
|
||||||
try {
|
try {
|
||||||
|
const publicPaste = document.getElementById('publicPaste').checked;
|
||||||
const payload = {
|
const payload = {
|
||||||
language: document.getElementById('language').value,
|
language: document.getElementById('language').value,
|
||||||
expiresHours: Number(document.getElementById('expiresHours').value),
|
expiresHours: Number(document.getElementById('expiresHours').value),
|
||||||
maxReads: Number(document.getElementById('maxReads').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;
|
const plain = document.getElementById('content').value;
|
||||||
if (!plain || !plain.trim()) {
|
if (!plain || !plain.trim()) {
|
||||||
throw new Error('content is required');
|
throw new Error('content is required');
|
||||||
}
|
}
|
||||||
const encrypted = await encryptContent(plain);
|
let linkSuffix = '';
|
||||||
payload.encrypted = encrypted.encrypted;
|
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 msg = await sendWs({ type: 'create-paste', payload });
|
||||||
const data = msg.data;
|
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;
|
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.value = '';
|
||||||
content.focus();
|
content.focus();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -210,11 +228,15 @@ document.getElementById('clear-btn').addEventListener('click', () => {
|
|||||||
document.getElementById('expiresHours').value = '24';
|
document.getElementById('expiresHours').value = '24';
|
||||||
document.getElementById('maxReads').value = '0';
|
document.getElementById('maxReads').value = '0';
|
||||||
document.getElementById('destroyOnRead').checked = false;
|
document.getElementById('destroyOnRead').checked = false;
|
||||||
|
document.getElementById('publicPaste').checked = false;
|
||||||
|
updateCreateButtonLabel();
|
||||||
document.getElementById('content').value = '';
|
document.getElementById('content').value = '';
|
||||||
document.getElementById('share-link').value = '';
|
document.getElementById('share-link').value = '';
|
||||||
setCreateState('');
|
setCreateState('');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.getElementById('publicPaste').addEventListener('change', updateCreateButtonLabel);
|
||||||
|
|
||||||
document.getElementById('list').addEventListener('click', async (e) => {
|
document.getElementById('list').addEventListener('click', async (e) => {
|
||||||
const link = e.target.closest('[data-copy-url]');
|
const link = e.target.closest('[data-copy-url]');
|
||||||
const destroy = e.target.closest('[data-destroy-id]');
|
const destroy = e.target.closest('[data-destroy-id]');
|
||||||
@@ -289,3 +311,4 @@ window.addEventListener('scroll', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
connectWebSocket();
|
connectWebSocket();
|
||||||
|
updateCreateButtonLabel();
|
||||||
|
|||||||
Reference in New Issue
Block a user