@coffer-org/server 1.7.1 → 1.9.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/dist/auth-api.d.ts +1 -0
- package/dist/auth-api.js +20 -10
- package/dist/background-scheduler.d.ts +22 -0
- package/dist/background-scheduler.js +101 -0
- package/dist/collection-io.d.ts +7 -7
- package/dist/collection-io.js +2 -2
- package/dist/connector-identity.d.ts +5 -0
- package/dist/connector-identity.js +32 -0
- package/dist/embed-openai.d.ts +10 -0
- package/dist/embed-openai.js +28 -0
- package/dist/entity-schema.d.ts +9 -5
- package/dist/entity-schema.js +64 -10
- package/dist/extend-io.d.ts +5 -3
- package/dist/extend-io.js +17 -10
- package/dist/extend-table.d.ts +4 -3
- package/dist/extend-table.js +10 -18
- package/dist/field-masking.d.ts +7 -0
- package/dist/field-masking.js +60 -0
- package/dist/index-signal.d.ts +3 -0
- package/dist/index-signal.js +14 -0
- package/dist/index.js +68 -160
- package/dist/local-api.d.ts +3 -3
- package/dist/local-api.js +6 -6
- package/dist/mcp-http.d.ts +5 -0
- package/dist/mcp-http.js +37 -0
- package/dist/mcp-http.test-helpers.d.ts +17 -0
- package/dist/mcp-http.test-helpers.js +117 -0
- package/dist/mcp-local.d.ts +10 -0
- package/dist/mcp-local.js +57 -0
- package/dist/mcp-tools.d.ts +61 -0
- package/dist/mcp-tools.js +225 -0
- package/dist/msg-log.d.ts +0 -1
- package/dist/msg-log.js +2 -2
- package/dist/mutate.d.ts +7 -5
- package/dist/mutate.js +20 -8
- package/dist/oauth-api.d.ts +2 -0
- package/dist/oauth-api.js +281 -0
- package/dist/oauth-store.d.ts +56 -0
- package/dist/oauth-store.js +159 -0
- package/dist/plugin-hooks.d.ts +10 -0
- package/dist/plugin-hooks.js +8 -0
- package/dist/plugin-runtime.d.ts +1 -0
- package/dist/plugin-runtime.js +30 -6
- package/dist/plugins-api.d.ts +1 -1
- package/dist/plugins-api.js +21 -11
- package/dist/public-url.d.ts +4 -0
- package/dist/public-url.js +35 -0
- package/dist/records-api.d.ts +8 -8
- package/dist/records-api.js +35 -32
- package/dist/registry-context.d.ts +1 -1
- package/dist/registry-context.js +2 -2
- package/dist/schema-api.js +5 -5
- package/dist/settings-write.d.ts +19 -0
- package/dist/settings-write.js +63 -0
- package/dist/temporal.d.ts +3 -3
- package/dist/temporal.js +1 -1
- package/dist/thread-store.d.ts +20 -0
- package/dist/thread-store.js +27 -0
- package/dist/uploads.d.ts +1 -0
- package/dist/uploads.js +4 -0
- package/package.json +6 -6
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { fieldEntries, collectionGroups } from '@coffer-org/sdk/shelf';
|
|
2
|
+
export const MASK = '********';
|
|
3
|
+
export function passwordKeys(fields) {
|
|
4
|
+
return fieldEntries(fields)
|
|
5
|
+
.filter(([, f]) => f.kind === 'password')
|
|
6
|
+
.map(([k]) => k);
|
|
7
|
+
}
|
|
8
|
+
export function maskSecrets(fields, row) {
|
|
9
|
+
const out = { ...row };
|
|
10
|
+
for (const k of passwordKeys(fields))
|
|
11
|
+
if (out[k])
|
|
12
|
+
out[k] = MASK;
|
|
13
|
+
return out;
|
|
14
|
+
}
|
|
15
|
+
export function preserveSecrets(fields, incoming, existing) {
|
|
16
|
+
const out = { ...incoming };
|
|
17
|
+
for (const k of passwordKeys(fields)) {
|
|
18
|
+
if (out[k] !== MASK)
|
|
19
|
+
continue;
|
|
20
|
+
if (existing[k] !== undefined && existing[k] !== null)
|
|
21
|
+
out[k] = existing[k];
|
|
22
|
+
else
|
|
23
|
+
delete out[k];
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
export function maskTree(fields, obj) {
|
|
28
|
+
const out = maskSecrets(fields, obj);
|
|
29
|
+
for (const c of collectionGroups(fields)) {
|
|
30
|
+
const key = c.key;
|
|
31
|
+
const sub = c.group.fields;
|
|
32
|
+
const arr = out[key];
|
|
33
|
+
if (Array.isArray(arr)) {
|
|
34
|
+
out[key] = arr.map((item) => item && typeof item === 'object' ? maskTree(sub, item) : item);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
export function preserveTree(fields, incoming, existing) {
|
|
40
|
+
const out = preserveSecrets(fields, incoming, existing);
|
|
41
|
+
for (const c of collectionGroups(fields)) {
|
|
42
|
+
const key = c.key;
|
|
43
|
+
const sub = c.group.fields;
|
|
44
|
+
const uniq = c.group.unique ?? [];
|
|
45
|
+
const inArr = out[key];
|
|
46
|
+
if (!Array.isArray(inArr))
|
|
47
|
+
continue;
|
|
48
|
+
const exArr = Array.isArray(existing[key]) ? existing[key] : [];
|
|
49
|
+
out[key] = inArr.map((item) => {
|
|
50
|
+
if (!item || typeof item !== 'object')
|
|
51
|
+
return item;
|
|
52
|
+
const rec = item;
|
|
53
|
+
const match = uniq.length > 0
|
|
54
|
+
? exArr.find((e) => e && uniq.every((u) => e[u] === rec[u]))
|
|
55
|
+
: undefined;
|
|
56
|
+
return preserveTree(sub, rec, (match ?? {}));
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { getLogger } from '@coffer-org/sdk/logger';
|
|
2
|
+
const log = getLogger('rag-signal');
|
|
3
|
+
let listener = null;
|
|
4
|
+
export function onRecordsChanged(cb) {
|
|
5
|
+
if (listener)
|
|
6
|
+
log.warn('onRecordsChanged: overwriting an already-registered listener without offRecordsChanged');
|
|
7
|
+
listener = cb;
|
|
8
|
+
}
|
|
9
|
+
export function offRecordsChanged() {
|
|
10
|
+
listener = null;
|
|
11
|
+
}
|
|
12
|
+
export function notifyRecordsChanged() {
|
|
13
|
+
listener?.();
|
|
14
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,3 @@
|
|
|
1
|
-
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
-
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
-
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
-
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
-
});
|
|
6
|
-
}
|
|
7
|
-
return path;
|
|
8
|
-
};
|
|
9
1
|
import { randomUUID } from 'node:crypto';
|
|
10
2
|
import { createWriteStream, existsSync } from 'node:fs';
|
|
11
3
|
import { realpath } from 'node:fs/promises';
|
|
@@ -17,21 +9,26 @@ import cors from '@fastify/cors';
|
|
|
17
9
|
import multipart from '@fastify/multipart';
|
|
18
10
|
import fastifyStatic from '@fastify/static';
|
|
19
11
|
import { serialize } from '@mikro-orm/core';
|
|
20
|
-
import { textSearchKeys, recordTitle
|
|
12
|
+
import { textSearchKeys, recordTitle } from '@coffer-org/sdk/shelf';
|
|
21
13
|
import { tokenize } from '@coffer-org/core/search';
|
|
22
|
-
import {
|
|
14
|
+
import { shelfTableName } from "./entity-schema.js";
|
|
23
15
|
import { getEm, closeDb } from "./db.js";
|
|
24
|
-
import { ValidationError, NotFoundError
|
|
16
|
+
import { ValidationError, NotFoundError } from "./mutate.js";
|
|
25
17
|
import { resolveEmbed } from "./embed.js";
|
|
26
18
|
import { resolveWithinHome, filterSort, listDir } from "./fs-list.js";
|
|
27
19
|
import { initPlugins, teardownPlugins, readDisabled, purgePluginData, getPluginSettings, getPlugins, } from "./plugin-runtime.js";
|
|
28
20
|
import { registerPluginsApi } from "./plugins-api.js";
|
|
21
|
+
import { pluginHooks, HttpError } from "./plugin-hooks.js";
|
|
29
22
|
import { registerAuthApi, resolveRequestUser, requireAdmin, PUBLIC_API_PATHS } from "./auth-api.js";
|
|
23
|
+
import { registerMcpHttp } from "./mcp-http.js";
|
|
24
|
+
import { registerOAuthApi } from "./oauth-api.js";
|
|
25
|
+
import { baseUrl } from "./public-url.js";
|
|
30
26
|
import { discoverPluginAssets, discoverRuntime } from "./plugin-discovery.js";
|
|
31
27
|
import { checkLatestVersion, resolveUpdateTarget, resolveAllUpdateTargets, resolveRuntimeTarget, runNpmInstall, } from "./plugin-updates.js";
|
|
32
28
|
import { buildClientSchema } from "./schema-api.js";
|
|
33
29
|
import { recordList, recordGet, recordCreate, recordUpdate, recordDelete, rowMatch, UnknownTypeError } from "./records-api.js";
|
|
34
|
-
import { maskTree, preserveTree } from "./
|
|
30
|
+
import { maskTree, preserveTree } from "./field-masking.js";
|
|
31
|
+
import { writePluginSettings } from "./settings-write.js";
|
|
35
32
|
import { rootLogger, getLogger } from "./log.js";
|
|
36
33
|
import { uploadsDir } from "./uploads.js";
|
|
37
34
|
const ENV_FILE = join(process.cwd(), '.env');
|
|
@@ -39,12 +36,12 @@ if (existsSync(ENV_FILE))
|
|
|
39
36
|
process.loadEnvFile(ENV_FILE);
|
|
40
37
|
const PORT = Number(process.env.PORT ?? 7023);
|
|
41
38
|
const reg = await initPlugins();
|
|
42
|
-
const {
|
|
39
|
+
const { libraries, shelves, getShelf, extends_, getExtendsFor } = reg;
|
|
43
40
|
const entityNames = new Map();
|
|
44
|
-
const
|
|
45
|
-
for (const m of
|
|
46
|
-
entityNames.set(`${m.
|
|
47
|
-
|
|
41
|
+
const shelfDefs = new Map();
|
|
42
|
+
for (const m of shelves) {
|
|
43
|
+
entityNames.set(`${m.library}/${m.shelf}`, shelfTableName(m.library, m.shelf));
|
|
44
|
+
shelfDefs.set(`${m.library}/${m.shelf}`, m);
|
|
48
45
|
}
|
|
49
46
|
const UPLOADS = uploadsDir();
|
|
50
47
|
const app = Fastify({
|
|
@@ -72,14 +69,19 @@ app.addHook('onSend', async (req, reply, payload) => {
|
|
|
72
69
|
return payload;
|
|
73
70
|
});
|
|
74
71
|
app.addHook('onRequest', async (req, reply) => {
|
|
75
|
-
const
|
|
72
|
+
const isMcp = req.url === '/mcp' || req.url.startsWith('/mcp?');
|
|
73
|
+
const gated = req.url.startsWith('/api/') || req.url.startsWith('/uploads/') || isMcp;
|
|
76
74
|
if (!gated)
|
|
77
75
|
return;
|
|
78
76
|
if (PUBLIC_API_PATHS.some((p) => req.url.startsWith(p)))
|
|
79
77
|
return;
|
|
80
78
|
const user = await resolveRequestUser(req);
|
|
81
|
-
if (!user)
|
|
79
|
+
if (!user) {
|
|
80
|
+
if (isMcp) {
|
|
81
|
+
reply.header('WWW-Authenticate', `Bearer resource_metadata="${await baseUrl(req)}/.well-known/oauth-protected-resource"`);
|
|
82
|
+
}
|
|
82
83
|
return reply.code(401).send({ error: 'unauthorized' });
|
|
84
|
+
}
|
|
83
85
|
req.user = user;
|
|
84
86
|
});
|
|
85
87
|
app.post('/api/upload', async (req, reply) => {
|
|
@@ -100,8 +102,8 @@ app.post('/api/embed', async (req, reply) => {
|
|
|
100
102
|
return reply.code(422).send({ error: 'unresolved' });
|
|
101
103
|
return reply.send(rec);
|
|
102
104
|
});
|
|
103
|
-
function key(
|
|
104
|
-
return `${
|
|
105
|
+
function key(library, type) {
|
|
106
|
+
return `${library}/${type}`;
|
|
105
107
|
}
|
|
106
108
|
async function guard(reply, fn) {
|
|
107
109
|
try {
|
|
@@ -125,11 +127,11 @@ app.get('/health', async (_req, reply) => {
|
|
|
125
127
|
return reply.code(503).send({ status: 'db_unavailable' });
|
|
126
128
|
}
|
|
127
129
|
});
|
|
128
|
-
app.get('/api/
|
|
130
|
+
app.get('/api/libraries', () => buildClientSchema());
|
|
129
131
|
app.get('/api/counts', async () => {
|
|
130
132
|
const fork = getEm().fork();
|
|
131
133
|
const out = {};
|
|
132
|
-
for (const [k, mdef] of
|
|
134
|
+
for (const [k, mdef] of shelfDefs) {
|
|
133
135
|
if (mdef.standalone === false)
|
|
134
136
|
continue;
|
|
135
137
|
const ename = entityNames.get(k);
|
|
@@ -147,7 +149,7 @@ app.get('/api/search', async (req) => {
|
|
|
147
149
|
const lim = Math.min(Number(limit) || 20, 100);
|
|
148
150
|
const fork = getEm().fork();
|
|
149
151
|
const scored = [];
|
|
150
|
-
for (const [k, mdef] of
|
|
152
|
+
for (const [k, mdef] of shelfDefs) {
|
|
151
153
|
if (mdef.standalone === false)
|
|
152
154
|
continue;
|
|
153
155
|
const ename = entityNames.get(k);
|
|
@@ -155,7 +157,7 @@ app.get('/api/search', async (req) => {
|
|
|
155
157
|
continue;
|
|
156
158
|
if (!textSearchKeys(mdef).length)
|
|
157
159
|
continue;
|
|
158
|
-
const [
|
|
160
|
+
const [library, type] = k.split('/');
|
|
159
161
|
const rows = (await fork.find(ename, {}, { limit: 1000 })).map((r) => serialize(r));
|
|
160
162
|
for (const row of rows) {
|
|
161
163
|
const m = rowMatch(mdef, row, tokens);
|
|
@@ -164,7 +166,7 @@ app.get('/api/search', async (req) => {
|
|
|
164
166
|
const label = recordTitle(mdef, row);
|
|
165
167
|
scored.push({
|
|
166
168
|
score: m.score,
|
|
167
|
-
result: {
|
|
169
|
+
result: { library, type, id: String(row.id), label, snippet: m.snippet },
|
|
168
170
|
});
|
|
169
171
|
}
|
|
170
172
|
}
|
|
@@ -291,123 +293,27 @@ app.put('/api/plugins/:id/settings', (req, reply) => {
|
|
|
291
293
|
if (!p || !p.settings || p.settings.fields.length === 0) {
|
|
292
294
|
return reply.code(404).send({ error: 'no_settings' });
|
|
293
295
|
}
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
module: id,
|
|
297
|
-
label: `${id}.plugin.label`,
|
|
298
|
-
fields: p.settings.fields,
|
|
299
|
-
};
|
|
300
|
-
const existing = await getPluginSettings(id);
|
|
301
|
-
const body = preserveTree(p.settings.fields, (req.body ?? {}), existing);
|
|
302
|
-
const parsed = buildZodObject(syntheticMod).safeParse(body);
|
|
303
|
-
if (!parsed.success) {
|
|
304
|
-
throw new ValidationError(parsed.error.issues.map(toIssue));
|
|
305
|
-
}
|
|
306
|
-
const entityName = `_settings__${id}`;
|
|
307
|
-
const row = { plugin_id: id, ...parsed.data };
|
|
308
|
-
await getEm()
|
|
309
|
-
.fork()
|
|
310
|
-
.upsert(entityName, row);
|
|
311
|
-
return { ok: true, row: maskTree(p.settings.fields, row) };
|
|
296
|
+
const row = await writePluginSettings(getEm().fork(), id, (req.body ?? {}), req.user.login, plugins);
|
|
297
|
+
return { ok: true, row };
|
|
312
298
|
});
|
|
313
299
|
});
|
|
314
|
-
app.post('/api/plugins
|
|
315
|
-
if (!requireAdmin(req, reply))
|
|
316
|
-
return;
|
|
317
|
-
const settings = await getPluginSettings('finance');
|
|
318
|
-
const url = settings['firefly_url'];
|
|
319
|
-
const token = settings['token'];
|
|
320
|
-
if (!url || !token) {
|
|
321
|
-
return reply.code(400).send({ error: 'Finance settings not configured. Save them first.' });
|
|
322
|
-
}
|
|
323
|
-
const finRuntime = '@coffer-org/plugin-finance/runtime';
|
|
324
|
-
const { checkFireflyConnection } = (await import(__rewriteRelativeImportExtension(finRuntime)));
|
|
325
|
-
const result = await checkFireflyConnection(url, token);
|
|
326
|
-
if (result.ok)
|
|
327
|
-
return { ok: true };
|
|
328
|
-
return reply.code(400).send({ error: result.error });
|
|
329
|
-
});
|
|
330
|
-
app.post('/api/plugins/finance/sync', async (req, reply) => {
|
|
331
|
-
if (!requireAdmin(req, reply))
|
|
332
|
-
return;
|
|
333
|
-
const finRuntime = '@coffer-org/plugin-finance/runtime';
|
|
334
|
-
const { runSync } = (await import(__rewriteRelativeImportExtension(finRuntime)));
|
|
335
|
-
try {
|
|
336
|
-
const counts = await runSync();
|
|
337
|
-
return { ok: true, ...counts };
|
|
338
|
-
}
|
|
339
|
-
catch (e) {
|
|
340
|
-
return reply.code(400).send({ error: e.message });
|
|
341
|
-
}
|
|
342
|
-
});
|
|
343
|
-
app.post('/api/plugins/finance/import-wise', async (req, reply) => {
|
|
344
|
-
if (!requireAdmin(req, reply))
|
|
345
|
-
return;
|
|
346
|
-
const finRuntime = '@coffer-org/plugin-finance/runtime';
|
|
347
|
-
const { importWise } = (await import(__rewriteRelativeImportExtension(finRuntime)));
|
|
348
|
-
try {
|
|
349
|
-
return await importWise();
|
|
350
|
-
}
|
|
351
|
-
catch (e) {
|
|
352
|
-
return reply.code(400).send({ error: e.message });
|
|
353
|
-
}
|
|
354
|
-
});
|
|
355
|
-
app.post('/api/plugins/devices/scan', async (req, reply) => {
|
|
300
|
+
app.post('/api/plugins/:id/:action', async (req, reply) => {
|
|
356
301
|
if (!requireAdmin(req, reply))
|
|
357
302
|
return;
|
|
358
|
-
const
|
|
359
|
-
const
|
|
303
|
+
const { id, action } = req.params;
|
|
304
|
+
const fn = pluginHooks[id]?.actions?.[action];
|
|
305
|
+
if (!fn)
|
|
306
|
+
return reply.code(404).send({ error: `unknown action ${id}/${action}` });
|
|
360
307
|
try {
|
|
361
|
-
|
|
362
|
-
return { ok: true, result };
|
|
308
|
+
return await fn((req.body ?? {}));
|
|
363
309
|
}
|
|
364
310
|
catch (e) {
|
|
365
311
|
if (e instanceof ValidationError) {
|
|
366
312
|
const detail = e.issues.map((i) => `${i.path.join('.') || i.field}: ${i.code}`).join('; ');
|
|
367
|
-
return reply.code(422).send({ error: `Validation error
|
|
313
|
+
return reply.code(422).send({ error: `Validation error — ${detail}`, issues: e.issues });
|
|
368
314
|
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
});
|
|
372
|
-
app.post('/api/plugins/media/test', async (req, reply) => {
|
|
373
|
-
if (!requireAdmin(req, reply))
|
|
374
|
-
return;
|
|
375
|
-
const settings = await getPluginSettings('media');
|
|
376
|
-
const url = settings['jellyfin_url'];
|
|
377
|
-
const token = settings['jellyfin_token'];
|
|
378
|
-
if (!url || !token) {
|
|
379
|
-
return reply.code(400).send({ error: 'Jellyfin settings not configured. Save them first.' });
|
|
380
|
-
}
|
|
381
|
-
const mediaRuntime = '@coffer-org/plugin-media/runtime';
|
|
382
|
-
const { testConnection } = (await import(__rewriteRelativeImportExtension(mediaRuntime)));
|
|
383
|
-
const result = await testConnection(url, token);
|
|
384
|
-
if (result.ok)
|
|
385
|
-
return { ok: true };
|
|
386
|
-
return reply.code(400).send({ error: result.error });
|
|
387
|
-
});
|
|
388
|
-
app.post('/api/plugins/media/sync', async (req, reply) => {
|
|
389
|
-
if (!requireAdmin(req, reply))
|
|
390
|
-
return;
|
|
391
|
-
const mediaRuntime = '@coffer-org/plugin-media/runtime';
|
|
392
|
-
const { runJellyfinSync } = (await import(__rewriteRelativeImportExtension(mediaRuntime)));
|
|
393
|
-
try {
|
|
394
|
-
const counts = await runJellyfinSync();
|
|
395
|
-
return { ok: true, ...counts };
|
|
396
|
-
}
|
|
397
|
-
catch (e) {
|
|
398
|
-
return reply.code(400).send({ error: e.message });
|
|
399
|
-
}
|
|
400
|
-
});
|
|
401
|
-
app.post('/api/plugins/media/add', async (req, reply) => {
|
|
402
|
-
if (!requireAdmin(req, reply))
|
|
403
|
-
return;
|
|
404
|
-
const { query, tmdb_id, kind } = (req.body ?? {});
|
|
405
|
-
const mediaRuntime = '@coffer-org/plugin-media/runtime';
|
|
406
|
-
const { addTitle } = (await import(__rewriteRelativeImportExtension(mediaRuntime)));
|
|
407
|
-
try {
|
|
408
|
-
return await addTitle(query, tmdb_id, kind);
|
|
409
|
-
}
|
|
410
|
-
catch (e) {
|
|
315
|
+
if (e instanceof HttpError)
|
|
316
|
+
return reply.code(e.status).send({ error: e.message });
|
|
411
317
|
return reply.code(400).send({ error: e.message });
|
|
412
318
|
}
|
|
413
319
|
});
|
|
@@ -434,12 +340,12 @@ app.get('/api/fs/list', async (req, reply) => {
|
|
|
434
340
|
return { dir: abs, entries: [] };
|
|
435
341
|
}
|
|
436
342
|
});
|
|
437
|
-
function maskRecordRow(
|
|
438
|
-
const m =
|
|
343
|
+
function maskRecordRow(library, type, row) {
|
|
344
|
+
const m = getShelf(library, type);
|
|
439
345
|
let out = m ? maskTree(m.fields, row) : row;
|
|
440
346
|
const ext = out._extends;
|
|
441
347
|
if (ext) {
|
|
442
|
-
const defs = getExtendsFor(
|
|
348
|
+
const defs = getExtendsFor(library, type);
|
|
443
349
|
const maskedExt = {};
|
|
444
350
|
for (const [id, sub] of Object.entries(ext)) {
|
|
445
351
|
const def = defs.find((e) => e.id === id);
|
|
@@ -449,12 +355,12 @@ function maskRecordRow(vault, type, row) {
|
|
|
449
355
|
}
|
|
450
356
|
return out;
|
|
451
357
|
}
|
|
452
|
-
app.get('/api/:
|
|
453
|
-
const {
|
|
358
|
+
app.get('/api/:library/:type', async (req, reply) => {
|
|
359
|
+
const { library, type } = req.params;
|
|
454
360
|
const query = req.query;
|
|
455
361
|
try {
|
|
456
|
-
const rows = await recordList(
|
|
457
|
-
return rows.map((r) => maskRecordRow(
|
|
362
|
+
const rows = await recordList(library, type, query);
|
|
363
|
+
return rows.map((r) => maskRecordRow(library, type, r));
|
|
458
364
|
}
|
|
459
365
|
catch (e) {
|
|
460
366
|
if (e instanceof UnknownTypeError)
|
|
@@ -462,16 +368,16 @@ app.get('/api/:vault/:type', async (req, reply) => {
|
|
|
462
368
|
throw e;
|
|
463
369
|
}
|
|
464
370
|
});
|
|
465
|
-
app.get('/api/:
|
|
466
|
-
const {
|
|
371
|
+
app.get('/api/:library/:type/:id', async (req, reply) => {
|
|
372
|
+
const { library, type, id } = req.params;
|
|
467
373
|
const rid = Number(id);
|
|
468
374
|
if (isNaN(rid))
|
|
469
375
|
return reply.code(400).send({ error: 'invalid_id' });
|
|
470
376
|
try {
|
|
471
|
-
const row = await recordGet(
|
|
377
|
+
const row = await recordGet(library, type, rid);
|
|
472
378
|
if (!row)
|
|
473
379
|
return reply.code(404).send({ error: 'not_found' });
|
|
474
|
-
return maskRecordRow(
|
|
380
|
+
return maskRecordRow(library, type, row);
|
|
475
381
|
}
|
|
476
382
|
catch (e) {
|
|
477
383
|
if (e instanceof UnknownTypeError)
|
|
@@ -479,15 +385,15 @@ app.get('/api/:vault/:type/:id', async (req, reply) => {
|
|
|
479
385
|
throw e;
|
|
480
386
|
}
|
|
481
387
|
});
|
|
482
|
-
app.post('/api/:
|
|
483
|
-
const {
|
|
388
|
+
app.post('/api/:library/:type', (req, reply) => guard(reply, async () => {
|
|
389
|
+
const { library, type } = req.params;
|
|
484
390
|
try {
|
|
485
|
-
const m =
|
|
391
|
+
const m = getShelf(library, type);
|
|
486
392
|
let body = (req.body ?? {});
|
|
487
393
|
if (m)
|
|
488
394
|
body = preserveTree(m.fields, body, {});
|
|
489
|
-
const row = await recordCreate(
|
|
490
|
-
return reply.code(201).send(maskRecordRow(
|
|
395
|
+
const row = await recordCreate(library, type, body);
|
|
396
|
+
return reply.code(201).send(maskRecordRow(library, type, row));
|
|
491
397
|
}
|
|
492
398
|
catch (e) {
|
|
493
399
|
if (e instanceof UnknownTypeError)
|
|
@@ -495,18 +401,18 @@ app.post('/api/:vault/:type', (req, reply) => guard(reply, async () => {
|
|
|
495
401
|
throw e;
|
|
496
402
|
}
|
|
497
403
|
}));
|
|
498
|
-
app.patch('/api/:
|
|
499
|
-
const {
|
|
404
|
+
app.patch('/api/:library/:type/:id', (req, reply) => guard(reply, async () => {
|
|
405
|
+
const { library, type, id } = req.params;
|
|
500
406
|
const rid = Number(id);
|
|
501
407
|
if (isNaN(rid))
|
|
502
408
|
return reply.code(400).send({ error: 'invalid_id' });
|
|
503
409
|
try {
|
|
504
|
-
const m =
|
|
410
|
+
const m = getShelf(library, type);
|
|
505
411
|
let body = (req.body ?? {});
|
|
506
412
|
if (m) {
|
|
507
|
-
const existing = (await recordGet(
|
|
413
|
+
const existing = (await recordGet(library, type, rid)) ?? {};
|
|
508
414
|
body = preserveTree(m.fields, body, existing);
|
|
509
|
-
const exDefs = getExtendsFor(
|
|
415
|
+
const exDefs = getExtendsFor(library, type);
|
|
510
416
|
const exRecs = (existing._extends ?? {});
|
|
511
417
|
for (const def of exDefs) {
|
|
512
418
|
const bk = `_extend_${def.id}`;
|
|
@@ -516,8 +422,8 @@ app.patch('/api/:vault/:type/:id', (req, reply) => guard(reply, async () => {
|
|
|
516
422
|
}
|
|
517
423
|
}
|
|
518
424
|
}
|
|
519
|
-
const row = await recordUpdate(
|
|
520
|
-
return maskRecordRow(
|
|
425
|
+
const row = await recordUpdate(library, type, rid, body);
|
|
426
|
+
return maskRecordRow(library, type, row);
|
|
521
427
|
}
|
|
522
428
|
catch (e) {
|
|
523
429
|
if (e instanceof UnknownTypeError)
|
|
@@ -525,13 +431,13 @@ app.patch('/api/:vault/:type/:id', (req, reply) => guard(reply, async () => {
|
|
|
525
431
|
throw e;
|
|
526
432
|
}
|
|
527
433
|
}));
|
|
528
|
-
app.delete('/api/:
|
|
529
|
-
const {
|
|
434
|
+
app.delete('/api/:library/:type/:id', (req, reply) => guard(reply, async () => {
|
|
435
|
+
const { library, type, id } = req.params;
|
|
530
436
|
const rid = Number(id);
|
|
531
437
|
if (isNaN(rid))
|
|
532
438
|
return reply.code(400).send({ error: 'invalid_id' });
|
|
533
439
|
try {
|
|
534
|
-
await recordDelete(
|
|
440
|
+
await recordDelete(library, type, rid);
|
|
535
441
|
return reply.code(204).send();
|
|
536
442
|
}
|
|
537
443
|
catch (e) {
|
|
@@ -541,6 +447,8 @@ app.delete('/api/:vault/:type/:id', (req, reply) => guard(reply, async () => {
|
|
|
541
447
|
}
|
|
542
448
|
}));
|
|
543
449
|
await registerAuthApi(app);
|
|
450
|
+
registerOAuthApi(app);
|
|
451
|
+
await registerMcpHttp(app);
|
|
544
452
|
await registerPluginsApi(app);
|
|
545
453
|
const WEB_DIST = process.env.WEB_DIST;
|
|
546
454
|
if (WEB_DIST && existsSync(join(WEB_DIST, 'index.html'))) {
|
|
@@ -562,7 +470,7 @@ if (WEB_DIST && existsSync(join(WEB_DIST, 'index.html'))) {
|
|
|
562
470
|
}
|
|
563
471
|
app
|
|
564
472
|
.listen({ port: PORT, host: '0.0.0.0' })
|
|
565
|
-
.then(() => app.log.info(`
|
|
473
|
+
.then(() => app.log.info(`library-server :${PORT}`))
|
|
566
474
|
.catch((e) => {
|
|
567
475
|
app.log.error(e);
|
|
568
476
|
process.exit(1);
|
package/dist/local-api.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { UnknownTypeError } from './records-api.ts';
|
|
2
2
|
export { UnknownTypeError };
|
|
3
|
-
export declare function localExists(
|
|
4
|
-
export declare function localPost(
|
|
5
|
-
export declare function localPatch(
|
|
3
|
+
export declare function localExists(library: string, type: string, id: number): Promise<boolean>;
|
|
4
|
+
export declare function localPost(library: string, type: string, body: unknown): Promise<Record<string, unknown>>;
|
|
5
|
+
export declare function localPatch(library: string, type: string, id: number, body: unknown): Promise<Record<string, unknown>>;
|
package/dist/local-api.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { recordCreate, recordUpdate, recordGet, UnknownTypeError } from "./records-api.js";
|
|
2
2
|
export { UnknownTypeError };
|
|
3
|
-
export async function localExists(
|
|
3
|
+
export async function localExists(library, type, id) {
|
|
4
4
|
try {
|
|
5
|
-
return Boolean(await recordGet(
|
|
5
|
+
return Boolean(await recordGet(library, type, id));
|
|
6
6
|
}
|
|
7
7
|
catch (e) {
|
|
8
8
|
if (e instanceof UnknownTypeError)
|
|
@@ -10,9 +10,9 @@ export async function localExists(vault, type, id) {
|
|
|
10
10
|
throw e;
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
|
-
export function localPost(
|
|
14
|
-
return recordCreate(
|
|
13
|
+
export function localPost(library, type, body) {
|
|
14
|
+
return recordCreate(library, type, body, 'import');
|
|
15
15
|
}
|
|
16
|
-
export function localPatch(
|
|
17
|
-
return recordUpdate(
|
|
16
|
+
export function localPatch(library, type, id, body) {
|
|
17
|
+
return recordUpdate(library, type, id, body, 'import');
|
|
18
18
|
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { FastifyInstance } from 'fastify';
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import type { AuthRole } from './plugin-hooks.ts';
|
|
4
|
+
export declare function buildMcpServer(role: AuthRole, actor: string): Promise<McpServer>;
|
|
5
|
+
export declare function registerMcpHttp(app: FastifyInstance): Promise<void>;
|
package/dist/mcp-http.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
3
|
+
import { collectMcpTools, resolveRagDeps, buildDomainSections, buildMcpInstructions } from "./mcp-tools.js";
|
|
4
|
+
import { getLogger } from "./log.js";
|
|
5
|
+
const log = getLogger('mcp-http');
|
|
6
|
+
export async function buildMcpServer(role, actor) {
|
|
7
|
+
const rag = await resolveRagDeps();
|
|
8
|
+
const tools = (await collectMcpTools({ rag, includeAdmin: role === 'admin', actor })).filter((t) => role === 'admin' || t.role === 'member');
|
|
9
|
+
const sections = await buildDomainSections();
|
|
10
|
+
const server = new McpServer({ name: 'coffer', version: '1.0.0' }, { instructions: buildMcpInstructions(sections) });
|
|
11
|
+
const registerTool = server.registerTool.bind(server);
|
|
12
|
+
for (const t of tools) {
|
|
13
|
+
registerTool(t.httpName, { description: t.description, inputSchema: t.inputSchema }, async (args) => t.handler(args));
|
|
14
|
+
}
|
|
15
|
+
return server;
|
|
16
|
+
}
|
|
17
|
+
export async function registerMcpHttp(app) {
|
|
18
|
+
app.all('/mcp', async (req, reply) => {
|
|
19
|
+
const role = req.user.role;
|
|
20
|
+
const actor = req.user.login;
|
|
21
|
+
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
22
|
+
reply.hijack();
|
|
23
|
+
reply.raw.on('close', () => void transport.close());
|
|
24
|
+
try {
|
|
25
|
+
const server = await buildMcpServer(role, actor);
|
|
26
|
+
await server.connect(transport);
|
|
27
|
+
await transport.handleRequest(req.raw, reply.raw, req.body);
|
|
28
|
+
}
|
|
29
|
+
catch (e) {
|
|
30
|
+
log.error(`/mcp handler error: ${e.message}`);
|
|
31
|
+
if (!reply.raw.headersSent) {
|
|
32
|
+
reply.raw.writeHead(500, { 'content-type': 'application/json' });
|
|
33
|
+
reply.raw.end(JSON.stringify({ jsonrpc: '2.0', error: { code: -32603, message: 'internal' }, id: null }));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type FastifyInstance } from 'fastify';
|
|
2
|
+
import { closeDb } from './db.ts';
|
|
3
|
+
export declare function freshMcpApp(opts?: {
|
|
4
|
+
withAdminTool?: boolean;
|
|
5
|
+
withInstructions?: boolean;
|
|
6
|
+
}): Promise<FastifyInstance>;
|
|
7
|
+
export declare function adminToken(): Promise<string>;
|
|
8
|
+
export declare function memberToken(): Promise<string>;
|
|
9
|
+
export declare function parseRpcResult(res: {
|
|
10
|
+
body: string;
|
|
11
|
+
headers: Record<string, unknown>;
|
|
12
|
+
}): Record<string, unknown> | undefined;
|
|
13
|
+
export declare function parseToolNames(res: {
|
|
14
|
+
body: string;
|
|
15
|
+
headers: Record<string, unknown>;
|
|
16
|
+
}): string[];
|
|
17
|
+
export { closeDb };
|