@json-to-office/jto 1.11.1 → 2.2.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 (55) hide show
  1. package/dist/cli.js +444 -99
  2. package/dist/cli.js.map +1 -1
  3. package/dist/client/assets/HomePage-DMpdYyZ0.js +251 -0
  4. package/dist/client/assets/HomePage-DMpdYyZ0.js.map +1 -0
  5. package/dist/client/assets/JsonEditorPage-Ct7wb9iA.js +3 -0
  6. package/dist/client/assets/JsonEditorPage-Ct7wb9iA.js.map +1 -0
  7. package/dist/client/assets/MonacoPluginProvider-B2fWYflh.js +85 -0
  8. package/dist/client/assets/MonacoPluginProvider-B2fWYflh.js.map +1 -0
  9. package/dist/client/assets/_virtual_jto-plugin-type-libs-BlMraosv.js +25700 -0
  10. package/dist/client/assets/_virtual_jto-plugin-type-libs-BlMraosv.js.map +1 -0
  11. package/dist/client/assets/_virtual_jto-sandbox-runtime-CNoAasd3.js +2 -0
  12. package/dist/client/assets/_virtual_jto-sandbox-runtime-CNoAasd3.js.map +1 -0
  13. package/dist/client/assets/editor-DFnlAOyt.js +16 -0
  14. package/dist/client/assets/editor-DFnlAOyt.js.map +1 -0
  15. package/dist/client/assets/{editor-monaco-json-BYUK4OUI.js → editor-monaco-json-B_1-Wxq5.js} +5 -5
  16. package/dist/client/assets/{editor-monaco-json-BYUK4OUI.js.map → editor-monaco-json-B_1-Wxq5.js.map} +1 -1
  17. package/dist/client/assets/{editor-refs-store-ChL2fzuX.js → editor-refs-store-DHeIvmr6.js} +2 -2
  18. package/dist/client/assets/{editor-refs-store-ChL2fzuX.js.map → editor-refs-store-DHeIvmr6.js.map} +1 -1
  19. package/dist/client/assets/index-Bj6ZAKUc.css +1 -0
  20. package/dist/client/assets/index-CeZaIyj_.js +5 -0
  21. package/dist/client/assets/index-CeZaIyj_.js.map +1 -0
  22. package/dist/client/assets/preview-CugO7y1j.js +4 -0
  23. package/dist/client/assets/preview-CugO7y1j.js.map +1 -0
  24. package/dist/client/assets/radix-ui-BzbaiLpZ.js +51 -0
  25. package/dist/client/assets/radix-ui-BzbaiLpZ.js.map +1 -0
  26. package/dist/client/assets/{ui-vendor-Bq4-oNFL.js → ui-vendor-BsAtAN16.js} +94 -64
  27. package/dist/client/assets/ui-vendor-BsAtAN16.js.map +1 -0
  28. package/dist/client/assets/useQualityAnalysis-DH2idzAY.js +6 -0
  29. package/dist/client/assets/useQualityAnalysis-DH2idzAY.js.map +1 -0
  30. package/dist/client/index.html +4 -4
  31. package/dist/client/templates/modern-annual-report-1.docx.json +0 -1
  32. package/dist/client/templates/modern-annual-report-2.docx.json +2 -4
  33. package/dist/client/templates/modern-annual-report-3.docx.json +1 -2
  34. package/dist/client/templates/standard-annual-report.docx.json +0 -1
  35. package/dist/client/templates/vermilion-annual-report.docx.json +0 -9
  36. package/dist/render-server.js.map +1 -1
  37. package/package.json +8 -6
  38. package/dist/client/assets/HomePage-r7ryeW_v.js +0 -106
  39. package/dist/client/assets/HomePage-r7ryeW_v.js.map +0 -1
  40. package/dist/client/assets/JsonEditorPage-BxUyYH8Z.js +0 -3
  41. package/dist/client/assets/JsonEditorPage-BxUyYH8Z.js.map +0 -1
  42. package/dist/client/assets/MonacoPluginProvider-CGvtVJKz.js +0 -3
  43. package/dist/client/assets/MonacoPluginProvider-CGvtVJKz.js.map +0 -1
  44. package/dist/client/assets/editor-gT4UjmPk.js +0 -9
  45. package/dist/client/assets/editor-gT4UjmPk.js.map +0 -1
  46. package/dist/client/assets/index-CgCY8SiY.js +0 -5
  47. package/dist/client/assets/index-CgCY8SiY.js.map +0 -1
  48. package/dist/client/assets/index-CrzIwHLS.css +0 -1
  49. package/dist/client/assets/preview-ZqhmQZJG.js +0 -2
  50. package/dist/client/assets/preview-ZqhmQZJG.js.map +0 -1
  51. package/dist/client/assets/radix-ui-DFNP5I4e.js +0 -51
  52. package/dist/client/assets/radix-ui-DFNP5I4e.js.map +0 -1
  53. package/dist/client/assets/ui-vendor-Bq4-oNFL.js.map +0 -1
  54. package/dist/client/assets/useQualityAnalysis-eKT8VcIt.js +0 -6
  55. package/dist/client/assets/useQualityAnalysis-eKT8VcIt.js.map +0 -1
