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