@opengeni/api-router 0.11.2 → 0.11.8
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/app.d.ts +3 -2
- package/dist/app.js +3 -1
- package/dist/{chunk-BFWSDESE.js → chunk-DQWFAIPE.js} +2309 -1660
- package/dist/chunk-DQWFAIPE.js.map +1 -0
- package/dist/index.js +21 -3
- package/dist/index.js.map +1 -1
- package/package.json +10 -10
- package/src/app.ts +108 -2
- package/src/github-access.ts +117 -9
- package/src/github-browser-flow.ts +4 -4
- package/src/http/auth.ts +2 -3
- package/src/index.ts +20 -1
- package/src/mcp/documents.ts +19 -5
- package/src/mcp/server.ts +38 -12
- package/src/routes/documents.ts +191 -9
- package/src/routes/files.ts +1 -1
- package/src/routes/github.ts +331 -23
- package/src/routes/sessions.ts +27 -0
- package/dist/chunk-BFWSDESE.js.map +0 -1
package/src/routes/documents.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
AddDocumentRequest,
|
|
3
|
+
CreateKnowledgeDropRequest,
|
|
3
4
|
CreateKnowledgeMemoryRequest,
|
|
4
5
|
CreateDocumentBaseRequest,
|
|
5
6
|
Document,
|
|
@@ -7,11 +8,14 @@ import {
|
|
|
7
8
|
DocumentSearchRequest,
|
|
8
9
|
KnowledgeMemory,
|
|
9
10
|
KnowledgeMemorySearchRequest,
|
|
11
|
+
MoveDocumentRequest,
|
|
10
12
|
UpdateKnowledgeMemoryRequest,
|
|
11
13
|
WorkspaceMemorySearchRequest,
|
|
12
14
|
WorkspaceMemorySearchResponse,
|
|
13
15
|
} from "@opengeni/contracts";
|
|
14
16
|
import {
|
|
17
|
+
completeFileUpload,
|
|
18
|
+
createFileUpload,
|
|
15
19
|
createKnowledgeMemory,
|
|
16
20
|
getKnowledgeMemory,
|
|
17
21
|
listKnowledgeMemories,
|
|
@@ -23,10 +27,12 @@ import {
|
|
|
23
27
|
addDocumentToBase,
|
|
24
28
|
createDocumentBase,
|
|
25
29
|
deleteDocumentFromBase,
|
|
30
|
+
ensureDefaultBase,
|
|
26
31
|
getDocument,
|
|
27
32
|
getDocumentBase,
|
|
28
33
|
listDocumentBases,
|
|
29
34
|
listDocuments,
|
|
35
|
+
moveDocumentToBase,
|
|
30
36
|
queueDocumentForReindex,
|
|
31
37
|
searchDocuments,
|
|
32
38
|
} from "@opengeni/documents";
|
|
@@ -37,6 +43,7 @@ import { requireAccessGrant } from "@opengeni/core";
|
|
|
37
43
|
import { recordWorkspaceUsage, requireLimit } from "@opengeni/core";
|
|
38
44
|
import type { ApiRouteDeps } from "@opengeni/core";
|
|
39
45
|
import { buildDocumentsMcpServer } from "../mcp/documents";
|
|
46
|
+
import { sanitizeFilename } from "./files";
|
|
40
47
|
|
|
41
48
|
export function registerDocumentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
42
49
|
const { db, objectStorage, documentIndexer, getDocumentServices } = deps;
|
|
@@ -90,6 +97,8 @@ export function registerDocumentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
90
97
|
accountId: grant.accountId,
|
|
91
98
|
workspaceId,
|
|
92
99
|
baseId: c.req.param("baseId"),
|
|
100
|
+
createdBy: grant.subjectId,
|
|
101
|
+
access: { viewerSubjectId: grant.subjectId },
|
|
93
102
|
});
|
|
94
103
|
const wasCreated =
|
|
95
104
|
document.status === "queued" && document.chunkCount === 0 && document.error === null;
|
|
@@ -122,11 +131,13 @@ export function registerDocumentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
122
131
|
|
|
123
132
|
app.get("/v1/workspaces/:workspaceId/document-bases/:baseId/documents", async (c) => {
|
|
124
133
|
const workspaceId = c.req.param("workspaceId");
|
|
125
|
-
await requireAccessGrant(c, deps, workspaceId, "documents:search");
|
|
134
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "documents:search");
|
|
126
135
|
return c.json(
|
|
127
|
-
(
|
|
128
|
-
|
|
129
|
-
|
|
136
|
+
(
|
|
137
|
+
await listDocuments(db, workspaceId, c.req.param("baseId"), {
|
|
138
|
+
viewerSubjectId: grant.subjectId,
|
|
139
|
+
})
|
|
140
|
+
).map((document) => Document.parse(document)),
|
|
130
141
|
);
|
|
131
142
|
});
|
|
132
143
|
|
|
@@ -141,9 +152,13 @@ export function registerDocumentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
141
152
|
workspaceId,
|
|
142
153
|
baseId: c.req.param("baseId"),
|
|
143
154
|
documentId: c.req.param("documentId"),
|
|
155
|
+
access: { viewerSubjectId: grant.subjectId },
|
|
144
156
|
});
|
|
145
157
|
return c.body(null, 204);
|
|
146
158
|
} catch (error) {
|
|
159
|
+
if (error instanceof HTTPException) {
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
147
162
|
throw documentHttpException(error);
|
|
148
163
|
}
|
|
149
164
|
},
|
|
@@ -164,7 +179,9 @@ export function registerDocumentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
164
179
|
quantity: 0,
|
|
165
180
|
});
|
|
166
181
|
try {
|
|
167
|
-
const document = await getDocument(db, workspaceId, c.req.param("documentId")
|
|
182
|
+
const document = await getDocument(db, workspaceId, c.req.param("documentId"), {
|
|
183
|
+
viewerSubjectId: grant.subjectId,
|
|
184
|
+
});
|
|
168
185
|
if (!document) {
|
|
169
186
|
throw new HTTPException(404, { message: "document not found" });
|
|
170
187
|
}
|
|
@@ -174,7 +191,9 @@ export function registerDocumentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
174
191
|
if (document.baseId !== c.req.param("baseId")) {
|
|
175
192
|
throw new HTTPException(404, { message: "document not found" });
|
|
176
193
|
}
|
|
177
|
-
const queued = await queueDocumentForReindex(db, workspaceId, document.id
|
|
194
|
+
const queued = await queueDocumentForReindex(db, workspaceId, document.id, {
|
|
195
|
+
viewerSubjectId: grant.subjectId,
|
|
196
|
+
});
|
|
178
197
|
const indexed =
|
|
179
198
|
(await documentIndexer.indexDocument({
|
|
180
199
|
accountId: grant.accountId,
|
|
@@ -206,7 +225,7 @@ export function registerDocumentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
206
225
|
|
|
207
226
|
app.post("/v1/workspaces/:workspaceId/document-bases/:baseId/search", async (c) => {
|
|
208
227
|
const workspaceId = c.req.param("workspaceId");
|
|
209
|
-
await requireAccessGrant(c, deps, workspaceId, "documents:search");
|
|
228
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "documents:search");
|
|
210
229
|
const payload = DocumentSearchRequest.parse(await c.req.json());
|
|
211
230
|
const base = await getDocumentBase(db, workspaceId, c.req.param("baseId"));
|
|
212
231
|
if (!base) {
|
|
@@ -223,6 +242,7 @@ export function registerDocumentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
223
242
|
mode: payload.mode,
|
|
224
243
|
sourceKinds: payload.sourceKinds,
|
|
225
244
|
aclTags: payload.aclTags,
|
|
245
|
+
access: { viewerSubjectId: grant.subjectId },
|
|
226
246
|
},
|
|
227
247
|
getDocumentServices(),
|
|
228
248
|
),
|
|
@@ -231,7 +251,7 @@ export function registerDocumentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
231
251
|
|
|
232
252
|
app.post("/v1/workspaces/:workspaceId/knowledge/search", async (c) => {
|
|
233
253
|
const workspaceId = c.req.param("workspaceId");
|
|
234
|
-
await requireAccessGrant(c, deps, workspaceId, "documents:search");
|
|
254
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "documents:search");
|
|
235
255
|
const payload = DocumentSearchRequest.parse(await c.req.json());
|
|
236
256
|
return c.json({
|
|
237
257
|
results: await searchDocuments(
|
|
@@ -244,12 +264,162 @@ export function registerDocumentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
244
264
|
mode: payload.mode,
|
|
245
265
|
sourceKinds: payload.sourceKinds,
|
|
246
266
|
aclTags: payload.aclTags,
|
|
267
|
+
access: { viewerSubjectId: grant.subjectId },
|
|
247
268
|
},
|
|
248
269
|
getDocumentServices(),
|
|
249
270
|
),
|
|
250
271
|
});
|
|
251
272
|
});
|
|
252
273
|
|
|
274
|
+
// Knowledge drop: raw text or an uploaded file, no metadata required. Lands
|
|
275
|
+
// in the workspace Default base with curationStatus 'pending'; indexing then
|
|
276
|
+
// applies the configured curation provider, or leaves it as 'none' when
|
|
277
|
+
// curation is disabled.
|
|
278
|
+
app.post("/v1/workspaces/:workspaceId/knowledge/drops", async (c) => {
|
|
279
|
+
const workspaceId = c.req.param("workspaceId");
|
|
280
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "documents:manage");
|
|
281
|
+
if (!objectStorage) {
|
|
282
|
+
throw new HTTPException(503, { message: "object storage is not configured" });
|
|
283
|
+
}
|
|
284
|
+
await requireLimit(deps, {
|
|
285
|
+
accountId: grant.accountId,
|
|
286
|
+
workspaceId,
|
|
287
|
+
action: "document:index",
|
|
288
|
+
quantity: 0,
|
|
289
|
+
});
|
|
290
|
+
const payload = CreateKnowledgeDropRequest.parse(await c.req.json());
|
|
291
|
+
try {
|
|
292
|
+
let fileId: string;
|
|
293
|
+
if (payload.text !== undefined) {
|
|
294
|
+
const bytes = new TextEncoder().encode(payload.text);
|
|
295
|
+
await requireLimit(deps, {
|
|
296
|
+
accountId: grant.accountId,
|
|
297
|
+
workspaceId,
|
|
298
|
+
action: "file:upload",
|
|
299
|
+
quantity: bytes.length,
|
|
300
|
+
});
|
|
301
|
+
if (bytes.length > objectStorage.maxSinglePutSizeBytes) {
|
|
302
|
+
throw new HTTPException(413, {
|
|
303
|
+
message: `drop exceeds single PUT limit of ${objectStorage.maxSinglePutSizeBytes} bytes`,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
const filename = dropFilename(payload.filename ?? payload.title);
|
|
307
|
+
const newFileId = crypto.randomUUID();
|
|
308
|
+
const safeFilename = sanitizeFilename(filename);
|
|
309
|
+
const objectKey = `workspaces/${workspaceId}/files/${newFileId}/original/${safeFilename}`;
|
|
310
|
+
const upload = await createFileUpload(db, {
|
|
311
|
+
accountId: grant.accountId,
|
|
312
|
+
workspaceId,
|
|
313
|
+
fileId: newFileId,
|
|
314
|
+
filename,
|
|
315
|
+
safeFilename,
|
|
316
|
+
contentType: "text/plain; charset=utf-8",
|
|
317
|
+
sizeBytes: bytes.length,
|
|
318
|
+
sha256: null,
|
|
319
|
+
bucket: objectStorage.bucket,
|
|
320
|
+
objectKey,
|
|
321
|
+
expiresAt: new Date(Date.now() + 15 * 60 * 1000),
|
|
322
|
+
});
|
|
323
|
+
await objectStorage.putObject({
|
|
324
|
+
key: objectKey,
|
|
325
|
+
contentType: "text/plain; charset=utf-8",
|
|
326
|
+
body: bytes,
|
|
327
|
+
});
|
|
328
|
+
const file = await completeFileUpload(db, workspaceId, upload.uploadId);
|
|
329
|
+
await recordWorkspaceUsage(deps, {
|
|
330
|
+
accountId: grant.accountId,
|
|
331
|
+
workspaceId,
|
|
332
|
+
subjectId: grant.subjectId,
|
|
333
|
+
eventType: "file.uploaded",
|
|
334
|
+
quantity: file.sizeBytes,
|
|
335
|
+
unit: "byte",
|
|
336
|
+
sourceResourceType: "file",
|
|
337
|
+
sourceResourceId: file.id,
|
|
338
|
+
idempotencyKey: `file.uploaded:${workspaceId}:${file.id}`,
|
|
339
|
+
});
|
|
340
|
+
fileId = file.id;
|
|
341
|
+
} else {
|
|
342
|
+
fileId = payload.fileId as string;
|
|
343
|
+
}
|
|
344
|
+
const defaultBase = await ensureDefaultBase(db, {
|
|
345
|
+
accountId: grant.accountId,
|
|
346
|
+
workspaceId,
|
|
347
|
+
});
|
|
348
|
+
const document = await addDocumentToBase(db, {
|
|
349
|
+
fileId,
|
|
350
|
+
...(payload.title ? { title: payload.title } : {}),
|
|
351
|
+
...(payload.visibility ? { visibility: payload.visibility } : {}),
|
|
352
|
+
...(payload.agentAccess !== undefined ? { agentAccess: payload.agentAccess } : {}),
|
|
353
|
+
accountId: grant.accountId,
|
|
354
|
+
workspaceId,
|
|
355
|
+
baseId: defaultBase.id,
|
|
356
|
+
createdBy: grant.subjectId,
|
|
357
|
+
curationStatus: "pending",
|
|
358
|
+
access: { viewerSubjectId: grant.subjectId },
|
|
359
|
+
});
|
|
360
|
+
const wasCreated =
|
|
361
|
+
document.status === "queued" && document.chunkCount === 0 && document.error === null;
|
|
362
|
+
const indexed =
|
|
363
|
+
document.status === "ready"
|
|
364
|
+
? document
|
|
365
|
+
: ((await documentIndexer.indexDocument({
|
|
366
|
+
accountId: grant.accountId,
|
|
367
|
+
workspaceId,
|
|
368
|
+
documentId: document.id,
|
|
369
|
+
})) ?? document);
|
|
370
|
+
if (indexed.status === "ready") {
|
|
371
|
+
await recordWorkspaceUsage(deps, {
|
|
372
|
+
accountId: grant.accountId,
|
|
373
|
+
workspaceId,
|
|
374
|
+
subjectId: grant.subjectId,
|
|
375
|
+
eventType: "document.indexed",
|
|
376
|
+
quantity: indexed.chunkCount,
|
|
377
|
+
unit: "chunk",
|
|
378
|
+
sourceResourceType: "document",
|
|
379
|
+
sourceResourceId: indexed.id,
|
|
380
|
+
idempotencyKey: `document.indexed:${workspaceId}:${indexed.id}:${indexed.updatedAt}`,
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
return c.json(Document.parse(indexed), wasCreated ? 201 : 200);
|
|
384
|
+
} catch (error) {
|
|
385
|
+
if (error instanceof HTTPException) {
|
|
386
|
+
throw error;
|
|
387
|
+
}
|
|
388
|
+
throw documentHttpException(error);
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
// Apply a curation suggestion (no body target) or move to an explicit base.
|
|
393
|
+
app.post("/v1/workspaces/:workspaceId/documents/:documentId/move", async (c) => {
|
|
394
|
+
const workspaceId = c.req.param("workspaceId");
|
|
395
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "documents:manage");
|
|
396
|
+
const payload = MoveDocumentRequest.parse(await c.req.json().catch(() => ({})));
|
|
397
|
+
try {
|
|
398
|
+
const document = await getDocument(db, workspaceId, c.req.param("documentId"), {
|
|
399
|
+
viewerSubjectId: grant.subjectId,
|
|
400
|
+
});
|
|
401
|
+
if (!document) {
|
|
402
|
+
throw new HTTPException(404, { message: "document not found" });
|
|
403
|
+
}
|
|
404
|
+
return c.json(
|
|
405
|
+
Document.parse(
|
|
406
|
+
await moveDocumentToBase(db, {
|
|
407
|
+
accountId: grant.accountId,
|
|
408
|
+
workspaceId,
|
|
409
|
+
documentId: document.id,
|
|
410
|
+
targetBaseId: payload.targetBaseId ?? null,
|
|
411
|
+
access: { viewerSubjectId: grant.subjectId },
|
|
412
|
+
}),
|
|
413
|
+
),
|
|
414
|
+
);
|
|
415
|
+
} catch (error) {
|
|
416
|
+
if (error instanceof HTTPException) {
|
|
417
|
+
throw error;
|
|
418
|
+
}
|
|
419
|
+
throw documentHttpException(error);
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
|
|
253
423
|
app.get("/v1/workspaces/:workspaceId/knowledge/memories", async (c) => {
|
|
254
424
|
const workspaceId = c.req.param("workspaceId");
|
|
255
425
|
await requireAccessGrant(c, deps, workspaceId, "documents:search");
|
|
@@ -390,18 +560,30 @@ export function registerDocumentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
390
560
|
grant.accountId,
|
|
391
561
|
workspaceId,
|
|
392
562
|
getDocumentServices(),
|
|
393
|
-
{ createdBySessionId: sessionId },
|
|
563
|
+
{ createdBySessionId: sessionId, viewerSubjectId: grant.subjectId },
|
|
394
564
|
);
|
|
395
565
|
await server.connect(transport);
|
|
396
566
|
return await transport.handleRequest(c.req.raw);
|
|
397
567
|
});
|
|
398
568
|
}
|
|
399
569
|
|
|
570
|
+
/** Derive a .txt filename for a raw-text drop from its optional title/filename. */
|
|
571
|
+
function dropFilename(preferred: string | undefined): string {
|
|
572
|
+
const stem = (preferred ?? "").trim() || `note-${new Date().toISOString().slice(0, 10)}`;
|
|
573
|
+
return /\.[A-Za-z0-9]{1,8}$/.test(stem) ? stem : `${stem}.txt`;
|
|
574
|
+
}
|
|
575
|
+
|
|
400
576
|
function documentHttpException(error: unknown): HTTPException {
|
|
401
577
|
const message = error instanceof Error ? error.message : String(error);
|
|
402
578
|
if (message.includes("not found")) {
|
|
403
579
|
return new HTTPException(404, { message });
|
|
404
580
|
}
|
|
581
|
+
if (message.includes("already exists")) {
|
|
582
|
+
return new HTTPException(409, { message });
|
|
583
|
+
}
|
|
584
|
+
if (message.includes("no suggested base")) {
|
|
585
|
+
return new HTTPException(422, { message });
|
|
586
|
+
}
|
|
405
587
|
if (message.includes("pending") || message.includes("failed") || message.includes("deleted")) {
|
|
406
588
|
return new HTTPException(422, { message });
|
|
407
589
|
}
|
package/src/routes/files.ts
CHANGED
|
@@ -351,7 +351,7 @@ export function registerFileRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
351
351
|
});
|
|
352
352
|
}
|
|
353
353
|
|
|
354
|
-
function sanitizeFilename(filename: string): string {
|
|
354
|
+
export function sanitizeFilename(filename: string): string {
|
|
355
355
|
const trimmed = filename.trim().replace(/[/\\]/g, "_");
|
|
356
356
|
const safe = trimmed
|
|
357
357
|
.replace(/[^A-Za-z0-9._ -]+/g, "_")
|