From 5608c04f2d27ccd790a40449331d7d8e4178fe51 Mon Sep 17 00:00:00 2001 From: "Rian.be" Date: Wed, 16 Sep 2026 01:45:54 +0200 Subject: [PATCH 1/6] feat: scaffold DevTools web UI --- src/Plugins/Solutions/DevTools/UI/.gitignore | 2 + src/Plugins/Solutions/DevTools/UI/build.mjs | 39 + .../Solutions/DevTools/UI/package-lock.json | 875 ++++++++++++++++++ .../Solutions/DevTools/UI/package.json | 16 + .../Solutions/DevTools/UI/tsconfig.json | 13 + 5 files changed, 945 insertions(+) create mode 100644 src/Plugins/Solutions/DevTools/UI/.gitignore create mode 100644 src/Plugins/Solutions/DevTools/UI/build.mjs create mode 100644 src/Plugins/Solutions/DevTools/UI/package-lock.json create mode 100644 src/Plugins/Solutions/DevTools/UI/package.json create mode 100644 src/Plugins/Solutions/DevTools/UI/tsconfig.json diff --git a/src/Plugins/Solutions/DevTools/UI/.gitignore b/src/Plugins/Solutions/DevTools/UI/.gitignore new file mode 100644 index 0000000..04c01ba --- /dev/null +++ b/src/Plugins/Solutions/DevTools/UI/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ \ No newline at end of file diff --git a/src/Plugins/Solutions/DevTools/UI/build.mjs b/src/Plugins/Solutions/DevTools/UI/build.mjs new file mode 100644 index 0000000..ab61c42 --- /dev/null +++ b/src/Plugins/Solutions/DevTools/UI/build.mjs @@ -0,0 +1,39 @@ +import { build, context } from "esbuild"; +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +/** Marker replaced with the compiled JS bundle inside the HTML template. */ +const BUNDLE_MARKER = "/*__BUNDLE__*/"; + +const root = dirname(fileURLToPath(import.meta.url)); +const watch = process.argv.includes("--watch"); + +const options = { + entryPoints: [join(root, "src", "app.ts")], + bundle: true, + format: "iife", + minify: true, + target: ["es2020"], + outfile: join(root, "dist", "bundle.js"), +}; + +async function inlineBundle() { + const [template, bundle] = await Promise.all([ + readFile(join(root, "template.html"), "utf8"), + readFile(join(root, "dist", "bundle.js"), "utf8"), + ]); + const html = template.replace(BUNDLE_MARKER, () => bundle); + await writeFile(join(root, "dist", "ui.html"), html); +} + +if (watch) { + const ctx = await context(options); + await ctx.watch(); + await inlineBundle(); + console.log("Watching for changes…"); +} else { + await build(options); + await inlineBundle(); + console.log("TypeScript UI written to dist/ui.html"); +} \ No newline at end of file diff --git a/src/Plugins/Solutions/DevTools/UI/package-lock.json b/src/Plugins/Solutions/DevTools/UI/package-lock.json new file mode 100644 index 0000000..a501ad4 --- /dev/null +++ b/src/Plugins/Solutions/DevTools/UI/package-lock.json @@ -0,0 +1,875 @@ +{ + "name": "@authkit/devtools-ui", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@authkit/devtools-ui", + "version": "1.0.0", + "devDependencies": { + "esbuild": "^0.28.2", + "typescript": "^7.0.2" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@typescript/typescript-aix-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", + "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", + "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", + "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", + "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", + "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", + "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", + "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-loong64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", + "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-mips64el": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", + "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", + "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-riscv64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", + "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-s390x": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", + "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", + "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", + "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", + "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", + "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", + "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-sunos-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", + "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", + "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", + "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/typescript": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", + "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc" + }, + "engines": { + "node": ">=16.20.0" + }, + "optionalDependencies": { + "@typescript/typescript-aix-ppc64": "7.0.2", + "@typescript/typescript-darwin-arm64": "7.0.2", + "@typescript/typescript-darwin-x64": "7.0.2", + "@typescript/typescript-freebsd-arm64": "7.0.2", + "@typescript/typescript-freebsd-x64": "7.0.2", + "@typescript/typescript-linux-arm": "7.0.2", + "@typescript/typescript-linux-arm64": "7.0.2", + "@typescript/typescript-linux-loong64": "7.0.2", + "@typescript/typescript-linux-mips64el": "7.0.2", + "@typescript/typescript-linux-ppc64": "7.0.2", + "@typescript/typescript-linux-riscv64": "7.0.2", + "@typescript/typescript-linux-s390x": "7.0.2", + "@typescript/typescript-linux-x64": "7.0.2", + "@typescript/typescript-netbsd-arm64": "7.0.2", + "@typescript/typescript-netbsd-x64": "7.0.2", + "@typescript/typescript-openbsd-arm64": "7.0.2", + "@typescript/typescript-openbsd-x64": "7.0.2", + "@typescript/typescript-sunos-x64": "7.0.2", + "@typescript/typescript-win32-arm64": "7.0.2", + "@typescript/typescript-win32-x64": "7.0.2" + } + } + } +} diff --git a/src/Plugins/Solutions/DevTools/UI/package.json b/src/Plugins/Solutions/DevTools/UI/package.json new file mode 100644 index 0000000..e2dc1e1 --- /dev/null +++ b/src/Plugins/Solutions/DevTools/UI/package.json @@ -0,0 +1,16 @@ +{ + "name": "@authkit/devtools-ui", + "private": true, + "version": "1.0.0", + "type": "module", + "description": "TypeScript UI for the AuthKit gRPC DevTools plugin.", + "scripts": { + "typecheck": "tsc --noEmit", + "build": "node build.mjs", + "watch": "node build.mjs --watch" + }, + "devDependencies": { + "esbuild": "^0.28.2", + "typescript": "^7.0.2" + } +} \ No newline at end of file diff --git a/src/Plugins/Solutions/DevTools/UI/tsconfig.json b/src/Plugins/Solutions/DevTools/UI/tsconfig.json new file mode 100644 index 0000000..f915d2f --- /dev/null +++ b/src/Plugins/Solutions/DevTools/UI/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "strict": true, + "noEmit": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src"] +} \ No newline at end of file From c40234e891e7f4b491310963342afc6b3fd7a036 Mon Sep 17 00:00:00 2001 From: "Rian.be" Date: Wed, 16 Sep 2026 01:46:52 +0200 Subject: [PATCH 2/6] feat: bundle DevTools UI into plugin assembly --- Dockerfile | 9 +++++++++ src/Plugins/Solutions/DevTools/DevTools.csproj | 2 +- src/Plugins/Solutions/DevTools/Taskfile.yml | 16 +++++++++++++++- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 4bdce5e..aeb8a6a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -34,12 +34,21 @@ RUN mkdir /root/certs WORKDIR "/src" RUN dotnet build "src/Host/Host.csproj" -c Release -o /app/build +FROM node:22 AS ui +WORKDIR /ui +COPY ["src/Plugins/Solutions/DevTools/UI/package.json", "src/Plugins/Solutions/DevTools/UI/package-lock.json", "./"] +RUN npm ci +COPY ["src/Plugins/Solutions/DevTools/UI/template.html", "src/Plugins/Solutions/DevTools/UI/build.mjs", "./"] +COPY ["src/Plugins/Solutions/DevTools/UI/src/", "./src/"] +RUN npm run build + FROM build AS publish WORKDIR /src RUN dotnet publish "src/Host/Host.csproj" -c Release -o /app/publish RUN dotnet publish "src/Plugins/Solutions/DevTokens/DevTokens.csproj" -c Release -o /app/publish/plugins/DevTokens COPY src/Plugins/Solutions/DevTokens/manifest.json /app/publish/plugins/DevTokens/manifest.json +COPY --from=ui /ui/dist/ui.html src/Plugins/Solutions/DevTools/UI/dist/ui.html RUN dotnet publish "src/Plugins/Solutions/DevTools/DevTools.csproj" -c Release -o /app/publish/plugins/DevTools COPY src/Plugins/Solutions/DevTools/manifest.json /app/publish/plugins/DevTools/manifest.json diff --git a/src/Plugins/Solutions/DevTools/DevTools.csproj b/src/Plugins/Solutions/DevTools/DevTools.csproj index 8965d52..34e28cc 100644 --- a/src/Plugins/Solutions/DevTools/DevTools.csproj +++ b/src/Plugins/Solutions/DevTools/DevTools.csproj @@ -20,6 +20,6 @@ - + \ No newline at end of file diff --git a/src/Plugins/Solutions/DevTools/Taskfile.yml b/src/Plugins/Solutions/DevTools/Taskfile.yml index 32d9314..c908722 100644 --- a/src/Plugins/Solutions/DevTools/Taskfile.yml +++ b/src/Plugins/Solutions/DevTools/Taskfile.yml @@ -9,6 +9,7 @@ vars: ASSEMBLY_FILE: '{{.USER_WORKING_DIR}}/{{.OUTPUT_DIR}}/{{.PROJECT_NAME}}.dll' # Path to the compiled DLL MANIFEST_OUTPUT: '{{.USER_WORKING_DIR}}/manifest.json' # Output path for the generated manifest MANIFEST_GENERATOR: ../../../../tools/AuthKit.ManifestGenerator # Path to the manifest generator tool + UI_DIR: '{{.USER_WORKING_DIR}}/UI' # Path to the TypeScript UI project env: DOTNET_CLI_TELEMETRY_OPTOUT: '1' # Disables .NET CLI telemetry @@ -20,10 +21,23 @@ tasks: - task --list build: - desc: Builds the DevTools project without restoring NuGet packages + desc: Builds the TypeScript UI and the DevTools project without restoring NuGet packages cmds: + - task: build-ui - dotnet build {{.PROJECT_FILE}} --configuration {{.CONFIGURATION}} --no-restore + build-ui: + desc: Installs UI dependencies and compiles the TypeScript UI into dist/ui.html + cmds: + - npm --prefix {{.UI_DIR}} ci + - npm --prefix {{.UI_DIR}} run typecheck + - npm --prefix {{.UI_DIR}} run build + + watch-ui: + desc: Rebuilds the TypeScript UI on changes + cmds: + - npm --prefix {{.UI_DIR}} run watch + clean: desc: Cleans the DevTools project (deletes build artifacts) cmds: From 146a8ea7a1ea06191a646f41ee151055936551e3 Mon Sep 17 00:00:00 2001 From: "Rian.be" Date: Wed, 16 Sep 2026 01:47:24 +0200 Subject: [PATCH 3/6] refactor: templatize DevTools gRPC UI page --- .../Solutions/DevTools/UI/template.html | 104 ++++++ src/Plugins/Solutions/DevTools/UI/ui.html | 328 ------------------ 2 files changed, 104 insertions(+), 328 deletions(-) create mode 100644 src/Plugins/Solutions/DevTools/UI/template.html delete mode 100644 src/Plugins/Solutions/DevTools/UI/ui.html diff --git a/src/Plugins/Solutions/DevTools/UI/template.html b/src/Plugins/Solutions/DevTools/UI/template.html new file mode 100644 index 0000000..6ceaff2 --- /dev/null +++ b/src/Plugins/Solutions/DevTools/UI/template.html @@ -0,0 +1,104 @@ + + + + + +gRPC UI AuthKit + + + +
+

