Bump Peer.paste
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user