@json-to-office/jto 2.0.0 → 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.
- package/dist/cli.js +444 -99
- 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 +6 -4
- 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
|
}
|
|
@@ -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
|
|
2009
|
-
if (
|
|
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 (
|
|
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
|
|
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 = {
|
|
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({
|
|
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
|
-
|
|
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
|
|
3490
|
-
import { bodyLimit as
|
|
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 = `"${
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
3915
|
-
import { HTTPException as
|
|
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
|
-
|
|
4250
|
+
bodyLimit5({
|
|
3940
4251
|
maxSize: config.requestLimits.maxBodySize,
|
|
3941
4252
|
onError: () => {
|
|
3942
|
-
throw new
|
|
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 ? "2.
|
|
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, {
|