[ Web Proxy ]
URL:
Viewing: https://raw.githubusercontent.com/levelcodeai/levelcode/develop/tools/sync-server/server.js [Back]  [Original]

/*---------------------------------------------------------------------------------------------
 *  LevelCode  reference Settings Sync server
 *
 *  A tiny, dependency-free implementation of the Code-OSS user-data-sync REST contract
 *  (the same protocol the editor's built-in Settings Sync speaks). Use it to:
 *     develop/test LevelCode Sync end-to-end with no cloud backend, and
 *     self-host the FREE sync tier (BYO storage).
 *  LevelCode Cloud implements the same contract for managed sync.
 *
 *  Storage is file-backed and isolated per bearer token (account). Bodies are opaque text 
 *  the server never parses settings, so client-side E2E encryption is trivial to add later.
 *  Supports user-scoped resources AND profile collections (/v1/collection/{id}/resource/...).
 *
 *  Run:  PORT=9595 DATA_DIR=./.data node server.js
 *--------------------------------------------------------------------------------------------*/
// @ts-check
'use strict';

const http = require('http');
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');

const PORT = parseInt(process.env.PORT || '9595', 10);
const DATA_DIR = process.env.DATA_DIR || path.join(__dirname, '.data');
const LOG = require.main === module; // log requests only when run as a server (quiet under tests)

function shortHash(s) { return crypto.createHash('sha256').update(String(s)).digest('hex').slice(0, 16); }
function accountDir(token) { return path.join(DATA_DIR, shortHash(token)); }
function ensureDir(d) { fs.mkdirSync(d, { recursive: true }); }

/** Read the persisted session id for an account, or null (none until the first write). */
function getSession(acct) {
	try { return fs.readFileSync(path.join(acct, 'session'), 'utf8') || null; } catch { return null; }
}

/** Ensure a session id exists (created at first write, rotated by clear()); returns it. */
function ensureSession(acct) {
	let s = getSession(acct);
	if (!s) {
		s = crypto.randomBytes(8).toString('hex');
		ensureDir(acct);
		fs.writeFileSync(path.join(acct, 'session'), s, 'utf8');
	}
	return s;
}

// Resources live under a BASE dir: the account root (user resources) or a collection dir.

/** Read a resource's {ref, content} under `base`, or null. */
function readResource(base, type) {
	try { return JSON.parse(fs.readFileSync(path.join(base, 'resource', type + '.json'), 'utf8')); } catch { return null; }
}

/** Write a resource under `base`, bumping its ref. Returns the new ref (string). */
function writeResource(base, type, content) {
	const prev = readResource(base, type);
	const nextRef = String((prev ? parseInt(prev.ref, 10) : 0) + 1);
	ensureDir(path.join(base, 'resource'));
	fs.writeFileSync(path.join(base, 'resource', type + '.json'), JSON.stringify({ ref: nextRef, content }), 'utf8');
	// keep history so GET /resource/:type/:ref works (a self-host superset of the core contract)
	ensureDir(path.join(base, 'history', type));
	fs.writeFileSync(path.join(base, 'history', type, nextRef), content, 'utf8');
	return nextRef;
}

function deleteResource(base, type) {
	fs.rmSync(path.join(base, 'resource', type + '.json'), { force: true });
	fs.rmSync(path.join(base, 'history', type), { recursive: true, force: true });
}

/** Map of {type: ref} for every resource currently stored under `base`. */
function latestOf(base) {
	const out = {};
	const rdir = path.join(base, 'resource');
	if (fs.existsSync(rdir)) {
		for (const f of fs.readdirSync(rdir)) {
			if (!f.endsWith('.json')) { continue; }
			const t = f.slice(0, -5);
			const r = readResource(base, t);
			if (r) { out[t] = r.ref; }
		}
	}
	return out;
}

function collectionsDir(acct) { return path.join(acct, 'collections'); }
function collectionDir(acct, id) { return path.join(collectionsDir(acct), id); }

