76 lines
3.7 KiB
JavaScript
76 lines
3.7 KiB
JavaScript
/**
|
|
* Geräte-Sync (optional). GET/PUT /sync mit Header X-Sync-Code.
|
|
* Speichert nur den Hash des Codes. Last-write-wins mit Versionszähler.
|
|
* Lehnt Spielstände mit erkennbaren Secrets ab.
|
|
*/
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const crypto = require('crypto');
|
|
|
|
const SECRET_RE = /(sk-[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9]{20,}|xox[abp]-[A-Za-z0-9-]{10,}|AKIA[0-9A-Z]{16}|-----BEGIN [A-Z ]*PRIVATE KEY-----)/;
|
|
const MAX_BYTES = 2 * 1024 * 1024;
|
|
|
|
function createStore(file) {
|
|
const f = file || process.env.SYNC_FILE || path.join(__dirname, 'data', 'sync.json');
|
|
let db = {};
|
|
try { db = JSON.parse(fs.readFileSync(f, 'utf8')); } catch (e) { db = {}; }
|
|
const persist = () => { fs.mkdirSync(path.dirname(f), { recursive: true }); fs.writeFileSync(f + '.tmp', JSON.stringify(db)); fs.renameSync(f + '.tmp', f); };
|
|
return {
|
|
get: (id) => db[id] || null,
|
|
put: (id, rec) => { db[id] = rec; persist(); },
|
|
file: f
|
|
};
|
|
}
|
|
|
|
function idFor(code) {
|
|
if (!code || String(code).length < 12) return null;
|
|
return crypto.createHash('sha256').update('ck-sync:' + code).digest('hex');
|
|
}
|
|
|
|
/** returns {status, body} */
|
|
function handle(store, method, code, body) {
|
|
const id = idFor(code);
|
|
if (!id) return { status: 401, body: { error: 'sync_code_invalid' } };
|
|
if (method === 'GET') {
|
|
const rec = store.get(id);
|
|
if (!rec) return { status: 404, body: { error: 'no_state' } };
|
|
return { status: 200, body: rec };
|
|
}
|
|
if (method === 'PUT' || method === 'POST') {
|
|
if (!body || typeof body.version !== 'number' || typeof body.state !== 'object' || body.state === null) return { status: 400, body: { error: 'bad_request' } };
|
|
const raw = JSON.stringify(body.state);
|
|
if (raw.length > MAX_BYTES) return { status: 413, body: { error: 'too_large' } };
|
|
if (SECRET_RE.test(raw)) return { status: 422, body: { error: 'secret_detected' } };
|
|
const cur = store.get(id);
|
|
if (cur && body.version <= cur.version) return { status: 409, body: { error: 'stale', version: cur.version, updatedAt: cur.updatedAt } };
|
|
const rec = { version: body.version, updatedAt: Number(body.updatedAt) || Date.now(), state: body.state };
|
|
store.put(id, rec);
|
|
return { status: 200, body: { ok: true, version: rec.version } };
|
|
}
|
|
return { status: 405, body: { error: 'method' } };
|
|
}
|
|
|
|
/** Async-Variante für db.js-KV (Namespace 'sync'). id: 'user:<uuid>' (Konto) oder Hash des Sync-Codes. */
|
|
async function handleAsync(kv, method, id, body) {
|
|
const store = { get: (k) => kv.get('sync', k), put: (k, v) => kv.put('sync', k, v) };
|
|
if (!id) return { status: 401, body: { error: 'unauthorized' } };
|
|
if (method === 'GET') {
|
|
const rec = await store.get(id);
|
|
if (!rec) return { status: 404, body: { error: 'no_state' } };
|
|
return { status: 200, body: rec };
|
|
}
|
|
if (method === 'PUT' || method === 'POST') {
|
|
if (!body || typeof body.version !== 'number' || typeof body.state !== 'object' || body.state === null) return { status: 400, body: { error: 'bad_request' } };
|
|
const raw = JSON.stringify(body.state);
|
|
if (raw.length > MAX_BYTES) return { status: 413, body: { error: 'too_large' } };
|
|
if (SECRET_RE.test(raw)) return { status: 422, body: { error: 'secret_detected' } };
|
|
const cur = await store.get(id);
|
|
if (cur && body.version <= cur.version) return { status: 409, body: { error: 'stale', version: cur.version, updatedAt: cur.updatedAt } };
|
|
const rec = { version: body.version, updatedAt: Number(body.updatedAt) || Date.now(), state: body.state };
|
|
await store.put(id, rec);
|
|
return { status: 200, body: { ok: true, version: rec.version } };
|
|
}
|
|
return { status: 405, body: { error: 'method' } };
|
|
}
|
|
|
|
module.exports = { createStore, handle, handleAsync, idFor };
|