@json-to-office/jto 0.33.2 → 0.35.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.
Files changed (42) hide show
  1. package/dist/cli.js +214 -516
  2. package/dist/cli.js.map +1 -1
  3. package/dist/client/assets/HomePage-DgVVosCp.js +106 -0
  4. package/dist/client/assets/HomePage-DgVVosCp.js.map +1 -0
  5. package/dist/client/assets/{JsonEditorPage-GZV7oJD2.js → JsonEditorPage-13oxEHfY.js} +3 -3
  6. package/dist/client/assets/{JsonEditorPage-GZV7oJD2.js.map → JsonEditorPage-13oxEHfY.js.map} +1 -1
  7. package/dist/client/assets/{MonacoPluginProvider-btNzJ41o.js → MonacoPluginProvider-DiUyU9px.js} +3 -3
  8. package/dist/client/assets/{MonacoPluginProvider-btNzJ41o.js.map → MonacoPluginProvider-DiUyU9px.js.map} +1 -1
  9. package/dist/client/assets/editor-monaco-json-CTrPZKuA.js +24 -0
  10. package/dist/client/assets/{editor-monaco-json-rhvIaBDI.js.map → editor-monaco-json-CTrPZKuA.js.map} +1 -1
  11. package/dist/client/assets/{editor-Cugq4RzH.js → editor-ouhvdGgK.js} +2 -2
  12. package/dist/client/assets/{editor-Cugq4RzH.js.map → editor-ouhvdGgK.js.map} +1 -1
  13. package/dist/client/assets/{editor-refs-store-DeJZ8FB9.js → editor-refs-store-DNQzN0_z.js} +2 -2
  14. package/dist/client/assets/{editor-refs-store-DeJZ8FB9.js.map → editor-refs-store-DNQzN0_z.js.map} +1 -1
  15. package/dist/client/assets/index-CRuLxurM.js +5 -0
  16. package/dist/client/assets/index-CRuLxurM.js.map +1 -0
  17. package/dist/client/assets/index-CnUTzeor.css +1 -0
  18. package/dist/client/assets/preview-D3QYaLPp.js +4 -0
  19. package/dist/client/assets/preview-D3QYaLPp.js.map +1 -0
  20. package/dist/client/assets/{ui-vendor-Cs3OXpi7.js → ui-vendor-DyQLnWB8.js} +16 -11
  21. package/dist/client/assets/ui-vendor-DyQLnWB8.js.map +1 -0
  22. package/dist/client/index.html +3 -3
  23. package/dist/client/templates/Alternative deck 16_9.pptx.json +91 -49
  24. package/dist/client/templates/Company deck 16_9.pptx.json +134 -70
  25. package/dist/client/templates/Company deck 4_3.pptx.json +84 -44
  26. package/dist/client/templates/data-report-presentation.pptx.json +100 -52
  27. package/dist/client/templates/modern-annual-report-1.docx.json +207 -116
  28. package/dist/client/templates/modern-annual-report-2.docx.json +52 -37
  29. package/dist/client/templates/modern-annual-report-3.docx.json +40 -20
  30. package/dist/client/templates/tech-report.docx.json +96 -48
  31. package/dist/render-server.js +168 -106
  32. package/dist/render-server.js.map +1 -1
  33. package/package.json +7 -8
  34. package/dist/client/assets/HomePage-Cq6woGMG.js +0 -106
  35. package/dist/client/assets/HomePage-Cq6woGMG.js.map +0 -1
  36. package/dist/client/assets/editor-monaco-json-rhvIaBDI.js +0 -24
  37. package/dist/client/assets/index-C9ply7vX.css +0 -1
  38. package/dist/client/assets/index-C9uVroFQ.js +0 -5
  39. package/dist/client/assets/index-C9uVroFQ.js.map +0 -1
  40. package/dist/client/assets/preview-B_B09gpA.js +0 -3
  41. package/dist/client/assets/preview-B_B09gpA.js.map +0 -1
  42. package/dist/client/assets/ui-vendor-Cs3OXpi7.js.map +0 -1
