texlite 0.7.8 → 0.8.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.
@@ -1,16 +1,4 @@
1
1
  export function accessibleProject(db, projectId, user) {
2
- if (user.role === "admin") {
3
- const row = db.prepare(`
4
- SELECT p.*, 'owner' AS permission, owner.username AS owner_username,
5
- owner.display_name AS owner_display_name,
6
- modifier.username AS last_modified_username, modifier.display_name AS last_modified_display_name
7
- FROM projects p
8
- JOIN users owner ON owner.id = p.owner_id
9
- LEFT JOIN users modifier ON modifier.id = p.last_modified_by
10
- WHERE p.id = ?
11
- `).get(projectId);
12
- return row ?? null;
13
- }
14
2
  const row = db.prepare(`
15
3
  SELECT p.*,
16
4
  CASE WHEN p.owner_id = ? THEN 'owner' ELSE pm.permission END AS permission,
@@ -129,7 +129,7 @@ export function requireEditableProject(db, projectId, user) {
129
129
  throw httpError(403, "PROJECT_EDIT_FORBIDDEN");
130
130
  return project;
131
131
  }
132
- /** Owner permission includes an administrator's effective owner access. */
132
+ /** Owner permission is granted only to the stored project owner. */
133
133
  export function requireProjectOwnerPermission(db, projectId, user) {
134
134
  requireActiveUser(db, user);
135
135
  const project = accessibleProject(db, projectId, user);
@@ -9,6 +9,7 @@ import { isMainDocumentCandidate } from "../latexRoot.js";
9
9
  import { accessibleProject, canEdit } from "../projects.js";
10
10
  import { writeProjectArchive } from "../archive.js";
11
11
  import { extractProjectZip, ZipValidationError } from "../zip.js";
12
+ import { HarperUnavailableError } from "../harper.js";
12
13
  import { commentsSummaryForProject, commentsSummaryForProjects, dictionaryWord, escapeLikePattern, now, projectJson, requireProjectOwnerPermission, tagColors, tagsForProject, tagsForProjects, text } from "./projectShared.js";
13
14
  /** Register project catalog, metadata, archive, dictionary, tag, export, and deletion routes. */
14
15
  export function registerProjectCatalogRoutes(app, context) {
@@ -50,14 +51,11 @@ export function registerProjectCatalogRoutes(app, context) {
50
51
  const archiveCondition = archivedOnly
51
52
  ? "EXISTS (SELECT 1 FROM user_project_archives archive WHERE archive.project_id = p.id AND archive.user_id = :userId)"
52
53
  : "NOT EXISTS (SELECT 1 FROM user_project_archives archive WHERE archive.project_id = p.id AND archive.user_id = :userId)";
53
- const from = user.role === "admin"
54
- ? `FROM projects p JOIN users owner ON owner.id = p.owner_id
55
- LEFT JOIN users modifier ON modifier.id = p.last_modified_by`
56
- : `FROM projects p JOIN users owner ON owner.id = p.owner_id
57
- LEFT JOIN users modifier ON modifier.id = p.last_modified_by
58
- LEFT JOIN project_members pm ON pm.project_id = p.id AND pm.user_id = :userId`;
54
+ const from = `FROM projects p JOIN users owner ON owner.id = p.owner_id
55
+ LEFT JOIN users modifier ON modifier.id = p.last_modified_by
56
+ LEFT JOIN project_members pm ON pm.project_id = p.id AND pm.user_id = :userId`;
59
57
  const conditions = [
60
- ...(user.role === "admin" ? [] : ["(p.owner_id = :userId OR pm.user_id = :userId)"]),
58
+ "(p.owner_id = :userId OR pm.user_id = :userId)",
61
59
  archiveCondition
62
60
  ];
63
61
  const params = { userId: user.id };
@@ -79,14 +77,10 @@ export function registerProjectCatalogRoutes(app, context) {
79
77
  const totalPages = Math.ceil(total / pageSize);
80
78
  const currentPage = totalPages === 0 ? 1 : Math.min(page, totalPages);
81
79
  const rowsParams = { ...params, limit: pageSize, offset: (currentPage - 1) * pageSize };
82
- const select = user.role === "admin"
83
- ? `SELECT p.*, 'owner' AS permission, owner.username AS owner_username,
84
- owner.display_name AS owner_display_name,
85
- modifier.username AS last_modified_username, modifier.display_name AS last_modified_display_name`
86
- : `SELECT DISTINCT p.*,
87
- CASE WHEN p.owner_id = :userId THEN 'owner' ELSE pm.permission END AS permission,
88
- owner.username AS owner_username, owner.display_name AS owner_display_name,
89
- modifier.username AS last_modified_username, modifier.display_name AS last_modified_display_name`;
80
+ const select = `SELECT DISTINCT p.*,
81
+ CASE WHEN p.owner_id = :userId THEN 'owner' ELSE pm.permission END AS permission,
82
+ owner.username AS owner_username, owner.display_name AS owner_display_name,
83
+ modifier.username AS last_modified_username, modifier.display_name AS last_modified_display_name`;
90
84
  const rows = db.prepare(`${select} ${from} ${where}
91
85
  ORDER BY ${sortColumn} DESC, p.name COLLATE NOCASE ASC
92
86
  LIMIT :limit OFFSET :offset`).all(rowsParams);
@@ -259,16 +253,21 @@ export function registerProjectCatalogRoutes(app, context) {
259
253
  if (!accessibleProject(db, id, user))
260
254
  return apiError(reply, 404, "PROJECT_NOT_FOUND");
261
255
  const body = request.body;
262
- if (typeof body?.source !== "string")
256
+ if (typeof body?.source !== "string" || typeof body.path !== "string")
263
257
  return apiError(reply, 400, "SPELLCHECK_SOURCE_INVALID");
264
258
  if (Buffer.byteLength(body.source, "utf8") > maxCollaborativeFileBytes(config)) {
265
259
  return apiError(reply, 413, "SPELLCHECK_SOURCE_TOO_LARGE");
266
260
  }
267
261
  try {
268
- return { lints: await harper.lint(body.source) };
262
+ return { lints: await harper.lint(body.source, body.path) };
269
263
  }
270
264
  catch (error) {
271
- request.log.error({ err: error, projectId: id }, "Harper writing check failed");
265
+ // A missing optional command is an expected fallback condition. Keep it
266
+ // out of normal logs while preserving diagnostics for an actual failure.
267
+ if (error instanceof HarperUnavailableError)
268
+ request.log.debug({ projectId: id }, "Host Harper CLI unavailable; using browser fallback");
269
+ else
270
+ request.log.error({ err: error, projectId: id }, "Harper writing check failed");
272
271
  return apiError(reply, 503, "HARPER_UNAVAILABLE");
273
272
  }
274
273
  });
@@ -459,7 +458,9 @@ export function registerProjectCatalogRoutes(app, context) {
459
458
  return;
460
459
  const { id } = request.params;
461
460
  const project = accessibleProject(db, id, user);
462
- if (!project || project.permission !== "owner")
461
+ if (!project)
462
+ return apiError(reply, 404, "PROJECT_NOT_FOUND");
463
+ if (project.permission !== "owner")
463
464
  return apiError(reply, 403, "PROJECT_DELETE_FORBIDDEN");
464
465
  return await projectMutations.runExclusive(id, "project deletion", () => {
465
466
  collaboration.resetProject(id);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "texlite",
3
- "version": "0.7.8",
3
+ "version": "0.8.0",
4
4
  "description": "A lightweight local-first collaborative LaTeX workspace",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {
@@ -24,6 +24,9 @@
24
24
  "dist",
25
25
  "README.md",
26
26
  "README.zh-CN.md",
27
+ "OPERATIONS.md",
28
+ "DESIGN.md",
29
+ "NPM_TESTING.md",
27
30
  "LICENSE",
28
31
  "preview.png",
29
32
  "texlite.config.example.json"
@@ -72,7 +75,6 @@
72
75
  "bibtex-tidy": "1.14.0",
73
76
  "diff-match-patch": "^1.0.5",
74
77
  "fastify": "^5.4.0",
75
- "harper.js": "^2.7.0",
76
78
  "i18next": "^25.3.2",
77
79
  "i18next-browser-languagedetector": "^8.2.0",
78
80
  "lib0": "^0.2.117",