function listCollections(acct) {
	try { return fs.readdirSync(collectionsDir(acct)).filter(id => fs.statSync(collectionDir(acct, id)).isDirectory()); }
	catch { return []; }
}

/** Build the manifest for an account, or null if it has never been written ( 204). */
function buildManifest(acct) {
	const session = getSession(acct);
	if (!session) { return null; }
	const latest = latestOf(acct);
	const collections = {};
	for (const id of listCollections(acct)) { collections[id] = { latest: latestOf(collectionDir(acct, id)) }; }
	const ref = shortHash(session + ':' + JSON.stringify(latest) + ':' + JSON.stringify(collections));
	return { session, ref, latest, collections };
}

/** Build the bulk "download all" payload (IDownloadLatestDataType shape). */
function buildDownload(acct) {
	const dump = (base) => {
		const resources = {};
		for (const t of Object.keys(latestOf(base))) {
			const r = readResource(base, t);
			if (r) { resources[t] = [{ ref: r.ref, content: r.content }]; }
		}
		return resources;
	};
	const collections = {};
	for (const id of listCollections(acct)) { collections[id] = { resources: dump(collectionDir(acct, id)) }; }
	return { resources: dump(acct), collections };
}

function send(res, status, body, headers) {
	res.writeHead(status, Object.assign({ 'Cache-Control': 'no-store' }, headers || {}));
	res.end(body);
}

function readBody(req) {
	return new Promise((resolve) => {
		let data = '';
		req.on('data', (c) => { data += c; });
		req.on('end', () => resolve(data));
	});
}

/** GET a resource under `base`: ref='latest'  current (204+ETag '0' if none); else a historical ref. */
function resourceGet(req, res, base, type, ref) {
	if (ref === 'latest') {
		const rec = readResource(base, type);
		if (!rec) { return send(res, 204, '', { 'ETag': '0' }); }
		const inm = req.headers['if-none-match'];
		if (inm && inm === rec.ref) { return send(res, 304, '', { 'ETag': rec.ref }); }
		return send(res, 200, rec.content, { 'Content-Type': 'text/plain', 'ETag': rec.ref });
	}
	try { return send(res, 200, fs.readFileSync(path.join(base, 'history', type, ref), 'utf8'), { 'Content-Type': 'text/plain', 'ETag': ref }); }
	catch { return send(res, 404, 'No such ref'); }
}

/** POST a resource under `base` with If-Match optimistic concurrency ('0' = non-existing ref). */
async function resourcePost(req, res, base, type) {
	const prev = readResource(base, type);
	const cur = prev ? prev.ref : '0';
	const ifMatch = req.headers['if-match'];
	if (ifMatch !== undefined && ifMatch !== '*' && ifMatch !== cur) {
		return send(res, 412, 'Precondition Failed', { 'ETag': cur });
	}
	const content = await readBody(req);
	return send(res, 200, '', { 'ETag': writeResource(base, type, content) });
}