@@ -16,13 +16,111 @@ import {
16
16
  DEFAULT_VISUAL_DPI,
17
17
  MIN_VISUAL_DPI,
18
18
  MAX_VISUAL_DPI,
19
- MAX_RASTERIZE_BATCH_SLIDES
19
+ MAX_RASTERIZE_BATCH_SLIDES,
20
+ MAX_RASTERIZE_FONTS,
21
+ MAX_RASTERIZE_FONT_BYTES
20
22
  } from "@json-to-office/shared";
21
23
  import {
22
24
  createLibreOfficePptxRasterizer,
23
25
  createLibreOfficePptxBatchRasterizer
24
26
  } from "@json-to-office/jto-cli";
25
27
 
28
+ // src/server/config/index.ts
29
+ import dotenv from "dotenv";
30
+ dotenv.config();
31
+ function positiveInteger(value, fallback) {
32
+ const parsed = Number.parseInt(value ?? "", 10);
33
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
34
+ }
35
+ function parseAuthMode(value, nodeEnv) {
36
+ if (value === "auto" || value === "required" || value === "disabled") {
37
+ return value;
38
+ }
39
+ return nodeEnv === "production" ? "required" : "auto";
40
+ }
41
+ function parseOutboundSourceMode(value, nodeEnv) {
42
+ if (value === "development" || value === "safe") return value;
43
+ return nodeEnv === "production" ? "safe" : "development";
44
+ }
45
+ function normalizeNodeEnv(value) {
46
+ return value === "development" || value === "test" ? value : "production";
47
+ }
48
+ function parseEnv(env) {
49
+ const nodeEnv = normalizeNodeEnv(env.NODE_ENV || "development");
50
+ return {
51
+ NODE_ENV: nodeEnv,
52
+ // No PORT here: the listener is opened by the dev server from the CLI
53
+ // config (`-p` > `server.port` > `PORT` env > format default), so a second
54
+ // copy of it in this module would be silently ignored.
55
+ CORS_ORIGIN: env.CORS_ORIGIN || "*",
56
+ API_KEY: env.API_KEY,
57
+ API_KEY_HEADER: env.API_KEY_HEADER || "x-api-key",
58
+ API_AUTH_MODE: parseAuthMode(env.API_AUTH_MODE, nodeEnv),
59
+ RATE_LIMIT_WINDOW_MS: positiveInteger(env.RATE_LIMIT_WINDOW_MS, 9e5),
60
+ RATE_LIMIT_MAX: positiveInteger(
61
+ env.RATE_LIMIT_MAX,
62
+ nodeEnv === "production" ? 100 : 1e3
63
+ ),
64
+ TRUST_PROXY_HEADERS: env.TRUST_PROXY_HEADERS === "true",
65
+ MAX_FILE_SIZE: positiveInteger(env.MAX_FILE_SIZE, 10 * 1024 * 1024),
66
+ MAX_REQUEST_BODY_SIZE: positiveInteger(
67
+ env.MAX_REQUEST_BODY_SIZE,
68
+ 32 * 1024 * 1024
69
+ ),
70
+ MAX_CONCURRENT_REQUESTS: positiveInteger(
71
+ env.MAX_CONCURRENT_REQUESTS,
72
+ nodeEnv === "production" ? 8 : 64
73
+ ),
74
+ OUTBOUND_SOURCE_MODE: parseOutboundSourceMode(
75
+ env.OUTBOUND_SOURCE_MODE,
76
+ nodeEnv
77
+ ),
78
+ OUTBOUND_HOST_ALLOWLIST: (env.OUTBOUND_HOST_ALLOWLIST || "").split(",").map((host) => host.trim().toLowerCase()).filter(Boolean),
79
+ LIBREOFFICE_PATH: env.LIBREOFFICE_PATH,
80
+ LIBREOFFICE_TIMEOUT_MS: env.LIBREOFFICE_TIMEOUT_MS ? positiveInteger(env.LIBREOFFICE_TIMEOUT_MS, 3e4) : 3e4,
81
+ LOG_LEVEL: env.LOG_LEVEL || "info",
82
+ CACHE_ENABLED: env.CACHE_ENABLED !== "false",
83
+ CACHE_MAX_SIZE_MB: positiveInteger(env.CACHE_MAX_SIZE_MB, 100),
84
+ CACHE_TTL_SECONDS: positiveInteger(env.CACHE_TTL_SECONDS, 3600),
85
+ CACHE_MAX_ITEMS: positiveInteger(env.CACHE_MAX_ITEMS, 1e3)
86
+ };
87
+ }
88
+ var parsedEnv = parseEnv(process.env);
89
+ var config = {
90
+ ...parsedEnv,
91
+ isDevelopment: parsedEnv.NODE_ENV === "development",
92
+ isProduction: parsedEnv.NODE_ENV === "production",
93
+ isTest: parsedEnv.NODE_ENV === "test",
94
+ features: {
95
+ apiKey: parsedEnv.API_AUTH_MODE !== "disabled",
96
+ cache: parsedEnv.CACHE_ENABLED
97
+ },
98
+ cors: {
99
+ origin: parsedEnv.CORS_ORIGIN === "*" ? parsedEnv.CORS_ORIGIN : parsedEnv.CORS_ORIGIN.split(",").map((o) => o.trim()),
100
+ credentials: true
101
+ },
102
+ rateLimit: {
103
+ windowMs: parsedEnv.RATE_LIMIT_WINDOW_MS,
104
+ max: parsedEnv.RATE_LIMIT_MAX,
105
+ trustProxy: parsedEnv.TRUST_PROXY_HEADERS
106
+ },
107
+ requestLimits: {
108
+ maxBodySize: parsedEnv.MAX_REQUEST_BODY_SIZE,
109
+ maxConcurrent: parsedEnv.MAX_CONCURRENT_REQUESTS,
110
+ maxFileSize: parsedEnv.MAX_FILE_SIZE
111
+ },
112
+ outboundSources: {
113
+ mode: parsedEnv.OUTBOUND_SOURCE_MODE,
114
+ allowedHosts: parsedEnv.OUTBOUND_HOST_ALLOWLIST
115
+ },
116
+ cache: {
117
+ enabled: parsedEnv.CACHE_ENABLED,
118
+ maxSizeMB: parsedEnv.CACHE_MAX_SIZE_MB,
119
+ ttlSeconds: parsedEnv.CACHE_TTL_SECONDS,
120
+ maxItems: parsedEnv.CACHE_MAX_ITEMS
121
+ }
122
+ };
123
+
26
124
  // src/server/lib/typebox-validator.ts
