openvibers/openvibe.media Commits

Paste cutover switches: PASTES_FROZEN_APPS answers 410 to paste writes for apps that moved to Community; PASTES_MOVED_TO 301s /p/:slug and its raw text there; screenshot bytes stay served here

OP OpenVibers committed 22 Sept 2026 · ddfefcf477 · 3 files, +71 −0


Changes

ddiff --git a/server/pastes/routes.js b/server/pastes/routes.js
iindex 8e37a4f..af73e13 100644
---- a/server/pastes/routes.js
++++ b/server/pastes/routes.js
@@@ -44,6 +44,15 @@ const { tenantAuth, tenantCors } = require('../auth');
const router = express.Router({ mergeParams: true });
router.use(tenantCors);
++// Apps whose pastes moved to OpenVibe.Community (PASTES_FROZEN_APPS, roadmap Wave 5) get no more writes
++// here; reads keep working for anything still pointed at Media during the switch.
++router.use((req, res, next) => {
++ const frozen = String(process.env.PASTES_FROZEN_APPS || '').split(',').map(s => s.trim()).filter(Boolean);
++ if (req.method !== 'GET' && req.method !== 'HEAD' && req.method !== 'OPTIONS' && frozen.includes(String(req.params.app))) {
++ return res.status(410).json({ error: 'Pastes for this app moved to OpenVibe.Community', code: 'pastes.moved' });
++ }
++ next();
++});
// ── Screenshot upload storage ───────────────────────────────
const SCREENSHOTS_DIR = path.join(config.pastes.path, 'screenshots');
ddiff --git a/server/public/routes.js b/server/public/routes.js
iindex 29a5cad..fa98422 100644
---- a/server/public/routes.js
++++ b/server/public/routes.js
@@@ -434,7 +434,12 @@ router.get('/f/:key', (req, res) => {
// ── Pastes ───────────────────────────────────────────────────
// The page itself is rendered in pages.js (thin viewer; canonical on Community).
++// Pastes moved to OpenVibe.Community (roadmap Wave 5, PASTES_MOVED_TO): the page and its text are
++// Community's now, so send people there. Screenshot bytes keep being served from here.
++const movedTo = () => String(process.env.PASTES_MOVED_TO || '').replace(/\/$/, '');
++
router.get('/p/:slug', optionalIdentity, (req, res) => {
++ if (movedTo()) return res.redirect(301, `${movedTo()}/p/${encodeURIComponent(req.params.slug)}`);
try {
const paste = db.getPasteBySlug(String(req.params.slug));
if (!paste) return res.status(404).send('Paste not found');
@@@ -478,6 +483,7 @@ router.get('/p/:slug/raw', (req, res) => {
return res.redirect(302, `/p/${encodeURIComponent(paste.slug)}/screenshot`);
}
if (!paste || paste.type !== 'paste') return res.status(404).send('Not found');
++ if (movedTo()) return res.redirect(301, `${movedTo()}/p/${encodeURIComponent(paste.slug)}/raw`);
if (paste.visibility === 'private') return res.status(404).send('Not found');
// Burn after read
ddiff --git a/test/pastes-moved.test.js b/test/pastes-moved.test.js
nnew file mode 100644
iindex 0000000..a607c6b
---- /dev/null
++++ b/test/pastes-moved.test.js
@@@ -0,0 +1,56 @@
++'use strict';
++// After the Wave 5 cutover: paste writes for a frozen app answer 410, the paste page and its text 301 to
++// OpenVibe.Community, and screenshot bytes are still served from here. Unset, nothing changes.
++const assert = require('assert');
++const fs = require('fs');
++const os = require('os');
++const path = require('path');
++const http = require('http');
++const express = require('express');
++
++const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-moved-'));
++process.env.DB_PATH = path.join(tmp, 'media.db');
++const db = require('../server/db/database');
++require('../server/views/service').ensureSchema();
++db.upsertApp({ app_id: 'live', api_key: 'live-key' });
++const shot = path.join(tmp, 'shot.png');
++fs.writeFileSync(shot, Buffer.from('\x89PNG\r\n\x1a\n'));
++db.getDb().prepare("INSERT INTO pastes (app_id, slug, type, title, content, visibility, screenshot_path) VALUES ('live', 'text-1', 'paste', 't', 'hello', 'public', NULL), ('live', 'shot-1', 'screenshot', 's', '', 'public', ?)").run(shot);
++
++const app = express();
++app.use(express.json());
++app.use('/api/v1/:app/pastes', require('../server/pastes/routes'));
++app.use(require('../server/public/routes'));
++const server = http.createServer(app);
++
++(async () => {
++ await new Promise((r) => server.listen(0, '127.0.0.1', r));
++ const base = `http://127.0.0.1:${server.address().port}`;
++ const get = (p) => fetch(base + p, { redirect: 'manual' });
++ const post = () => fetch(`${base}/api/v1/live/pastes`, { method: 'POST', headers: { authorization: 'Bearer live-key', 'content-type': 'application/json' }, body: JSON.stringify({ content: 'x' }) });
++
++ // Before the switch: business as usual.
++ assert.strictEqual((await get('/p/text-1')).status, 200);
++ assert.strictEqual(await (await get('/p/text-1/raw')).text(), 'hello');
++ assert.strictEqual((await post()).status, 201);
++
++ process.env.PASTES_MOVED_TO = 'https://openvibe.community/';
++ process.env.PASTES_FROZEN_APPS = 'live';
++ let r = await post();
++ assert.strictEqual(r.status, 410); assert.strictEqual((await r.json()).code, 'pastes.moved');
++ r = await fetch(`${base}/api/v1/live/pastes/text-1`, { headers: { authorization: 'Bearer live-key' } });
++ assert.strictEqual(r.status, 200, 'reads still work');
++ r = await get('/p/text-1');
++ assert.strictEqual(r.status, 301); assert.strictEqual(r.headers.get('location'), 'https://openvibe.community/p/text-1');
++ r = await get('/p/text-1/raw');
++ assert.strictEqual(r.status, 301); assert.strictEqual(r.headers.get('location'), 'https://openvibe.community/p/text-1/raw');
++ r = await get('/p/shot-1/raw');
++ assert.strictEqual(r.status, 302); assert.strictEqual(r.headers.get('location'), '/p/shot-1/screenshot', 'image pastes keep their local bytes');
++ r = await get('/p/shot-1/screenshot');
++ assert.strictEqual(r.status, 200, 'screenshot bytes are still served here');
++ assert.ok(db.getPasteBySlug('text-1'), 'no read deletes anything after the switch');
++
++ server.close();
++ fs.rmSync(tmp, { recursive: true, force: true });
++ console.log('pastes moved: all checks passed');
++})().catch((err) => { console.error(err); process.exit(1); });