@coffer-org/server 1.9.0 → 1.11.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/index.js +6 -0
- package/dist/mcp-tools.d.ts +1 -1
- package/dist/mcp-tools.js +22 -0
- package/dist/upload-ticket.d.ts +7 -0
- package/dist/upload-ticket.js +29 -0
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -31,6 +31,7 @@ import { maskTree, preserveTree } from "./field-masking.js";
|
|
|
31
31
|
import { writePluginSettings } from "./settings-write.js";
|
|
32
32
|
import { rootLogger, getLogger } from "./log.js";
|
|
33
33
|
import { uploadsDir } from "./uploads.js";
|
|
34
|
+
import { verifyUploadTicket } from "./upload-ticket.js";
|
|
34
35
|
const ENV_FILE = join(process.cwd(), '.env');
|
|
35
36
|
if (existsSync(ENV_FILE))
|
|
36
37
|
process.loadEnvFile(ENV_FILE);
|
|
@@ -75,6 +76,11 @@ app.addHook('onRequest', async (req, reply) => {
|
|
|
75
76
|
return;
|
|
76
77
|
if (PUBLIC_API_PATHS.some((p) => req.url.startsWith(p)))
|
|
77
78
|
return;
|
|
79
|
+
if (req.method === 'POST' && req.url === '/api/upload') {
|
|
80
|
+
const auth = req.headers.authorization;
|
|
81
|
+
if (auth?.startsWith('Bearer ') && verifyUploadTicket(auth.slice('Bearer '.length)))
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
78
84
|
const user = await resolveRequestUser(req);
|
|
79
85
|
if (!user) {
|
|
80
86
|
if (isMcp) {
|
package/dist/mcp-tools.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export interface McpToolDef {
|
|
|
10
10
|
httpName: string;
|
|
11
11
|
description: string;
|
|
12
12
|
inputSchema: z.ZodRawShape;
|
|
13
|
-
scope: 'crud' | 'plugin' | 'rag' | 'settings';
|
|
13
|
+
scope: 'crud' | 'plugin' | 'rag' | 'settings' | 'upload';
|
|
14
14
|
role: AuthRole;
|
|
15
15
|
handler: (args: Record<string, unknown>) => Promise<ToolResult>;
|
|
16
16
|
}
|
package/dist/mcp-tools.js
CHANGED
|
@@ -2,6 +2,7 @@ import { z } from 'zod';
|
|
|
2
2
|
import { buildTools } from '@coffer-org/mcp';
|
|
3
3
|
import { SchemaCache } from '@coffer-org/mcp/schema';
|
|
4
4
|
import { LocalClient } from "./mcp-local.js";
|
|
5
|
+
import { mintUploadTicket } from "./upload-ticket.js";
|
|
5
6
|
import { pluginHooks, pluginCtx } from "./plugin-hooks.js";
|
|
6
7
|
import { getActiveRegistry } from "./registry-context.js";
|
|
7
8
|
import { describeCondition } from '@coffer-org/sdk/condition';
|
|
@@ -49,6 +50,27 @@ export async function collectMcpTools(opts = {}) {
|
|
|
49
50
|
handler: t.handler,
|
|
50
51
|
});
|
|
51
52
|
}
|
|
53
|
+
{
|
|
54
|
+
const actor = opts.actor ?? 'mcp';
|
|
55
|
+
out.push({
|
|
56
|
+
server: 'coffer',
|
|
57
|
+
bareName: 'create_upload_ticket',
|
|
58
|
+
httpName: 'create_upload_ticket',
|
|
59
|
+
description: 'Mint a short-lived (60 min) upload-only token for POST /api/upload. Use it as a Bearer header to stream local files to the server without sending their bytes through this conversation. The token cannot read or modify records. Response of each upload is {"filename":"<name>"} — store it in a file/image field as {"name":"<name>"}.',
|
|
60
|
+
inputSchema: {},
|
|
61
|
+
scope: 'upload',
|
|
62
|
+
role: 'member',
|
|
63
|
+
handler: async () => {
|
|
64
|
+
const { token, expiresInSec } = mintUploadTicket(actor);
|
|
65
|
+
return ok({
|
|
66
|
+
token,
|
|
67
|
+
upload_url: '/api/upload',
|
|
68
|
+
expires_in: expiresInSec,
|
|
69
|
+
how_to: 'curl -H "Authorization: Bearer <token>" -F "file=@<path>" <base-url>/api/upload → {"filename":"<name>"}. Then set a file field to {"name":"<name>"}.',
|
|
70
|
+
});
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
}
|
|
52
74
|
for (const [id, h] of Object.entries(pluginHooks)) {
|
|
53
75
|
for (const t of h.agent?.tools ?? []) {
|
|
54
76
|
out.push({
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare function mintUploadTicket(actor: string): {
|
|
2
|
+
token: string;
|
|
3
|
+
expiresInSec: number;
|
|
4
|
+
};
|
|
5
|
+
export declare function verifyUploadTicket(raw: string): boolean;
|
|
6
|
+
export declare function __resetUploadTickets(): void;
|
|
7
|
+
export declare function __injectUploadTicket(raw: string, exp: number): void;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { generateToken, hashToken } from "./auth-crypto.js";
|
|
2
|
+
const TTL_MS = 60 * 60 * 1000;
|
|
3
|
+
const store = new Map();
|
|
4
|
+
export function mintUploadTicket(actor) {
|
|
5
|
+
const raw = generateToken();
|
|
6
|
+
const exp = Date.now() + TTL_MS;
|
|
7
|
+
store.set(hashToken(raw), { actor, exp });
|
|
8
|
+
const t = setTimeout(() => store.delete(hashToken(raw)), TTL_MS);
|
|
9
|
+
if (typeof t === 'object' && 'unref' in t)
|
|
10
|
+
t.unref();
|
|
11
|
+
return { token: raw, expiresInSec: TTL_MS / 1000 };
|
|
12
|
+
}
|
|
13
|
+
export function verifyUploadTicket(raw) {
|
|
14
|
+
const key = hashToken(raw);
|
|
15
|
+
const entry = store.get(key);
|
|
16
|
+
if (!entry)
|
|
17
|
+
return false;
|
|
18
|
+
if (entry.exp <= Date.now()) {
|
|
19
|
+
store.delete(key);
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
export function __resetUploadTickets() {
|
|
25
|
+
store.clear();
|
|
26
|
+
}
|
|
27
|
+
export function __injectUploadTicket(raw, exp) {
|
|
28
|
+
store.set(hashToken(raw), { actor: 'test', exp });
|
|
29
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coffer-org/server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=24"
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"@fastify/static": "^9.1.3",
|
|
33
33
|
"@mikro-orm/core": "^7.1.6",
|
|
34
34
|
"@mikro-orm/sqlite": "^7.1.6",
|
|
35
|
+
"@modelcontextprotocol/sdk": "^1.5.0",
|
|
35
36
|
"fastify": "^5.2.1",
|
|
36
37
|
"open-graph-scraper": "^6.11.0",
|
|
37
38
|
"pino": "^9.14.0",
|