27
125
  import { Value } from "@sinclair/typebox/value";
28
126
  import { HTTPException } from "hono/http-exception";
@@ -386,13 +484,39 @@ function assertSafeRendererPayload(value, policy) {
386
484
  }
387
485
 
388
486
  // src/server/rasterize-route.ts
487
+ var BASE64_PATTERN = "^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$";
488
+ var RasterizeFontFaceSchema = Type.Object(
489
+ {
490
+ family: Type.String({ minLength: 1, maxLength: 128 }),
491
+ weight: Type.Integer({ minimum: 1, maximum: 1e3 }),
492
+ italic: Type.Boolean(),
493
+ data: Type.String({
494
+ minLength: 1,
495
+ maxLength: 4 * 1024 * 1024,
496
+ pattern: BASE64_PATTERN
497
+ }),
498
+ format: Type.Optional(
499
+ Type.Union([
500
+ Type.Literal("ttf"),
501
+ Type.Literal("otf"),
502
+ Type.Literal("woff"),
503
+ Type.Literal("woff2")
504
+ ])
505
+ )
506
+ },
507
+ { additionalProperties: false }
508
+ );
509
+ var FontsSchema = Type.Optional(
510
+ Type.Array(RasterizeFontFaceSchema, { maxItems: MAX_RASTERIZE_FONTS })
511
+ );
389
512
  var RasterizeRequestSchema = Type.Object(
390
513
  {
391
514
  presentation: Type.Object({}, { additionalProperties: true }),
392
515
  dpi: Type.Optional(
393
516
  Type.Number({ minimum: MIN_VISUAL_DPI, maximum: MAX_VISUAL_DPI })
394
517
  ),
395
- baseDir: Type.Optional(Type.String())
518
+ baseDir: Type.Optional(Type.String()),
519
+ fonts: FontsSchema
396
520
  },
397
521
  { additionalProperties: false }
398
522
  );
