boondmanager-mcp-server 2.17.0 → 2.19.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 +60 -2
- package/dist/config/transport-kind.d.ts +8 -0
- package/dist/config/transport-kind.d.ts.map +1 -0
- package/dist/config/transport-kind.js +13 -0
- package/dist/config/transport-kind.js.map +1 -0
- package/dist/constants.d.ts +7 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +7 -0
- package/dist/constants.js.map +1 -1
- package/dist/index.js +1 -7
- package/dist/index.js.map +1 -1
- package/dist/schemas/index.d.ts +62 -1
- package/dist/schemas/index.d.ts.map +1 -1
- package/dist/schemas/index.js +141 -7
- package/dist/schemas/index.js.map +1 -1
- package/dist/services/format/summary.d.ts +7 -0
- package/dist/services/format/summary.d.ts.map +1 -1
- package/dist/services/format/summary.js +24 -2
- package/dist/services/format/summary.js.map +1 -1
- package/dist/services/http/transport.d.ts +16 -4
- package/dist/services/http/transport.d.ts.map +1 -1
- package/dist/services/http/transport.js +15 -4
- package/dist/services/http/transport.js.map +1 -1
- package/dist/services/upload-relay.d.ts +88 -0
- package/dist/services/upload-relay.d.ts.map +1 -0
- package/dist/services/upload-relay.js +325 -0
- package/dist/services/upload-relay.js.map +1 -0
- package/dist/services/upload-source.d.ts +31 -0
- package/dist/services/upload-source.d.ts.map +1 -0
- package/dist/services/upload-source.js +180 -0
- package/dist/services/upload-source.js.map +1 -0
- package/dist/tools/candidates.d.ts +7 -0
- package/dist/tools/candidates.d.ts.map +1 -1
- package/dist/tools/candidates.js +96 -1
- package/dist/tools/candidates.js.map +1 -1
- package/dist/tools/crud-factory.d.ts.map +1 -1
- package/dist/tools/crud-factory.js +4 -3
- package/dist/tools/crud-factory.js.map +1 -1
- package/dist/tools/description-builders.d.ts.map +1 -1
- package/dist/tools/description-builders.js +1 -1
- package/dist/tools/description-builders.js.map +1 -1
- package/dist/tools/documents.d.ts.map +1 -1
- package/dist/tools/documents.js +122 -8
- package/dist/tools/documents.js.map +1 -1
- package/dist/tools/usage-guidance.d.ts.map +1 -1
- package/dist/tools/usage-guidance.js +17 -3
- package/dist/tools/usage-guidance.js.map +1 -1
- package/manifest.json +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/** How long a slot stays usable after creation. */
|
|
2
|
+
export declare const SLOT_TTL_MS: number;
|
|
3
|
+
export interface SlotHandle {
|
|
4
|
+
/** Backend-private reference to the stored object (e.g. Graph folder id). */
|
|
5
|
+
ref: string;
|
|
6
|
+
/** Pre-authenticated URL the client PUTs the file to (single request). */
|
|
7
|
+
uploadUrl: string;
|
|
8
|
+
expiresAt: Date;
|
|
9
|
+
}
|
|
10
|
+
export interface StoredObject {
|
|
11
|
+
size: number;
|
|
12
|
+
/** Short-lived, pre-authenticated URL BoondManager can download. */
|
|
13
|
+
downloadUrl: string;
|
|
14
|
+
}
|
|
15
|
+
export interface RelayBackend {
|
|
16
|
+
readonly name: string;
|
|
17
|
+
createSlot(slotId: string, filename: string): Promise<SlotHandle>;
|
|
18
|
+
/** `undefined` when nothing was uploaded to the slot yet. */
|
|
19
|
+
stat(ref: string, filename: string): Promise<StoredObject | undefined>;
|
|
20
|
+
/** First bytes of the stored object, for type sniffing. */
|
|
21
|
+
head(downloadUrl: string, bytes: number): Promise<Buffer>;
|
|
22
|
+
purge(ref: string): Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
export interface SharePointConfig {
|
|
25
|
+
tenantId: string;
|
|
26
|
+
clientId: string;
|
|
27
|
+
clientSecret: string;
|
|
28
|
+
/** `host:/sites/path` (Graph site key). */
|
|
29
|
+
site: string;
|
|
30
|
+
library: string;
|
|
31
|
+
}
|
|
32
|
+
/** Accept `https://host/sites/x`, `host/sites/x` or `host:/sites/x`. */
|
|
33
|
+
export declare function normalizeSiteKey(raw: string): string;
|
|
34
|
+
/** SharePoint forbids `" * : < > ? / \ |` and `#`/`%` cause URL trouble. */
|
|
35
|
+
export declare function sanitizeSharePointName(filename: string): string;
|
|
36
|
+
export declare class SharePointRelay implements RelayBackend {
|
|
37
|
+
private readonly cfg;
|
|
38
|
+
readonly name = "sharepoint";
|
|
39
|
+
private token?;
|
|
40
|
+
private driveId?;
|
|
41
|
+
constructor(cfg: SharePointConfig);
|
|
42
|
+
private accessToken;
|
|
43
|
+
private graph;
|
|
44
|
+
private graphOk;
|
|
45
|
+
private drive;
|
|
46
|
+
createSlot(slotId: string, filename: string): Promise<SlotHandle>;
|
|
47
|
+
stat(ref: string, filename: string): Promise<StoredObject | undefined>;
|
|
48
|
+
/** `/content` answers 302 to the same pre-authenticated URL; read it without following. */
|
|
49
|
+
private contentRedirect;
|
|
50
|
+
head(downloadUrl: string, bytes: number): Promise<Buffer>;
|
|
51
|
+
purge(ref: string): Promise<void>;
|
|
52
|
+
}
|
|
53
|
+
/** The configured backend, or a French explanation of why there is none. */
|
|
54
|
+
export declare function relayFromEnv(env?: NodeJS.ProcessEnv): RelayBackend | string;
|
|
55
|
+
export interface CreatedSlot {
|
|
56
|
+
uploadSlot: string;
|
|
57
|
+
uploadUrl: string;
|
|
58
|
+
expiresAt: string;
|
|
59
|
+
maxBytes: number;
|
|
60
|
+
filename: string;
|
|
61
|
+
}
|
|
62
|
+
export interface ClaimedUpload {
|
|
63
|
+
downloadUrl: string;
|
|
64
|
+
filename: string;
|
|
65
|
+
contentType: string;
|
|
66
|
+
size: number;
|
|
67
|
+
/** Purge the stored object; call once BoondManager has fetched it. */
|
|
68
|
+
release: () => Promise<void>;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* In-memory registry of open slots. Single-use: a claimed slot is removed
|
|
72
|
+
* before BoondManager is called, so a retry needs a new slot. Expired slots
|
|
73
|
+
* are purged opportunistically whenever a new one is created.
|
|
74
|
+
*/
|
|
75
|
+
export declare class UploadRelay {
|
|
76
|
+
private readonly env;
|
|
77
|
+
private readonly slots;
|
|
78
|
+
private cachedBackend?;
|
|
79
|
+
constructor(env?: NodeJS.ProcessEnv);
|
|
80
|
+
private backend;
|
|
81
|
+
private maxBytes;
|
|
82
|
+
private sweep;
|
|
83
|
+
open(filename: string): Promise<CreatedSlot>;
|
|
84
|
+
claim(uploadSlot: string): Promise<ClaimedUpload>;
|
|
85
|
+
}
|
|
86
|
+
/** Process-wide registry (one server process = one set of slots). */
|
|
87
|
+
export declare const uploadRelay: UploadRelay;
|
|
88
|
+
//# sourceMappingURL=upload-relay.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"upload-relay.d.ts","sourceRoot":"","sources":["../../src/services/upload-relay.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Upload relay for files that live neither at a public URL nor on the
|
|
3
|
+
* server's host — typically a conversation attachment, which sits in the MCP
|
|
4
|
+
* client's code-execution sandbox. The model cannot re-emit those bytes
|
|
5
|
+
* reliably (see `fileContent`), so they must travel outside its output:
|
|
6
|
+
*
|
|
7
|
+
* 1. `boond_documents_upload_slot` asks the relay for a single-use,
|
|
8
|
+
* pre-authenticated upload URL on an object store the operator controls;
|
|
9
|
+
* 2. the client's sandbox PUTs the file there (`curl`), bytes never passing
|
|
10
|
+
* through the model;
|
|
11
|
+
* 3. `boond_documents_create({ uploadSlot })` checks the stored file (size,
|
|
12
|
+
* magic bytes), hands BoondManager a short-lived pre-authenticated
|
|
13
|
+
* download URL through the existing `fileUrl` mechanism, then purges the
|
|
14
|
+
* file — permanently, bypassing the recycle bin when the store allows.
|
|
15
|
+
*
|
|
16
|
+
* Store credentials stay on the server; the client only ever sees URLs scoped
|
|
17
|
+
* to one file and one slot. Off unless `BOOND_MCP_UPLOAD_RELAY` is set.
|
|
18
|
+
*
|
|
19
|
+
* Backends implement {@link RelayBackend}; `sharepoint` (Microsoft Graph,
|
|
20
|
+
* app-only `Sites.Selected`) is the first.
|
|
21
|
+
*/
|
|
22
|
+
import { randomUUID } from "node:crypto";
|
|
23
|
+
import { extname } from "node:path";
|
|
24
|
+
import { readPositiveInt, readString } from "../config/env.js";
|
|
25
|
+
import { DEFAULT_UPLOAD_MAX_BYTES } from "../constants.js";
|
|
26
|
+
import { sniffDocumentType, UploadRejectedError } from "./upload-source.js";
|
|
27
|
+
/** How long a slot stays usable after creation. */
|
|
28
|
+
export const SLOT_TTL_MS = 15 * 60 * 1000;
|
|
29
|
+
const GRAPH = "https://graph.microsoft.com/v1.0";
|
|
30
|
+
const REQUEST_TIMEOUT_MS = 30_000;
|
|
31
|
+
/** Accept `https://host/sites/x`, `host/sites/x` or `host:/sites/x`. */
|
|
32
|
+
export function normalizeSiteKey(raw) {
|
|
33
|
+
const s = raw
|
|
34
|
+
.trim()
|
|
35
|
+
.replace(/^https?:\/\//i, "")
|
|
36
|
+
.replace(/\/+$/, "");
|
|
37
|
+
if (s.includes(":/"))
|
|
38
|
+
return s;
|
|
39
|
+
const slash = s.indexOf("/");
|
|
40
|
+
return slash === -1 ? s : `${s.slice(0, slash)}:${s.slice(slash)}`;
|
|
41
|
+
}
|
|
42
|
+
/** SharePoint forbids `" * : < > ? / \ |` and `#`/`%` cause URL trouble. */
|
|
43
|
+
export function sanitizeSharePointName(filename) {
|
|
44
|
+
const cleaned = filename
|
|
45
|
+
.replace(/["*:<>?/\\|#%]/g, "_")
|
|
46
|
+
.split("")
|
|
47
|
+
.filter((c) => c.charCodeAt(0) >= 0x20)
|
|
48
|
+
.join("")
|
|
49
|
+
.trim()
|
|
50
|
+
.replace(/^\.+|\.+$/g, "");
|
|
51
|
+
return (cleaned.length > 0 ? cleaned : "document").slice(0, 200);
|
|
52
|
+
}
|
|
53
|
+
export class SharePointRelay {
|
|
54
|
+
cfg;
|
|
55
|
+
name = "sharepoint";
|
|
56
|
+
token;
|
|
57
|
+
driveId;
|
|
58
|
+
constructor(cfg) {
|
|
59
|
+
this.cfg = cfg;
|
|
60
|
+
}
|
|
61
|
+
async accessToken() {
|
|
62
|
+
if (this.token && this.token.expiresAt > Date.now() + 60_000)
|
|
63
|
+
return this.token.value;
|
|
64
|
+
const body = new URLSearchParams({
|
|
65
|
+
grant_type: "client_credentials",
|
|
66
|
+
client_id: this.cfg.clientId,
|
|
67
|
+
client_secret: this.cfg.clientSecret,
|
|
68
|
+
scope: "https://graph.microsoft.com/.default",
|
|
69
|
+
});
|
|
70
|
+
const res = await fetch(`https://login.microsoftonline.com/${encodeURIComponent(this.cfg.tenantId)}/oauth2/v2.0/token`, { method: "POST", body, signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) });
|
|
71
|
+
const json = (await res.json().catch(() => ({})));
|
|
72
|
+
if (!res.ok || !json.access_token) {
|
|
73
|
+
// Never echo the request (it carries the secret); the error code is enough.
|
|
74
|
+
throw new Error(`Relais SharePoint : authentification Entra ID refusée (${json.error ?? res.status}).`);
|
|
75
|
+
}
|
|
76
|
+
this.token = { value: json.access_token, expiresAt: Date.now() + (json.expires_in ?? 3600) * 1000 };
|
|
77
|
+
return this.token.value;
|
|
78
|
+
}
|
|
79
|
+
async graph(method, path, body) {
|
|
80
|
+
const res = await fetch(`${GRAPH}${path}`, {
|
|
81
|
+
method,
|
|
82
|
+
headers: {
|
|
83
|
+
Authorization: `Bearer ${await this.accessToken()}`,
|
|
84
|
+
...(body === undefined ? {} : { "Content-Type": "application/json" }),
|
|
85
|
+
},
|
|
86
|
+
body: body === undefined ? null : JSON.stringify(body),
|
|
87
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
88
|
+
});
|
|
89
|
+
const text = await res.text();
|
|
90
|
+
let json = {};
|
|
91
|
+
try {
|
|
92
|
+
if (text)
|
|
93
|
+
json = JSON.parse(text);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// Non-JSON error page (proxy, throttling HTML): the status says enough.
|
|
97
|
+
}
|
|
98
|
+
return { status: res.status, json };
|
|
99
|
+
}
|
|
100
|
+
async graphOk(method, path, body) {
|
|
101
|
+
const { status, json } = await this.graph(method, path, body);
|
|
102
|
+
if (status >= 400) {
|
|
103
|
+
const e = json.error;
|
|
104
|
+
throw new Error(`Relais SharePoint : ${method} ${path.split("?")[0]} → ${status} ${e?.code ?? ""} ${e?.message ?? ""}`.trim());
|
|
105
|
+
}
|
|
106
|
+
return json;
|
|
107
|
+
}
|
|
108
|
+
async drive() {
|
|
109
|
+
if (this.driveId)
|
|
110
|
+
return this.driveId;
|
|
111
|
+
const site = await this.graphOk("GET", `/sites/${this.cfg.site}?$select=id`);
|
|
112
|
+
const drives = await this.graphOk("GET", `/sites/${site.id}/drives?$select=id,name,webUrl`);
|
|
113
|
+
const wanted = this.cfg.library.toLowerCase();
|
|
114
|
+
const match = drives.value.find((d) => d.name.toLowerCase() === wanted || (d.webUrl ?? "").toLowerCase().endsWith(`/${wanted}`));
|
|
115
|
+
if (!match) {
|
|
116
|
+
const names = drives.value.map((d) => d.name).join(", ");
|
|
117
|
+
throw new Error(`Relais SharePoint : bibliothèque « ${this.cfg.library} » introuvable sur le site (trouvées : ${names}).`);
|
|
118
|
+
}
|
|
119
|
+
this.driveId = match.id;
|
|
120
|
+
return match.id;
|
|
121
|
+
}
|
|
122
|
+
async createSlot(slotId, filename) {
|
|
123
|
+
const driveId = await this.drive();
|
|
124
|
+
// One folder per slot keeps the file's own name intact — BoondManager
|
|
125
|
+
// names the document after the download's Content-Disposition.
|
|
126
|
+
const folder = await this.graphOk("POST", `/drives/${driveId}/root/children`, {
|
|
127
|
+
name: `mcp-${slotId}`,
|
|
128
|
+
folder: {},
|
|
129
|
+
"@microsoft.graph.conflictBehavior": "fail",
|
|
130
|
+
});
|
|
131
|
+
const name = encodeURIComponent(sanitizeSharePointName(filename));
|
|
132
|
+
const session = await this.graphOk("POST", `/drives/${driveId}/items/${folder.id}:/${name}:/createUploadSession`, { item: { "@microsoft.graph.conflictBehavior": "fail" } });
|
|
133
|
+
const graphExpiry = session.expirationDateTime ? Date.parse(session.expirationDateTime) : Infinity;
|
|
134
|
+
return {
|
|
135
|
+
ref: folder.id,
|
|
136
|
+
uploadUrl: session.uploadUrl,
|
|
137
|
+
expiresAt: new Date(Math.min(Date.now() + SLOT_TTL_MS, graphExpiry)),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
async stat(ref, filename) {
|
|
141
|
+
const driveId = await this.drive();
|
|
142
|
+
// List the slot's own folder instead of addressing the file by name: the
|
|
143
|
+
// folder is private to the slot, so whatever file it holds is the upload,
|
|
144
|
+
// and a name normalised differently by SharePoint cannot cause a miss.
|
|
145
|
+
// No $select: SharePoint does not reliably return the download-URL
|
|
146
|
+
// annotation under $select with app-only tokens.
|
|
147
|
+
const { status, json } = await this.graph("GET", `/drives/${driveId}/items/${ref}/children`);
|
|
148
|
+
if (status === 404) {
|
|
149
|
+
throw new UploadRejectedError("Dossier de transit du slot introuvable (supprimé à la main ?). Créer un nouveau slot avec `boond_documents_upload_slot`.");
|
|
150
|
+
}
|
|
151
|
+
if (status >= 400)
|
|
152
|
+
throw new Error(`Relais SharePoint : lecture du dossier de transit → ${status}.`);
|
|
153
|
+
const files = (json.value ?? []).filter((item) => item.file !== undefined);
|
|
154
|
+
if (files.length === 0)
|
|
155
|
+
return undefined;
|
|
156
|
+
const wanted = sanitizeSharePointName(filename).toLowerCase();
|
|
157
|
+
const item = files.find((f) => (f.name ?? "").toLowerCase() === wanted) ?? files[0];
|
|
158
|
+
if (item.size === undefined)
|
|
159
|
+
return undefined;
|
|
160
|
+
const downloadUrl = item["@microsoft.graph.downloadUrl"] ?? (await this.contentRedirect(driveId, item.id));
|
|
161
|
+
if (!downloadUrl) {
|
|
162
|
+
throw new Error(`Relais SharePoint : fichier déposé trouvé (${item.size} octets) mais Graph ne fournit pas d'URL de lecture temporaire.`);
|
|
163
|
+
}
|
|
164
|
+
return { size: item.size, downloadUrl };
|
|
165
|
+
}
|
|
166
|
+
/** `/content` answers 302 to the same pre-authenticated URL; read it without following. */
|
|
167
|
+
async contentRedirect(driveId, itemId) {
|
|
168
|
+
const res = await fetch(`${GRAPH}/drives/${driveId}/items/${itemId}/content`, {
|
|
169
|
+
headers: { Authorization: `Bearer ${await this.accessToken()}` },
|
|
170
|
+
redirect: "manual",
|
|
171
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
172
|
+
});
|
|
173
|
+
await res.body?.cancel().catch(() => undefined);
|
|
174
|
+
return res.status >= 300 && res.status < 400 ? (res.headers.get("location") ?? undefined) : undefined;
|
|
175
|
+
}
|
|
176
|
+
async head(downloadUrl, bytes) {
|
|
177
|
+
const res = await fetch(downloadUrl, {
|
|
178
|
+
headers: { Range: `bytes=0-${bytes - 1}` },
|
|
179
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
180
|
+
});
|
|
181
|
+
if (!res.ok)
|
|
182
|
+
throw new Error(`Relais SharePoint : lecture de l'en-tête du fichier → ${res.status}.`);
|
|
183
|
+
return Buffer.from(await res.arrayBuffer()).subarray(0, bytes);
|
|
184
|
+
}
|
|
185
|
+
async purge(ref) {
|
|
186
|
+
const driveId = await this.drive();
|
|
187
|
+
// permanentDelete skips the recycle bin (RGPD: CVs, payslips...). Fall
|
|
188
|
+
// back to a regular delete if the tenant refuses it.
|
|
189
|
+
const { status } = await this.graph("POST", `/drives/${driveId}/items/${ref}/permanentDelete`);
|
|
190
|
+
if (status < 400 || status === 404)
|
|
191
|
+
return;
|
|
192
|
+
const del = await this.graph("DELETE", `/drives/${driveId}/items/${ref}`);
|
|
193
|
+
if (del.status >= 400 && del.status !== 404) {
|
|
194
|
+
throw new Error(`Relais SharePoint : suppression du dossier de transit → ${del.status}.`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
// Configuration and slot registry
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
/** The configured backend, or a French explanation of why there is none. */
|
|
202
|
+
export function relayFromEnv(env = process.env) {
|
|
203
|
+
const kind = (readString("BOOND_MCP_UPLOAD_RELAY", env) ?? "").toLowerCase();
|
|
204
|
+
if (kind === "") {
|
|
205
|
+
return "Relais d'upload désactivé : l'opérateur du serveur doit définir `BOOND_MCP_UPLOAD_RELAY` (valeur : `sharepoint`).";
|
|
206
|
+
}
|
|
207
|
+
if (kind !== "sharepoint") {
|
|
208
|
+
return `Relais d'upload inconnu : « ${kind} » (valeur acceptée : \`sharepoint\`).`;
|
|
209
|
+
}
|
|
210
|
+
const required = {
|
|
211
|
+
tenantId: "BOOND_MCP_SHAREPOINT_TENANT_ID",
|
|
212
|
+
clientId: "BOOND_MCP_SHAREPOINT_CLIENT_ID",
|
|
213
|
+
clientSecret: "BOOND_MCP_SHAREPOINT_CLIENT_SECRET",
|
|
214
|
+
site: "BOOND_MCP_SHAREPOINT_SITE",
|
|
215
|
+
};
|
|
216
|
+
const missing = Object.values(required).filter((name) => !readString(name, env));
|
|
217
|
+
if (missing.length > 0) {
|
|
218
|
+
return `Relais SharePoint incomplet : variable(s) manquante(s) ${missing.map((m) => `\`${m}\``).join(", ")}.`;
|
|
219
|
+
}
|
|
220
|
+
return new SharePointRelay({
|
|
221
|
+
tenantId: readString(required.tenantId, env),
|
|
222
|
+
clientId: readString(required.clientId, env),
|
|
223
|
+
clientSecret: readString(required.clientSecret, env),
|
|
224
|
+
site: normalizeSiteKey(readString(required.site, env)),
|
|
225
|
+
library: readString("BOOND_MCP_SHAREPOINT_LIBRARY", env) ?? "Documents",
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* In-memory registry of open slots. Single-use: a claimed slot is removed
|
|
230
|
+
* before BoondManager is called, so a retry needs a new slot. Expired slots
|
|
231
|
+
* are purged opportunistically whenever a new one is created.
|
|
232
|
+
*/
|
|
233
|
+
export class UploadRelay {
|
|
234
|
+
env;
|
|
235
|
+
slots = new Map();
|
|
236
|
+
cachedBackend;
|
|
237
|
+
constructor(env = process.env) {
|
|
238
|
+
this.env = env;
|
|
239
|
+
}
|
|
240
|
+
backend() {
|
|
241
|
+
if (this.cachedBackend)
|
|
242
|
+
return this.cachedBackend;
|
|
243
|
+
const resolved = relayFromEnv(this.env);
|
|
244
|
+
if (typeof resolved === "string")
|
|
245
|
+
throw new UploadRejectedError(resolved);
|
|
246
|
+
this.cachedBackend = resolved;
|
|
247
|
+
return resolved;
|
|
248
|
+
}
|
|
249
|
+
maxBytes() {
|
|
250
|
+
return readPositiveInt("BOOND_MCP_UPLOAD_MAX_BYTES", DEFAULT_UPLOAD_MAX_BYTES, this.env);
|
|
251
|
+
}
|
|
252
|
+
async sweep() {
|
|
253
|
+
const now = Date.now();
|
|
254
|
+
for (const [id, slot] of this.slots) {
|
|
255
|
+
if (slot.handle.expiresAt.getTime() <= now) {
|
|
256
|
+
this.slots.delete(id);
|
|
257
|
+
await slot.backend.purge(slot.handle.ref).catch(() => undefined);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
async open(filename) {
|
|
262
|
+
const backend = this.backend();
|
|
263
|
+
const ext = extname(filename).toLowerCase();
|
|
264
|
+
if (ext === "") {
|
|
265
|
+
throw new UploadRejectedError("`fileName` doit comporter une extension (ex. « cv.pdf »).");
|
|
266
|
+
}
|
|
267
|
+
await this.sweep();
|
|
268
|
+
const uploadSlot = randomUUID();
|
|
269
|
+
const clean = sanitizeSharePointName(filename);
|
|
270
|
+
const handle = await backend.createSlot(uploadSlot, clean);
|
|
271
|
+
this.slots.set(uploadSlot, { backend, handle, filename: clean });
|
|
272
|
+
return {
|
|
273
|
+
uploadSlot,
|
|
274
|
+
uploadUrl: handle.uploadUrl,
|
|
275
|
+
expiresAt: handle.expiresAt.toISOString(),
|
|
276
|
+
maxBytes: this.maxBytes(),
|
|
277
|
+
filename: clean,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
async claim(uploadSlot) {
|
|
281
|
+
const slot = this.slots.get(uploadSlot);
|
|
282
|
+
if (!slot) {
|
|
283
|
+
throw new UploadRejectedError(`\`uploadSlot\` inconnu ou déjà utilisé : « ${uploadSlot} ». Créer un nouveau slot avec \`boond_documents_upload_slot\`.`);
|
|
284
|
+
}
|
|
285
|
+
const purge = () => slot.backend.purge(slot.handle.ref);
|
|
286
|
+
if (slot.handle.expiresAt.getTime() <= Date.now()) {
|
|
287
|
+
this.slots.delete(uploadSlot);
|
|
288
|
+
await purge().catch(() => undefined);
|
|
289
|
+
throw new UploadRejectedError("Slot d'upload expiré. Créer un nouveau slot avec `boond_documents_upload_slot`.");
|
|
290
|
+
}
|
|
291
|
+
const stored = await slot.backend.stat(slot.handle.ref, slot.filename);
|
|
292
|
+
if (!stored) {
|
|
293
|
+
// Not consumed: the upload may still be in flight.
|
|
294
|
+
throw new UploadRejectedError("Aucun fichier dans le dossier de transit de ce slot : le dépôt n'est pas terminé ou n'a pas eu lieu. " +
|
|
295
|
+
"Exécuter la commande d'upload (réponse HTTP 200/201 attendue), puis réessayer.");
|
|
296
|
+
}
|
|
297
|
+
this.slots.delete(uploadSlot);
|
|
298
|
+
try {
|
|
299
|
+
const max = this.maxBytes();
|
|
300
|
+
if (stored.size > max) {
|
|
301
|
+
throw new UploadRejectedError(`Fichier trop volumineux : ${(stored.size / 1024 / 1024).toFixed(1)} Mo (max ${(max / 1024 / 1024).toFixed(1)} Mo).`);
|
|
302
|
+
}
|
|
303
|
+
const head = await slot.backend.head(stored.downloadUrl, 16);
|
|
304
|
+
const sniffed = sniffDocumentType(head, slot.filename);
|
|
305
|
+
if (!sniffed || !sniffed.extensions.includes(extname(slot.filename).toLowerCase())) {
|
|
306
|
+
throw new UploadRejectedError(`Format non accepté pour « ${slot.filename} » : le contenu déposé ne correspond pas à un format autorisé ` +
|
|
307
|
+
"ou à son extension.");
|
|
308
|
+
}
|
|
309
|
+
return {
|
|
310
|
+
downloadUrl: stored.downloadUrl,
|
|
311
|
+
filename: slot.filename,
|
|
312
|
+
contentType: sniffed.contentType,
|
|
313
|
+
size: stored.size,
|
|
314
|
+
release: purge,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
catch (error) {
|
|
318
|
+
await purge().catch(() => undefined);
|
|
319
|
+
throw error;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
/** Process-wide registry (one server process = one set of slots). */
|
|
324
|
+
export const uploadRelay = new UploadRelay();
|
|
325
|
+
//# sourceMappingURL=upload-relay.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"upload-relay.js","sourceRoot":"","sources":["../../src/services/upload-relay.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export interface UploadFile {
|
|
2
|
+
data: Buffer;
|
|
3
|
+
filename: string;
|
|
4
|
+
contentType: string;
|
|
5
|
+
}
|
|
6
|
+
/** A refusal whose message is meant for the caller, not a crash. */
|
|
7
|
+
export declare class UploadRejectedError extends Error {
|
|
8
|
+
constructor(message: string);
|
|
9
|
+
}
|
|
10
|
+
interface SniffedType {
|
|
11
|
+
contentType: string;
|
|
12
|
+
/** Extensions a file of this family may carry (lower case, with dot). */
|
|
13
|
+
extensions: readonly string[];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The document type of `data`, from its leading bytes. ZIP and OLE containers
|
|
17
|
+
* are ambiguous by content alone (a .docx and a .xlsx share a signature), so
|
|
18
|
+
* for those two families the extension picks the member — but only among
|
|
19
|
+
* formats of that container: a ZIP named `.pdf` is still refused.
|
|
20
|
+
*/
|
|
21
|
+
export declare function sniffDocumentType(data: Buffer, filename: string): SniffedType | undefined;
|
|
22
|
+
/**
|
|
23
|
+
* Read a local file for upload, enforcing the operator's policy: stdio
|
|
24
|
+
* transport, an allowed directory (after symlink resolution), a regular file,
|
|
25
|
+
* the size ceiling, and an accepted document format.
|
|
26
|
+
*/
|
|
27
|
+
export declare function readLocalUpload(filePath: string, env?: NodeJS.ProcessEnv): Promise<UploadFile>;
|
|
28
|
+
/** Decode base64 content passed inline, with an optional `data:` URI prefix. */
|
|
29
|
+
export declare function decodeInlineUpload(fileContent: string, fileName: string): UploadFile;
|
|
30
|
+
export {};
|
|
31
|
+
//# sourceMappingURL=upload-source.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"upload-source.d.ts","sourceRoot":"","sources":["../../src/services/upload-source.ts"],"names":[],"mappings":"AA0BA,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,oEAAoE;AACpE,qBAAa,mBAAoB,SAAQ,KAAK;gBAChC,OAAO,EAAE,MAAM;CAI5B;AAED,UAAU,WAAW;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,yEAAyE;IACzE,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;CAC/B;AAqBD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CAmBzF;AA8CD;;;;GAIG;AACH,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,OAAO,CAAC,UAAU,CAAC,CA2CjH;AAID,gFAAgF;AAChF,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,UAAU,CAkBpF"}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File sources for `boond_documents_create` beyond `fileUrl`.
|
|
3
|
+
*
|
|
4
|
+
* Until this module, the only way to attach a document was a public https URL
|
|
5
|
+
* that BoondManager downloads itself, and the server never read a byte of file
|
|
6
|
+
* content. That posture stays the default: both sources below are bounded, and
|
|
7
|
+
* the one that touches the disk is **off unless the operator opts in**.
|
|
8
|
+
*
|
|
9
|
+
* - `filePath` (stdio only, opt-in): the server reads a local file, but only
|
|
10
|
+
* under a directory listed in `BOOND_MCP_UPLOAD_DIRS`. The check runs on the
|
|
11
|
+
* `realpath` of both sides, so `..` segments and symlinks pointing outside an
|
|
12
|
+
* allowed directory are refused. Over HTTP the server does not run on the
|
|
13
|
+
* user's machine, so a path would name a file on the *server* host — refused.
|
|
14
|
+
* - `fileContent` (base64, every transport, always on): no disk access at all,
|
|
15
|
+
* capped at {@link INLINE_UPLOAD_MAX_BYTES} because every byte is paid ~1.33×
|
|
16
|
+
* in the model's output tokens.
|
|
17
|
+
*
|
|
18
|
+
* Both sources are sniffed by magic bytes, not by extension, and must match a
|
|
19
|
+
* short allow-list of document formats: the declared name decides nothing.
|
|
20
|
+
*/
|
|
21
|
+
import { readFile, realpath, stat } from "node:fs/promises";
|
|
22
|
+
import { basename, extname, isAbsolute, sep } from "node:path";
|
|
23
|
+
import { readCsv, readPositiveInt } from "../config/env.js";
|
|
24
|
+
import { resolveTransport } from "../config/transport-kind.js";
|
|
25
|
+
import { DEFAULT_UPLOAD_MAX_BYTES, INLINE_UPLOAD_MAX_BYTES } from "../constants.js";
|
|
26
|
+
/** A refusal whose message is meant for the caller, not a crash. */
|
|
27
|
+
export class UploadRejectedError extends Error {
|
|
28
|
+
constructor(message) {
|
|
29
|
+
super(message);
|
|
30
|
+
this.name = "UploadRejectedError";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const OOXML = {
|
|
34
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
35
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
36
|
+
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
37
|
+
".odt": "application/vnd.oasis.opendocument.text",
|
|
38
|
+
".ods": "application/vnd.oasis.opendocument.spreadsheet",
|
|
39
|
+
};
|
|
40
|
+
const LEGACY_OFFICE = {
|
|
41
|
+
".doc": "application/msword",
|
|
42
|
+
".xls": "application/vnd.ms-excel",
|
|
43
|
+
".ppt": "application/vnd.ms-powerpoint",
|
|
44
|
+
};
|
|
45
|
+
function startsWith(data, bytes, offset = 0) {
|
|
46
|
+
if (data.length < offset + bytes.length)
|
|
47
|
+
return false;
|
|
48
|
+
return bytes.every((b, i) => data[offset + i] === b);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The document type of `data`, from its leading bytes. ZIP and OLE containers
|
|
52
|
+
* are ambiguous by content alone (a .docx and a .xlsx share a signature), so
|
|
53
|
+
* for those two families the extension picks the member — but only among
|
|
54
|
+
* formats of that container: a ZIP named `.pdf` is still refused.
|
|
55
|
+
*/
|
|
56
|
+
export function sniffDocumentType(data, filename) {
|
|
57
|
+
const ext = extname(filename).toLowerCase();
|
|
58
|
+
if (startsWith(data, [0x25, 0x50, 0x44, 0x46, 0x2d]))
|
|
59
|
+
return { contentType: "application/pdf", extensions: [".pdf"] };
|
|
60
|
+
if (startsWith(data, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]))
|
|
61
|
+
return { contentType: "image/png", extensions: [".png"] };
|
|
62
|
+
if (startsWith(data, [0xff, 0xd8, 0xff]))
|
|
63
|
+
return { contentType: "image/jpeg", extensions: [".jpg", ".jpeg"] };
|
|
64
|
+
if (startsWith(data, [0x47, 0x49, 0x46, 0x38]))
|
|
65
|
+
return { contentType: "image/gif", extensions: [".gif"] };
|
|
66
|
+
if (startsWith(data, [0x52, 0x49, 0x46, 0x46]) && startsWith(data, [0x57, 0x45, 0x42, 0x50], 8))
|
|
67
|
+
return { contentType: "image/webp", extensions: [".webp"] };
|
|
68
|
+
if (startsWith(data, [0x7b, 0x5c, 0x72, 0x74, 0x66]))
|
|
69
|
+
return { contentType: "application/rtf", extensions: [".rtf"] };
|
|
70
|
+
if (startsWith(data, [0x50, 0x4b, 0x03, 0x04])) {
|
|
71
|
+
const contentType = OOXML[ext];
|
|
72
|
+
return contentType ? { contentType, extensions: [ext] } : undefined;
|
|
73
|
+
}
|
|
74
|
+
if (startsWith(data, [0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1])) {
|
|
75
|
+
const contentType = LEGACY_OFFICE[ext];
|
|
76
|
+
return contentType ? { contentType, extensions: [ext] } : undefined;
|
|
77
|
+
}
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
const ACCEPTED_FORMATS = "PDF, DOCX/XLSX/PPTX, ODT/ODS, DOC/XLS/PPT, RTF, PNG, JPEG, GIF, WebP";
|
|
81
|
+
function typeOrReject(data, filename) {
|
|
82
|
+
const sniffed = sniffDocumentType(data, filename);
|
|
83
|
+
if (!sniffed) {
|
|
84
|
+
throw new UploadRejectedError(`Format non accepté pour « ${filename} » : le contenu ne correspond à aucun format autorisé (${ACCEPTED_FORMATS}). ` +
|
|
85
|
+
"Le type est déterminé par les premiers octets du fichier, pas par son extension.");
|
|
86
|
+
}
|
|
87
|
+
if (!sniffed.extensions.includes(extname(filename).toLowerCase())) {
|
|
88
|
+
throw new UploadRejectedError(`Extension incohérente pour « ${filename} » : le contenu est de type ${sniffed.contentType} ` +
|
|
89
|
+
`(extension attendue : ${sniffed.extensions.join(" ou ")}).`);
|
|
90
|
+
}
|
|
91
|
+
return sniffed.contentType;
|
|
92
|
+
}
|
|
93
|
+
function mb(bytes) {
|
|
94
|
+
return (bytes / 1024 / 1024).toFixed(1);
|
|
95
|
+
}
|
|
96
|
+
/** The allowed upload directories, resolved to real paths. Missing ones are skipped. */
|
|
97
|
+
async function allowedDirs(env) {
|
|
98
|
+
const raw = readCsv("BOOND_MCP_UPLOAD_DIRS", env);
|
|
99
|
+
if (!raw)
|
|
100
|
+
return undefined;
|
|
101
|
+
const resolved = [];
|
|
102
|
+
for (const dir of raw) {
|
|
103
|
+
if (!isAbsolute(dir))
|
|
104
|
+
continue;
|
|
105
|
+
try {
|
|
106
|
+
resolved.push(await realpath(dir));
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// A listed directory that does not exist allows nothing; not fatal.
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return resolved;
|
|
113
|
+
}
|
|
114
|
+
function isInside(file, dir) {
|
|
115
|
+
const prefix = dir.endsWith(sep) ? dir : dir + sep;
|
|
116
|
+
return file.startsWith(prefix);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Read a local file for upload, enforcing the operator's policy: stdio
|
|
120
|
+
* transport, an allowed directory (after symlink resolution), a regular file,
|
|
121
|
+
* the size ceiling, and an accepted document format.
|
|
122
|
+
*/
|
|
123
|
+
export async function readLocalUpload(filePath, env = process.env) {
|
|
124
|
+
if (resolveTransport(env) === "http") {
|
|
125
|
+
throw new UploadRejectedError("`filePath` est indisponible en transport HTTP : le serveur ne tourne pas sur le poste de l'utilisateur. " +
|
|
126
|
+
"Utiliser `fileUrl`, ou `fileContent` (base64) pour un petit fichier.");
|
|
127
|
+
}
|
|
128
|
+
const dirs = await allowedDirs(env);
|
|
129
|
+
if (dirs === undefined) {
|
|
130
|
+
throw new UploadRejectedError("Téléversement de fichiers locaux désactivé. L'opérateur du serveur doit définir `BOOND_MCP_UPLOAD_DIRS` " +
|
|
131
|
+
"(répertoires autorisés, chemins absolus). En attendant : `fileUrl`, ou `fileContent` (base64) pour un petit fichier.");
|
|
132
|
+
}
|
|
133
|
+
if (!isAbsolute(filePath)) {
|
|
134
|
+
throw new UploadRejectedError(`\`filePath\` doit être un chemin absolu (reçu : « ${filePath} »).`);
|
|
135
|
+
}
|
|
136
|
+
let real;
|
|
137
|
+
try {
|
|
138
|
+
real = await realpath(filePath);
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
throw new UploadRejectedError(`Fichier introuvable : « ${filePath} ».`);
|
|
142
|
+
}
|
|
143
|
+
if (!dirs.some((dir) => isInside(real, dir))) {
|
|
144
|
+
throw new UploadRejectedError(`« ${filePath} » est hors des répertoires autorisés par \`BOOND_MCP_UPLOAD_DIRS\` (liens symboliques résolus).`);
|
|
145
|
+
}
|
|
146
|
+
const info = await stat(real);
|
|
147
|
+
if (!info.isFile())
|
|
148
|
+
throw new UploadRejectedError(`« ${filePath} » n'est pas un fichier.`);
|
|
149
|
+
const maxBytes = readPositiveInt("BOOND_MCP_UPLOAD_MAX_BYTES", DEFAULT_UPLOAD_MAX_BYTES, env);
|
|
150
|
+
if (info.size > maxBytes) {
|
|
151
|
+
throw new UploadRejectedError(`Fichier trop volumineux : ${mb(info.size)} Mo (max ${mb(maxBytes)} Mo, \`BOOND_MCP_UPLOAD_MAX_BYTES\`).`);
|
|
152
|
+
}
|
|
153
|
+
const data = await readFile(real);
|
|
154
|
+
// The file may have grown between stat() and readFile().
|
|
155
|
+
if (data.length > maxBytes) {
|
|
156
|
+
throw new UploadRejectedError(`Fichier trop volumineux : ${mb(data.length)} Mo (max ${mb(maxBytes)} Mo).`);
|
|
157
|
+
}
|
|
158
|
+
const filename = basename(real);
|
|
159
|
+
return { data, filename, contentType: typeOrReject(data, filename) };
|
|
160
|
+
}
|
|
161
|
+
const BASE64 = /^[A-Za-z0-9+/]*={0,2}$/;
|
|
162
|
+
/** Decode base64 content passed inline, with an optional `data:` URI prefix. */
|
|
163
|
+
export function decodeInlineUpload(fileContent, fileName) {
|
|
164
|
+
const filename = basename(fileName.replace(/\\/g, "/")).trim();
|
|
165
|
+
if (filename.length === 0 || filename === "." || filename === "..") {
|
|
166
|
+
throw new UploadRejectedError("`fileName` doit être un nom de fichier avec son extension (ex. « cv.pdf »).");
|
|
167
|
+
}
|
|
168
|
+
const payload = fileContent.replace(/^data:[^;,]*;base64,/, "").replace(/\s+/g, "");
|
|
169
|
+
if (payload.length === 0 || payload.length % 4 !== 0 || !BASE64.test(payload)) {
|
|
170
|
+
throw new UploadRejectedError("`fileContent` n'est pas du base64 valide.");
|
|
171
|
+
}
|
|
172
|
+
const decodedLength = (payload.length / 4) * 3 - (payload.endsWith("==") ? 2 : payload.endsWith("=") ? 1 : 0);
|
|
173
|
+
if (decodedLength > INLINE_UPLOAD_MAX_BYTES) {
|
|
174
|
+
throw new UploadRejectedError(`Contenu inline trop volumineux : ${mb(decodedLength)} Mo (max ${mb(INLINE_UPLOAD_MAX_BYTES)} Mo). ` +
|
|
175
|
+
"Pour un fichier plus gros : `filePath` (serveur local) ou `fileUrl`.");
|
|
176
|
+
}
|
|
177
|
+
const data = Buffer.from(payload, "base64");
|
|
178
|
+
return { data, filename, contentType: typeOrReject(data, filename) };
|
|
179
|
+
}
|
|
180
|
+
//# sourceMappingURL=upload-source.js.map
|