explicai 0.1.0 → 0.2.1

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.
Files changed (131) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/LICENSE +21 -21
  3. package/README.md +87 -27
  4. package/dist/cli.js +30 -11
  5. package/dist/core/annotations.js +2 -0
  6. package/dist/core/assets.d.ts +2 -0
  7. package/dist/core/assets.js +51 -16
  8. package/dist/core/errors.d.ts +9 -0
  9. package/dist/core/errors.js +14 -0
  10. package/dist/core/export/annotationCss.d.ts +5 -0
  11. package/dist/core/export/annotationCss.js +26 -0
  12. package/dist/core/export/annotationShapes.d.ts +37 -0
  13. package/dist/core/export/annotationShapes.js +38 -0
  14. package/dist/core/export/callout.d.ts +11 -0
  15. package/dist/core/export/callout.js +26 -0
  16. package/dist/core/export/collect.d.ts +2 -2
  17. package/dist/core/export/collect.js +2 -2
  18. package/dist/core/export/color.d.ts +11 -0
  19. package/dist/core/export/color.js +33 -0
  20. package/dist/core/export/fonts.d.ts +35 -0
  21. package/dist/core/export/fonts.js +40 -0
  22. package/dist/core/export/headings.d.ts +7 -0
  23. package/dist/core/export/headings.js +17 -0
  24. package/dist/core/export/html.d.ts +1 -3
  25. package/dist/core/export/html.js +86 -39
  26. package/dist/core/export/imageSize.js +20 -3
  27. package/dist/core/export/index.d.ts +12 -6
  28. package/dist/core/export/index.js +106 -26
  29. package/dist/core/export/labels.d.ts +1 -0
  30. package/dist/core/export/labels.js +2 -0
  31. package/dist/core/export/logo.d.ts +10 -0
  32. package/dist/core/export/logo.js +29 -0
  33. package/dist/core/export/markdown.js +20 -11
  34. package/dist/core/export/pdf.js +2 -1
  35. package/dist/core/export/refs.d.ts +2 -2
  36. package/dist/core/export/refs.js +22 -9
  37. package/dist/core/export/textColors.d.ts +7 -0
  38. package/dist/core/export/textColors.js +18 -0
  39. package/dist/core/export/theme.d.ts +28 -0
  40. package/dist/core/export/theme.js +140 -0
  41. package/dist/core/fs.d.ts +4 -0
  42. package/dist/core/fs.js +13 -1
  43. package/dist/core/index.d.ts +5 -2
  44. package/dist/core/index.js +5 -2
  45. package/dist/core/manuals.d.ts +7 -1
  46. package/dist/core/manuals.js +29 -9
  47. package/dist/core/pages.d.ts +5 -0
  48. package/dist/core/pages.js +56 -15
  49. package/dist/core/redact.d.ts +9 -0
  50. package/dist/core/redact.js +107 -0
  51. package/dist/core/text.d.ts +2 -0
  52. package/dist/core/text.js +9 -0
  53. package/dist/core/trash.js +13 -8
  54. package/dist/core/types.d.ts +26 -1
  55. package/dist/core/types.js +2 -0
  56. package/dist/core/versions.js +3 -3
  57. package/dist/server/errorPage.d.ts +9 -0
  58. package/dist/server/errorPage.js +122 -0
  59. package/dist/server/http.d.ts +11 -3
  60. package/dist/server/http.js +65 -11
  61. package/dist/server/index.js +35 -5
  62. package/dist/server/routes/assets.js +21 -2
  63. package/dist/server/routes/export.js +18 -8
  64. package/dist/server/routes/fonts.d.ts +3 -0
  65. package/dist/server/routes/fonts.js +24 -0
  66. package/dist/server/routes/pages.js +3 -1
  67. package/dist/server/routes/web.js +7 -1
  68. package/dist/server/security.d.ts +1 -1
  69. package/dist/server/security.js +7 -6
  70. package/dist/web/assets/index-BlWnNe8d.js +262 -0
  71. package/dist/web/assets/index-DFBrhkII.css +1 -0
  72. package/dist/web/index.html +2 -3
  73. package/fonts/OFL-archivo.txt +93 -0
  74. package/fonts/OFL-atkinson-hyperlegible-next.txt +93 -0
  75. package/fonts/OFL-inter.txt +93 -0
  76. package/fonts/OFL-jetbrains-mono.txt +93 -0
  77. package/fonts/OFL-lora.txt +93 -0
  78. package/fonts/OFL-montserrat.txt +93 -0
  79. package/fonts/OFL-playfair-display.txt +93 -0
  80. package/fonts/OFL-source-sans-3.txt +93 -0
  81. package/fonts/OFL-source-serif-4.txt +93 -0
  82. package/fonts/archivo-latin-wght-normal.woff2 +0 -0
  83. package/fonts/atkinson-hyperlegible-next-latin-400-italic.woff2 +0 -0
  84. package/fonts/atkinson-hyperlegible-next-latin-400-normal.woff2 +0 -0
  85. package/fonts/atkinson-hyperlegible-next-latin-700-normal.woff2 +0 -0
  86. package/fonts/inter-latin-wght-italic.woff2 +0 -0
  87. package/fonts/inter-latin-wght-normal.woff2 +0 -0
  88. package/fonts/jetbrains-mono-latin-wght-normal.woff2 +0 -0
  89. package/fonts/lora-latin-wght-italic.woff2 +0 -0
  90. package/fonts/lora-latin-wght-normal.woff2 +0 -0
  91. package/fonts/montserrat-latin-wght-normal.woff2 +0 -0
  92. package/fonts/playfair-display-latin-wght-normal.woff2 +0 -0
  93. package/fonts/source-sans-3-latin-wght-italic.woff2 +0 -0
  94. package/fonts/source-sans-3-latin-wght-normal.woff2 +0 -0
  95. package/fonts/source-serif-4-latin-wght-italic.woff2 +0 -0
  96. package/fonts/source-serif-4-latin-wght-normal.woff2 +0 -0
  97. package/package.json +41 -7
  98. package/dist/cli.js.map +0 -1
  99. package/dist/core/annotations.js.map +0 -1
  100. package/dist/core/assets.js.map +0 -1
  101. package/dist/core/export/collect.js.map +0 -1
  102. package/dist/core/export/html.js.map +0 -1
  103. package/dist/core/export/imageSize.js.map +0 -1
  104. package/dist/core/export/index.js.map +0 -1
  105. package/dist/core/export/labels.js.map +0 -1
  106. package/dist/core/export/markdown.js.map +0 -1
  107. package/dist/core/export/pdf.js.map +0 -1
  108. package/dist/core/export/refs.js.map +0 -1
  109. package/dist/core/frontmatter.js.map +0 -1
  110. package/dist/core/fs.js.map +0 -1
  111. package/dist/core/index.js.map +0 -1
  112. package/dist/core/manuals.js.map +0 -1
  113. package/dist/core/pages.js.map +0 -1
  114. package/dist/core/text.js.map +0 -1
  115. package/dist/core/trash.js.map +0 -1
  116. package/dist/core/tree.js.map +0 -1
  117. package/dist/core/types.js.map +0 -1
  118. package/dist/core/versions.js.map +0 -1
  119. package/dist/server/http.js.map +0 -1
  120. package/dist/server/index.js.map +0 -1
  121. package/dist/server/router.js.map +0 -1
  122. package/dist/server/routes/assets.js.map +0 -1
  123. package/dist/server/routes/export.js.map +0 -1
  124. package/dist/server/routes/manuals.js.map +0 -1
  125. package/dist/server/routes/pages.js.map +0 -1
  126. package/dist/server/routes/versions.js.map +0 -1
  127. package/dist/server/routes/web.js.map +0 -1
  128. package/dist/server/security.js.map +0 -1
  129. package/dist/server/workspace.js.map +0 -1
  130. package/dist/web/assets/index-BXkBk-2Z.css +0 -1
  131. package/dist/web/assets/index-C02XKPHS.js +0 -260