package/dist/cli.js CHANGED
@@ -34,6 +34,14 @@ function parseOutboundSourceMode(value, nodeEnv) {
34
34
  if (value === "development" || value === "safe") return value;
35
35
  return nodeEnv === "production" ? "safe" : "development";
36
36
  }
37
+ function pluginAutoloadEnabled(env = process.env) {
38
+ const explicit = env.PLUGIN_AUTOLOAD?.trim();
39
+ if (explicit) return explicit === "true" || explicit === "1";
40
+ return normalizeNodeEnv(env.NODE_ENV || "development") !== "production";
41
+ }
42
+ function requestTriggeredPluginLoadAllowed(env = process.env) {
43
+ return pluginAutoloadEnabled(env) && normalizeNodeEnv(env.NODE_ENV || "development") !== "production";
44
+ }
37
45
  function normalizeNodeEnv(value) {
38
46
  return value === "development" || value === "test" ? value : "production";
39
47
  }
@@ -2005,8 +2013,8 @@ async function toDataUrl(source, baseDir, state, mimeByExtension) {
2005
2013
  if (cached !== void 0) return cached;
2006
2014
  const contained = resolved === state.baseDirReal || resolved.startsWith(state.baseDirReal + path4.sep);
2007
2015
  if (contained) {
2008
- const stat = await fs2.stat(resolved);
2009
- if (stat.isFile() && stat.size <= state.maxFileBytes && stat.size <= state.totalBudget) {
2016
+ const stat2 = await fs2.stat(resolved);
2017
+ if (stat2.isFile() && stat2.size <= state.maxFileBytes && stat2.size <= state.totalBudget) {
2010
2018
  const buffer = await fs2.readFile(resolved);
2011
2019
  state.totalBudget -= buffer.length;
2012
2020
  result = `data:${mime};base64,${buffer.toString("base64")}`;
@@ -2712,13 +2720,213 @@ var init_format = __esm({
2712
2720
  }
2713
2721
  });
2714
2722
 
2723
+ // src/server/middleware/hono/auth.ts
2724
+ import { createHash, timingSafeEqual } from "crypto";
2725
+ function keysEqual(received, expected) {
2726
+ const receivedDigest = createHash("sha256").update(received).digest();
2727
+ const expectedDigest = createHash("sha256").update(expected).digest();
2728
+ return timingSafeEqual(receivedDigest, expectedDigest);
2729
+ }
2730
+ function readCredential(headers, headerName) {
2731
+ const direct = headers.get(headerName)?.trim();
2732
+ if (direct) {
2733
+ if (headerName.toLowerCase() === "authorization" && direct.toLowerCase().startsWith("bearer ")) {
2734
+ return direct.slice(7).trim() || void 0;
2735
+ }
2736
+ return direct;
2737
+ }
2738
+ const authorization = headers.get("authorization")?.trim();
2739
+ if (authorization?.toLowerCase().startsWith("bearer ")) {
2740
+ return authorization.slice(7).trim() || void 0;
2741
+ }
2742
+ return void 0;
2743
+ }
2744
+ function hasValidApiKey(headers, env = process.env) {
2745
+ const expected = env.API_KEY?.trim();
2746
+ if (!expected) return false;
2747
+ const received = readCredential(headers, env.API_KEY_HEADER || "x-api-key");
2748
+ if (!received) return false;
2749
+ return keysEqual(received, expected);
2750
+ }
2751
+ function createApiKeyAuthMiddleware(options) {
2752
+ const headerName = options.headerName || "x-api-key";
2753
+ return async (c, next) => {
2754
+ if (c.req.method === "OPTIONS" || options.mode === "disabled") {
2755
+ return next();
2756
+ }
2757
+ if (!options.apiKey) {
2758
+ if (options.mode === "auto") return next();
2759
+ return c.json(
2760
+ {
2761
+ success: false,
2762
+ error: "API authentication is not configured",
2763
+ code: "AUTH_CONFIGURATION_ERROR"
2764
+ },
2765
+ 503
2766
+ );
2767
+ }
2768
+ const apiKey = readCredential(c.req.raw.headers, headerName);
2769
+ if (!apiKey) {
2770
+ return c.json(
2771
+ {
2772
+ success: false,
2773
+ error: "API key required",
2774
+ code: "UNAUTHORIZED"
2775
+ },
2776
+ 401
2777
+ );
2778
+ }
2779
+ if (!keysEqual(apiKey, options.apiKey)) {
2780
+ return c.json(
2781
+ {
2782
+ success: false,
2783
+ error: "Invalid API key",
2784
+ code: "UNAUTHORIZED"
2785
+ },
2786
+ 401
2787
+ );
2788
+ }
2789
+ await next();
2790
+ };
2791
+ }
2792
+ var apiKeyAuthMiddleware;
2793
+ var init_auth = __esm({
2794
+ "src/server/middleware/hono/auth.ts"() {
2795
+ "use strict";
2796
+ init_esm_shims();
2797
+ init_config();
2798
+ apiKeyAuthMiddleware = createApiKeyAuthMiddleware({
2799
+ mode: config.API_AUTH_MODE,
2800
+ apiKey: config.API_KEY,
2801
+ headerName: config.API_KEY_HEADER
2802
+ });
2803
+ }
2804
+ });
2805
+
2806
+ // src/server/lib/browser-plugin-schema.ts
2807
+ function jsonDepth(value) {
2808
+ let deepest = 0;
2809
+ const stack = [{ value, depth: 1 }];
2810
+ while (stack.length > 0) {
2811
+ const { value: current, depth } = stack.pop();
2812
+ if (current === null || typeof current !== "object") continue;
2813
+ if (depth > deepest) deepest = depth;
2814
+ const children = Array.isArray(current) ? current : Object.values(current);
2815
+ for (const child of children) {
2816
+ if (child !== null && typeof child === "object") {
2817
+ stack.push({ value: child, depth: depth + 1 });
2818
+ }
2819
+ }
2820
+ }
2821
+ return deepest;
2822
+ }
2823
+ function isRecord3(value) {
2824
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2825
+ }
2826
+ function namespaceSchemaIds(schema, namespace) {
2827
+ const ids = /* @__PURE__ */ new Set();
2828
+ const collect = (node) => {
2829
+ if (Array.isArray(node)) {
2830
+ node.forEach(collect);
2831
+ return;
2832
+ }
2833
+ if (!isRecord3(node)) return;
2834
+ if (typeof node.$id === "string") ids.add(node.$id);
2835
+ Object.values(node).forEach(collect);
2836
+ };
2837
+ collect(schema);
2838
+ const rename = (id) => `${namespace}:${id}`;
2839
+ const rewrite = (node) => {
2840
+ if (Array.isArray(node)) return node.map(rewrite);
2841
+ if (!isRecord3(node)) return node;
2842
+ const out = {};
2843
+ for (const [key, value] of Object.entries(node)) {
2844
+ if (key === "$id" && typeof value === "string") {
2845
+ out[key] = rename(value);
2846
+ } else if (key === "$ref" && typeof value === "string") {
2847
+ if (ids.has(value)) out[key] = rename(value);
2848
+ else if (value.startsWith("#")) out[key] = value;
2849
+ else {
2850
+ throw new BrowserPluginSchemaError(
2851
+ `propsSchema references "${value}"; only ids defined in the schema itself and local "#" pointers are allowed.`
2852
+ );
2853
+ }
2854
+ } else if (key === "$schema") {
2855
+ continue;
2856
+ } else {
2857
+ out[key] = rewrite(value);
2858
+ }
2859
+ }
2860
+ return out;
2861
+ };
2862
+ return rewrite(schema);
2863
+ }
2864
+ function prepareBrowserPlugins(components) {
2865
+ const bytes = JSON.stringify(components).length;
2866
+ if (bytes > MAX_BROWSER_SCHEMA_BYTES) {
2867
+ throw new BrowserPluginSchemaError(
2868
+ `customComponents is ${Math.round(bytes / 1024)} KB serialized; the limit is ${MAX_BROWSER_SCHEMA_BYTES / 1024} KB.`
2869
+ );
2870
+ }
2871
+ const seen = /* @__PURE__ */ new Set();
2872
+ return components.map((component) => {
2873
+ if (seen.has(component.name)) {
2874
+ throw new BrowserPluginSchemaError(
2875
+ `customComponents lists "${component.name}" more than once.`
2876
+ );
2877
+ }
2878
+ seen.add(component.name);
2879
+ return {
2880
+ ...component,
2881
+ versions: component.versions.map((version) => {
2882
+ const depth = jsonDepth(version.propsSchema);
2883
+ if (depth > MAX_BROWSER_SCHEMA_DEPTH) {
2884
+ throw new BrowserPluginSchemaError(
2885
+ `propsSchema of "${component.name}" v${version.version} nests ${depth} levels deep; the limit is ${MAX_BROWSER_SCHEMA_DEPTH}.`
2886
+ );
2887
+ }
2888
+ return {
2889
+ ...version,
2890
+ propsSchema: namespaceSchemaIds(
2891
+ version.propsSchema,
2892
+ `${component.name}@${version.version}`
2893
+ )
2894
+ };
2895
+ })
2896
+ };
2897
+ });
2898
+ }
2899
+ var MAX_BROWSER_SCHEMA_BYTES, MAX_BROWSER_SCHEMA_DEPTH, BrowserPluginSchemaError;
2900
+ var init_browser_plugin_schema = __esm({
2901
+ "src/server/lib/browser-plugin-schema.ts"() {
2902
+ "use strict";
2903
+ init_esm_shims();
2904
+ MAX_BROWSER_SCHEMA_BYTES = 256 * 1024;
2905
+ MAX_BROWSER_SCHEMA_DEPTH = 32;
2906
+ BrowserPluginSchemaError = class extends Error {
2907
+ constructor(message) {
2908
+ super(message);
2909
+ this.name = "BrowserPluginSchemaError";
2910
+ }
2911
+ };
2912
+ }
2913
+ });
2914
+
2715
2915
  // src/server/routes/discovery.ts
2716
2916
  import { Hono as Hono3 } from "hono";
2917
+ import { HTTPException as HTTPException5 } from "hono/http-exception";
2918
+ import { bodyLimit as bodyLimit3 } from "hono/body-limit";
2919
+ import { Type as Type3 } from "@sinclair/typebox";
2920
+ import { readFile, stat } from "fs/promises";
2717
2921
  import {
2718
2922
  PluginDiscoveryService as PluginDiscoveryService2,
2719
2923
  PluginRegistry as PluginRegistry3
2720
2924
  } from "@json-to-office/jto-cli";
2721
2925
  import { latestVersion } from "@json-to-office/shared";
2926
+ function sameSiteOnly(c) {
2927
+ const site = c.req.header("sec-fetch-site");
2928
+ return site === void 0 || site === "same-origin" || site === "none";
2929
+ }
2722
2930
  function cleanupTypeBoxIds(schema) {
2723
2931
  if (typeof schema !== "object" || schema === null) return;
2724
2932
  if (schema.$id && /^T\d+$/.test(schema.$id)) delete schema.$id;
@@ -2734,7 +2942,7 @@ function cleanupTypeBoxIds(schema) {
2734
2942
  });
2735
2943
  }
2736
2944
  async function ensurePluginsRegistered(format, pluginNames) {
2737
- if (process.env.NODE_ENV === "production") return;
2945
+ if (!requestTriggeredPluginLoadAllowed()) return;
2738
2946
  const registry = PluginRegistry3.getInstance();
2739
2947
  const satisfied = pluginNames ? pluginNames.every((name) => registry.getPlugin(name)) : registry.hasPlugins();
2740
2948
  if (satisfied) return;
@@ -2759,9 +2967,12 @@ function getSelectedPlugins(pluginNames) {
2759
2967
  return { name: plugin.name, versions, versionKeys, latest, latestEntry };
2760
2968
  });
2761
2969
  }
2762
- async function generateDocumentSchema(format, pluginNames) {
2970
+ async function generateDocumentSchema(format, pluginNames, browserPlugins = []) {
2763
2971
  await ensurePluginsRegistered(format, pluginNames);
2764
- const selected = getSelectedPlugins(pluginNames);
2972
+ const browserNames = new Set(browserPlugins.map((p) => p.name));
2973
+ const selected = getSelectedPlugins(pluginNames).filter(
2974
+ (p) => !browserNames.has(p.name)
2975
+ );
2765
2976
  if (format === "docx") {
2766
2977
  const shared = await import("@json-to-office/shared-docx");
2767
2978
  const customComponents = selected.filter((p) => p.latestEntry?.propsSchema).map((p) => {
@@ -2781,6 +2992,25 @@ async function generateDocumentSchema(format, pluginNames) {
2781
2992
  }
2782
2993
  return info;
2783
2994
  });
2995
+ for (const plugin of browserPlugins) {
2996
+ const latest = latestVersion(plugin.versions.map((v) => v.version));
2997
+ const latestEntry = plugin.versions.find((v) => v.version === latest);
2998
+ const info = {
2999
+ name: plugin.name,
3000
+ propsSchema: latestEntry.propsSchema,
3001
+ hasChildren: latestEntry.hasChildren,
3002
+ description: latestEntry.description
3003
+ };
3004
+ if (plugin.versions.length > 1) {
3005
+ info.versionedProps = plugin.versions.map((v) => ({
3006
+ version: v.version,
3007
+ propsSchema: v.propsSchema,
3008
+ description: v.description,
3009
+ hasChildren: v.hasChildren
3010
+ }));
3011
+ }
3012
+ customComponents.push(info);
3013
+ }
2784
3014
  const unified = shared.generateUnifiedDocumentSchema({ customComponents });
2785
3015
  return shared.convertToJsonSchema(unified, {
2786
3016
  $schema: "http://json-schema.org/draft-07/schema#",
@@ -2799,6 +3029,17 @@ async function generateDocumentSchema(format, pluginNames) {
2799
3029
  description: p.versions[v].description
2800
3030
  }))
2801
3031
  }));
3032
+ for (const plugin of browserPlugins) {
3033
+ customComponents.push({
3034
+ name: plugin.name,
3035
+ versions: plugin.versions.map((v) => ({
3036
+ version: v.version,
3037
+ propsSchema: v.propsSchema,
3038
+ hasChildren: v.hasChildren,
3039
+ description: v.description
3040
+ }))
3041
+ });
3042
+ }
2802
3043
  const unified = shared.generateUnifiedDocumentSchema({ customComponents });
2803
3044
  return shared.convertToJsonSchema(unified, {
2804
3045
  $schema: "http://json-schema.org/draft-07/schema#",
@@ -2830,14 +3071,54 @@ async function generateThemeSchema(format) {
2830
3071
  description: `Theme definition for JSON to ${label} ${format === "docx" ? "documents" : "presentations"}`
2831
3072
  };
2832
3073
  }
2833
- var discoveryRouter;
3074
+ var discoveryRouter, BrowserPluginVersionSchema, BrowserPluginSchema, DocumentSchemaRequestSchema, MAX_PLUGIN_SOURCE_BYTES;
2834
3075
  var init_discovery = __esm({
2835
3076
  "src/server/routes/discovery.ts"() {
2836
3077
  "use strict";
2837
3078
  init_esm_shims();
2838
3079
  init_logger();
2839
3080
  init_container();
3081
+ init_typebox_validator();
3082
+ init_rate_limit();
3083
+ init_auth();
3084
+ init_config();
3085
+ init_browser_plugin_schema();
2840
3086
  discoveryRouter = new Hono3();
3087
+ BrowserPluginVersionSchema = Type3.Object(
3088
+ {
3089
+ version: Type3.String({ pattern: "^\\d+\\.\\d+\\.\\d+$" }),
3090
+ propsSchema: Type3.Object({}, { additionalProperties: true }),
3091
+ hasChildren: Type3.Optional(Type3.Boolean()),
3092
+ description: Type3.Optional(Type3.String({ maxLength: 2e3 }))
3093
+ },
3094
+ { additionalProperties: false }
3095
+ );
3096
+ BrowserPluginSchema = Type3.Object(
3097
+ {
3098
+ name: Type3.String({
3099
+ minLength: 1,
3100
+ maxLength: 64,
3101
+ pattern: "^[a-zA-Z][a-zA-Z0-9_-]*$"
3102
+ }),
3103
+ versions: Type3.Array(BrowserPluginVersionSchema, {
3104
+ minItems: 1,
3105
+ maxItems: 10
3106
+ })
3107
+ },
3108
+ { additionalProperties: false }
3109
+ );
3110
+ DocumentSchemaRequestSchema = Type3.Object(
3111
+ {
3112
+ plugins: Type3.Optional(
3113
+ Type3.Array(Type3.String({ maxLength: 128 }), { maxItems: 100 })
3114
+ ),
3115
+ customComponents: Type3.Optional(
3116
+ Type3.Array(BrowserPluginSchema, { maxItems: 20 })
3117
+ )
3118
+ },
3119
+ { additionalProperties: false }
3120
+ );
3121
+ MAX_PLUGIN_SOURCE_BYTES = 512 * 1024;
2841
3122
  discoveryRouter.get("/all", async (c) => {
2842
3123
  try {
2843
3124
  const format = Container.getAdapter().name;
@@ -2851,7 +3132,12 @@ var init_discovery = __esm({
2851
3132
  discovery.discoverDocuments(format),
2852
3133
  discovery.discoverThemes(format)
2853
3134
  ]);
2854
- const results = { plugins, documents, themes };
3135
+ const results = {
3136
+ plugins,
3137
+ documents,
3138
+ themes,
3139
+ pluginAutoload: pluginAutoloadEnabled()
3140
+ };
2855
3141
  return c.json({
2856
3142
  success: true,
2857
3143
  data: results,
@@ -2886,7 +3172,15 @@ var init_discovery = __esm({
2886
3172
  if (!includeExamples) delete result.examples;
2887
3173
  return result;
2888
3174
  });
2889
- return c.json({ success: true, data: processed, count: plugins.length });
3175
+ return c.json({
3176
+ success: true,
3177
+ data: processed,
3178
+ count: plugins.length,
3179
+ // Whether switching one of these on will actually reach a schema or a
3180
+ // build here. The rail shows them either way — the details are worth
3181
+ // reading — but a switch it cannot honour is a lie.
3182
+ autoload: pluginAutoloadEnabled()
3183
+ });
2890
3184
  } catch (error) {
2891
3185
  logger.error("Plugin discovery failed", { error: error.message });
2892
3186
  return c.json({ success: false, error: error.message }, 500);
@@ -2922,6 +3216,55 @@ var init_discovery = __esm({
2922
3216
  return c.json({ success: false, error: error.message }, 500);
2923
3217
  }
2924
3218
  });
3219
+ discoveryRouter.get("/themes/builtin", async (c) => {
3220
+ try {
3221
+ const adapter = Container.getAdapter();
3222
+ const themes = adapter.getBuiltinThemeValues ? await adapter.getBuiltinThemeValues() : adapter.getBuiltinThemes();
3223
+ return c.json({ success: true, data: themes });
3224
+ } catch (error) {
3225
+ logger.error("Built-in theme lookup failed", { error: error.message });
3226
+ return c.json({ success: false, error: error.message }, 500);
3227
+ }
3228
+ });
3229
+ discoveryRouter.get("/plugins/:name/source", async (c) => {
3230
+ if (!sameSiteOnly(c)) {
3231
+ return c.json(
3232
+ {
3233
+ success: false,
3234
+ error: "Plugin source is only served to the playground"
3235
+ },
3236
+ 403
3237
+ );
3238
+ }
3239
+ try {
3240
+ const pluginName = c.req.param("name");
3241
+ const format = Container.getAdapter().name;
3242
+ const discovery = new PluginDiscoveryService2({
3243
+ maxDepth: 10,
3244
+ includeNodeModules: false,
3245
+ verbose: false
3246
+ });
3247
+ const plugins = await discovery.discoverPlugins(format);
3248
+ const plugin = plugins.find((p) => p.name === pluginName);
3249
+ if (!plugin) {
3250
+ return c.json(
3251
+ { success: false, error: `Plugin '${pluginName}' not found` },
3252
+ 404
3253
+ );
3254
+ }
3255
+ const info = await stat(plugin.filePath);
3256
+ if (info.size > MAX_PLUGIN_SOURCE_BYTES) {
3257
+ return c.json(
3258
+ { success: false, error: `Plugin '${pluginName}' source is too large` },
3259
+ 413
3260
+ );
3261
+ }
3262
+ return c.text(await readFile(plugin.filePath, "utf-8"));
3263
+ } catch (error) {
3264
+ logger.error("Plugin source read failed", { error: error.message });
3265
+ return c.json({ success: false, error: error.message }, 500);
3266
+ }
3267
+ });
2925
3268
  discoveryRouter.get("/plugin/:name", async (c) => {
2926
3269
  try {
2927
3270
  const pluginName = c.req.param("name");
@@ -2944,8 +3287,7 @@ var init_discovery = __esm({
2944
3287
  }
2945
3288
  });
2946
3289
  discoveryRouter.post("/load-plugins", async (c) => {
2947
- const apiKey = c.req.header("X-API-Key") || c.req.header("Authorization");
2948
- if (!apiKey && process.env.NODE_ENV === "production") {
3290
+ if (!requestTriggeredPluginLoadAllowed() && !hasValidApiKey(c.req.raw.headers)) {
2949
3291
  return c.json({ success: false, error: "Authentication required" }, 401);
2950
3292
  }
2951
3293
  try {
@@ -3002,6 +3344,51 @@ var init_discovery = __esm({
3002
3344
  return c.json({ success: false, error: error.message }, 500);
3003
3345
  }
3004
3346
  });
3347
+ discoveryRouter.post(
3348
+ "/schemas/document",
3349
+ bodyLimit3({
3350
+ // The aggregate cap in prepareBrowserPlugins is the real bound; this only
3351
+ // keeps a hostile body from being parsed at all.
3352
+ maxSize: 512 * 1024,
3353
+ onError: () => {
3354
+ throw new HTTPException5(413, { message: "Request body too large" });
3355
+ }
3356
+ }),
3357
+ rateLimiter({
3358
+ limit: process.env.NODE_ENV === "production" ? 60 : 1e3,
3359
+ window: 60 * 1e3,
3360
+ namespace: "schemas-document",
3361
+ trustProxy: config.rateLimit.trustProxy
3362
+ }),
3363
+ tbValidator(DocumentSchemaRequestSchema),
3364
+ async (c) => {
3365
+ try {
3366
+ const adapter = Container.getAdapter();
3367
+ const { plugins, customComponents } = getValidated(c, "json");
3368
+ let prepared = [];
3369
+ try {
3370
+ prepared = prepareBrowserPlugins(customComponents ?? []);
3371
+ } catch (error) {
3372
+ if (error instanceof BrowserPluginSchemaError) {
3373
+ throw new HTTPException5(400, { message: error.message });
3374
+ }
3375
+ throw error;
3376
+ }
3377
+ const schema = await generateDocumentSchema(
3378
+ adapter.name,
3379
+ plugins,
3380
+ prepared
3381
+ );
3382
+ return c.json({ success: true, data: schema });
3383
+ } catch (error) {
3384
+ if (error instanceof HTTPException5) throw error;
3385
+ logger.error("Document schema generation failed", {
3386
+ error: error.message
3387
+ });
3388
+ return c.json({ success: false, error: error.message }, 500);
3389
+ }
3390
+ }
3391
+ );
3005
3392
  discoveryRouter.get("/schemas/theme", async (c) => {
3006
3393
  try {
3007
3394
  const adapter = Container.getAdapter();
@@ -3484,10 +3871,10 @@ var init_ai = __esm({
3484
3871
  });
3485
3872
 
3486
3873
  // src/server/routes/fonts.ts
3487
- import { createHash } from "crypto";
3874
+ import { createHash as createHash2 } from "crypto";
3488
3875
  import { Hono as Hono5 } from "hono";
3489
- import { HTTPException as HTTPException5 } from "hono/http-exception";
3490
- import { bodyLimit as bodyLimit3 } from "hono/body-limit";
3876
+ import { HTTPException as HTTPException6 } from "hono/http-exception";
3877
+ import { bodyLimit as bodyLimit4 } from "hono/body-limit";
3491
3878
  import {
3492
3879
  SAFE_FONTS,
3493
3880
  POPULAR_GOOGLE_FONTS as POPULAR_GOOGLE_FONTS2,
@@ -3507,7 +3894,7 @@ var init_fonts = __esm({
3507
3894
  safe: SAFE_FONTS,
3508
3895
  google: POPULAR_GOOGLE_FONTS2
3509
3896
  });
3510
- CATALOG_ETAG = `"${createHash("sha256").update(CATALOG_BODY).digest("base64").slice(0, 24)}"`;
3897
+ CATALOG_ETAG = `"${createHash2("sha256").update(CATALOG_BODY).digest("base64").slice(0, 24)}"`;
3511
3898
  fontsRouter.get("/catalog", (c) => {
3512
3899
  const ifNoneMatch = c.req.header("If-None-Match");
3513
3900
  if (ifNoneMatch && ifNoneMatch === CATALOG_ETAG) {
@@ -3522,11 +3909,11 @@ var init_fonts = __esm({
3522
3909
  });
3523
3910
  fontsRouter.post(
3524
3911
  "/materialize",
3525
- bodyLimit3({
3912
+ bodyLimit4({
3526
3913
  // Body is just {family, weights, italics} — 16 KB is generous.
3527
3914
  maxSize: 16 * 1024,
3528
3915
  onError: () => {
3529
- throw new HTTPException5(413, { message: "Request body too large" });
3916
+ throw new HTTPException6(413, { message: "Request body too large" });
3530
3917
  }
3531
3918
  }),
3532
3919
  rateLimiter({
@@ -3604,84 +3991,8 @@ var init_request_id = __esm({
3604
3991
  }
3605
3992
  });
3606
3993
 
3607
- // src/server/middleware/hono/auth.ts
3608
- import { createHash as createHash2, timingSafeEqual } from "crypto";
3609
- function keysEqual(received, expected) {
3610
- const receivedDigest = createHash2("sha256").update(received).digest();
3611
- const expectedDigest = createHash2("sha256").update(expected).digest();
3612
- return timingSafeEqual(receivedDigest, expectedDigest);
3613
- }
3614
- function readCredential(headers, headerName) {
3615
- const direct = headers.get(headerName)?.trim();
3616
- if (direct) {
3617
- if (headerName.toLowerCase() === "authorization" && direct.toLowerCase().startsWith("bearer ")) {
3618
- return direct.slice(7).trim() || void 0;
3619
- }
3620
- return direct;
3621
- }
3622
- const authorization = headers.get("authorization")?.trim();
3623
- if (authorization?.toLowerCase().startsWith("bearer ")) {
3624
- return authorization.slice(7).trim() || void 0;
3625
- }
3626
- return void 0;
3627
- }
3628
- function createApiKeyAuthMiddleware(options) {
3629
- const headerName = options.headerName || "x-api-key";
3630
- return async (c, next) => {
3631
- if (c.req.method === "OPTIONS" || options.mode === "disabled") {
3632
- return next();
3633
- }
3634
- if (!options.apiKey) {
3635
- if (options.mode === "auto") return next();
3636
- return c.json(
3637
- {
3638
- success: false,
3639
- error: "API authentication is not configured",
3640
- code: "AUTH_CONFIGURATION_ERROR"
3641
- },
3642
- 503
3643
- );
3644
- }
3645
- const apiKey = readCredential(c.req.raw.headers, headerName);
3646
- if (!apiKey) {
3647
- return c.json(
3648
- {
3649
- success: false,
3650
- error: "API key required",
3651
- code: "UNAUTHORIZED"
3652
- },
3653
- 401
3654
- );
3655
- }
3656
- if (!keysEqual(apiKey, options.apiKey)) {
3657
- return c.json(
3658
- {
3659
- success: false,
3660
- error: "Invalid API key",
3661
- code: "UNAUTHORIZED"
3662
- },
3663
- 401
3664
- );
3665
- }
3666
- await next();
3667
- };
3668
- }
3669
- var apiKeyAuthMiddleware;
3670
- var init_auth = __esm({
3671
- "src/server/middleware/hono/auth.ts"() {
3672
- "use strict";
3673
- init_esm_shims();
3674
- init_config();
3675
- apiKeyAuthMiddleware = createApiKeyAuthMiddleware({
3676
- mode: config.API_AUTH_MODE,
3677
- apiKey: config.API_KEY,
3678
- headerName: config.API_KEY_HEADER
3679
- });
3680
- }
3681
- });
3682
-
3683
3994
  // src/server/middleware/hono/error-handler.ts
3684
- import { HTTPException as HTTPException6 } from "hono/http-exception";
3995
+ import { HTTPException as HTTPException7 } from "hono/http-exception";
3685
3996
  var errorHandler;
3686
3997
  var init_error_handler = __esm({
3687
3998
  "src/server/middleware/hono/error-handler.ts"() {
@@ -3691,7 +4002,7 @@ var init_error_handler = __esm({
3691
4002
  init_config();
3692
4003
  errorHandler = (err, c) => {
3693
4004
  const requestId = c.get("requestId") || "unknown";
3694
- if (err instanceof HTTPException6) {
4005
+ if (err instanceof HTTPException7) {
3695
4006
  const cause = err.cause;
3696
4007
  if (cause?.code === "QUALITY_GATE_FAILED") {
3697
4008
  return c.json(
@@ -3911,8 +4222,8 @@ import { cors } from "hono/cors";
3911
4222
  import { secureHeaders } from "hono/secure-headers";
3912
4223
  import { logger as honoLogger } from "hono/logger";
3913
4224
  import { timing } from "hono/timing";
3914
- import { bodyLimit as bodyLimit4 } from "hono/body-limit";
3915
- import { HTTPException as HTTPException7 } from "hono/http-exception";
4225
+ import { bodyLimit as bodyLimit5 } from "hono/body-limit";
4226
+ import { HTTPException as HTTPException8 } from "hono/http-exception";
3916
4227
  function createAPIApp(adapter) {
3917
4228
  Container.initialize(adapter);
3918
4229
  const honoApp = new Hono6();
@@ -3936,10 +4247,10 @@ function createAPIApp(adapter) {
3936
4247
  honoApp.use("*", securityMiddleware);
3937
4248
  honoApp.use(
3938
4249
  "/api/*",
3939
- bodyLimit4({
4250
+ bodyLimit5({
3940
4251
  maxSize: config.requestLimits.maxBodySize,
3941
4252
  onError: () => {
3942
- throw new HTTPException7(413, { message: "Request body too large" });
4253
+ throw new HTTPException8(413, { message: "Request body too large" });
3943
4254
  }
3944
4255
  })
3945
4256
  );
@@ -4026,6 +4337,7 @@ import { logger as logger2 } from "hono/logger";
4026
4337
  import { compress } from "hono/compress";
4027
4338
  import { resolve, dirname as dirname3 } from "path";
4028
4339
  import { fileURLToPath as fileURLToPath3 } from "url";
4340
+ import { PluginRegistry as PluginRegistry4 } from "@json-to-office/jto-cli";
4029
4341
  var __filename2, __dirname3, UnifiedServer;
4030
4342
  var init_unified_server = __esm({
4031
4343
  "src/server/unified-server.ts"() {
@@ -4033,6 +4345,7 @@ var init_unified_server = __esm({
4033
4345
  init_esm_shims();
4034
4346
  init_app();
4035
4347
  init_logger();
4348
+ init_config();
4036
4349
  __filename2 = fileURLToPath3(import.meta.url);
4037
4350
  __dirname3 = dirname3(__filename2);
4038
4351
  UnifiedServer = class {
@@ -4049,6 +4362,38 @@ var init_unified_server = __esm({
4049
4362
  async initialize() {
4050
4363
  await this.setupMiddleware();
4051
4364
  await this.setupRoutes();
4365
+ await this.preloadPlugins();
4366
+ }
4367
+ /**
4368
+ * Load the disk plugins once, before the first request.
4369
+ *
4370
+ * The registry is what generation reads, and nothing else fills it in a
4371
+ * hosted deployment: the load route wants a key a public playground has
4372
+ * none of, and no request may make the server walk its disk there anyway
4373
+ * (see `requestTriggeredPluginLoadAllowed`). Loading here instead makes it
4374
+ * the operator's decision, taken at boot from the image's own filesystem —
4375
+ * see `pluginAutoloadEnabled`.
4376
+ *
4377
+ * Awaited rather than backgrounded so the first schema request cannot beat
4378
+ * it and cache a plugin-less schema. A plugin that fails to import is a
4379
+ * defect in that plugin, not a reason to refuse to serve.
4380
+ */
4381
+ async preloadPlugins() {
4382
+ if (!pluginAutoloadEnabled()) return;
4383
+ const registry = PluginRegistry4.getInstance();
4384
+ registry.setFormat(this.adapter.name);
4385
+ try {
4386
+ const { discovered, loaded } = await registry.discoverAndLoad();
4387
+ if (discovered > 0) {
4388
+ logger.info(
4389
+ `Loaded ${loaded} of ${discovered} plugin(s) found on disk`
4390
+ );
4391
+ }
4392
+ } catch (error) {
4393
+ logger.warn(
4394
+ `Plugin autoload failed: ${error?.message ?? "unknown error"}`
4395
+ );
4396
+ }
4052
4397
  }
4053
4398
  async setupMiddleware() {
4054
4399
  this.app.use("*", logger2());
@@ -4388,7 +4733,7 @@ function createDevCommand(adapter) {
4388
4733
  }
4389
4734
 
4390
4735
  // src/cli.ts
4391
- var PACKAGE_VERSION = true ? "1.11.1" : "dev-mode";
4736
+ var PACKAGE_VERSION = true ? "2.2.0" : "dev-mode";
4392
4737
  var program = new Command2();
4393
4738
  program.name("jto").description("JSON to Office CLI - Generate .docx and .pptx from JSON").version(PACKAGE_VERSION);
4394
4739
  registerCoreCommands(program, {