@hiai-gg/docsmint 0.6.6 → 0.6.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/README.md CHANGED
@@ -40,7 +40,26 @@ server.
40
40
  - **Own the full stack**: application data, vectors, graph, queue, and files run
41
41
  on infrastructure you control.
42
42
 
43
- ## What's new in DocsMint 0.6.6?
43
+ ## What's new in DocsMint 0.6.8?
44
+
45
+ - **Workspace-safe recovery.** Explicit operator reindexing carries the
46
+ document's workspace into every durable pipeline stage, including RLS reads.
47
+ - **Consistent operator authentication.** Both documented admin key headers
48
+ bypass interactive rate limits during maintenance operations.
49
+
50
+ DocsMint 0.6.8 includes the self-host reliability fixes introduced in 0.6.7:
51
+
52
+ - **Reliable HTTPS sessions.** Every protected SvelteKit route accepts Better
53
+ Auth's secure production cookie as well as the local-development cookie.
54
+ - **Cleaner retrieval.** Deleted documents are excluded before exact, lexical,
55
+ fuzzy, vector, chunk, and graph ranking.
56
+ - **Explicit GraphRAG providers.** Entity extraction requires its own
57
+ chat-completion URL and never sends chat requests to an embedding endpoint.
58
+ - **Safer self-host defaults.** Auth origins are validated configuration, the
59
+ SeaweedFS Filer UI binds to loopback, folder moves refresh knowledge
60
+ metadata, and backups exclude `.env`.
61
+
62
+ DocsMint 0.6.7 includes the safe index recovery introduced in 0.6.6:
44
63
 
45
64
  - **Safe explicit index recovery.** Admin document reindexing keeps the active
46
65
  generation searchable until a new generation has passed embedding, graph,