const server = http.createServer(async (req, res) => {
	if (LOG) {
		res.on('finish', () => {
			const u = req.url || '';
			if (u !== '/' && u !== '/health') { // log everything (incl. misdirected /api/update  404)
				console.log('[sync] ' + (req.method || 'GET') + ' ' + u + '  ' + res.statusCode);
			}
		});
	}
	const url = (req.url || '').split('?')[0];
	const method = req.method || 'GET';

	if (url === '/' || url === '/health') {
		return send(res, 200, 'LevelCode reference sync server\n', { 'Content-Type': 'text/plain' });
	}

	const auth = req.headers['authorization'] || '';
	const tok = /^Bearer\s+(.+)$/i.exec(String(auth));
	if (!url.startsWith('/v1')) { return send(res, 404, 'Not found'); }
	if (!tok) { return send(res, 401, 'Missing bearer token'); }
	const acct = accountDir(tok[1]);

	// GET /v1/manifest  (204 with no body until the first write creates a session)
	if (url === '/v1/manifest' && method === 'GET') {
		const manifest = buildManifest(acct);
		if (!manifest) { return send(res, 204, '', { 'ETag': '0' }); }
		const inm = req.headers['if-none-match'];
		if (inm && inm === manifest.ref) { return send(res, 304, ''); }
		return send(res, 200, JSON.stringify(manifest), { 'Content-Type': 'application/json', 'ETag': manifest.ref });
	}

	// GET /v1/download/latest  (bulk snapshot; 204 before any data)
	if (url === '/v1/download/latest' && method === 'GET') {
		if (!getSession(acct)) { return send(res, 204, ''); }
		return send(res, 200, JSON.stringify(buildDownload(acct)), { 'Content-Type': 'application/json' });
	}

	// Collection-scoped resource: /v1/collection/{cid}/resource/{type}[/{ref}]
	let m = /^\/v1\/collection\/([^/]+)\/resource\/([^/]+)(?:\/([^/]+))?$/.exec(url);
	if (m) {
		const base = collectionDir(acct, m[1]);
		if (!fs.existsSync(base)) { return send(res, 404, 'No such collection'); }
		if (method === 'GET' && m[3]) { return resourceGet(req, res, base, m[2], m[3]); }
		if (method === 'POST' && !m[3]) { ensureSession(acct); return resourcePost(req, res, base, m[2]); }
		if (method === 'DELETE') { deleteResource(base, m[2]); return send(res, 200, ''); }
		return send(res, 404, 'Not found');
	}

	// Collections (user-data profiles): list / create / delete
	if (url === '/v1/collection' && method === 'GET') {
		return send(res, 200, JSON.stringify(listCollections(acct).map(id => ({ id }))), { 'Content-Type': 'application/json' });
	}
	if (url === '/v1/collection' && method === 'POST') {
		ensureSession(acct);
		const id = shortHash(crypto.randomBytes(8).toString('hex'));
		ensureDir(collectionDir(acct, id));
		return send(res, 200, id, { 'Content-Type': 'text/plain' });
	}
	if (url === '/v1/collection' && method === 'DELETE') {
		fs.rmSync(collectionsDir(acct), { recursive: true, force: true });
		return send(res, 200, '');
	}
	m = /^\/v1\/collection\/([^/]+)$/.exec(url);
	if (m && method === 'DELETE') {
		fs.rmSync(collectionDir(acct, m[1]), { recursive: true, force: true });
		return send(res, 200, '');
	}

	// DELETE /v1/resource  (clear all  the client's clear() / resetRemote()). Rotates the session.
	if (url === '/v1/resource' && method === 'DELETE') {
		fs.rmSync(path.join(acct, 'resource'), { recursive: true, force: true });
		fs.rmSync(path.join(acct, 'history'), { recursive: true, force: true });
		fs.rmSync(collectionsDir(acct), { recursive: true, force: true });
		fs.rmSync(path.join(acct, 'session'), { force: true });
		return send(res, 204, '');
	}

	// Top-level resource: /v1/resource/{type}[/{ref}]
	m = /^\/v1\/resource\/([^/]+)(?:\/([^/]+))?$/.exec(url);
	if (m) {
		if (method === 'GET' && m[2]) { return resourceGet(req, res, acct, m[1], m[2]); }
		if (method === 'POST' && !m[2]) { ensureSession(acct); return resourcePost(req, res, acct, m[1]); }
		if (method === 'DELETE') { deleteResource(acct, m[1]); return send(res, 200, ''); }
	}

	return send(res, 404, 'Not found: ' + method + ' ' + url);
});

if (require.main === module) {
	ensureDir(DATA_DIR);
	server.listen(PORT, () => {
		// eslint-disable-next-line no-console
		console.log('LevelCode reference sync server on http://localhost:' + PORT + ' (data: ' + DATA_DIR + ')');
		console.log('Watch this terminal  every editor sync request prints as a [sync] line.');
	});
}

module.exports = { server, buildManifest, buildDownload, readResource, writeResource, accountDir };

Web Proxy Viewer  |  New URL  |  Original Page