gRPC UI

+ AuthKit plugin + scanning… +
+
+ +
+
Select method to inspect its schema and execute a request.
+
+
+ + + + diff --git a/src/Plugins/Solutions/DevTools/UI/ui.html b/src/Plugins/Solutions/DevTools/UI/ui.html deleted file mode 100644 index 9c3eae2..0000000 --- a/src/Plugins/Solutions/DevTools/UI/ui.html +++ /dev/null @@ -1,328 +0,0 @@ - - - - - -gRPC UI — AuthKit - - - -
-

gRPC UI

- AuthKit plugin - scanning… -
-
- -
-
Select a method to inspect its schema and execute a request.
-
-
- - - \ No newline at end of file From 49b108cd1658e09e78d48b255b2365e4a998a9e0 Mon Sep 17 00:00:00 2001 From: "Rian.be" Date: Wed, 16 Sep 2026 01:48:14 +0200 Subject: [PATCH 4/6] feat: add gRPC DevTools catalog and invocation UI --- src/Plugins/Solutions/DevTools/UI/src/api.ts | 52 +++++ src/Plugins/Solutions/DevTools/UI/src/app.ts | 59 +++++ .../Solutions/DevTools/UI/src/controller.ts | 57 +++++ src/Plugins/Solutions/DevTools/UI/src/dom.ts | 18 ++ .../Solutions/DevTools/UI/src/render.ts | 208 ++++++++++++++++++ .../Solutions/DevTools/UI/src/schema.ts | 112 ++++++++++ .../Solutions/DevTools/UI/src/storage.ts | 11 + .../Solutions/DevTools/UI/src/store.ts | 33 +++ .../Solutions/DevTools/UI/src/types.ts | 56 +++++ 9 files changed, 606 insertions(+) create mode 100644 src/Plugins/Solutions/DevTools/UI/src/api.ts create mode 100644 src/Plugins/Solutions/DevTools/UI/src/app.ts create mode 100644 src/Plugins/Solutions/DevTools/UI/src/controller.ts create mode 100644 src/Plugins/Solutions/DevTools/UI/src/dom.ts create mode 100644 src/Plugins/Solutions/DevTools/UI/src/render.ts create mode 100644 src/Plugins/Solutions/DevTools/UI/src/schema.ts create mode 100644 src/Plugins/Solutions/DevTools/UI/src/storage.ts create mode 100644 src/Plugins/Solutions/DevTools/UI/src/store.ts create mode 100644 src/Plugins/Solutions/DevTools/UI/src/types.ts diff --git a/src/Plugins/Solutions/DevTools/UI/src/api.ts b/src/Plugins/Solutions/DevTools/UI/src/api.ts new file mode 100644 index 0000000..e544d6e --- /dev/null +++ b/src/Plugins/Solutions/DevTools/UI/src/api.ts @@ -0,0 +1,52 @@ +import type { GrpcCatalogResponse, InvocationResult } from "./types"; + +export interface InvokeGrpcRequest +{ + service: string; + method: string; + requestJson: string; + headers: Record; +} + +export interface GrpcApi +{ + fetchServices(): Promise; + invoke(request: InvokeGrpcRequest): Promise; +} + +export class HttpGrpcApi implements GrpcApi +{ + constructor(private readonly base: string) {} + + async fetchServices(): Promise + { + const response = await fetch(`${this.base}/api/services`); + + if (!response.ok) { + throw new Error( + `Failed to load catalog: HTTP ${response.status}`, + ); + } + + return (await response.json()) as GrpcCatalogResponse; + } + + async invoke(request: InvokeGrpcRequest): Promise + { + const response = await fetch(`${this.base}/api/invoke`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(request), + }); + + if (!response.ok) { + throw new Error( + `Failed to invoke gRPC method: HTTP ${response.status}`, + ); + } + + return (await response.json()) as InvocationResult; + } +} diff --git a/src/Plugins/Solutions/DevTools/UI/src/app.ts b/src/Plugins/Solutions/DevTools/UI/src/app.ts new file mode 100644 index 0000000..5d65c6a --- /dev/null +++ b/src/Plugins/Solutions/DevTools/UI/src/app.ts @@ -0,0 +1,59 @@ +import { HttpGrpcApi, type GrpcApi } from "./api"; +import { createInvokeAction } from "./controller"; +import { el, getById } from "./dom"; +import { highlightSelectedMethod, renderMethodDetail, renderServiceList } from "./render"; +import { errorMessage } from "./schema"; +import { InMemoryDevToolsStore, type DevToolsStore } from "./store"; +import { loadPersistedHeaders } from "./storage"; +import type { GrpcCatalogResponse } from "./types"; + +declare global { + interface Window { + __GRPC_UI_BASE__?: string; + } +} + +const base = window.__GRPC_UI_BASE__ ?? ""; +const api: GrpcApi = new HttpGrpcApi(base); + +function bootstrap(api: GrpcApi, store: DevToolsStore): void +{ + const servicesContainer = getById("services"); + const detailContainer = getById("detail"); + const target = getById("target"); + + function onSelect(serviceIndex: number, methodIndex: number): void + { + store.select(serviceIndex, methodIndex); + highlightSelectedMethod(store.state.selected); + + const service = store.state.services[serviceIndex]; + const method = service.methods[methodIndex]; + renderMethodDetail( + detailContainer, + method, + loadPersistedHeaders(), + createInvokeAction(api, service, method), + ); + } + + async function onCatalog(catalog: GrpcCatalogResponse): Promise + { + store.setServices(catalog.services ?? []); + target.textContent = catalog.target ?? ""; + renderServiceList(servicesContainer, store.state.services, store.state.selected, onSelect); + } + + void (async () => { + try { + await onCatalog(await api.fetchServices()); + } catch (err: unknown) { + servicesContainer.replaceChildren(); + const message = el("div", "empty", `Failed to load catalog: ${errorMessage(err)}`); + message.style.padding = "14px"; + servicesContainer.append(message); + } + })(); +} + +bootstrap(api, new InMemoryDevToolsStore()); \ No newline at end of file diff --git a/src/Plugins/Solutions/DevTools/UI/src/controller.ts b/src/Plugins/Solutions/DevTools/UI/src/controller.ts new file mode 100644 index 0000000..8897da9 --- /dev/null +++ b/src/Plugins/Solutions/DevTools/UI/src/controller.ts @@ -0,0 +1,57 @@ +import type { GrpcApi, InvokeGrpcRequest } from "./api"; +import { + errorMessage, + formatInvocationOutput, + parseHeaders, +} from "./schema"; +import { persistHeaders } from "./storage"; +import type { MethodDetail } from "./render"; +import type { GrpcMethodInfo, GrpcServiceInfo } from "./types"; + +export function createInvokeAction( + api: GrpcApi, + service: GrpcServiceInfo, + method: GrpcMethodInfo, +): (detail: MethodDetail) => void +{ + return (detail) => + { + void invoke(api, service, method, detail); + }; +} + +async function invoke( + api: GrpcApi, + service: GrpcServiceInfo, + method: GrpcMethodInfo, + detail: MethodDetail, +): Promise +{ + const headersText = detail.headers.value; + persistHeaders(headersText); + + const request: InvokeGrpcRequest = { + service: service.fullName || `${service.package}.${service.name}`, + method: method.name, + requestJson: detail.request.value, + headers: parseHeaders(headersText), + }; + + detail.setStatus("idle", "Invoking…"); + detail.writeResponse(""); + + try + { + const result = await api.invoke(request); + const success = result.success && result.statusCode === 0; + + detail.setStatus( + success ? "ok" : "err", + `${result.statusName} · ${result.elapsedMs} ms`, + ); + detail.writeResponse(formatInvocationOutput(result)); + } catch (err: unknown) { + detail.setStatus("err", "Request failed"); + detail.writeResponse(errorMessage(err)); + } +} diff --git a/src/Plugins/Solutions/DevTools/UI/src/dom.ts b/src/Plugins/Solutions/DevTools/UI/src/dom.ts new file mode 100644 index 0000000..5ae1f51 --- /dev/null +++ b/src/Plugins/Solutions/DevTools/UI/src/dom.ts @@ -0,0 +1,18 @@ +export function getById(id: string): T +{ + const node = document.getElementById(id); + if (node === null) throw new Error(`Element #${id} not found.`); + return node as T; +} + +export function el( + tag: K, + className?: string, + text?: string, +): HTMLElementTagNameMap[K] +{ + const node = document.createElement(tag); + if (className !== undefined) node.className = className; + if (text !== undefined) node.textContent = text; + return node; +} \ No newline at end of file diff --git a/src/Plugins/Solutions/DevTools/UI/src/render.ts b/src/Plugins/Solutions/DevTools/UI/src/render.ts new file mode 100644 index 0000000..7c8af7d --- /dev/null +++ b/src/Plugins/Solutions/DevTools/UI/src/render.ts @@ -0,0 +1,208 @@ +import { describeStreaming, fieldTypeText, jsonFromSchema, prettyPrint, streamingKind } from "./schema"; +import { el } from "./dom"; +import type { SelectedIndex } from "./store"; +import type { + GrpcFieldSchema, + GrpcMessageSchema, + GrpcMethodInfo, + GrpcServiceInfo, +} from "./types"; + +export interface MethodDetail +{ + readonly request: HTMLTextAreaElement; + readonly headers: HTMLTextAreaElement; + setStatus(state: "idle" | "ok" | "err", text: string): void; + writeResponse(text: string): void; +} + +export function renderServiceList( + container: HTMLElement, + services: readonly GrpcServiceInfo[], + selected: SelectedIndex | null, + onSelect: (serviceIndex: number, methodIndex: number) => void, +): void +{ + container.replaceChildren(); + + if (services.length === 0) + { + container.append(emptyMessage("No gRPC services found.")); + return; + } + + for (const [serviceIndex, svc] of services.entries()) + { + const details = el("details", "service"); + if (serviceIndex === 0) details.open = true; + + const summary = el("summary"); + summary.append(svc.package || svc.fileName); + const count = el("span", "muted", `(${svc.methods?.length ?? 0})`); + count.style.cssText = "font-weight:400;font-size:12px;"; + summary.append(count); + details.append(summary); + + const list = el("ul", "methods"); + for (const [methodIndex, method] of (svc.methods ?? []).entries()) + { + const isSelected = selected?.[0] === serviceIndex && selected?.[1] === methodIndex; + const item = el("li", isSelected ? "active" : undefined); + item.dataset.service = String(serviceIndex); + item.dataset.method = String(methodIndex); + + const kind = streamingKind(method); + const pill = el("span", `pill ${kind}`, method.name); + pill.title = `${kind} streaming`; + item.append(pill); + item.addEventListener("click", () => onSelect(serviceIndex, methodIndex)); + + list.append(item); + } + details.append(list); + container.append(details); + } +} + +export function highlightSelectedMethod(selected: SelectedIndex | null): void +{ + for (const item of document.querySelectorAll(".methods li")) { + const isActive = selected !== null + && item.dataset.service === String(selected[0]) + && item.dataset.method === String(selected[1]); + item.classList.toggle("active", isActive); + } +} + +export function renderMethodDetail( + container: HTMLElement, + method: GrpcMethodInfo, + initialHeaders: string, + onInvoke: (detail: MethodDetail) => void, +): void +{ + container.replaceChildren(); + const streaming = method.isClientStreaming || method.isServerStreaming; + + const methodPill = el("span", `pill ${method.isClientStreaming ? "cli" : "uni"}`, describeStreaming(method)); + container.append(card(heading(method.fullName, [methodPill]), [renderSchema("Request", method.request)])); + container.append(card(heading("Response"), [renderSchema("Message", method.response)])); + + const headers = el("textarea"); + headers.placeholder = "Authorization: Bearer \nX-Custom: value"; + headers.value = initialHeaders; + container.append(card(heading("Metadata headers"), [headers])); + + const request = el("textarea"); + request.spellcheck = false; + request.value = prettyPrint(jsonFromSchema(method.request)); + + const defaultsButton = el("button", "secondary", "Fill defaults"); + const formatButton = el("button", "secondary", "Format JSON"); + const invokeButton = el("button", undefined, "Invoke"); + const status = el("span", "status"); + status.hidden = true; + const response = el("pre"); + response.hidden = true; + + const group = el("div", "row"); + group.append(defaultsButton, formatButton, invokeButton, status); + container.append(card(heading("Request JSON"), [request, group, response])); + + if (streaming) + { + invokeButton.disabled = true; + invokeButton.title = "Streaming is not supported"; + } + + defaultsButton.addEventListener("click", () => { + request.value = prettyPrint(jsonFromSchema(method.request)); + }); + formatButton.addEventListener("click", () => { + request.value = prettyPrint(request.value); + }); + + const detail: MethodDetail = { + request, + headers, + setStatus(state, text) { + status.classList.remove("ok", "err"); + if (state === "ok" || state === "err") status.classList.add(state); + status.hidden = false; + status.textContent = text; + }, + writeResponse(text) { + response.hidden = false; + response.textContent = text; + }, + }; + + invokeButton.addEventListener("click", () => onInvoke(detail)); +} + +function emptyMessage(text: string): HTMLDivElement +{ + const message = el("div", "empty", text); + message.style.padding = "14px"; + return message; +} + +function heading(text: string, additions: Node[] = []): HTMLHeadingElement +{ + const h = el("h2"); + h.append(text, ...additions); + return h; +} + +function card(h: HTMLHeadingElement, bodyNodes: (Node | string)[]): HTMLDivElement +{ + const card = el("div", "card"); + const body = el("div", "body"); + body.append(...bodyNodes); + card.append(h, body); + return card; +} + +function renderField(field: GrpcFieldSchema, depth: number, into: HTMLElement): void +{ + const row = el("div", "row-line"); + const indent = el("span", "indent", "\u00A0".repeat(depth * 4)); + row.append(indent, `${field.name}: `, el("span", "type", fieldTypeText(field))); + + if (field.isRepeated && !field.isMap) + { + row.append(" ", el("span", "rep", "[repeated]")); + } + if (field.enumType) + { + row.append(" ", el("span", "enum", `${field.enumType} (${(field.enumValues ?? []).join("|")})`)); + } + into.append(row); + + if (field.message && depth < 3) { + for (const nested of field.message.fields ?? []) renderField(nested, depth + 1, into); + } +} + +function renderSchema(title: string, msg: GrpcMessageSchema | null | undefined): HTMLDivElement +{ + const wrap = el("div"); + if (!msg) + { + wrap.append(el("div", "empty", "—")); + return wrap; + } + + const fields = msg.fields ?? []; + const schema = el("div", "schema"); + if (fields.length === 0) { + schema.append(el("div", "empty", "(empty)")); + } + else + { + for (const field of fields) renderField(field, 0, schema); + } + + wrap.append(el("h3", undefined, `${title} — ${msg.fullName}`), schema); + return wrap; +} diff --git a/src/Plugins/Solutions/DevTools/UI/src/schema.ts b/src/Plugins/Solutions/DevTools/UI/src/schema.ts new file mode 100644 index 0000000..74cebca --- /dev/null +++ b/src/Plugins/Solutions/DevTools/UI/src/schema.ts @@ -0,0 +1,112 @@ +import type { + GrpcFieldSchema, + GrpcMessageSchema, + GrpcMethodInfo, + InvocationResult, +} from "./types"; + +export type StreamingKind = "uni" | "cli" | "srv" | "bidi"; + +export function streamingKind(method: GrpcMethodInfo): StreamingKind +{ + if (method.isClientStreaming && method.isServerStreaming) return "bidi"; + if (method.isClientStreaming) return "cli"; + if (method.isServerStreaming) return "srv"; + return "uni"; +} + +export function describeStreaming(method: GrpcMethodInfo): string +{ + if (method.isClientStreaming && method.isServerStreaming) return "Bidirectional stream"; + if (method.isClientStreaming) return "Client streaming"; + if (method.isServerStreaming) return "Server streaming"; + return "Unary"; +} + +export function fieldTypeText(field: GrpcFieldSchema): string +{ + if (field.isMap) + { + const value = field.mapValue?.fullName ?? field.mapValueType ?? "?"; + return `map<${field.mapKeyType ?? "?"}, ${value}>`; + } + if (field.message && field.message.fullName !== field.message.name) return field.message.fullName; + return field.fieldType; +} + +export function jsonFromSchema(msg: GrpcMessageSchema, depth = 0): Record +{ + const out: Record = {}; + const seen = depth > 4; + for (const field of msg.fields ?? []) + { + if (seen) { out[field.name] = null; continue; } + if (field.isMap) { out[field.name] = {}; continue; } + if (field.isRepeated) { out[field.name] = []; continue; } + if (field.message) { out[field.name] = jsonFromSchema(field.message, depth + 1); continue; } + if (field.fieldType === "Enum") { out[field.name] = field.enumValues?.[0] ?? "UNKNOWN"; continue; } + switch (field.fieldType) + { + case "String": case "Bytes": out[field.name] = ""; break; + case "Bool": out[field.name] = false; break; + case "Double": case "Float": out[field.name] = 0.0; break; + case "Int32": case "Int64": case "SInt32": case "SInt64": + case "UInt32": case "UInt64": case "Fixed32": case "Fixed64": + case "SFixed32": case "SFixed64": out[field.name] = 0; break; + default: out[field.name] = null; + } + } + return out; +} + +export function parseHeaders(text: string): Record +{ + const headers: Record = {}; + for (const line of text.split("\n")) + { + const separator = line.indexOf(":"); + if (separator <= 0) continue; + const key = line.slice(0, separator).trim(); + const value = line.slice(separator + 1).trim(); + if (key) headers[key] = value; + } + return headers; +} + +export function prettyPrint(text: unknown): string +{ + if (typeof text === "string") + { + try { + return JSON.stringify(JSON.parse(text), null, 2); + } + catch { return text; } + } + if (text && typeof text === "object") return JSON.stringify(text, null, 2); + return String(text ?? ""); +} + +export function formatInvocationOutput(body: InvocationResult): string +{ + const parts: string[] = []; + if (body.detail) parts.push(`Error: ${body.detail}`); + + if (body.responseJson) + { + try { parts.push(JSON.stringify(JSON.parse(body.responseJson), null, 2)); } + catch { parts.push(body.responseJson); } + } + + if (body.trailers && Object.keys(body.trailers).length > 0) + { + parts.push("Trailers:\n" + Object.entries(body.trailers) + .map(([key, value]) => `${key}: ${value}`).join("\n")); + } + + return parts.length > 0 ? parts.join("\n\n") : "(no response body)"; +} + +export function errorMessage(err: unknown): string +{ + return err instanceof Error ? err.message : String(err); +} diff --git a/src/Plugins/Solutions/DevTools/UI/src/storage.ts b/src/Plugins/Solutions/DevTools/UI/src/storage.ts new file mode 100644 index 0000000..5090b35 --- /dev/null +++ b/src/Plugins/Solutions/DevTools/UI/src/storage.ts @@ -0,0 +1,11 @@ +const HEADERS_KEY = "grpc-ui-headers"; + +export function loadPersistedHeaders(): string +{ + return sessionStorage.getItem(HEADERS_KEY) ?? "Authorization: Bearer "; +} + +export function persistHeaders(text: string): void +{ + sessionStorage.setItem(HEADERS_KEY, text); +} diff --git a/src/Plugins/Solutions/DevTools/UI/src/store.ts b/src/Plugins/Solutions/DevTools/UI/src/store.ts new file mode 100644 index 0000000..8b04a21 --- /dev/null +++ b/src/Plugins/Solutions/DevTools/UI/src/store.ts @@ -0,0 +1,33 @@ +import type { GrpcServiceInfo } from "./types"; + +export type SelectedIndex = readonly [service: number, method: number]; + +export interface DevToolsState +{ + readonly services: readonly GrpcServiceInfo[]; + readonly selected: SelectedIndex | null; +} + +export interface DevToolsStore +{ + readonly state: DevToolsState; + setServices(services: readonly GrpcServiceInfo[]): void; + select(serviceIndex: number, methodIndex: number): void; +} + +export class InMemoryDevToolsStore implements DevToolsStore +{ + private current: DevToolsState = { services: [], selected: null }; + + get state(): DevToolsState { + return this.current; + } + + setServices(services: readonly GrpcServiceInfo[]): void { + this.current = { ...this.current, services }; + } + + select(serviceIndex: number, methodIndex: number): void { + this.current = { ...this.current, selected: [serviceIndex, methodIndex] as const }; + } +} \ No newline at end of file diff --git a/src/Plugins/Solutions/DevTools/UI/src/types.ts b/src/Plugins/Solutions/DevTools/UI/src/types.ts new file mode 100644 index 0000000..ded66f2 --- /dev/null +++ b/src/Plugins/Solutions/DevTools/UI/src/types.ts @@ -0,0 +1,56 @@ +/** Schema of protobuf scalar or composite field, mirroring GrpcFieldSchema. */ +export interface GrpcFieldSchema { + name: string; + fieldType: string; + isRepeated: boolean; + isMap: boolean; + mapKeyType?: string | null; + mapValueType?: string | null; + mapValue?: GrpcMessageSchema | null; + message?: GrpcMessageSchema | null; + enumType?: string | null; + enumValues?: string[] | null; +} + +/** Protobuf message layout, mirroring GrpcMessageSchema. */ +export interface GrpcMessageSchema { + name: string; + fullName: string; + fields: GrpcFieldSchema[]; +} + +/** Descriptor of single RPC method, mirroring GrpcMethodInfo. */ +export interface GrpcMethodInfo { + name: string; + fullName: string; + isClientStreaming: boolean; + isServerStreaming: boolean; + request: GrpcMessageSchema; + response: GrpcMessageSchema; +} + +/** Descriptor of gRPC service together with its methods, mirroring GrpcServiceInfo. */ +export interface GrpcServiceInfo { + name: string; + fullName: string; + package: string; + fileName: string; + methods: GrpcMethodInfo[]; +} + +/** Payload returned by the catalog API endpoint. */ +export interface GrpcCatalogResponse { + target: string; + services: GrpcServiceInfo[]; +} + +/** Result of aRPC invocation, mirroring GrpcInvocationResult. */ +export interface InvocationResult { + success: boolean; + statusName: string; + statusCode: number; + detail?: string | null; + responseJson?: string | null; + elapsedMs: number; + trailers?: Record | null; +} From 4bd15dc63082e8d679b1623fc43c664999b6c6e4 Mon Sep 17 00:00:00 2001 From: "Rian.be" Date: Wed, 16 Sep 2026 01:51:57 +0200 Subject: [PATCH 5/6] fix: correct CodeRabbit review profile value --- .coderabbit.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 34e39e9..2bf16a1 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -5,7 +5,7 @@ language: en-US early_access: false reviews: - profile: thorough + profile: assertive request_changes_workflow: false high_level_summary: true poem: false From 5dd83e436c455e2ae84deae50c8240fc6c8f00ba Mon Sep 17 00:00:00 2001 From: "Rian.be" Date: Wed, 16 Sep 2026 01:55:24 +0200 Subject: [PATCH 6/6] ci: build plugin UIs before solution and plugin publish --- .github/workflows/main.yml | 14 +++++++++++++ .github/workflows/plugin-validation.yml | 14 +++++++++++++ .github/workflows/security.yml | 28 +++++++++++++++++++++++++ 3 files changed, 56 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 159b125..a8b7ae6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -37,6 +37,20 @@ jobs: with: dotnet-version: ${{ env.DOTNET_VERSION }} + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Build plugin UIs + run: | + for pkg in $(find src/Plugins -name package.json -not -path '*/node_modules/*'); do + ui=$(dirname "$pkg") + echo "Building UI: $ui" + npm --prefix "$ui" ci + npm --prefix "$ui" run build + done + - name: Cache NuGet packages uses: actions/cache@v3 with: diff --git a/.github/workflows/plugin-validation.yml b/.github/workflows/plugin-validation.yml index e315ddc..69e83b0 100644 --- a/.github/workflows/plugin-validation.yml +++ b/.github/workflows/plugin-validation.yml @@ -37,6 +37,20 @@ jobs: with: dotnet-version: ${{ env.DOTNET_VERSION }} + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Build plugin UIs + run: | + for pkg in $(find src/Plugins -name package.json -not -path '*/node_modules/*'); do + ui=$(dirname "$pkg") + echo "Building UI: $ui" + npm --prefix "$ui" ci + npm --prefix "$ui" run build + done + - name: Publish plugins run: | mkdir -p "${{ env.PLUGINS_STAGING }}" diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 3d7391f..0ec049b 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -61,6 +61,20 @@ jobs: with: dotnet-version: ${{ env.DOTNET_VERSION }} + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Build plugin UIs + run: | + for pkg in $(find src/Plugins -name package.json -not -path '*/node_modules/*'); do + ui=$(dirname "$pkg") + echo "Building UI: $ui" + npm --prefix "$ui" ci + npm --prefix "$ui" run build + done + - name: Restore dependencies run: dotnet restore ${{ env.SOLUTION_FILE }} @@ -134,6 +148,20 @@ jobs: with: dotnet-version: ${{ env.DOTNET_VERSION }} + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Build plugin UIs + run: | + for pkg in $(find src/Plugins -name package.json -not -path '*/node_modules/*'); do + ui=$(dirname "$pkg") + echo "Building UI: $ui" + npm --prefix "$ui" ci + npm --prefix "$ui" run build + done + - name: Restore dependencies run: dotnet restore ${{ env.SOLUTION_FILE }}