@@ -410,7 +534,13 @@ var RasterizeBatchRequestSchema = Type.Object(
410
534
  ),
411
535
  { minItems: 1, maxItems: MAX_RASTERIZE_BATCH_SLIDES }
412
536
  ),
413
- baseDir: Type.Optional(Type.String())
537
+ baseDir: Type.Optional(Type.String()),
538
+ // Request-level, shared by every slide, exactly like `baseDir`. NOT
539
+ // inside the per-slide object above, whose `additionalProperties: false`
540
+ // must keep rejecting per-slide fonts: a uniform slide shape is what
541
+ // lets batch dedupe and the disk-cache key stay identical to the
542
+ // single-slide path.
543
+ fonts: FontsSchema
414
544
  },
415
545
  { additionalProperties: false }
416
546
  );
@@ -465,6 +595,18 @@ function assertPixelBudget(slides) {
465
595
  });
466
596
  }
467
597
  }
598
+ function assertFontBudget(fonts) {
599
+ if (!fonts?.length) return;
600
+ const bytes = fonts.reduce(
601
+ (sum, f) => sum + Math.ceil(f.data.length * 0.75),
602
+ 0
603
+ );
604
+ if (bytes > MAX_RASTERIZE_FONT_BYTES) {
605
+ throw new HTTPException2(400, {
606
+ message: "Requested font payload is too large"
607
+ });
608
+ }
609
+ }
468
610
  function resolveSafeBaseDir(baseDir) {
469
611
  if (baseDir === void 0) return void 0;
470
612
  const resolved = path.resolve(baseDir);
@@ -492,13 +634,23 @@ function toHttpException(error) {
492
634
  message: "Internal server error during rasterization"
493
635
  });
494
636
  }