@@ -1,7 +1,11 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
2
  export declare class HttpError extends Error {
3
3
  readonly status: number;
4
- constructor(status: number, message: string);
4
+ /** Identifica o erro para a interface traduzir (a mensagem fica em português para curl e MCP). */
5
+ readonly code?: string | undefined;
6
+ constructor(status: number, message: string,
7
+ /** Identifica o erro para a interface traduzir (a mensagem fica em português para curl e MCP). */
8
+ code?: string | undefined);
5
9
  }
6
10
  /** O que um handler devolve. O servidor transforma em resposta HTTP. */
7
11
  export type Reply = {
@@ -12,12 +16,16 @@ export type Reply = {
12
16
  kind: "file";
13
17
  path: string;
14
18
  cache: string;
19
+ untrusted: boolean;
15
20
  };
16
21
  export declare const json: (body: unknown, status?: number) => Reply;
17
- export declare const file: (filePath: string, cache?: string) => Reply;
18
- export declare function send(res: ServerResponse, reply: Reply): Promise<void>;
22
+ /** `untrusted`: arquivo do manual (/files/), que pode ser um SVG com script; sai com CSP e sandbox. */
23
+ export declare const file: (filePath: string, cache?: string, untrusted?: boolean) => Reply;
24
+ export declare function send(res: ServerResponse, reply: Reply, req?: IncomingMessage): Promise<void>;
19
25
  export declare function sendJson(res: ServerResponse, status: number, body: unknown): void;
20
26
  export declare function readBody(req: IncomingMessage, limit: number): Promise<Uint8Array>;
21
27
  export declare function readJson(req: IncomingMessage): Promise<Record<string, unknown>>;
22
28
  /** Status HTTP para um erro vindo do core: "não encontrado" e arquivo inexistente viram 404, o resto 400. */
23
29
  export declare function statusOf(err: unknown): number;
30
+ /** Texto do erro para a resposta: erro de arquivo do Node traz o caminho absoluto, então vira frase fixa. */
31
+ export declare function publicMessage(err: unknown): string;
@@ -1,14 +1,29 @@
1
- import { readFile } from "node:fs/promises";
1
+ import { createReadStream } from "node:fs";
2
+ import { stat } from "node:fs/promises";
2
3
  import path from "node:path";
4
+ import { pipeline } from "node:stream/promises";
5
+ import { ExplicaiError } from "../core/errors.js";
3
6
  export class HttpError extends Error {
4
7
  status;
5
- constructor(status, message) {
8
+ code;
9
+ constructor(status, message,
10
+ /** Identifica o erro para a interface traduzir (a mensagem fica em português para curl e MCP). */
11
+ code) {
6
12
  super(message);
7
13
  this.status = status;
14
+ this.code = code;
8
15
  }
9
16
  }
10
17
  export const json = (body, status = 200) => ({ kind: "json", status, body });
11
- export const file = (filePath, cache = "no-cache") => ({ kind: "file", path: filePath, cache });
18
+ /** `untrusted`: arquivo do manual (/files/), que pode ser um SVG com script; sai com CSP e sandbox. */
19
+ export const file = (filePath, cache = "no-cache", untrusted = false) => ({
20
+ kind: "file",
21
+ path: filePath,
22
+ cache,
23
+ untrusted,
24
+ });
25
+ const RASTER = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp"]);
26
+ const UNTRUSTED_CSP = "default-src 'none'; style-src 'unsafe-inline'; sandbox";
12
27
  const MIME = {
13
28
  ".html": "text/html; charset=utf-8",
14
29
  ".js": "text/javascript; charset=utf-8",
@@ -24,16 +39,30 @@ const MIME = {
24
39
  ".pdf": "application/pdf",
25
40
  ".md": "text/markdown; charset=utf-8",
26
41
  };
27
- export async function send(res, reply) {
42
+ export async function send(res, reply, req) {
28
43
  if (reply.kind === "json")
29
44
  return sendJson(res, reply.status, reply.body);
30
- const data = await readFile(reply.path);
31
- res.writeHead(200, {
32
- "content-type": MIME[path.extname(reply.path).toLowerCase()] ?? "application/octet-stream",
45
+ const info = await stat(reply.path); // ENOENT vira 404 no chamador, antes de qualquer cabeçalho
46
+ if (!info.isFile())
47
+ throw new HttpError(404, "Arquivo não encontrado.");
48
+ const ext = path.extname(reply.path).toLowerCase();
49
+ // ETag fraco por tamanho e mtime: barato, sem ler o arquivo.
50
+ const etag = `W/"${info.size.toString(16)}-${Math.floor(info.mtimeMs).toString(16)}"`;
51
+ const headers = {
52
+ etag,
33
53
  "cache-control": reply.cache,
34
54
  "x-content-type-options": "nosniff",
35
- });
36
- res.end(data);
55
+ ...(reply.untrusted && !RASTER.has(ext) ? { "content-security-policy": UNTRUSTED_CSP } : {}),
56
+ };
57
+ // shortcut: só um valor em If-None-Match (o do próprio navegador), sem lista nem "*"; Range/HEAD não tratados.
58
+ if (req?.headers["if-none-match"] === etag) {
59
+ res.writeHead(304, headers);
60
+ res.end();
61
+ return;
62
+ }
63
+ res.writeHead(200, { "content-type": MIME[ext] ?? "application/octet-stream", "content-length": info.size, ...headers });
64
+ // Erro no meio do envio ou cliente que abortou: o pipeline destrói os dois lados; os cabeçalhos já saíram.
65
+ await pipeline(createReadStream(reply.path), res).catch(() => { });
37
66
  }
38
67
  export function sendJson(res, status, body) {
39
68
  res.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
@@ -54,7 +83,19 @@ export async function readBody(req, limit) {
54
83
  return Buffer.concat(chunks);
55
84
  }
56
85
  export async function readJson(req) {
57
- const text = Buffer.from(await readBody(req, 5 * 1024 * 1024)).toString("utf8");
86
+ const raw = await readBody(req, 5 * 1024 * 1024);
87
+ // Um POST text/plain de outro site não dispara preflight; exigir JSON fecha essa brecha. Corpo vazio não lê nada.
88
+ if (raw.length > 0 && !/^application\/json\s*(;|$)/i.test(req.headers["content-type"] ?? "")) {
89
+ throw new HttpError(415, "Envie o corpo como JSON, com o cabeçalho content-type: application/json.", "unsupported-media-type");
90
+ }
91
+ let text;
92
+ try {
93
+ // fatal: um cliente em Latin-1/cp1252 (curl no Windows) mandaria "í" como 0xED, e o decode comum o trocaria por U+FFFD gravado em disco.
94
+ text = new TextDecoder("utf-8", { fatal: true }).decode(raw);
95
+ }
96
+ catch {
97
+ throw new HttpError(400, "O corpo da requisição não está em UTF-8 válido. Envie o JSON em UTF-8.", "invalid-utf8");
98
+ }
58
99
  let data;
59
100
  try {
60
101
  data = text ? JSON.parse(text) : {};
@@ -71,8 +112,21 @@ export async function readJson(req) {
71
112
  export function statusOf(err) {
72
113
  if (err instanceof HttpError)
73
114
  return err.status;
115
+ if (err instanceof ExplicaiError && err.code === "disk-conflict")
116
+ return 409;
74
117
  const missing = (err instanceof Error && /não encontrad/i.test(err.message)) ||
75
- (typeof err === "object" && err !== null && err.code === "ENOENT");
118
+ ["ENOENT", "EISDIR"].includes(err?.code ?? "");
76
119
  return missing ? 404 : 400;
77
120
  }
121
+ const FILE_ERRORS = {
122
+ ENOENT: "Arquivo não encontrado.",
123
+ EISDIR: "Arquivo não encontrado.",
124
+ EACCES: "Sem permissão para acessar o arquivo.",
125
+ EPERM: "Sem permissão para acessar o arquivo.",
126
+ };
127
+ /** Texto do erro para a resposta: erro de arquivo do Node traz o caminho absoluto, então vira frase fixa. */
128
+ export function publicMessage(err) {
129
+ const fixed = FILE_ERRORS[err?.code ?? ""];
130
+ return fixed ?? (err instanceof Error ? err.message : String(err));
131
+ }
78
132
  //# sourceMappingURL=http.js.map
@@ -1,29 +1,59 @@
1
1
  import { createServer } from "node:http";
2
- import { HttpError, send, sendJson, statusOf } from "./http.js";
2
+ import { ExplicaiError } from "../core/errors.js";
3
+ import { errorPage, pickLang } from "./errorPage.js";
4
+ import { HttpError, publicMessage, send, sendJson, statusOf } from "./http.js";
3
5
  import { matchRoute } from "./router.js";
4
6
  import { assetRoutes } from "./routes/assets.js";
5
7
  import { exportRoutes } from "./routes/export.js";
8
+ import { fontRoutes } from "./routes/fonts.js";
6
9
  import { manualRoutes } from "./routes/manuals.js";
7
10
  import { pageRoutes } from "./routes/pages.js";
8
11
  import { versionRoutes } from "./routes/versions.js";
9
12
  import { webRoutes } from "./routes/web.js";
10
13
  import { assertLocalRequest } from "./security.js";
11
14
  /** A ordem importa: a rota genérica do editor (`/*`) fica por último. */
12
- const routes = [...manualRoutes, ...pageRoutes, ...versionRoutes, ...exportRoutes, ...assetRoutes, ...webRoutes];
15
+ const routes = [
16
+ ...manualRoutes,
17
+ ...pageRoutes,
18
+ ...versionRoutes,
19
+ ...exportRoutes,
20
+ ...fontRoutes,
21
+ ...assetRoutes,
22
+ ...webRoutes,
23
+ ];
24
+ /** Um `%` malformado no endereço é erro de quem pediu (400 com código estável), não falha do servidor. */
25
+ function decodeSegment(segment) {
26
+ try {
27
+ return decodeURIComponent(segment);
28
+ }
29
+ catch {
30
+ throw new HttpError(400, "O endereço pedido tem um % malformado. Confira o endereço e tente de novo.", "malformed-url");
31
+ }
32
+ }
13
33
  export function createStudioServer(opts) {
14
34
  return createServer(async (req, res) => {
15
35
  try {
16
36
  assertLocalRequest(req);
17
37
  const url = new URL(req.url ?? "/", "http://localhost");
18
- const segments = url.pathname.split("/").filter(Boolean).map(decodeURIComponent);
38
+ const segments = url.pathname.split("/").filter(Boolean).map(decodeSegment);
19
39
  const method = req.method ?? "GET";
20
40
  const match = matchRoute(routes, method, segments);
21
41
  if (!match)
22
42
  throw new HttpError(segments[0] === "api" ? 405 : 404, "Rota não encontrada.");
23
- await send(res, await match.route.handle({ req, url, params: match.params, rest: match.rest, opts }));
43
+ await send(res, await match.route.handle({ req, url, params: match.params, rest: match.rest, opts }), req);
24
44
  }
25
45
  catch (err) {
26
- sendJson(res, statusOf(err), { error: err instanceof Error ? err.message : String(err) });
46
+ const { code, params } = err instanceof ExplicaiError ? err : {};
47
+ const errorCode = code ?? (err instanceof HttpError ? err.code : undefined);
48
+ const status = statusOf(err);
49
+ // Quem abre o Studio no navegador recebe uma página legível; a API e os arquivos continuam em JSON.
50
+ const [first] = (req.url ?? "/").split("?")[0]?.split("/").filter(Boolean) ?? [];
51
+ if (first !== "api" && first !== "files" && req.headers.accept?.includes("text/html")) {
52
+ const html = errorPage(status, errorCode ?? (status === 404 ? "not-found" : "error"), pickLang(req.headers["accept-language"]));
53
+ res.writeHead(status, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
54
+ return void res.end(html);
55
+ }
56
+ sendJson(res, status, { error: publicMessage(err), code: errorCode, params });
27
57
  }
28
58
  });
29
59
  }
@@ -1,4 +1,4 @@
1
- import { cleanUnusedAssets, findUnusedAssets, readAnnotations, resolveInside, saveAsset, writeAnnotations, } from "../../core/index.js";
1
+ import { cleanUnusedAssets, findUnusedAssets, insideGitRepo, readAnnotations, redactAsset, resolveInsideReal, saveAsset, writeAnnotations, } from "../../core/index.js";
2
2
  import { file, json, readBody, readJson } from "../http.js";
3
3
  import { manualDir } from "../workspace.js";
4
4
  const MAX_UPLOAD = 20 * 1024 * 1024;
@@ -22,6 +22,21 @@ export const assetRoutes = [
22
22
  path: "/api/manuals/:slug/annotations",
23
23
  handle: async (ctx) => json(await writeAnnotations(manualDir(ctx), ctx.url.searchParams.get("asset") ?? "", await readJson(ctx.req))),
24
24
  },
25
+ {
26
+ // Troca o print pela versão com as tarjas já pintadas (o navegador desenha) e apaga o original.
27
+ method: "POST",
28
+ path: "/api/manuals/:slug/assets/redact",
29
+ handle: async (ctx) => {
30
+ const asset = ctx.url.searchParams.get("asset") ?? "";
31
+ await redactAsset(manualDir(ctx), asset, await readBody(ctx.req, MAX_UPLOAD));
32
+ return json({ path: asset });
33
+ },
34
+ },
35
+ {
36
+ method: "GET",
37
+ path: "/api/manuals/:slug/git",
38
+ handle: async (ctx) => json({ git: await insideGitRepo(manualDir(ctx)) }),
39
+ },
25
40
  {
26
41
  // Imagens e marcações que nenhuma página, versão ou item da lixeira usa.
27
42
  method: "GET",
@@ -38,7 +53,11 @@ export const assetRoutes = [
38
53
  // Imagens do manual, usadas pelo editor: /files/<manual>/assets/<arquivo>
39
54
  method: "GET",
40
55
  path: "/files/:slug/*",
41
- handle: (ctx) => file(resolveInside(manualDir(ctx), ctx.rest)),
56
+ // Só raster de assets/ (nome único, gravado uma vez) fica em cache; manual.json, logo e .md revalidam por ETag.
57
+ handle: async (ctx) => {
58
+ const cacheable = /^assets\/[^/]+\.(png|jpe?g|gif|webp)$/i.test(ctx.rest);
59
+ return file(await resolveInsideReal(manualDir(ctx), ctx.rest), cacheable ? "private, max-age=3600" : "no-cache", true);
60
+ },
42
61
  },
43
62
  ];
44
63
  //# sourceMappingURL=assets.js.map
@@ -1,5 +1,5 @@
1
- import { EXPORT_FORMATS, exportManual, setManualTheme } from "../../core/index.js";
2
- import { HttpError, json, readJson } from "../http.js";
1
+ import { EXPORT_FORMATS, exportManual, logoSizeError, setManualLogo, setManualTheme, } from "../../core/index.js";
2
+ import { HttpError, json, readBody, readJson } from "../http.js";
3
3
  import { manualDir, optionalString } from "../workspace.js";
4
4
  export const exportRoutes = [
5
5
  {
@@ -17,17 +17,27 @@ export const exportRoutes = [
17
17
  },
18
18
  },
19
19
  {
20
- // { accent: "#2b4fe0" } escolhe a cor do manual exportado; { accent: null } volta à da marca.
20
+ // Muda só os campos enviados: { accent?, text?, highlight?, headings?: { cover?, page?, h1?, h2?, h3? } },
21
+ // cada um em hexadecimal ou null para voltar à cor da marca. O core confere formato e contraste (400).
21
22
  method: "PUT",
22
23
  path: "/api/manuals/:slug/theme",
23
24
  handle: async (ctx) => {
24
- const body = await readJson(ctx.req);
25
- const accent = body.accent === null ? undefined : optionalString(body.accent);
26
- if (accent === undefined && body.accent !== null)
27
- throw new HttpError(400, "Informe accent (texto ou null).");
28
- await setManualTheme(manualDir(ctx), accent ? { accent } : {});
25
+ await setManualTheme(manualDir(ctx), await readJson(ctx.req));
29
26
  return json({ ok: true });
30
27
  },
31
28
  },
29
+ {
30
+ // Corpo = o arquivo do logo (PNG, JPG ou SVG, até 1 MB), nome em ?name=. Guarda em assets/ e põe no tema.
31
+ // O limite de leitura é maior que o do logo para o core recusar com o motivo.
32
+ method: "POST",
33
+ path: "/api/manuals/:slug/logo",
34
+ handle: async (ctx) => {
35
+ const name = ctx.url.searchParams.get("name") ?? "logo.png";
36
+ const data = await readBody(ctx.req, 2 * 1024 * 1024).catch((err) => {
37
+ throw err instanceof HttpError && err.status === 413 ? logoSizeError() : err;
38
+ });
39
+ return json({ path: await setManualLogo(manualDir(ctx), name, data) }, 201);
40
+ },
41
+ },
32
42
  ];
33
43
  //# sourceMappingURL=export.js.map
@@ -0,0 +1,3 @@
1
+ import type { Route } from "../router.js";
2
+ /** As fontes que o manual exportado pode usar, para o Studio mostrar a prévia de cada uma. */
3
+ export declare const fontRoutes: Route[];
@@ -0,0 +1,24 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import { FONTS, FONTS_DIR, fontFiles } from "../../core/index.js";
3
+ import { file, HttpError, json } from "../http.js";
4
+ const FILES = new Set(FONTS.flatMap((f) => fontFiles(f).map((x) => x.file)));
5
+ /** As fontes que o manual exportado pode usar, para o Studio mostrar a prévia de cada uma. */
6
+ export const fontRoutes = [
7
+ {
8
+ method: "GET",
9
+ path: "/api/fonts",
10
+ handle: () => json(FONTS.map((f) => ({ id: f.id, family: f.family, kind: f.kind, italic: f.italic, files: fontFiles(f) }))),
11
+ },
12
+ {
13
+ method: "GET",
14
+ path: "/api/fonts/:file",
15
+ handle: ({ params }) => {
16
+ const name = params.file ?? "";
17
+ // Só os arquivos da lista: o nome vem da URL e não vira caminho de disco.
18
+ if (!FILES.has(name))
19
+ throw new HttpError(404, "Fonte não encontrada.");
20
+ return file(fileURLToPath(new URL(name, FONTS_DIR)), "max-age=86400");
21
+ },
22
+ },
23
+ ];
24
+ //# sourceMappingURL=fonts.js.map
@@ -37,7 +37,9 @@ export const pageRoutes = [
37
37
  ...(text === undefined ? {} : { body: text }),
38
38
  ...(status === undefined ? {} : { status }),
39
39
  };
40
- return json(await updatePage(manualDir(ctx), ctx.params.id ?? "", patch));
40
+ const fileName = body.fileName === "keep" || body.fileName === "sync" ? body.fileName : undefined;
41
+ const ifMatch = ctx.req.headers["if-match"];
42
+ return json(await updatePage(manualDir(ctx), ctx.params.id ?? "", patch, { fileName, ifMatch }));
41
43
  },
42
44
  },
43
45
  {
@@ -16,7 +16,13 @@ export const webRoutes = [
16
16
  // Só os arquivos com hash no nome (assets/) podem ficar em cache; o index.html muda a cada build.
17
17
  if (isFile && rest.startsWith("assets/"))
18
18
  return file(target, "max-age=31536000, immutable");
19
- return file(isFile ? target : path.join(opts.webDir, "index.html"));
19
+ if (isFile)
20
+ return file(target);
21
+ const index = path.join(opts.webDir, "index.html");
22
+ const hasIndex = await stat(index).then((s) => s.isFile(), () => false);
23
+ if (!hasIndex)
24
+ throw new HttpError(500, "O editor não foi encontrado: rode npm run build.", "web-missing");
25
+ return file(index);
20
26
  },
21
27
  },
22
28
  ];
@@ -1,6 +1,6 @@
1
1
  import type { IncomingMessage } from "node:http";
2
2
  /**
3
- * O Studio só atende o próprio computador. Checar Host e Origin impede que um site aberto
3
+ * O Studio só atende o próprio computador. Checar Host e Origin (que deve ser igual ao Host) impede que um site aberto
4
4
  * no navegador use o Studio (DNS rebinding e requisições de outra origem).
5
5
  */
6
6
  export declare function assertLocalRequest(req: IncomingMessage): void;
@@ -1,16 +1,17 @@
1
1
  import { HttpError } from "./http.js";
2
2
  const LOCAL_HOSTS = ["localhost", "127.0.0.1", "[::1]"];
3
- const LOCAL_ORIGIN = /^http:\/\/(localhost|127\.0\.0\.1|\[::1\])(:\d+)?$/;
4
3
  /**
5
- * O Studio só atende o próprio computador. Checar Host e Origin impede que um site aberto
4
+ * O Studio só atende o próprio computador. Checar Host e Origin (que deve ser igual ao Host) impede que um site aberto
6
5
  * no navegador use o Studio (DNS rebinding e requisições de outra origem).
7
6
  */
8
7
  export function assertLocalRequest(req) {
9
- const host = (req.headers.host ?? "").replace(/:\d+$/, "");
8
+ const fullHost = req.headers.host ?? "";
9
+ const host = fullHost.replace(/:\d+$/, "");
10
10
  if (!LOCAL_HOSTS.includes(host))
11
- throw new HttpError(403, "Acesso permitido só a partir deste computador.");
11
+ throw new HttpError(403, "Acesso permitido só a partir deste computador.", "forbidden-host");
12
12
  const origin = req.headers.origin;
13
- if (origin && !LOCAL_ORIGIN.test(origin))
14
- throw new HttpError(403, "Origem não permitida.");
13
+ // Mesma origem do Host, porta inclusa. O proxy do Vite não reescreve o Host (changeOrigin desligado), então 5173 bate com 5173.
14
+ if (origin && origin !== `http://${fullHost}`)
15
+ throw new HttpError(403, "Origem não permitida.", "forbidden-origin");
15
16
  }
16
17
  //# sourceMappingURL=security.js.map