openvibers/openvibe.community Commits
Unlisted pastes get slugs nobody can guess: a new unlisted or private paste (and a fork of one) gets adj-noun- plus 16 random base62 characters (~2^95) instead of the ~1.5M adj-noun-NN space that could be walked; public pastes keep the short form, service-named slugs are unchanged, and existing slugs keep working
Changes
@@@ -64,6 +64,9 @@ redirects its old paste URLs here after cutover).(`POST ${OV_MEDIA_INTERNAL_URL}/api/v1/community/files`, service token with`media.object.upload`); the paste keeps the public URL and `media_ref``legacy:community:file:<key>`. Imported pastes keep their existing Media screenshot URLs.++- **Slugs**: public pastes get a short `adj-noun-NN` slug; a new unlisted or private paste (and++ a fork of one) gets `adj-noun-` + 16 random base62 characters, because its slug is the only++ thing keeping it unlisted. Existing slugs never change and keep working.- **Deletes are soft** (content scrubbed, slug kept reserved); edits append `paste_versions`.Moving the data: `node scripts/import-pastes.js <bundle.json> [--dry-run] [--id-fix-cutoff@@@ -262,8 +262,9 @@ function createPasteService({ db, network = null, media = null, config = {}, limreturn { ai_summary: String(body.ai_summary).slice(0, 2000), ai_tags: tags, ai_analyzed_at: new Date().toISOString().replace('T', ' ').slice(0, 19) };}-- function slugFor(v, body) {-- if (!isService(v) || body.slug == null || body.slug === '') return store.generateSlug(db);++ /** A service may name the slug (Live's imports); otherwise it is generated — secret unless the paste is public. */++ function slugFor(v, body, visibility) {++ if (!isService(v) || body.slug == null || body.slug === '') return store.generateSlug(db, { secret: visibility !== 'public' });const slug = String(body.slug);if (!SERVICE_SLUG_RE.test(slug) || RESERVED_SLUGS.has(slug.toLowerCase())) fail(400, 'Invalid slug');if (db.prepare('SELECT 1 FROM pastes WHERE slug = ?').get(slug)) fail(409, 'Slug already taken');@@@ -356,15 +357,16 @@ function createPasteService({ db, network = null, media = null, config = {}, limif (!content || typeof content !== 'string' || content.trim().length === 0) fail(400, 'Content is required');if (content.length > L.maxSizeKb * 1024) fail(400, `Paste too large (max ${L.maxSizeKb} KB)`);const metadata = serviceMetadata(v, body);++ const visibility = visibilityOf(body.visibility, !!v.subject);const row = store.insertPaste(db, {-- slug: slugFor(v, body),++ slug: slugFor(v, body, visibility),owner_subject: v.subject || null,origin: v.origin === 'ai' ? 'ai' : 'user',type: 'paste',title: store.sanitizeTitle(body.title),content: content.trim(),language: store.detectLanguage(content, body.language),-- visibility: visibilityOf(body.visibility, !!v.subject),++ visibility,stream_ref: streamRefFor(v, body),metadata: metadata ? JSON.stringify(metadata) : null,burn_after_read: truthy(body.burn_after_read) ? 1 : 0,@@@ -381,7 +383,8 @@ function createPasteService({ db, network = null, media = null, config = {}, limif (!IMAGE_MIME.test(file.mimetype || '')) fail(400, 'Only PNG, JPEG, WebP, or GIF images allowed');if (file.buffer.length > L.screenshotMaxSizeMb * 1024 * 1024) fail(400, `File too large (max ${L.screenshotMaxSizeMb} MB)`);if (!media) fail(503, 'Media service unavailable');-- const slug = slugFor(v, body);++ const visibility = visibilityOf(body.visibility, !!v.subject);++ const slug = slugFor(v, body, visibility);const extra = serviceMetadata(v, body);const bytes = stripImageMetadata(file.buffer, file.mimetype);let stored;@@@ -407,7 +410,7 @@ function createPasteService({ db, network = null, media = null, config = {}, limtitle: store.sanitizeTitle(body.title || 'Screenshot'),content: String(body.description || body.content || ''),language: 'text',-- visibility: visibilityOf(body.visibility, !!v.subject),++ visibility,screenshot_url: stored.url,media_ref: stored.media_ref,stream_ref: streamRefFor(v, body),@@@ -467,16 +470,17 @@ function createPasteService({ db, network = null, media = null, config = {}, lim// A lasting copy would outlive the burn; only the owner (or staff) may fork one.if (original.burn_after_read && !canSeeHidden(v, original)) fail(403, 'Burn-after-read pastes cannot be forked');pasteRateCheck(v);++ // A fork never widens who can find the content: unlisted stays unlisted (and gets a secret slug).++ const visibility = visibilityOf(original.visibility, !!v.subject);const row = store.insertPaste(db, {-- slug: store.generateSlug(db),++ slug: store.generateSlug(db, { secret: visibility !== 'public' }),owner_subject: v.subject || null,origin: v.origin === 'ai' ? 'ai' : 'user',type: 'paste',title: store.sanitizeTitle(`Fork of ${original.title}`),content: original.content,language: original.language,-- // A fork never widens who can find the content: unlisted stays unlisted.-- visibility: visibilityOf(original.visibility, !!v.subject),++ visibility,forked_from: original.id,});return created(row, { paste: await shapeOne(row, v) });@@@ -50,9 +50,29 @@ const SLUG_NOUNS = ['trout', 'tulip', 'vale', 'vine', 'viper', 'wave', 'wren', 'wolf',];--/** A free adj-noun-NN slug. Deleted pastes keep theirs, so an old link never points somewhere new. */--function generateSlug(db) {++const SECRET_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';++const SECRET_CHARS = 16; // 62^16 ≈ 2^95, on top of the words++++/**++ * A free slug. Deleted pastes keep theirs, so an old link never points somewhere new.++ *++ * Public pastes get a short adj-noun-NN slug (listed anyway, so nothing to guess). An unlisted or++ * private paste's slug is the only thing keeping it unlisted, so `secret` adds 16 random base62++ * characters (adj-noun-XXXXXXXXXXXXXXXX): the ~1.5M word/number slugs could be walked, these++ * cannot. Existing slugs never change.++ */++function generateSlug(db, { secret = false } = {}) {const taken = db.prepare('SELECT 1 FROM pastes WHERE slug = ?');++ const words = () => `${SLUG_ADJECTIVES[crypto.randomInt(SLUG_ADJECTIVES.length)]}-${SLUG_NOUNS[crypto.randomInt(SLUG_NOUNS.length)]}`;++ if (secret) {++ for (let i = 0; i < 10; i++) {++ let tail = '';++ for (let j = 0; j < SECRET_CHARS; j++) tail += SECRET_ALPHABET[crypto.randomInt(SECRET_ALPHABET.length)];++ const slug = `${words()}-${tail}`;++ if (!taken.get(slug)) return slug;++ }++ throw new Error('Could not generate a unique paste slug');++ }for (let i = 0; i < 10; i++) {const slug = `${SLUG_ADJECTIVES[crypto.randomInt(SLUG_ADJECTIVES.length)]}-${SLUG_NOUNS[crypto.randomInt(SLUG_NOUNS.length)]}-${crypto.randomInt(10, 100)}`;if (!taken.get(slug)) return slug;@@@ -7,6 +7,7 @@* - burn-after-read pastes: every way of reading the content counts as the read (?no_view=1,* download, fork), and they never appear in public lists, search, feeds or the home page.* - a fork of an unlisted paste is not published as a public paste.++ * - unlisted (and private) pastes get slugs nobody can guess; existing slugs keep working.*/const assert = require('assert');const { boot, check, done } = require('./helpers/app');@@@ -137,6 +138,37 @@ const { boot, check, done } = require('./helpers/app');assert.strictEqual(pf.json().paste.visibility, 'public', 'forks of public pastes stay public');});++ // ── unlisted slugs cannot be guessed ─────────────────────++ await check('new unlisted/private pastes get a high-entropy slug; public ones keep the short form; old slugs still open', async () => {++ const SECRET = /^[a-z]+-[a-z]+-[A-Za-z0-9]{16}$/;++ const SHORT = /^[a-z]+-[a-z]+-\d{2,4}$/;++ const slugs = new Set();++ for (const [who, visibility] of [[{ cookie: alexJwt }, 'unlisted'], [{ cookie: alexJwt }, 'private'], [{}, 'unlisted'], [{}, 'private']]) {++ const r = await call('/api/pastes', { method: 'POST', ...who, json: { content: `hidden ${visibility}`, visibility }, ip: '192.0.2.61' });++ assert.strictEqual(r.status, 201, r.text);++ assert.match(r.json().slug, SECRET, `${visibility} (${who.cookie ? 'signed in' : 'anonymous'})`);++ slugs.add(r.json().slug);++ }++ const fork = await call(`/api/pastes/${[...slugs][0]}/fork`, { method: 'POST', cookie: samJwt, json: {} });++ assert.strictEqual(fork.status, 201, fork.text);++ assert.match(fork.json().slug, SECRET, 'a fork of an unlisted paste is unlisted, with a secret slug');++ const service = await call('/api/pastes', { method: 'POST', token: net.signService({ cap: [CREATE] }), headers: { 'x-ov-subject': alex.subject_id }, json: { content: 'svc hidden', visibility: 'unlisted' } });++ assert.strictEqual(service.status, 201, service.text);++ assert.match(service.json().slug, SECRET, 'service-created unlisted pastes too, unless the service names the slug');++ const pub = await call('/api/pastes', { method: 'POST', cookie: alexJwt, json: { content: 'public words' } });++ assert.match(pub.json().slug, SHORT, 'public pastes are listed anyway: short slug');++ // A paste made before this change (short slug, unlisted) still opens by its slug.++ t.db.prepare("INSERT INTO pastes (slug, type, title, content, language, visibility) VALUES ('calm-otter-42', 'paste', 'old', 'old unlisted words', 'text', 'unlisted')").run();++ const old = await call('/api/pastes/calm-otter-42');++ assert.strictEqual(old.status, 200, old.text);++ assert.strictEqual(old.json().paste.content, 'old unlisted words');++ assert.strictEqual((await call('/p/calm-otter-42/raw')).status, 200);++ // The secret part is really random.++ const { generateSlug } = require('../server/pastes/store');++ const many = new Set(Array.from({ length: 2000 }, () => generateSlug(t.db, { secret: true }).split('-')[2]));++ assert.strictEqual(many.size, 2000);++ });++await t.close();done();})();
@@ -64,6 +64,9 @@ redirects its old paste URLs here after cutover).
@@ -64,6 +64,9 @@ redirects its old paste URLs here after cutover).
(`POST ${OV_MEDIA_INTERNAL_URL}/api/v1/community/files`, service token with
(`POST ${OV_MEDIA_INTERNAL_URL}/api/v1/community/files`, service token with
`media.object.upload`); the paste keeps the public URL and `media_ref`
`media.object.upload`); the paste keeps the public URL and `media_ref`
`legacy:community:file:<key>`. Imported pastes keep their existing Media screenshot URLs.
`legacy:community:file:<key>`. Imported pastes keep their existing Media screenshot URLs.
+- **Slugs**: public pastes get a short `adj-noun-NN` slug; a new unlisted or private paste (and
+ a fork of one) gets `adj-noun-` + 16 random base62 characters, because its slug is the only
+ thing keeping it unlisted. Existing slugs never change and keep working.
- **Deletes are soft** (content scrubbed, slug kept reserved); edits append `paste_versions`.
- **Deletes are soft** (content scrubbed, slug kept reserved); edits append `paste_versions`.
Moving the data: `node scripts/import-pastes.js <bundle.json> [--dry-run] [--id-fix-cutoff
Moving the data: `node scripts/import-pastes.js <bundle.json> [--dry-run] [--id-fix-cutoff
@@ -262,8 +262,9 @@ function createPasteService({ db, network = null, media = null, config = {}, lim
@@ -262,8 +262,9 @@ function createPasteService({ db, network = null, media = null, config = {}, lim
return { ai_summary: String(body.ai_summary).slice(0, 2000), ai_tags: tags, ai_analyzed_at: new Date().toISOString().replace('T', ' ').slice(0, 19) };
return { ai_summary: String(body.ai_summary).slice(0, 2000), ai_tags: tags, ai_analyzed_at: new Date().toISOString().replace('T', ' ').slice(0, 19) };
}
}
- function slugFor(v, body) {
- if (!isService(v) || body.slug == null || body.slug === '') return store.generateSlug(db);
+ /** A service may name the slug (Live's imports); otherwise it is generated — secret unless the paste is public. */
+ function slugFor(v, body, visibility) {
+ if (!isService(v) || body.slug == null || body.slug === '') return store.generateSlug(db, { secret: visibility !== 'public' });
const slug = String(body.slug);
const slug = String(body.slug);
if (!SERVICE_SLUG_RE.test(slug) || RESERVED_SLUGS.has(slug.toLowerCase())) fail(400, 'Invalid slug');
if (!SERVICE_SLUG_RE.test(slug) || RESERVED_SLUGS.has(slug.toLowerCase())) fail(400, 'Invalid slug');
if (db.prepare('SELECT 1 FROM pastes WHERE slug = ?').get(slug)) fail(409, 'Slug already taken');
if (db.prepare('SELECT 1 FROM pastes WHERE slug = ?').get(slug)) fail(409, 'Slug already taken');
@@ -356,15 +357,16 @@ function createPasteService({ db, network = null, media = null, config = {}, lim
@@ -356,15 +357,16 @@ function createPasteService({ db, network = null, media = null, config = {}, lim
if (!content || typeof content !== 'string' || content.trim().length === 0) fail(400, 'Content is required');
if (!content || typeof content !== 'string' || content.trim().length === 0) fail(400, 'Content is required');
if (content.length > L.maxSizeKb * 1024) fail(400, `Paste too large (max ${L.maxSizeKb} KB)`);
if (content.length > L.maxSizeKb * 1024) fail(400, `Paste too large (max ${L.maxSizeKb} KB)`);
const metadata = serviceMetadata(v, body);
const metadata = serviceMetadata(v, body);
+ const visibility = visibilityOf(body.visibility, !!v.subject);
const row = store.insertPaste(db, {
const row = store.insertPaste(db, {
- slug: slugFor(v, body),
+ slug: slugFor(v, body, visibility),
owner_subject: v.subject || null,
owner_subject: v.subject || null,
origin: v.origin === 'ai' ? 'ai' : 'user',
origin: v.origin === 'ai' ? 'ai' : 'user',
type: 'paste',
type: 'paste',
title: store.sanitizeTitle(body.title),
title: store.sanitizeTitle(body.title),
content: content.trim(),
content: content.trim(),
language: store.detectLanguage(content, body.language),
language: store.detectLanguage(content, body.language),
- visibility: visibilityOf(body.visibility, !!v.subject),
+ visibility,
stream_ref: streamRefFor(v, body),
stream_ref: streamRefFor(v, body),
metadata: metadata ? JSON.stringify(metadata) : null,
metadata: metadata ? JSON.stringify(metadata) : null,
burn_after_read: truthy(body.burn_after_read) ? 1 : 0,
burn_after_read: truthy(body.burn_after_read) ? 1 : 0,
@@ -381,7 +383,8 @@ function createPasteService({ db, network = null, media = null, config = {}, lim
@@ -381,7 +383,8 @@ function createPasteService({ db, network = null, media = null, config = {}, lim
if (!IMAGE_MIME.test(file.mimetype || '')) fail(400, 'Only PNG, JPEG, WebP, or GIF images allowed');
if (!IMAGE_MIME.test(file.mimetype || '')) fail(400, 'Only PNG, JPEG, WebP, or GIF images allowed');
if (file.buffer.length > L.screenshotMaxSizeMb * 1024 * 1024) fail(400, `File too large (max ${L.screenshotMaxSizeMb} MB)`);
if (file.buffer.length > L.screenshotMaxSizeMb * 1024 * 1024) fail(400, `File too large (max ${L.screenshotMaxSizeMb} MB)`);
if (!media) fail(503, 'Media service unavailable');
if (!media) fail(503, 'Media service unavailable');
- const slug = slugFor(v, body);
+ const visibility = visibilityOf(body.visibility, !!v.subject);
+ const slug = slugFor(v, body, visibility);
const extra = serviceMetadata(v, body);
const extra = serviceMetadata(v, body);
const bytes = stripImageMetadata(file.buffer, file.mimetype);
const bytes = stripImageMetadata(file.buffer, file.mimetype);
let stored;
let stored;
@@ -407,7 +410,7 @@ function createPasteService({ db, network = null, media = null, config = {}, lim
@@ -407,7 +410,7 @@ function createPasteService({ db, network = null, media = null, config = {}, lim
title: store.sanitizeTitle(body.title || 'Screenshot'),
title: store.sanitizeTitle(body.title || 'Screenshot'),
content: String(body.description || body.content || ''),
content: String(body.description || body.content || ''),
language: 'text',
language: 'text',
- visibility: visibilityOf(body.visibility, !!v.subject),
+ visibility,
screenshot_url: stored.url,
screenshot_url: stored.url,
media_ref: stored.media_ref,
media_ref: stored.media_ref,
stream_ref: streamRefFor(v, body),
stream_ref: streamRefFor(v, body),
@@ -467,16 +470,17 @@ function createPasteService({ db, network = null, media = null, config = {}, lim
@@ -467,16 +470,17 @@ function createPasteService({ db, network = null, media = null, config = {}, lim
// A lasting copy would outlive the burn; only the owner (or staff) may fork one.
// A lasting copy would outlive the burn; only the owner (or staff) may fork one.
if (original.burn_after_read && !canSeeHidden(v, original)) fail(403, 'Burn-after-read pastes cannot be forked');
if (original.burn_after_read && !canSeeHidden(v, original)) fail(403, 'Burn-after-read pastes cannot be forked');
pasteRateCheck(v);
pasteRateCheck(v);
+ // A fork never widens who can find the content: unlisted stays unlisted (and gets a secret slug).
+ const visibility = visibilityOf(original.visibility, !!v.subject);
const row = store.insertPaste(db, {
const row = store.insertPaste(db, {
- slug: store.generateSlug(db),
+ slug: store.generateSlug(db, { secret: visibility !== 'public' }),
owner_subject: v.subject || null,
owner_subject: v.subject || null,
origin: v.origin === 'ai' ? 'ai' : 'user',
origin: v.origin === 'ai' ? 'ai' : 'user',
type: 'paste',
type: 'paste',
title: store.sanitizeTitle(`Fork of ${original.title}`),
title: store.sanitizeTitle(`Fork of ${original.title}`),
content: original.content,
content: original.content,
language: original.language,
language: original.language,
- // A fork never widens who can find the content: unlisted stays unlisted.
- visibility: visibilityOf(original.visibility, !!v.subject),
+ visibility,
forked_from: original.id,
forked_from: original.id,
});
});
return created(row, { paste: await shapeOne(row, v) });
return created(row, { paste: await shapeOne(row, v) });
@@ -50,9 +50,29 @@ const SLUG_NOUNS = [
@@ -50,9 +50,29 @@ const SLUG_NOUNS = [
'trout', 'tulip', 'vale', 'vine', 'viper', 'wave', 'wren', 'wolf',
'trout', 'tulip', 'vale', 'vine', 'viper', 'wave', 'wren', 'wolf',
];
];
-/** A free adj-noun-NN slug. Deleted pastes keep theirs, so an old link never points somewhere new. */
-function generateSlug(db) {
+const SECRET_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+const SECRET_CHARS = 16; // 62^16 ≈ 2^95, on top of the words
+
+/**
+ * A free slug. Deleted pastes keep theirs, so an old link never points somewhere new.
+ *
+ * Public pastes get a short adj-noun-NN slug (listed anyway, so nothing to guess). An unlisted or
+ * private paste's slug is the only thing keeping it unlisted, so `secret` adds 16 random base62
+ * characters (adj-noun-XXXXXXXXXXXXXXXX): the ~1.5M word/number slugs could be walked, these
+ * cannot. Existing slugs never change.
+ */
+function generateSlug(db, { secret = false } = {}) {
const taken = db.prepare('SELECT 1 FROM pastes WHERE slug = ?');
const taken = db.prepare('SELECT 1 FROM pastes WHERE slug = ?');
+ const words = () => `${SLUG_ADJECTIVES[crypto.randomInt(SLUG_ADJECTIVES.length)]}-${SLUG_NOUNS[crypto.randomInt(SLUG_NOUNS.length)]}`;
+ if (secret) {
+ for (let i = 0; i < 10; i++) {
+ let tail = '';
+ for (let j = 0; j < SECRET_CHARS; j++) tail += SECRET_ALPHABET[crypto.randomInt(SECRET_ALPHABET.length)];
+ const slug = `${words()}-${tail}`;
+ if (!taken.get(slug)) return slug;
+ }
+ throw new Error('Could not generate a unique paste slug');
+ }
for (let i = 0; i < 10; i++) {
for (let i = 0; i < 10; i++) {
const slug = `${SLUG_ADJECTIVES[crypto.randomInt(SLUG_ADJECTIVES.length)]}-${SLUG_NOUNS[crypto.randomInt(SLUG_NOUNS.length)]}-${crypto.randomInt(10, 100)}`;
const slug = `${SLUG_ADJECTIVES[crypto.randomInt(SLUG_ADJECTIVES.length)]}-${SLUG_NOUNS[crypto.randomInt(SLUG_NOUNS.length)]}-${crypto.randomInt(10, 100)}`;
if (!taken.get(slug)) return slug;
if (!taken.get(slug)) return slug;
@@ -7,6 +7,7 @@
@@ -7,6 +7,7 @@
* - burn-after-read pastes: every way of reading the content counts as the read (?no_view=1,
* - burn-after-read pastes: every way of reading the content counts as the read (?no_view=1,
* download, fork), and they never appear in public lists, search, feeds or the home page.
* download, fork), and they never appear in public lists, search, feeds or the home page.
* - a fork of an unlisted paste is not published as a public paste.
* - a fork of an unlisted paste is not published as a public paste.
+ * - unlisted (and private) pastes get slugs nobody can guess; existing slugs keep working.
*/
*/
const assert = require('assert');
const assert = require('assert');
const { boot, check, done } = require('./helpers/app');
const { boot, check, done } = require('./helpers/app');
@@ -137,6 +138,37 @@ const { boot, check, done } = require('./helpers/app');
@@ -137,6 +138,37 @@ const { boot, check, done } = require('./helpers/app');
assert.strictEqual(pf.json().paste.visibility, 'public', 'forks of public pastes stay public');
assert.strictEqual(pf.json().paste.visibility, 'public', 'forks of public pastes stay public');
});
});
+ // ── unlisted slugs cannot be guessed ─────────────────────
+ await check('new unlisted/private pastes get a high-entropy slug; public ones keep the short form; old slugs still open', async () => {
+ const SECRET = /^[a-z]+-[a-z]+-[A-Za-z0-9]{16}$/;
+ const SHORT = /^[a-z]+-[a-z]+-\d{2,4}$/;
+ const slugs = new Set();
+ for (const [who, visibility] of [[{ cookie: alexJwt }, 'unlisted'], [{ cookie: alexJwt }, 'private'], [{}, 'unlisted'], [{}, 'private']]) {
+ const r = await call('/api/pastes', { method: 'POST', ...who, json: { content: `hidden ${visibility}`, visibility }, ip: '192.0.2.61' });
+ assert.strictEqual(r.status, 201, r.text);
+ assert.match(r.json().slug, SECRET, `${visibility} (${who.cookie ? 'signed in' : 'anonymous'})`);
+ slugs.add(r.json().slug);
+ }
+ const fork = await call(`/api/pastes/${[...slugs][0]}/fork`, { method: 'POST', cookie: samJwt, json: {} });
+ assert.strictEqual(fork.status, 201, fork.text);
+ assert.match(fork.json().slug, SECRET, 'a fork of an unlisted paste is unlisted, with a secret slug');
+ const service = await call('/api/pastes', { method: 'POST', token: net.signService({ cap: [CREATE] }), headers: { 'x-ov-subject': alex.subject_id }, json: { content: 'svc hidden', visibility: 'unlisted' } });
+ assert.strictEqual(service.status, 201, service.text);
+ assert.match(service.json().slug, SECRET, 'service-created unlisted pastes too, unless the service names the slug');
+ const pub = await call('/api/pastes', { method: 'POST', cookie: alexJwt, json: { content: 'public words' } });
+ assert.match(pub.json().slug, SHORT, 'public pastes are listed anyway: short slug');
+ // A paste made before this change (short slug, unlisted) still opens by its slug.
+ t.db.prepare("INSERT INTO pastes (slug, type, title, content, language, visibility) VALUES ('calm-otter-42', 'paste', 'old', 'old unlisted words', 'text', 'unlisted')").run();
+ const old = await call('/api/pastes/calm-otter-42');
+ assert.strictEqual(old.status, 200, old.text);
+ assert.strictEqual(old.json().paste.content, 'old unlisted words');
+ assert.strictEqual((await call('/p/calm-otter-42/raw')).status, 200);
+ // The secret part is really random.
+ const { generateSlug } = require('../server/pastes/store');
+ const many = new Set(Array.from({ length: 2000 }, () => generateSlug(t.db, { secret: true }).split('-')[2]));
+ assert.strictEqual(many.size, 2000);
+ });
+
await t.close();
await t.close();
done();
done();
})();
})();