@sealed-api-gateway/console
v1.0.2
Published
A password-gated, Network-tab-style console for reading the traffic @sealed-api-gateway/core seals.
Maintainers
Readme
@sealed-api-gateway/console
Read the traffic the gateway hid — a Network tab for calls the Network tab can no longer show.
What it does
@sealed-api-gateway/core turns every API call into one opaque POST. That is the point — and it also means DevTools can no longer help you debug.
This puts the plaintext back, behind a password:
| | | | --- | --- | | Request list | Status, method, name, page, tab, size, time | | Detail pane | Headers · Params · Payload · Response · Timing | | JSON tree | Folding, line-numbered, searchable | | Binary | Inline preview, hex dump, download | | Copy as cURL | Against the real backend, so a failing call goes straight to a backend engineer | | Analysis | Status distribution, size and duration over time, p95 and error-rate tiles |
[!IMPORTANT] Unset the password and the console does not exist. With no
SECURE_GATEWAY_PASSWORD, the component rendersnull, the unlock endpoint404s, and nothing is recorded anywhere. That is the off switch, and it is the default.
Contents
- Install · Quick start
- Opening it · Everything you can change
- Theming · The password gate
- Compatibility · Troubleshooting
- At a glance · FAQ · Design decisions
Install
npm i @sealed-api-gateway/consolePeers: @sealed-api-gateway/core (≥ 0.3.0), next, react, @mui/material, @mui/icons-material, @emotion/react, @emotion/styled.
If you already render with MUI, this package is the only new thing.
Quick start
One component and one environment variable. No route file, no middleware change.
1. Mount it beside the gateway
// app/layout.tsx
import { SecureApiGateway } from '@sealed-api-gateway/core';
import { GatewayConsole } from '@sealed-api-gateway/console';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<SecureApiGateway />
<GatewayConsole />
{children}
</body>
</html>
);
}2. Set a password
# .env
SECURE_GATEWAY_PASSWORD=pick-something-long3. Make sure the gateway route exists
npx sealed-gateway-initThe console adds no route of its own — but it reaches the gateway over HTTP for its handshake and unlock, so the gateway's route has to be mounted.
[!IMPORTANT] From
@sealed-api-gateway/core1.0 the gateway itself no longer needs that file — it uses Server Actions by default. If you installed core alone and skippedsealed-gateway-init, run it now, or the console will sit locked with its unlock endpoint returning 404.This is the console's one outstanding rough edge, and it is temporary: moving it onto core's Server Actions is the next change, and will not alter this package's API.
4. Restart and press Ctrl/Cmd + Shift + D
[!NOTE] The console renders MUI components, so it must sit inside whatever provides your theme. If your
ThemeProviderlives below the layout root, mount<GatewayConsole />inside it instead.
Earlier versions needed app/gw-debug/page.tsx and app/gw-debug/api/unlock/route.ts. A console rendered as a route needs route files, and Next derives routes from the filesystem — no package can create one.
So it no longer renders as a route. It is an overlay, mounted from your layout, and its unlock endpoint hangs off the gateway's route, which you had to create anyway. Two files became zero.
That also removes the old middleware note: there is no /gw-debug path for auth middleware to intercept, so nothing needs excluding.
Opening it
| | |
| --- | --- |
| Ctrl/Cmd + Shift + D | Toggles the overlay |
| #gw-debug in the URL | Opens it, survives a reload, and can be shared with a colleague |
| Escape | Closes it |
There is deliberately no visible launcher button. The console hangs off every page of your app, and a floating button would sit in front of real users who cannot open it anyway.
Ctrl/Cmd + Shift + I, J and C are DevTools in every major browser and cannot be intercepted — which is why the default avoids them.
Everything you can change
Props on <GatewayConsole />
| Prop | Type | Default | What it changes |
| --- | --- | --- | --- |
| gatewayRoot | string | '/api/gw' | Where the gateway route is mounted. Must match <SecureApiGateway basePath>. |
| hotkey | Hotkey \| null | Ctrl/Cmd + Shift + D | The shortcut. null disables it. |
| hash | string \| null | 'gw-debug' | The URL hash that opens it. null disables it. |
| colors | ConsoleThemeOptions | built-in palettes | Colour overrides → theming |
<GatewayConsole
gatewayRoot="/api/proxy"
hotkey={{ key: 'K', ctrlOrMeta: true, alt: true }}
hash="inspector"
colors={{ dark: { '--sg-ok': '#00d68f' } }}
/>A Hotkey is { key, ctrlOrMeta?, shift?, alt? }. Modifiers you do not ask for must be absent, so Ctrl+Shift+D does not also fire on Ctrl+Shift+Alt+D.
Environment variables
The console adds none of its own. It reads two from the gateway:
| Variable | Effect |
| --- | --- |
| SECURE_GATEWAY_PASSWORD | Unset ⇒ console does not exist and nothing is recorded. Set ⇒ console available behind it. |
| SECURE_GATEWAY_UNLOCK_TTL_HOURS | How long an unlock lasts. Default 8. |
Mounting it somewhere of your own
If you want the console inside an admin shell, a drawer or a tab rather than as an overlay, compose the pieces yourself:
import { DebugConsole, UnlockForm } from '@sealed-api-gateway/console';
const [unlocked, setUnlocked] = useState(false);
return unlocked
? <DebugConsole onClose={close} colors={colors} />
: <UnlockForm onUnlocked={() => setUnlocked(true)} onClose={close} />;Both accept gatewayRoot.
What you deliberately cannot change
| Fixed behaviour | Why | | --- | --- | | The log holds only your own browser's traffic | It is read from your browser's storage. There is no server-side log and no way to see another user's requests. | | Retention is 1 hour / 300 entries | A developer tool that grows without bound in every user's browser is a bug, not a feature. | | Binary bodies are capped (~3 MB in memory, ~110 KB shared across tabs) | So one screenshot cannot evict the whole log. | | Nothing is recorded without a password | The off switch has to be the default, or the tool becomes a liability. |
Theming
Every colour lives in one place, exported as data:
import { LIGHT, DARK } from '@sealed-api-gateway/console';42 variables as light/dark pairs — surfaces, text, borders, chart marks, JSON syntax, search highlight. Override any of them by name; everything you do not name keeps its default:
<GatewayConsole
colors={{
dark: { '--sg-ok': '#00d68f', '--sg-server': '#ff5c5c' },
light: { '--sg-bg': '#ffffff' },
}}
/>[!NOTE] In the copy-the-folder version you edited
theme.tsdirectly. Insidenode_modulesyou cannot, so the palettes became exported data plus a merge. That is the one place packaging made this worse — and it is why the reference application still ships the console as a folder you own outright.
It does not read your theme, on purpose
The console cannot assume the app it is dropped into has a dark scheme, a CSS-variable theme, or a usefully configured palette — so it ships its own defaults and stays legible anywhere. MUI components inside it (buttons, chips, inputs) still follow your theme, so it never looks foreign.
It does follow your light/dark choice, resolved in three tiers:
| Tier | Used when | How |
| --- | --- | --- |
| 1 | theme.applyStyles exists (MUI 5.18+) | Dark values nested under your theme's own colour-scheme selector — the only form correct for a createTheme({ cssVariables, colorSchemes }) theme, where palette.mode reports the default mode rather than the active one |
| 2 | Older themes | theme.palette.mode |
| 3 | Opt-in | prefers-color-scheme, via colors={{ followOsWhenHostIsLight: true }} |
The password gate
| State | Behaviour |
| --- | --- |
| Password unset | Component renders null, unlock endpoint 404s, nothing recorded |
| Wrong password | 401. Ten attempts per five minutes, then 429 |
| Unlocked | httpOnly; SameSite=Strict cookie, 8 hours by default |
| Password changed | Every outstanding unlock dies immediately |
The cookie is a self-verifying <expiry>.<hmac> pair keyed on the password itself. There is no session store to keep, and rotating the password is the revocation mechanism.
[!NOTE] What the password is actually for. The console shows a visitor their own browser's traffic — which they could already reach through DevTools. The password keeps the tool out of casual reach; it does not protect data from the person operating the browser. Nothing here can show another user's requests.
Compatibility
| | Supported |
| --- | --- |
| Next.js | 13.4 – 16, App Router |
| React | 18.2 – 19 |
| MUI | 5, 6, 7, 9 — there is no v8; the majors went 5 → 6 → 7 → 9 |
| Node | ≥ 18.17 |
| @sealed-api-gateway/core | ≥ 0.3.0 — earlier versions have no unlock endpoint |
Spanning MUI 5 through 9 takes care, because prop and slot APIs were renamed between majors and the old and new names are mutually exclusive: MUI 9 rejects PaperProps, MUI 5 does not know slotProps. The affected spots use composition and sx, which behave identically on every major.
Troubleshooting
| Symptom | Cause | Fix |
| --- | --- | --- |
| Hotkey does nothing | No password set, so the console is switched off | Set SECURE_GATEWAY_PASSWORD and restart |
| Hotkey does nothing, password is set | The gateway route is missing or mounted elsewhere | npx sealed-gateway-init, or pass gatewayRoot |
| Unlock returns 404, gateway itself works fine | Core 1.0+ runs on Server Actions, so you may never have created the route the console needs | npx sealed-gateway-init |
| Console opens but is empty | No traffic captured yet | Trigger an API call — only gateway-routed origins appear |
| Unlock succeeds, then the form returns | gatewayRoot disagrees with <SecureApiGateway basePath> | Make them match |
| Overlay renders unstyled | Mounted outside your ThemeProvider | Move <GatewayConsole /> inside it |
| Cannot find module '@sealed-api-gateway/core' | Peer not installed | npm i @sealed-api-gateway/core |
| Unlock returns 404 | Core older than 0.3.0 | npm i @sealed-api-gateway/core@latest |
At a glance
| | | | --- | --- | | Request list with status, method, size, timing | ✅ | | Detail pane — Headers · Params · Payload · Response · Timing | ✅ | | Folding, line-numbered JSON tree | ✅ | | Binary preview with hex dump and download | ✅ | | Copy as cURL, against the real backend | ✅ | | Analysis — status mix, size and duration over time, p95, error rate | ✅ | | Filters shared across both views | ✅ | | Cross-tab log | ✅ | | Opens with a hotkey or a URL hash | ✅ | | Needs a route file of its own | ❌ none | | Shows other users' traffic | ❌ never |
FAQ
Can it show another user's requests? No. The log is read from your own browser's storage. There is no server-side log, and no mechanism by which one browser could see another's traffic.
What does the password actually protect? It keeps the tool out of casual reach. The console shows a visitor their own traffic — which they could already reach through DevTools — so the password is a lock on a convenience, not a barrier around secrets.
What happens with no password set?
The component renders null, the unlock endpoint 404s, and nothing is
recorded. That is the default, and it is why installing this is safe in
production before you have decided whether to enable it.
How long is an unlock good for?
Eight hours by default; set SECURE_GATEWAY_UNLOCK_TTL_HOURS to change it.
Changing the password revokes every outstanding unlock immediately, because the
cookie is keyed on the password itself.
Does it slow the app down? Only when open. While closed it is a single component that has made one handshake request and rendered nothing.
Can I restyle it? Every colour is an exported variable you can override by name — see Theming. If you expect to restyle heavily, the reference app ships the console as a folder you own outright.
Can I put it somewhere other than an overlay?
Yes — compose DebugConsole and UnlockForm yourself. See
mounting it somewhere of your own.
Design decisions
Why an overlay instead of a route?
A console rendered as a route needs route files, and no package can create them —
Next derives routes from the filesystem. As an overlay it needs none, so
installing is npm i plus one component.
Why no visible launcher button? It hangs off every page of your app. A floating button would sit in front of real users, who cannot open it anyway.
Why Ctrl/Cmd + Shift + D?
Ctrl+Shift+I, +J and +C are DevTools in every major browser and cannot be
intercepted. A shortcut the browser eats is one nobody can use and nobody can
debug.
Why does it not read colours from my theme? It cannot assume the app it is dropped into has a dark scheme, a CSS-variable theme, or a usefully configured palette. It ships its own defaults so it stays legible anywhere — while MUI components inside it still follow your theme, so it never looks foreign.
Why does the password gate live in the gateway package? So the console needs no server route. The gateway already owns one, so the unlock endpoint hangs off it.
Relationship to the reference app
Extracted from Sealed-API-Gateway, a full Next.js app demonstrating the gateway and console together.
| | Reference app | This package |
| --- | --- | --- |
| Where it renders | its own route, /gw-debug | an overlay, on every page |
| Colours | edit theme.ts | colors prop + exported palettes |
| Install | copy a folder | npm i + one component |
| Customising the UI | fork freely, it is your code | props only |
If you expect to restyle heavily, the folder is still the better choice.
Licence
MIT © 2026 JAINEEL PATEL
