@fyresmith/hive-server 2.4.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +89 -54
- package/lib/socketHandler.js +38 -891
- package/lib/vaultManager.js +4 -220
- package/lib/yjsServer.js +1 -6
- package/package.json +2 -3
- package/lib/adapterRegistry.js +0 -152
- package/lib/collabProtocol.js +0 -25
- package/lib/collabStore.js +0 -448
- package/lib/discordWebhook.js +0 -81
- package/lib/mentionUtils.js +0 -13
package/lib/vaultManager.js
CHANGED
|
@@ -3,26 +3,15 @@ import { resolve, join, sep, extname, relative, dirname } from 'path';
|
|
|
3
3
|
import { readFile, writeFile as fsWriteFile, rename, unlink, mkdir, stat, readdir } from 'fs/promises';
|
|
4
4
|
import { existsSync } from 'fs';
|
|
5
5
|
import chokidar from 'chokidar';
|
|
6
|
-
import { getMetadataAllowlist, isMetadataPathAllowed } from './adapterRegistry.js';
|
|
7
6
|
|
|
8
7
|
// ---------------------------------------------------------------------------
|
|
9
8
|
// Deny / allow lists
|
|
10
9
|
// ---------------------------------------------------------------------------
|
|
11
10
|
|
|
12
|
-
const DENY_PREFIXES = ['.obsidian', '
|
|
11
|
+
const DENY_PREFIXES = ['.obsidian', 'Attachments', '.git'];
|
|
13
12
|
const DENY_FILES = ['.DS_Store', 'Thumbs.db'];
|
|
14
13
|
// Keep synced content text-based to ensure hash/read/write semantics stay safe.
|
|
15
14
|
const ALLOW_EXTS = new Set(['.md', '.canvas']);
|
|
16
|
-
const METADATA_ALLOWLIST = new Set(getMetadataAllowlist());
|
|
17
|
-
const HISTORY_DIR_NAME = '.hive-history';
|
|
18
|
-
const HISTORY_FILES_DIR = 'files';
|
|
19
|
-
const DEFAULT_HISTORY_LIMIT = 50;
|
|
20
|
-
const MAX_HISTORY_LIMIT = 500;
|
|
21
|
-
const HISTORY_RETENTION_PER_FILE = Math.max(
|
|
22
|
-
1,
|
|
23
|
-
parseInt(process.env.HIVE_HISTORY_RETENTION_PER_FILE ?? '200', 10) || 200,
|
|
24
|
-
);
|
|
25
|
-
const VERSION_ID_RE = /^\d{13}-[a-z0-9]+$/;
|
|
26
15
|
|
|
27
16
|
// ---------------------------------------------------------------------------
|
|
28
17
|
// Internals
|
|
@@ -45,137 +34,6 @@ function getVaultRoot() {
|
|
|
45
34
|
*/
|
|
46
35
|
const manifestCache = new Map();
|
|
47
36
|
|
|
48
|
-
function getHistoryRoot() {
|
|
49
|
-
const custom = process.env.HIVE_HISTORY_PATH?.trim();
|
|
50
|
-
if (custom) return resolve(custom);
|
|
51
|
-
return join(getVaultRoot(), HISTORY_DIR_NAME);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function encodeHistoryKey(relPath) {
|
|
55
|
-
return Buffer.from(relPath, 'utf-8').toString('base64url');
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function historyDirForPath(relPath) {
|
|
59
|
-
return join(getHistoryRoot(), HISTORY_FILES_DIR, encodeHistoryKey(relPath));
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function normalizeHistoryActor(actor) {
|
|
63
|
-
if (!actor || typeof actor !== 'object') return null;
|
|
64
|
-
const id = typeof actor.id === 'string' ? actor.id : null;
|
|
65
|
-
const username = typeof actor.username === 'string' ? actor.username : null;
|
|
66
|
-
if (!id && !username) return null;
|
|
67
|
-
return { id, username };
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function normalizeHistorySource(source) {
|
|
71
|
-
return typeof source === 'string' && source.length > 0 ? source : null;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function clampLimit(value, fallback) {
|
|
75
|
-
if (!Number.isFinite(value)) return fallback;
|
|
76
|
-
const n = Math.trunc(value);
|
|
77
|
-
if (n < 1) return 1;
|
|
78
|
-
if (n > MAX_HISTORY_LIMIT) return MAX_HISTORY_LIMIT;
|
|
79
|
-
return n;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
async function listHistoryFiles(relPath) {
|
|
83
|
-
const dir = historyDirForPath(relPath);
|
|
84
|
-
let names;
|
|
85
|
-
try {
|
|
86
|
-
names = await readdir(dir);
|
|
87
|
-
} catch (err) {
|
|
88
|
-
if (err?.code === 'ENOENT') return [];
|
|
89
|
-
throw err;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return names
|
|
93
|
-
.filter((name) => name.endsWith('.json'))
|
|
94
|
-
.sort();
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
async function readHistoryRecord(absPath) {
|
|
98
|
-
const raw = await readFile(absPath, 'utf-8');
|
|
99
|
-
return JSON.parse(raw);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function toHistoryMeta(record) {
|
|
103
|
-
return {
|
|
104
|
-
versionId: record.versionId,
|
|
105
|
-
relPath: record.relPath,
|
|
106
|
-
ts: record.ts,
|
|
107
|
-
action: record.action,
|
|
108
|
-
hash: record.hash,
|
|
109
|
-
size: record.size,
|
|
110
|
-
actor: record.actor ?? null,
|
|
111
|
-
source: record.source ?? null,
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
async function pruneHistory(relPath) {
|
|
116
|
-
const files = await listHistoryFiles(relPath);
|
|
117
|
-
if (files.length <= HISTORY_RETENTION_PER_FILE) return;
|
|
118
|
-
|
|
119
|
-
const dir = historyDirForPath(relPath);
|
|
120
|
-
const trimCount = files.length - HISTORY_RETENTION_PER_FILE;
|
|
121
|
-
for (const name of files.slice(0, trimCount)) {
|
|
122
|
-
try {
|
|
123
|
-
await unlink(join(dir, name));
|
|
124
|
-
} catch {
|
|
125
|
-
// Best effort; skip failures and continue pruning.
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
async function writeHistoryRecord(relPath, content, history = {}) {
|
|
131
|
-
if (!isAllowed(relPath)) return;
|
|
132
|
-
const action = typeof history.action === 'string' && history.action.length > 0
|
|
133
|
-
? history.action
|
|
134
|
-
: 'write';
|
|
135
|
-
|
|
136
|
-
const dir = historyDirForPath(relPath);
|
|
137
|
-
await mkdir(dir, { recursive: true });
|
|
138
|
-
|
|
139
|
-
const hash = hashContent(content);
|
|
140
|
-
const existing = await listHistoryFiles(relPath);
|
|
141
|
-
if (existing.length > 0) {
|
|
142
|
-
const lastFile = existing[existing.length - 1];
|
|
143
|
-
try {
|
|
144
|
-
const latest = await readHistoryRecord(join(dir, lastFile));
|
|
145
|
-
if (latest?.hash === hash && latest?.action === action) {
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
} catch {
|
|
149
|
-
// Ignore malformed historical entry and continue writing a new one.
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const ts = Date.now();
|
|
154
|
-
const versionId = `${ts}-${Math.random().toString(36).slice(2, 10)}`;
|
|
155
|
-
const record = {
|
|
156
|
-
versionId,
|
|
157
|
-
relPath,
|
|
158
|
-
ts,
|
|
159
|
-
action,
|
|
160
|
-
hash,
|
|
161
|
-
size: Buffer.byteLength(content, 'utf-8'),
|
|
162
|
-
actor: normalizeHistoryActor(history.actor),
|
|
163
|
-
source: normalizeHistorySource(history.source),
|
|
164
|
-
content,
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
await fsWriteFile(join(dir, `${versionId}.json`), JSON.stringify(record), 'utf-8');
|
|
168
|
-
await pruneHistory(relPath);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
async function writeHistoryRecordBestEffort(relPath, content, history = {}) {
|
|
172
|
-
try {
|
|
173
|
-
await writeHistoryRecord(relPath, content, history);
|
|
174
|
-
} catch (err) {
|
|
175
|
-
console.warn(`[history] Failed to record version for ${relPath}:`, err?.message ?? err);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
37
|
// ---------------------------------------------------------------------------
|
|
180
38
|
// Path helpers
|
|
181
39
|
// ---------------------------------------------------------------------------
|
|
@@ -200,15 +58,6 @@ export function safePath(relPath) {
|
|
|
200
58
|
*/
|
|
201
59
|
export function isDenied(relPath) {
|
|
202
60
|
const normalised = relPath.replace(/\\/g, '/');
|
|
203
|
-
if (isMetadataPathAllowed(normalised)) return false;
|
|
204
|
-
|
|
205
|
-
if (normalised === '.obsidian/' || normalised === '.obsidian') {
|
|
206
|
-
// Allow descending into .obsidian only when we explicitly whitelist files inside.
|
|
207
|
-
if ([...METADATA_ALLOWLIST].some((entry) => entry.startsWith('.obsidian/'))) {
|
|
208
|
-
return false;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
61
|
const parts = normalised.split('/');
|
|
213
62
|
const base = parts[parts.length - 1];
|
|
214
63
|
if (DENY_FILES.includes(base)) return true;
|
|
@@ -223,7 +72,6 @@ export function isDenied(relPath) {
|
|
|
223
72
|
* @param {string} relPath
|
|
224
73
|
*/
|
|
225
74
|
export function isAllowed(relPath) {
|
|
226
|
-
if (isMetadataPathAllowed(relPath)) return true;
|
|
227
75
|
if (isDenied(relPath)) return false;
|
|
228
76
|
return ALLOW_EXTS.has(extname(relPath).toLowerCase());
|
|
229
77
|
}
|
|
@@ -344,7 +192,7 @@ export { readFile_ as readFile };
|
|
|
344
192
|
* @param {string} relPath
|
|
345
193
|
* @param {string} content
|
|
346
194
|
*/
|
|
347
|
-
export async function writeFile(relPath, content
|
|
195
|
+
export async function writeFile(relPath, content) {
|
|
348
196
|
const abs = safePath(relPath);
|
|
349
197
|
const tmp = abs + '.tmp';
|
|
350
198
|
await mkdir(dirname(abs), { recursive: true });
|
|
@@ -352,22 +200,16 @@ export async function writeFile(relPath, content, options = {}) {
|
|
|
352
200
|
await rename(tmp, abs);
|
|
353
201
|
// Invalidate cache entry so next manifest reflects new content
|
|
354
202
|
manifestCache.delete(relPath);
|
|
355
|
-
await writeHistoryRecordBestEffort(relPath, content, options.history);
|
|
356
203
|
}
|
|
357
204
|
|
|
358
205
|
/**
|
|
359
206
|
* Delete a file.
|
|
360
207
|
* @param {string} relPath
|
|
361
208
|
*/
|
|
362
|
-
export async function deleteFile(relPath
|
|
209
|
+
export async function deleteFile(relPath) {
|
|
363
210
|
const abs = safePath(relPath);
|
|
364
|
-
const previous = await readFile(abs, 'utf-8');
|
|
365
211
|
await unlink(abs);
|
|
366
212
|
manifestCache.delete(relPath);
|
|
367
|
-
await writeHistoryRecordBestEffort(relPath, previous, {
|
|
368
|
-
...(options.history ?? {}),
|
|
369
|
-
action: options.history?.action ?? 'delete',
|
|
370
|
-
});
|
|
371
213
|
}
|
|
372
214
|
|
|
373
215
|
/**
|
|
@@ -375,71 +217,13 @@ export async function deleteFile(relPath, options = {}) {
|
|
|
375
217
|
* @param {string} oldRelPath
|
|
376
218
|
* @param {string} newRelPath
|
|
377
219
|
*/
|
|
378
|
-
export async function renameFile(oldRelPath, newRelPath
|
|
220
|
+
export async function renameFile(oldRelPath, newRelPath) {
|
|
379
221
|
const oldAbs = safePath(oldRelPath);
|
|
380
222
|
const newAbs = safePath(newRelPath);
|
|
381
|
-
const previous = await readFile(oldAbs, 'utf-8');
|
|
382
223
|
await mkdir(dirname(newAbs), { recursive: true });
|
|
383
224
|
await rename(oldAbs, newAbs);
|
|
384
225
|
manifestCache.delete(oldRelPath);
|
|
385
226
|
manifestCache.delete(newRelPath);
|
|
386
|
-
|
|
387
|
-
const history = options.history ?? {};
|
|
388
|
-
await writeHistoryRecordBestEffort(oldRelPath, previous, {
|
|
389
|
-
...history,
|
|
390
|
-
action: 'rename-out',
|
|
391
|
-
});
|
|
392
|
-
await writeHistoryRecordBestEffort(newRelPath, previous, {
|
|
393
|
-
...history,
|
|
394
|
-
action: 'rename-in',
|
|
395
|
-
});
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
/**
|
|
399
|
-
* List historical versions for a file (newest first).
|
|
400
|
-
* @param {string} relPath
|
|
401
|
-
* @param {number} [limit]
|
|
402
|
-
*/
|
|
403
|
-
export async function listFileHistory(relPath, limit = DEFAULT_HISTORY_LIMIT) {
|
|
404
|
-
if (!isAllowed(relPath)) return [];
|
|
405
|
-
const clampedLimit = clampLimit(Number(limit), DEFAULT_HISTORY_LIMIT);
|
|
406
|
-
const dir = historyDirForPath(relPath);
|
|
407
|
-
const names = await listHistoryFiles(relPath);
|
|
408
|
-
const out = [];
|
|
409
|
-
|
|
410
|
-
for (const name of names.reverse()) {
|
|
411
|
-
try {
|
|
412
|
-
const record = await readHistoryRecord(join(dir, name));
|
|
413
|
-
if (record?.relPath !== relPath) continue;
|
|
414
|
-
out.push(toHistoryMeta(record));
|
|
415
|
-
if (out.length >= clampedLimit) break;
|
|
416
|
-
} catch {
|
|
417
|
-
// Skip malformed entries.
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
return out;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
/**
|
|
425
|
-
* Read a historical file version.
|
|
426
|
-
* @param {string} relPath
|
|
427
|
-
* @param {string} versionId
|
|
428
|
-
*/
|
|
429
|
-
export async function readFileHistoryVersion(relPath, versionId) {
|
|
430
|
-
if (!isAllowed(relPath)) {
|
|
431
|
-
throw new Error('Path not allowed');
|
|
432
|
-
}
|
|
433
|
-
if (typeof versionId !== 'string' || !VERSION_ID_RE.test(versionId)) {
|
|
434
|
-
throw new Error('Invalid version ID');
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
const abs = join(historyDirForPath(relPath), `${versionId}.json`);
|
|
438
|
-
const record = await readHistoryRecord(abs);
|
|
439
|
-
if (record?.relPath !== relPath) {
|
|
440
|
-
throw new Error('Version/path mismatch');
|
|
441
|
-
}
|
|
442
|
-
return record;
|
|
443
227
|
}
|
|
444
228
|
|
|
445
229
|
// ---------------------------------------------------------------------------
|
package/lib/yjsServer.js
CHANGED
|
@@ -79,12 +79,7 @@ async function flushRoomState(state) {
|
|
|
79
79
|
|
|
80
80
|
const text = ydoc.getText('content').toString();
|
|
81
81
|
try {
|
|
82
|
-
await vault.writeFile(state.relPath, text
|
|
83
|
-
history: {
|
|
84
|
-
action: 'yjs-write',
|
|
85
|
-
source: 'yjs',
|
|
86
|
-
},
|
|
87
|
-
});
|
|
82
|
+
await vault.writeFile(state.relPath, text);
|
|
88
83
|
const hash = vault.hashContent(text);
|
|
89
84
|
state.lastPersistAt = Date.now();
|
|
90
85
|
state.lastPersistHash = hash;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fyresmith/hive-server",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Collaborative Obsidian vault server",
|
|
6
6
|
"main": "index.js",
|
|
@@ -27,9 +27,8 @@
|
|
|
27
27
|
"build": "npm run verify && npm pack --dry-run",
|
|
28
28
|
"status": "node bin/hive.js status",
|
|
29
29
|
"verify": "node scripts/verify.mjs",
|
|
30
|
-
"test:unit": "node --test test/**/*.test.js",
|
|
31
30
|
"install-hive": "node scripts/install-hive.mjs",
|
|
32
|
-
"test": "npm run
|
|
31
|
+
"test": "npm run verify"
|
|
33
32
|
},
|
|
34
33
|
"dependencies": {
|
|
35
34
|
"chalk": "^5.6.2",
|
package/lib/adapterRegistry.js
DELETED
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
const SAFE_METADATA_DEFAULTS = [
|
|
2
|
-
'.obsidian/appearance.json',
|
|
3
|
-
'.obsidian/community-plugins.json',
|
|
4
|
-
'.obsidian/core-plugins.json',
|
|
5
|
-
'.obsidian/hotkeys.json',
|
|
6
|
-
];
|
|
7
|
-
|
|
8
|
-
function normalizePath(path) {
|
|
9
|
-
if (typeof path !== 'string') return '';
|
|
10
|
-
return path.replace(/\\/g, '/').replace(/^\//, '');
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function parseMetadataAllowlist(raw) {
|
|
14
|
-
if (!raw) return [...SAFE_METADATA_DEFAULTS];
|
|
15
|
-
try {
|
|
16
|
-
const parsed = JSON.parse(raw);
|
|
17
|
-
if (!Array.isArray(parsed)) return [...SAFE_METADATA_DEFAULTS];
|
|
18
|
-
const out = parsed
|
|
19
|
-
.map((entry) => normalizePath(String(entry)))
|
|
20
|
-
.filter((entry) => entry.startsWith('.obsidian/') && entry.endsWith('.json'));
|
|
21
|
-
return out.length > 0 ? [...new Set(out)] : [...SAFE_METADATA_DEFAULTS];
|
|
22
|
-
} catch {
|
|
23
|
-
return [...SAFE_METADATA_DEFAULTS];
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const metadataAllowlist = parseMetadataAllowlist(process.env.HIVE_METADATA_ALLOWLIST_JSON);
|
|
28
|
-
|
|
29
|
-
function isMarkdownPath(path) {
|
|
30
|
-
return normalizePath(path).toLowerCase().endsWith('.md');
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function isCanvasPath(path) {
|
|
34
|
-
return normalizePath(path).toLowerCase().endsWith('.canvas');
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function isMetadataPathAllowed(path) {
|
|
38
|
-
const normalized = normalizePath(path);
|
|
39
|
-
return metadataAllowlist.includes(normalized);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function isMetadataPath(path) {
|
|
43
|
-
return isMetadataPathAllowed(path);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function isValidJson(content) {
|
|
47
|
-
if (typeof content !== 'string') return false;
|
|
48
|
-
try {
|
|
49
|
-
JSON.parse(content);
|
|
50
|
-
return true;
|
|
51
|
-
} catch {
|
|
52
|
-
return false;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
class AdapterRegistry {
|
|
57
|
-
constructor() {
|
|
58
|
-
this.adapters = new Map();
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
register(adapter) {
|
|
62
|
-
if (!adapter || typeof adapter.adapterId !== 'string') {
|
|
63
|
-
throw new Error('Adapter must define adapterId');
|
|
64
|
-
}
|
|
65
|
-
if (typeof adapter.version !== 'string' || adapter.version.length === 0) {
|
|
66
|
-
throw new Error(`Adapter ${adapter.adapterId} must define version`);
|
|
67
|
-
}
|
|
68
|
-
const normalized = {
|
|
69
|
-
...adapter,
|
|
70
|
-
capabilities: Array.isArray(adapter.capabilities)
|
|
71
|
-
? [...new Set(adapter.capabilities.map((c) => String(c)))]
|
|
72
|
-
: [],
|
|
73
|
-
validateContent: typeof adapter.validateContent === 'function'
|
|
74
|
-
? adapter.validateContent
|
|
75
|
-
: () => true,
|
|
76
|
-
supportsPath: typeof adapter.supportsPath === 'function'
|
|
77
|
-
? adapter.supportsPath
|
|
78
|
-
: () => false,
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
this.adapters.set(normalized.adapterId, normalized);
|
|
82
|
-
return () => {
|
|
83
|
-
this.adapters.delete(normalized.adapterId);
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
getById(adapterId) {
|
|
88
|
-
return this.adapters.get(adapterId) ?? null;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
getByPath(path) {
|
|
92
|
-
for (const adapter of this.adapters.values()) {
|
|
93
|
-
if (adapter.supportsPath(path)) return adapter;
|
|
94
|
-
}
|
|
95
|
-
return null;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
listCapabilities() {
|
|
99
|
-
const out = {};
|
|
100
|
-
for (const adapter of this.adapters.values()) {
|
|
101
|
-
out[adapter.adapterId] = {
|
|
102
|
-
version: adapter.version,
|
|
103
|
-
capabilities: [...adapter.capabilities],
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
return out;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
listDescriptors() {
|
|
110
|
-
return [...this.adapters.values()].map((adapter) => ({
|
|
111
|
-
adapterId: adapter.adapterId,
|
|
112
|
-
version: adapter.version,
|
|
113
|
-
capabilities: [...adapter.capabilities],
|
|
114
|
-
}));
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
export function createServerAdapterRegistry() {
|
|
119
|
-
const registry = new AdapterRegistry();
|
|
120
|
-
|
|
121
|
-
registry.register({
|
|
122
|
-
adapterId: 'markdown',
|
|
123
|
-
version: '1.0.0',
|
|
124
|
-
capabilities: ['yjs_text', 'awareness', 'cas'],
|
|
125
|
-
supportsPath: isMarkdownPath,
|
|
126
|
-
validateContent: (content) => typeof content === 'string',
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
registry.register({
|
|
130
|
-
adapterId: 'canvas',
|
|
131
|
-
version: '2.0.0',
|
|
132
|
-
capabilities: ['structured_model', 'legacy_text_bridge', 'deterministic_order'],
|
|
133
|
-
supportsPath: isCanvasPath,
|
|
134
|
-
validateContent: (content) => isValidJson(content),
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
registry.register({
|
|
138
|
-
adapterId: 'metadata',
|
|
139
|
-
version: '1.0.0',
|
|
140
|
-
capabilities: ['whitelist_policy', 'validation'],
|
|
141
|
-
supportsPath: isMetadataPath,
|
|
142
|
-
validateContent: (content) => isValidJson(content),
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
return registry;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
export const serverAdapterRegistry = createServerAdapterRegistry();
|
|
149
|
-
|
|
150
|
-
export function getMetadataAllowlist() {
|
|
151
|
-
return [...metadataAllowlist];
|
|
152
|
-
}
|
package/lib/collabProtocol.js
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
export const PROTOCOL_V1 = 1;
|
|
2
|
-
export const PROTOCOL_V2 = 2;
|
|
3
|
-
|
|
4
|
-
export const SERVER_CAPABILITIES = [
|
|
5
|
-
'presence_heartbeat',
|
|
6
|
-
'activity_feed',
|
|
7
|
-
'threads',
|
|
8
|
-
'tasks',
|
|
9
|
-
'notifications',
|
|
10
|
-
'adapter_negotiation',
|
|
11
|
-
'canvas_structured',
|
|
12
|
-
'metadata_whitelist',
|
|
13
|
-
];
|
|
14
|
-
|
|
15
|
-
export function negotiateProtocol(payload, adapterRegistry) {
|
|
16
|
-
const requested = Number(payload?.protocolVersion);
|
|
17
|
-
const wantsV2 = Number.isInteger(requested) && requested >= PROTOCOL_V2;
|
|
18
|
-
const negotiatedProtocol = wantsV2 ? PROTOCOL_V2 : PROTOCOL_V1;
|
|
19
|
-
|
|
20
|
-
return {
|
|
21
|
-
negotiatedProtocol,
|
|
22
|
-
serverCapabilities: [...SERVER_CAPABILITIES],
|
|
23
|
-
adapterCapabilities: adapterRegistry.listCapabilities(),
|
|
24
|
-
};
|
|
25
|
-
}
|