Access Token
Access Token
Nest Graph Inspector installs its endpoints on a native HTTP server inside your running application. Those endpoints expose your application's internal structure, and the Direct Run endpoint invokes live provider or controller methods, so by default every one of them is gated behind a short-lived access token.
Token protection is on unless you set accessToken.enabled to false. You do
not need to configure anything.
accessToken?: {
enabled?: boolean
ttlMs?: number
secret?: string
logToken?: boolean
bruteForce?: { /* see below */ }
}
What a token gates
Tokens gate every endpoint the inspector installs, and nothing else in your
application. For a Viewer output, the graph routes below are installed; the
Direct Run and history routes are installed only when that output's
directRun.enabled is not false (it defaults to true):
| Endpoint | What it serves |
|---|---|
GET /__graph-inspector/information.json | Which inspector version is installed and what it serves |
GET /__graph-inspector/output.json | Your dependency graph |
GET /__graph-inspector/output.schema.json | The JSON Schema that graph conforms to |
GET /__graph-inspector/output.md | The same graph as Markdown |
POST /direct-run | Invokes an advertised provider or controller method; exists only when Direct Run is enabled |
GET /direct-run/histories | Completed Direct Run traces in full; exists only when Direct Run is enabled |
GET /direct-run/history/index.json | A summary list of completed traces; exists only when Direct Run is enabled |
GET /direct-run/history/<traceId> | One completed trace; exists only when Direct Run is enabled |
The graph paths above are relative to the Viewer output's path.
/direct-run is the default Direct Run path and follows directRun.path if
you change it. With directRun.enabled: false, the graph remains available but
none of the Direct Run or history paths exist, and its graph omits Direct Run
metadata. The AI chat is not on this list because it is not an endpoint: the
model runs inside your browser and reads the graph through output.md, which
is already gated.
How a token travels
A token is issued when your application starts and is embedded directly in the
viewer link printed to your console, so opening that link just works. That link
is the only thing that hands a token over: the viewer reads it once, keeps it
for the tab, and immediately rewrites its own address bar without it. From then
on it authenticates with the x-graph-inspector-token header on every
follow-up request, Direct Run included.
That means the URL you can copy out of the viewer, share, or leave in your history is not a working credential — only the link printed in your console is.
When you call the endpoints yourself, send the token any of these ways:
curl -H 'Authorization: Bearer <token>' http://localhost:53371/__graph-inspector/output.json
curl -H 'x-graph-inspector-token: <token>' http://localhost:53371/__graph-inspector/output.json
curl 'http://localhost:53371/__graph-inspector/output.json?__inspector_token=<token>'
A request without a valid token is answered with 401 and a JSON body naming
the reason: missing, malformed, signature, or expired. Two cases do not
answer 401: a caller that has been locked out for repeated guesses gets 429
instead (see below), and a CORS preflight (OPTIONS) is not gated at all,
because it carries no token of its own and blocking it would stop the browser
from ever sending the request it is asking about.
Expiry
A token carries its own expiry. Issued at 13:00 with the default lifetime, it stops working at 16:00 — the payload is signed, so the expiry cannot be edited without invalidating the token.
Once a token expires, restart the application to print a fresh viewer link.
Can a token be guessed?
Not by trying. A token is accepted only if its HMAC-SHA256 signature checks out, which means guessing one means guessing a 256-bit value.
The part worth your attention is the signing secret. Left unset, it is 32
random bytes generated per process and there is nothing to guess. If you set
secret yourself, keep it long: a token exposes both the payload it signs and
the resulting signature, so anyone holding a single leaked token can test
candidate secrets offline, on their own hardware, without ever touching your
application. A short or dictionary secret falls quickly that way. The library
warns at startup when a configured secret is under 32 characters.
The other real risk is leakage rather than guessing. A token placed in a URL travels into access logs, proxy logs, and browser history, so treat a token as you would a password and prefer the header form for anything scripted — which is what the viewer itself does.
The startup message is itself one of those places. It is the only channel that
hands a token out, so it prints one by default, which means anything collecting
your application logs collects a working credential along with it. Where that
matters, set logToken: false: the token is then left out of the message and
out of the printed viewer link, and you supply your own by configuring a
secret and minting tokens with the exported AccessTokenService.
Brute force lockout
Repeated invalid tokens from the same client address are counted, and once a
client runs out of attempts it is refused with 429 and a Retry-After
header until the block expires.
accessToken?: {
bruteForce?: {
enabled?: boolean
maxFailures?: number
windowMs?: number
blockMs?: number
maxTrackedClients?: number
}
}
A block applies to the client, not the token: once blocked, even a valid token is refused until the block lifts. Other clients are unaffected.
Two kinds of rejection are deliberately not counted as guesses:
- a request with no token at all, which is not an attempt at one, and would otherwise lock out anyone who simply opened an endpoint in a browser
- a request with a genuine but expired token, which could only have come from something that already held a real one
Clients are identified by socket address. A forwarded header such as
X-Forwarded-For is intentionally ignored, since the caller sets it: trusting
one would let a brute force rotate past the lockout, and would let anyone lock
out an address of their choosing. If you place the inspector behind a reverse
proxy, every request arrives with the proxy's address and the lockout becomes
effectively global.
| API | Description | Default |
|---|---|---|
enabled?: boolean | Whether failed guesses are counted at all. | true |
maxFailures?: number | Invalid tokens allowed inside one window before the client is blocked. | 10 |
windowMs?: number | How long failures accumulate before the count resets. | 60000 |
blockMs?: number | How long a blocked client is refused. | 900000 |
maxTrackedClients?: number | Upper bound on tracked addresses, so the tracker cannot be grown without limit. | 1000 |
API
| API | Description | Benefit |
|---|---|---|
enabled?: boolean | Whether inspector endpoints require a token. Defaults to true. | Lets you opt out on a host where the inspector port is already unreachable from outside. |
ttlMs?: number | How long an issued token stays valid, in milliseconds. Defaults to three hours. | Matches the token lifetime to how long a debugging session actually lasts. |
secret?: string | Signing secret, 32 characters or more. Defaults to the NEST_GRAPH_INSPECTOR_TOKEN_SECRET environment variable, then to a random per-process secret. | Keeps issued tokens valid across restarts, which matters for containers that reboot often. |
logToken?: boolean | Whether the issued token is printed on startup. Defaults to true. | Keeps a live credential out of shipped application logs when set to false, at the cost of minting tokens yourself. |
bruteForce?: object | Per-client lockout for repeated invalid tokens. See above. | Caps how fast an attacker can try at all, and surfaces the attempt as a logged block. |
Example
NestGraphInspectorModule.forRoot({
outputs: [{ type: 'viewer' }],
accessToken: {
ttlMs: 30 * 60 * 1000,
secret: process.env.INSPECTOR_SECRET,
},
})
enabled off removes the only check in front of the Direct Run
endpoint, which executes provider or controller methods against your running
application with its real database connections. Leave it on unless the port is unreachable from
anywhere you do not control.