Technical / Build Reference — how the project is put together: the GitHub repository, the Vercel API proxy, routing, the data model, security, deployment and how to extend it. Launcher v3.23.0
BIJAY'S Toolkit is a static site + serverless functions on Vercel in front of a private GitHub repository. The repo holds all content (scripts, app catalogs, config, docs). Small Node functions in api/ talk to GitHub using a token that lives only on the server, so client PCs never see the token or the private repo — they just receive text over HTTPS.
Two independent front doors:
public/launcher.ps1 that renders the repo's folders as a UI and calls /api/list and /api/get to read content.public/admin/index.html that reads via /api/tree//api/get and writes via /api/write//api/delete.How the repo side was set up (and how to recreate it):
waytobijay/Msp-Toolkit). Private is important — push access equals code-execution on every client PC.api/, public/, data/, vercel.json, package.json). No build step and no runtime dependencies — package.json just pins the Node engine..env; example.env is a template only..gitignore keeps real secrets out of the repo. Never place a token in launcher.ps1, the admin page, or any committed file — the whole security model depends on it staying server-side.There is no database. GitHub is the store, accessed through two REST APIs:
GET /repos/{repo}/contents/{path} lists a folder or returns a file; PUT creates/updates a file (base64 body + the current sha for updates); DELETE removes one. Used by list/get/write/delete.GET /repos/{repo}/git/trees/{ref}?recursive=1 returns the entire file tree in one call. Used by the admin panel to build its sidebar.Every write is an ordinary commit, so the full history and audit trail live in git. Admin commits are messaged admin: <create|update|delete> <path>.
Each file in api/ is one serverless function. They share a pattern: read env config, check the caller's key, call GitHub with the server-side token, return text/JSON. Reads for client content send Cache-Control: no-store.
| Endpoint | Method | Gate | Purpose |
|---|---|---|---|
api/root.js | GET | — | Serves launcher.ps1 to PowerShell; redirects browsers to /admin. |
api/linux-root.js | GET | — | Serves the Linux bash launcher public/linux.sh to curl/wget/bash (the /linux route); redirects browsers to /admin. |
api/console.js | GET | — | Serves the classic text launcher console.ps1. |
api/list.js | GET | launcher key or admin key | Folder contents (Contents API) as {name,type}[]. |
api/get.js | GET | launcher key or admin key | Raw file text; restricted to data/; no-store. |
api/tree.js | GET | admin key | Full recursive tree (Git Trees API) for the admin sidebar. |
api/write.js | POST | admin key | Create/update a file (Contents PUT, base64 + sha); path-validated. |
api/delete.js | POST | admin key | Delete a file (Contents DELETE with sha). |
The launcher sends its access password as the x-launcher-key header on every call; the admin panel sends x-admin-key. When a launcher key is configured, list/get accept either key so the admin UI keeps reading files. api/write.js rejects paths containing .., leading slashes, or segments with trailing spaces/dots (which can't be checked out on Windows).
api/root.js sniffs the User-Agent:
powershell/curl/wget), it fetches public/launcher.ps1 from GitHub (Accept: application/vnd.github.raw) and returns it as text/plain with no-store — so irm | iex always runs the latest version, no cache-busting./admin so humans who open the domain land on the console.The launcher itself is UI-only: it pulls listings and file text through the API and never contains a token. Its version is shown in the window header ($script:Version).
{
"rewrites": [
{ "source": "/", "destination": "/api/root" },
{ "source": "/launcher", "destination": "/api/root" },
{ "source": "/linux", "destination": "/api/linux-root" },
{ "source": "/console", "destination": "/api/console" },
{ "source": "/admin", "destination": "/admin/index.html" }
],
"functions": { "api/*.js": { "maxDuration": 10 } }
}
Static assets under public/ are served directly, so the docs live at /documentation/…. Rewrites map the friendly paths to functions/pages.
data/scripts/. Section = a subfolder. Script = NN-name.ps1 (NN orders; display name derived from the filename).data/linux/ with .sh scripts. The Linux launcher (public/linux.sh, served at /linux) reads them via /api/tree + /api/get with the same x-launcher-key, and runs / schedules them in a terminal menu. .sh files are pinned to LF via .gitattributes so curl | bash never gets CRLF.config.json at a set root is data (never a section); it's hidden in the admin tree and edited via the gear.data/software folder that contains apps.json is an App Catalog; folders without it are custom-script folders.# Title:, # Description:, # Icon: drive the launcher cards.config.json is shallow-merged over data/shared/config.json (set values win), and passed to any script that declares a Config parameter.data/companies. It was renamed to Scripts / data/scripts (with git mv, history preserved). If you find old references anywhere, they mean the same thing.A catalog is a JSON file — an array of categories, each with an array of apps:
{
"categories": [
{ "name": "Browsers", "apps": [
{ "name": "Google Chrome", "winget": "Google.Chrome", "icon": "chrome",
"desc": "Fast web browser from Google." }
] }
]
}
The launcher renders it as a categorised checkbox grid (with a per-category Select all). Install Selected opens one elevated window that installs each ticked app:
winget install --id <Id> --exact --source winget --silent \
--accept-package-agreements --accept-source-agreements
--source winget pins the community repo so the interactive msstore region/agreement prompt can't block the silent batch.Publisher.App or a pasted full winget install … command both resolve to the Id.winget is missing, it attempts to install "App Installer" first.Multiple catalogs are supported: the root apps.json plus any data/software/<name>/apps.json. The launcher caches each by path and clears the selection when you switch catalogs. In the admin panel these are managed by the form editor (openCatalogEditor(path)), and + → New app catalog seeds a new folder catalog with one sample category and app.
Set in Vercel → Settings → Environment Variables (Production). Changes apply on the next deploy.
| Variable | Required | Purpose |
|---|---|---|
GITHUB_TOKEN | yes | Fine-grained PAT, this repo only, Contents: Read & write. |
GITHUB_REPO | yes | owner/repo, e.g. waytobijay/Msp-Toolkit. |
GITHUB_REF | no | Branch to serve; defaults to main. |
ADMIN_PASSWORD | yes | Password for /admin and all write/delete APIs. |
LAUNCHER_KEY | no | Access password for the PowerShell launcher; if set, the launcher prompts for it. |
/api/*; they never see the token or the private repo./api/list and /api/get send Cache-Control: no-store, so an authenticated response can't be cached and later served to an unauthenticated request. /api/get is restricted to data/.LAUNCHER_KEY protects the launcher; ADMIN_PASSWORD protects /admin and every write/delete. Destructive admin actions re-prompt for the password client-side before committing.irm | iex executes remote code as the launching user. Keep the repo private and push access tight; anyone who can push can run code on every client PC.api/, static public/.$script:BaseUrl at the top of public/launcher.ps1 to that domain and push once.public/admin/index.html), the API functions, vercel.json, and env-var changes. Pushing to main triggers the deploy automatically; hard-refresh the admin page afterwards.launcher.ps1 for content.public/launcher.ps1 (embedded XAML + PowerShell) and bump $script:Version. It's live on push.api/<name>.js function following the existing pattern (env config → key check → GitHub call). Add a rewrite in vercel.json if you want a friendly path.launcher.ps1); a script/app can force one with # Icon: / the catalog icon field.GITHUB_TOKEN in Vercel and redeploy.| Version | What changed |
|---|---|
| Linux v1.0.0 | Linux (bash) script sets. New admin Linux tab (mirror of Scripts, backed by data/linux) and a Linux launcher — curl -fsSL …/linux | bash — a terminal menu that runs / runs-as-root / views / schedules bash scripts, authenticated with the same x-launcher-key. Adds api/linux-root.js, the /linux route, and an LF .gitattributes pin for *.sh. |
| Admin | Command catalog (folders → categories → entries with copyable command/link items, searchable); theme-aware split login with animated icons; sharper HiDPI PDF preview; pane-aware (container-query) toolbar wrapping; window drag while the password gate is up. |
| v3.23.0 | Fluent (Windows 11-style) redesign: DynamicResource theme tokens with a Dark / Light / Auto selector (Auto follows Windows; preference saved to %APPDATA%\BijayToolkit\theme.txt; switches live), a Home / Welcome page (quick actions + info), rounded corners, refined spacing/dividers, and a friendlier empty state. Layout, navigation and functionality unchanged. |
| v3.23.0 | Top Scripts/Software tabs; tidy first-open loading; compact right-aligned per-category Select all; clearer window drag handle. |
| v3.23.0 | Multiple App Catalogs (any data/software/<name>/apps.json auto-detected); admin "+ → New app catalog / New script folder"; removed the redundant admin "+ Client". |
| v3.23.0 | Renamed Companies → Scripts (folder data/companies → data/scripts and all code/labels); per-category Select all in the App Catalog. |
| v3.1.x | WinUtil-style App Catalog with one-click winget install; robust Winget-ID handling (--source winget, Id normalisation); admin form-based catalog editor with three-dot menus and an app Edit modal. |
| v3.23.0 | Modern dark WPF GUI launcher (sidebar, search, breadcrumb, cards, Run / Run-as-Admin), async loading; classic text menu kept at /console; added the Software section. |
| v2.x | Restructured content under a single data/ root; responsive admin panel; documentation folder. |