637
+ var MAX_RASTERIZE_BODY_CEILING = 64 * 1024 * 1024;
638
+ function resolveMaxBodyBytes(explicit) {
639
+ const fromEnv = Number.parseInt(
640
+ process.env.MAX_RASTERIZE_BODY_SIZE ?? "",
641
+ 10
642
+ );
643
+ const configured = explicit ?? (Number.isFinite(fromEnv) && fromEnv > 0 ? fromEnv : config.requestLimits.maxBodySize);
644
+ return Math.min(configured, MAX_RASTERIZE_BODY_CEILING);
645
+ }
495
646
  function registerRasterizeRoute(router, options = {}) {
496
647
  const getRasterizer = options.getRasterizer ?? getSharedRasterizer;
497
648
  const getBatchRasterizer = options.getBatchRasterizer ?? getSharedBatchRasterizer;
649
+ const maxBodyBytes = resolveMaxBodyBytes(options.maxBodyBytes);
498
650
  const shared = [
499
651
  ...options.preMiddleware ?? [],
500
652
  bodyLimit({
501
- maxSize: 32 * 1024 * 1024,
653
+ maxSize: maxBodyBytes,
502
654
  onError: () => {
503
655
  throw new HTTPException2(413, { message: "Request body too large" });
504
656
  }
@@ -518,10 +670,11 @@ function registerRasterizeRoute(router, options = {}) {
518
670
  ...shared,
519
671
  tbValidator(RasterizeRequestSchema),
520
672
  async (c) => {
521
- const { presentation, dpi, baseDir } = getValidated(c, "json");
673
+ const { presentation, dpi, baseDir, fonts } = getValidated(c, "json");
522
674
  const safeBaseDir = resolveSafeBaseDir(baseDir);
523
675
  const effectiveDpi = clampVisualDpi(dpi ?? DEFAULT_VISUAL_DPI);
524
676
  assertPixelBudget([{ presentation, dpi: effectiveDpi }]);
677
+ assertFontBudget(fonts);
525
678
  const result = await guard(async () => {
526
679
  if (options.sourcePolicy) {
527
680
  assertSafeOutboundSources(
@@ -533,7 +686,12 @@ function registerRasterizeRoute(router, options = {}) {
533
686
  return getRasterizer()({
534
687
  presentation,
535
688
  dpi: effectiveDpi,
536
- baseDir: safeBaseDir
689
+ baseDir: safeBaseDir,
690
+ // Conditional so a fontless request reaches the engine with no
691
+ // `fonts` key at all — an `undefined` would still be an absent
692
+ // digest, but keeping the object identical to the pre-change shape
693
+ // makes the cache-key equivalence obvious.
694
+ ...fonts?.length && { fonts }
537
695
  });
538
696
  });
539
697
  return c.json(result);
@@ -544,13 +702,14 @@ function registerRasterizeRoute(router, options = {}) {
544
702
  ...shared,
545
703
  tbValidator(RasterizeBatchRequestSchema),
546
704
  async (c) => {
547
- const { slides, baseDir } = getValidated(c, "json");
705
+ const { slides, baseDir, fonts } = getValidated(c, "json");
548
706
  const safeBaseDir = resolveSafeBaseDir(baseDir);
549
707
  const effectiveSlides = slides.map((slide) => ({
550
708
  presentation: slide.presentation,
551
709
  dpi: clampVisualDpi(slide.dpi ?? DEFAULT_VISUAL_DPI)
552
710
  }));
553
711
  assertPixelBudget(effectiveSlides);
712
+ assertFontBudget(fonts);
554
713
  const result = await guard(async () => {
555
714
  if (options.sourcePolicy) {
556
715
  slides.forEach(
@@ -563,7 +722,8 @@ function registerRasterizeRoute(router, options = {}) {
563
722
  }
564
723
  return getBatchRasterizer()({
565
724
  slides: effectiveSlides,
566
- baseDir: safeBaseDir
725
+ baseDir: safeBaseDir,
726
+ ...fonts?.length && { fonts }
567
727
  });
568
728
  });
569
729
  return c.json({
@@ -673,104 +833,6 @@ function concurrencyLimiter(options) {
673
833
 
674
834
  // src/server/middleware/hono/auth.ts
675
835
  import { createHash, timingSafeEqual } from "crypto";
676
-
677
- // src/server/config/index.ts
678
- import dotenv from "dotenv";
679
- dotenv.config();
680
- function positiveInteger(value, fallback) {
681
- const parsed = Number.parseInt(value ?? "", 10);
682
- return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
683
- }
684
- function parseAuthMode(value, nodeEnv) {
685
- if (value === "auto" || value === "required" || value === "disabled") {
686
- return value;
687
- }
688
- return nodeEnv === "production" ? "required" : "auto";
689
- }
690
- function parseOutboundSourceMode(value, nodeEnv) {
691
- if (value === "development" || value === "safe") return value;
692
- return nodeEnv === "production" ? "safe" : "development";
693
- }
694
- function normalizeNodeEnv(value) {
695
- return value === "development" || value === "test" ? value : "production";
696
- }
697
- function parseEnv(env) {
698
- const nodeEnv = normalizeNodeEnv(env.NODE_ENV || "development");
699
- return {
700
- NODE_ENV: nodeEnv,
701
- // No PORT here: the listener is opened by the dev server from the CLI
702
- // config (`-p` > `server.port` > `PORT` env > format default), so a second
703
- // copy of it in this module would be silently ignored.
704
- CORS_ORIGIN: env.CORS_ORIGIN || "*",
705
- API_KEY: env.API_KEY,
706
- API_KEY_HEADER: env.API_KEY_HEADER || "x-api-key",
707
- API_AUTH_MODE: parseAuthMode(env.API_AUTH_MODE, nodeEnv),
708
- RATE_LIMIT_WINDOW_MS: positiveInteger(env.RATE_LIMIT_WINDOW_MS, 9e5),
709
- RATE_LIMIT_MAX: positiveInteger(
710
- env.RATE_LIMIT_MAX,
711
- nodeEnv === "production" ? 100 : 1e3
712
- ),
713
- TRUST_PROXY_HEADERS: env.TRUST_PROXY_HEADERS === "true",
714
- MAX_FILE_SIZE: positiveInteger(env.MAX_FILE_SIZE, 10 * 1024 * 1024),
715
- MAX_REQUEST_BODY_SIZE: positiveInteger(
716
- env.MAX_REQUEST_BODY_SIZE,
717
- 32 * 1024 * 1024
718
- ),
719
- MAX_CONCURRENT_REQUESTS: positiveInteger(
720
- env.MAX_CONCURRENT_REQUESTS,
721
- nodeEnv === "production" ? 8 : 64
722
- ),
723
- OUTBOUND_SOURCE_MODE: parseOutboundSourceMode(
724
- env.OUTBOUND_SOURCE_MODE,
725
- nodeEnv
726
- ),
727
- OUTBOUND_HOST_ALLOWLIST: (env.OUTBOUND_HOST_ALLOWLIST || "").split(",").map((host) => host.trim().toLowerCase()).filter(Boolean),
728
- LIBREOFFICE_PATH: env.LIBREOFFICE_PATH,
729
- LIBREOFFICE_TIMEOUT_MS: env.LIBREOFFICE_TIMEOUT_MS ? positiveInteger(env.LIBREOFFICE_TIMEOUT_MS, 3e4) : 3e4,
730
- LOG_LEVEL: env.LOG_LEVEL || "info",
731
- CACHE_ENABLED: env.CACHE_ENABLED !== "false",
732
- CACHE_MAX_SIZE_MB: positiveInteger(env.CACHE_MAX_SIZE_MB, 100),
733
- CACHE_TTL_SECONDS: positiveInteger(env.CACHE_TTL_SECONDS, 3600),
734
- CACHE_MAX_ITEMS: positiveInteger(env.CACHE_MAX_ITEMS, 1e3)
735
- };
736
- }
737
- var parsedEnv = parseEnv(process.env);
738
- var config = {
739
- ...parsedEnv,
740
- isDevelopment: parsedEnv.NODE_ENV === "development",
741
- isProduction: parsedEnv.NODE_ENV === "production",
742
- isTest: parsedEnv.NODE_ENV === "test",
743
- features: {
744
- apiKey: parsedEnv.API_AUTH_MODE !== "disabled",
745
- cache: parsedEnv.CACHE_ENABLED
746
- },
747
- cors: {
748
- origin: parsedEnv.CORS_ORIGIN === "*" ? parsedEnv.CORS_ORIGIN : parsedEnv.CORS_ORIGIN.split(",").map((o) => o.trim()),
749
- credentials: true
750
- },
751
- rateLimit: {
752
- windowMs: parsedEnv.RATE_LIMIT_WINDOW_MS,
753
- max: parsedEnv.RATE_LIMIT_MAX,
754
- trustProxy: parsedEnv.TRUST_PROXY_HEADERS
755
- },
756
- requestLimits: {
757
- maxBodySize: parsedEnv.MAX_REQUEST_BODY_SIZE,
758
- maxConcurrent: parsedEnv.MAX_CONCURRENT_REQUESTS,
759
- maxFileSize: parsedEnv.MAX_FILE_SIZE
760
- },
761
- outboundSources: {
762
- mode: parsedEnv.OUTBOUND_SOURCE_MODE,
763
- allowedHosts: parsedEnv.OUTBOUND_HOST_ALLOWLIST
764
- },
765
- cache: {
766
- enabled: parsedEnv.CACHE_ENABLED,
767
- maxSizeMB: parsedEnv.CACHE_MAX_SIZE_MB,
768
- ttlSeconds: parsedEnv.CACHE_TTL_SECONDS,
769
- maxItems: parsedEnv.CACHE_MAX_ITEMS
770
- }
771
- };
772
-
773
- // src/server/middleware/hono/auth.ts
774
836
  function keysEqual(received, expected) {
775
837
  const receivedDigest = createHash("sha256").update(received).digest();
776
838
  const expectedDigest = createHash("sha256").update(expected).digest();