@@ -77709,6 +77709,7 @@ var init_config_schema = __esm(() => {
77709
77709
  WEBHOOK_SECRET: exports_external2.string().min(1, "WEBHOOK_SECRET must not be empty").default("change-me-to-random-32-chars").refine((val) => val !== "change-me-to-random-32-chars", "WEBHOOK_SECRET must be set in production"),
77710
77710
  BETTER_AUTH_URL: exports_external2.string().default("http://localhost:50700"),
77711
77711
  CORS_ORIGINS: exports_external2.string().optional(),
77712
+ TRUSTED_ORIGINS: exports_external2.string().optional(),
77712
77713
  WEB_PORT: exports_external2.coerce.number().default(50701),
77713
77714
  EMBEDDING_BASE_URL: exports_external2.string().optional(),
77714
77715
  EMBEDDING_API_KEY: exports_external2.string().optional(),
@@ -192960,7 +192961,7 @@ var auth = betterAuth({
192960
192961
  }),
192961
192962
  secret: config3.BETTER_AUTH_SECRET,
192962
192963
  baseURL: config3.BETTER_AUTH_URL,
192963
- trustedOrigins: process.env.TRUSTED_ORIGINS ? process.env.TRUSTED_ORIGINS.split(",").map((s2) => s2.trim()) : ["http://localhost:50701", "http://127.0.0.1:50701"],
192964
+ trustedOrigins: config3.TRUSTED_ORIGINS ? config3.TRUSTED_ORIGINS.split(",").map((origin) => origin.trim()).filter(Boolean) : ["http://localhost:50701", "http://127.0.0.1:50701"],
192964
192965
  emailAndPassword: {
192965
192966
  enabled: true
192966
192967
  },
@@ -193195,11 +193196,14 @@ function createRateLimiter(config4, redisClient) {
193195
193196
  }
193196
193197
 
193197
193198
  // ../../backend/src/api/middleware/rate-limit.ts
193198
- function isInternalRequest(request) {
193199
+ function isInternalRequest(request, expected = config3.HIAI_DOCS_API_KEY) {
193199
193200
  if (!request)
193200
193201
  return false;
193201
- const apiKey = request.headers.get("x-api-key");
193202
- return apiKey === config3.HIAI_DOCS_API_KEY;
193202
+ if (!expected)
193203
+ return false;
193204
+ const headerKey = request.headers.get("x-api-key")?.trim();
193205
+ const bearer = /^Bearer\s+(.+)$/i.exec(request.headers.get("authorization")?.trim() ?? "")?.[1]?.trim();
193206
+ return headerKey === expected || bearer === expected;
193203
193207
  }
193204
193208
  function _limiterWithBypass(config4) {
193205
193209
  const base = createRateLimiter(config4);
@@ -194512,6 +194516,25 @@ async function loadAdminFolderDocumentIds(folderId, limit) {
194512
194516
  return limit > 0 ? query.limit(limit) : query;
194513
194517
  });
194514
194518
  }
194519
+ async function loadAdminDocumentTarget(documentId) {
194520
+ const rows = await withTenant(REEMBED_ADMIN_TENANT, (tx) => tx.select({ id: documents.id, workspaceId: documents.workspaceId }).from(documents).where(eq(documents.id, documentId)).limit(1));
194521
+ const row = rows[0];
194522
+ return row ? { id: row.id, workspaceId: row.workspaceId ?? undefined } : undefined;
194523
+ }
194524
+ async function reembedDocumentAdminWith(documentId, loadTarget) {
194525
+ const target2 = await loadTarget(documentId);
194526
+ if (!target2)
194527
+ return { found: false, enqueued: 0 };
194528
+ const enqueued = await enqueueReembed([target2.id], target2.workspaceId, {
194529
+ bypassDedup: true,
194530
+ forceNewGeneration: true,
194531
+ source: "reindex"
194532
+ });
194533
+ return { found: true, enqueued };
194534
+ }
194535
+ function reembedDocumentAdmin(documentId) {
194536
+ return reembedDocumentAdminWith(documentId, loadAdminDocumentTarget);
194537
+ }
194515
194538
  async function reembedDocsInFolderAdminWith(folderId, loadRows) {
194516
194539
  const limit = config3.FOLDER_REEMBED_BATCH_SIZE;
194517
194540
  const rows = await loadRows(folderId, limit);
@@ -194613,27 +194636,19 @@ var adminRoutes = new Elysia({ prefix: "/api/admin" }).post("/reindex/:docId", a
194613
194636
  return { error: "Invalid or missing admin API key" };
194614
194637
  }
194615
194638
  try {
194616
- const existing = await withTenant(adminTenantContextBound(), async (tx) => {
194617
- const rows = await tx.select({ id: documents.id }).from(documents).where(eq(documents.id, params.docId)).limit(1);
194618
- return rows.length > 0;
194619
- });
194620
- if (!existing) {
194639
+ const result = await reembedDocumentAdmin(params.docId);
194640
+ if (!result.found) {
194621
194641
  set2.status = 404;
194622
194642
  return { error: "Document not found" };
194623
194643
  }
194624
- const enqueued = await enqueueReembed([params.docId], undefined, {
194625
- bypassDedup: true,
194626
- forceNewGeneration: true,
194627
- source: "reindex"
194628
- });
194629
- if (enqueued !== 1) {
194644
+ if (result.enqueued !== 1) {
194630
194645
  set2.status = 503;
194631
194646
  return { error: "Failed to queue document reindex" };
194632
194647
  }
194633
194648
  return {
194634
194649
  success: true,
194635
194650
  documentId: params.docId,
194636
- enqueued,
194651
+ enqueued: result.enqueued,
194637
194652
  message: "Document reindex queued with a new generation"
194638
194653
  };
194639
194654
  } catch (err) {
@@ -229066,8 +229081,8 @@ var folderRoutes = new Elysia({ prefix: "/api/folders" }).get("/:id", async ({ p
229066
229081
  set2.status = 404;
229067
229082
  return { error: "Folder not found" };
229068
229083
  }
229069
- if (parsed.data.name !== undefined) {
229070
- reembedDocsInFolder(params2.id, userId, ctx.workspaceId).catch((err) => logger3.warn({ err, folderId: params2.id }, "Failed to enqueue re-embedding for folder rename"));
229084
+ if (parsed.data.name !== undefined || parsed.data.parentId !== undefined || parsed.data.categoryId !== undefined) {
229085
+ reembedDocsInFolder(params2.id, userId, ctx.workspaceId).catch((err) => logger3.warn({ err, folderId: params2.id }, "Failed to enqueue re-embedding for folder metadata change"));
229071
229086
  invalidateDocListCache(userId);
229072
229087
  }
229073
229088
  return result.updated;
@@ -229919,7 +229934,7 @@ async function resolveVisibleIds(ctx, ids, adapter, scope = _buildGraphVisibilit
229919
229934
  id: documents.id,
229920
229935
  ownerId: documents.ownerId,
229921
229936
  visibility: documents.visibility
229922
- }).from(documents).where(and(inArray(documents.id, ids), scope.kind === "admin" ? undefined : scope.kind === "public" ? eq(documents.visibility, "public") : scope.kind === "share" ? inArray(documents.id, scope.allowedDocumentIds) : or(eq(documents.ownerId, scope.ownerId), eq(documents.visibility, "public")))));
229937
+ }).from(documents).where(and(inArray(documents.id, ids), isNull(documents.deletedAt), scope.kind === "admin" ? undefined : scope.kind === "public" ? eq(documents.visibility, "public") : scope.kind === "share" ? inArray(documents.id, scope.allowedDocumentIds) : or(eq(documents.ownerId, scope.ownerId), eq(documents.visibility, "public")))));
229923
229938
  return new Set(rows.filter((row) => _isGraphDocumentVisible(scope, {
229924
229939
  id: row.id,
229925
229940
  ownerId: row.ownerId,
@@ -230361,6 +230376,7 @@ async function retrieveExact(ctx, plan, limit, execute, scope) {
230361
230376
  1.0::double precision AS score
230362
230377
  FROM documents d
230363
230378
  WHERE d.owner_id = ${ctx.userId}
230379
+ AND d.deleted_at IS NULL
230364
230380
  ${scope}
230365
230381
  AND (
230366
230382
  lower(trim(d.title)) = lower(trim(${plan.normalized}))
@@ -230388,6 +230404,7 @@ async function retrieveFts(ctx, plan, limit, execute, scope) {
230388
230404
  ts_rank(d.search_vector, websearch_to_tsquery('english', ${plan.normalized})) AS score
230389
230405
  FROM documents d
230390
230406
  WHERE d.owner_id = ${ctx.userId}
230407
+ AND d.deleted_at IS NULL
230391
230408
  ${scope}
230392
230409
  AND d.search_vector @@ websearch_to_tsquery('english', ${plan.normalized})
230393
230410
  UNION ALL
@@ -230395,6 +230412,7 @@ async function retrieveFts(ctx, plan, limit, execute, scope) {
230395
230412
  ts_rank(d.search_vector_simple, websearch_to_tsquery('simple', ${plan.normalized})) AS score
230396
230413
  FROM documents d
230397
230414
  WHERE d.owner_id = ${ctx.userId}
230415
+ AND d.deleted_at IS NULL
230398
230416
  ${scope}
230399
230417
  AND d.search_vector_simple @@ websearch_to_tsquery('simple', ${plan.normalized})
230400
230418
  )
@@ -230413,6 +230431,7 @@ async function retrieveFuzzy(ctx, plan, limit, minimum, execute, scope) {
230413
230431
  similarity(d.title, ${plan.normalized})::double precision AS score
230414
230432
  FROM documents d
230415
230433
  WHERE d.owner_id = ${ctx.userId}
230434
+ AND d.deleted_at IS NULL
230416
230435
  ${scope}
230417
230436
  AND d.title % ${plan.normalized}
230418
230437
  AND similarity(d.title, ${plan.normalized}) >= ${minimum}
@@ -230447,6 +230466,7 @@ async function retrieveVector(ctx, plan, limit, minimum, chunkLimit, execute, pr
230447
230466
  FROM document_embeddings de
230448
230467
  JOIN documents d ON d.id = de.document_id
230449
230468
  WHERE d.owner_id = ${ctx.userId}
230469
+ AND d.deleted_at IS NULL
230450
230470
  ${scope}
230451
230471
  AND d.active_embedding_generation IS NOT NULL
230452
230472
  AND de.generation_id = d.active_embedding_generation
@@ -231179,6 +231199,7 @@ async function hydrateResults(ctx, items, includeChunks, query, allowedDocumentI
231179
231199
  JOIN documents d ON d.id = de.document_id
231180
231200
  WHERE de.document_id IN (${sql.join(ids.map((id3) => sql`${id3}`), sql`, `)})
231181
231201
  AND ${tenantOwnerSql("d", ctx)}
231202
+ AND d.deleted_at IS NULL
231182
231203
  AND d.active_embedding_generation IS NOT NULL
231183
231204
  AND de.generation_id = d.active_embedding_generation
231184
231205
  AND de.is_valid = true
@@ -233301,13 +233322,13 @@ var extractionOutputSchema = exports_external2.object({
233301
233322
  entities: exports_external2.array(exports_external2.unknown())
233302
233323
  });
233303
233324
  async function callEntityExtractionLLM(text4, options) {
233304
- const primaryBase = options.llmBaseUrl ?? config3.GRAPH_EXTRACT_BASE_URL ?? config3.EMBEDDING_BASE_URL;
233325
+ const primaryBase = options.llmBaseUrl ?? config3.GRAPH_EXTRACT_BASE_URL;
233305
233326
  const primaryExplicitKey = options.llmApiKey ?? config3.GRAPH_EXTRACT_API_KEY;
233306
- const primaryModel = options.llmModel ?? config3.GRAPH_EXTRACT_MODEL ?? config3.EMBEDDING_MODEL ?? "gpt-4o-mini";
233327
+ const primaryModel = options.llmModel ?? config3.GRAPH_EXTRACT_MODEL ?? "gpt-4o-mini";
233307
233328
  const primaryKey = primaryBase ? resolveGraphProviderKey(primaryBase, primaryExplicitKey) : "";
233308
- const fallbackBase = config3.GRAPH_EXTRACT_FALLBACK_BASE_URL ?? config3.EMBEDDING_FALLBACK_BASE_URL;
233329
+ const fallbackBase = config3.GRAPH_EXTRACT_FALLBACK_BASE_URL;
233309
233330
  const fallbackExplicitKey = config3.GRAPH_EXTRACT_FALLBACK_API_KEY;
233310
- const fallbackModel = config3.GRAPH_EXTRACT_FALLBACK_MODEL ?? config3.EMBEDDING_FALLBACK_MODEL ?? primaryModel;
233331
+ const fallbackModel = config3.GRAPH_EXTRACT_FALLBACK_MODEL ?? primaryModel;
233311
233332
  const fallbackKey = fallbackBase ? resolveGraphProviderKey(fallbackBase, fallbackExplicitKey) : "";
233312
233333
  const providers = [];
233313
233334
  if (primaryBase) {
@@ -234953,7 +234974,7 @@ var swaggerConfig = {
234953
234974
  },
234954
234975
  info: {
234955
234976
  title: "DocsMint API",
234956
- version: "0.6.6",
234977
+ version: "0.6.8",
234957
234978
  description: "Self-hosted AI-first documentation platform. Full-text + semantic search, version history, sharing, and folder organization.",
234958
234979
  contact: { name: "HiAi-gg", url: "https://github.com/HiAi-gg/docsmint" },
234959
234980
  license: {
@@ -234995,7 +235016,7 @@ var app = new Elysia().use(bodySizeLimit).onError(({ error: error53, set: set2 }
234995
235016
  set2.headers["X-Content-Type-Options"] = "nosniff";
234996
235017
  set2.headers["X-Frame-Options"] = "DENY";
234997
235018
  }).use(cors({
234998
- origin: config3.CORS_ORIGINS?.split(",") ?? [config3.BETTER_AUTH_URL],
235019
+ origin: config3.CORS_ORIGINS ? config3.CORS_ORIGINS.split(",").map((origin) => origin.trim()).filter(Boolean) : [config3.BETTER_AUTH_URL],
234999
235020
  credentials: true,
235000
235021
  maxAge: 86400
235001
235022
  })).use(config3.NODE_ENV !== "production" ? swagger(swaggerConfig) : (e3) => e3).get("/api/health", async ({ request }) => {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@hiai-gg/docsmint",
3
3
  "mcpName": "io.github.HiAi-gg/docsmint",
4
- "version": "0.6.6",
4
+ "version": "0.6.8",
5
5
  "type": "module",
6
6
  "browser": {
7
7
  "./dist/backend-launcher.js": false,
@@ -24,7 +24,7 @@ import { registerSearch } from './commands/search.js';
24
24
  import { registerSnapshot } from './commands/snapshot.js';
25
25
  import { registerUpdate } from './commands/update.js';
26
26
 
27
- const VERSION = '0.6.6';
27
+ const VERSION = '0.6.8';
28
28
 
29
29
  const program = new Command();
30
30
  program.name('hiai-docs').description('CLI for the hiai-docs knowledge base').version(VERSION);
@@ -81,7 +81,7 @@ export interface CreateDocsmintMcpServerOptions {
81
81
  }
82
82
 
83
83
  export function createDocsmintMcpServer(options: CreateDocsmintMcpServerOptions = {}): McpServer {
84
- const server = new McpServer({ name: 'docsmint', version: '0.6.6' });
84
+ const server = new McpServer({ name: 'docsmint', version: '0.6.8' });
85
85
  registerDocsmintMcpCapabilities(server, options.client ?? defaultClient);
86
86
  return server;
87
87
  }
package/server.json CHANGED
@@ -8,12 +8,12 @@
8
8
  "source": "github",
9
9
  "id": "1249550690"
10
10
  },
11
- "version": "0.6.6",
11
+ "version": "0.6.8",
12
12
  "packages": [
13
13
  {
14
14
  "registryType": "npm",
15
15
  "identifier": "@hiai-gg/docsmint",
16
- "version": "0.6.6",
16
+ "version": "0.6.8",
17
17
  "transport": {
18
18
  "type": "stdio"
19
19
  },