@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.
- package/dist/cli.js +447 -100
- package/dist/cli.js.map +1 -1
- package/dist/client/assets/HomePage-DMpdYyZ0.js +251 -0
- package/dist/client/assets/HomePage-DMpdYyZ0.js.map +1 -0
- package/dist/client/assets/JsonEditorPage-Ct7wb9iA.js +3 -0
- package/dist/client/assets/JsonEditorPage-Ct7wb9iA.js.map +1 -0
- package/dist/client/assets/MonacoPluginProvider-B2fWYflh.js +85 -0
- package/dist/client/assets/MonacoPluginProvider-B2fWYflh.js.map +1 -0
- package/dist/client/assets/_virtual_jto-plugin-type-libs-BlMraosv.js +25700 -0
- package/dist/client/assets/_virtual_jto-plugin-type-libs-BlMraosv.js.map +1 -0
- package/dist/client/assets/_virtual_jto-sandbox-runtime-CNoAasd3.js +2 -0
- package/dist/client/assets/_virtual_jto-sandbox-runtime-CNoAasd3.js.map +1 -0
- package/dist/client/assets/editor-DFnlAOyt.js +16 -0
- package/dist/client/assets/editor-DFnlAOyt.js.map +1 -0
- package/dist/client/assets/{editor-monaco-json-DcTTqcqQ.js → editor-monaco-json-B_1-Wxq5.js} +5 -5
- package/dist/client/assets/{editor-monaco-json-DcTTqcqQ.js.map → editor-monaco-json-B_1-Wxq5.js.map} +1 -1
- package/dist/client/assets/{editor-refs-store-QHdkA6Kl.js → editor-refs-store-DHeIvmr6.js} +2 -2
- package/dist/client/assets/{editor-refs-store-QHdkA6Kl.js.map → editor-refs-store-DHeIvmr6.js.map} +1 -1
- package/dist/client/assets/index-Bj6ZAKUc.css +1 -0
- package/dist/client/assets/index-CeZaIyj_.js +5 -0
- package/dist/client/assets/index-CeZaIyj_.js.map +1 -0
- package/dist/client/assets/preview-CugO7y1j.js +4 -0
- package/dist/client/assets/preview-CugO7y1j.js.map +1 -0
- package/dist/client/assets/radix-ui-BzbaiLpZ.js +51 -0
- package/dist/client/assets/radix-ui-BzbaiLpZ.js.map +1 -0
- package/dist/client/assets/{ui-vendor-Bq4-oNFL.js → ui-vendor-BsAtAN16.js} +94 -64
- package/dist/client/assets/ui-vendor-BsAtAN16.js.map +1 -0
- package/dist/client/assets/useQualityAnalysis-DH2idzAY.js +6 -0
- package/dist/client/assets/useQualityAnalysis-DH2idzAY.js.map +1 -0
- package/dist/client/index.html +4 -4
- package/dist/render-server.js.map +1 -1
- package/package.json +5 -3
- package/dist/client/assets/HomePage-DQ0sOZFI.js +0 -106
- package/dist/client/assets/HomePage-DQ0sOZFI.js.map +0 -1
- package/dist/client/assets/JsonEditorPage-D1TS-GDF.js +0 -3
- package/dist/client/assets/JsonEditorPage-D1TS-GDF.js.map +0 -1
- package/dist/client/assets/MonacoPluginProvider-BQ2eYUHv.js +0 -3
- package/dist/client/assets/MonacoPluginProvider-BQ2eYUHv.js.map +0 -1
- package/dist/client/assets/editor-5uqC0zMy.js +0 -9
- package/dist/client/assets/editor-5uqC0zMy.js.map +0 -1
- package/dist/client/assets/index-CrzIwHLS.css +0 -1
- package/dist/client/assets/index-DlPjoC1v.js +0 -5
- package/dist/client/assets/index-DlPjoC1v.js.map +0 -1
- package/dist/client/assets/preview-C3z7o0yn.js +0 -2
- package/dist/client/assets/preview-C3z7o0yn.js.map +0 -1
- package/dist/client/assets/radix-ui-DFNP5I4e.js +0 -51
- package/dist/client/assets/radix-ui-DFNP5I4e.js.map +0 -1
- package/dist/client/assets/ui-vendor-Bq4-oNFL.js.map +0 -1
- package/dist/client/assets/useQualityAnalysis-D5-WU4pX.js +0 -6
- 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
|
|
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
|
|
2009
|
-
if (
|
|
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 (
|
|
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
|
|
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 = {
|
|
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({
|
|
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
|
-
|
|
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
|
|
3490
|
-
import { bodyLimit as
|
|
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 = `"${
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
3915
|
-
import { HTTPException as
|
|
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
|
-
|
|
4252
|
+
bodyLimit5({
|
|
3940
4253
|
maxSize: config.requestLimits.maxBodySize,
|
|
3941
4254
|
onError: () => {
|
|
3942
|
-
throw new
|
|
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.
|
|
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, {
|