mirror of
https://github.com/suitenumerique/docs.git
synced 2026-09-22 17:45:12 +02:00
♻️(collaboration) switch collaboration server from hocuspocus to yhub
Signed-off-by: Kevin Jahns <kevin.jahns@protonmail.com>
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
import { createAuthPlugin, createYHub } from '@y/hub';
|
||||
|
||||
// mirror y-provider's env.ts secret-file support
|
||||
const secret = (name, dflt) =>
|
||||
process.env[`${name}_FILE`]
|
||||
? readFileSync(process.env[`${name}_FILE`], 'utf8').trim()
|
||||
: process.env[name] || dflt;
|
||||
|
||||
const PORT = Number(process.env.PORT || 3002);
|
||||
const REDIS = process.env.REDIS;
|
||||
const POSTGRES = process.env.POSTGRES;
|
||||
const REDIS_PREFIX = process.env.REDIS_PREFIX || 'yhub';
|
||||
const COLLABORATION_BACKEND_BASE_URL =
|
||||
process.env.COLLABORATION_BACKEND_BASE_URL || 'http://app-dev:8000';
|
||||
const allowedOrigins = (
|
||||
process.env.COLLABORATION_SERVER_ORIGIN || 'http://localhost:3000'
|
||||
).split(',');
|
||||
const Y_PROVIDER_API_KEY = secret('Y_PROVIDER_API_KEY', 'yprovider-api-key');
|
||||
const ORG = process.env.YHUB_ORG || 'docs';
|
||||
const UUID4 =
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||
|
||||
const backendFetch = async (path, { cookie, origin }) => {
|
||||
const res = await fetch(`${COLLABORATION_BACKEND_BASE_URL}${path}`, {
|
||||
headers: {
|
||||
cookie,
|
||||
origin,
|
||||
'X-Y-Provider-Key': Y_PROVIDER_API_KEY,
|
||||
},
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to fetch ${path}: ${res.status}`);
|
||||
}
|
||||
return res.json();
|
||||
};
|
||||
|
||||
const auth = createAuthPlugin({
|
||||
// uws req is only valid synchronously — read headers AND query before first await.
|
||||
async readAuthInfo(req) {
|
||||
const cookie = req.getHeader('cookie');
|
||||
const origin = req.getHeader('origin');
|
||||
const gcOff = req.getQuery('gc') === 'false';
|
||||
if (gcOff) return null; // full-history connections: not for Docs users
|
||||
if (!origin || !allowedOrigins.includes(origin)) return null; // was 4001 'Origin not allowed'
|
||||
if (!cookie) return null; // was 4001 'No cookies'
|
||||
try {
|
||||
const user = await backendFetch('/api/v1.0/users/me/', {
|
||||
cookie,
|
||||
origin,
|
||||
});
|
||||
return { userid: String(user.id), cookie, origin }; // MUST be string (yhub server.js:667)
|
||||
} catch {
|
||||
// anonymous (public docs): stable per-session id — random ids would mint a new
|
||||
// permanent attribution identity per reconnect
|
||||
const anon = createHash('sha256')
|
||||
.update(cookie)
|
||||
.digest('base64url')
|
||||
.slice(0, 16);
|
||||
return { userid: `anon:${anon}`, cookie, origin };
|
||||
}
|
||||
},
|
||||
async getAccessType(authInfo, { org, docid, branch }) {
|
||||
if (org !== ORG || branch !== 'main' || !UUID4.test(docid)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const doc = await backendFetch(
|
||||
`/api/v1.0/documents/${docid}/`,
|
||||
authInfo,
|
||||
);
|
||||
if (!doc.abilities?.retrieve) {
|
||||
return null;
|
||||
}
|
||||
return doc.abilities.update ? 'rw' : 'r';
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
await createYHub({
|
||||
redis: {
|
||||
url: REDIS,
|
||||
prefix: REDIS_PREFIX,
|
||||
taskDebounce: 10000,
|
||||
minMessageLifetime: 60000,
|
||||
},
|
||||
postgres: POSTGRES,
|
||||
persistence: [], // blobs live in yhub's postgres
|
||||
server: { port: PORT, auth },
|
||||
worker: { taskConcurrency: 5 },
|
||||
// TODO(yhub): worker.events.docUpdate could push snapshots to Django and replace the
|
||||
// client useSaveDoc PATCH flow — blocked upstream: the payload is a DocTable without
|
||||
// room/org/docid (yhub src/index.js:90); needs an upstream change first.
|
||||
});
|
||||
Reference in New Issue
Block a user