-
Notifications
You must be signed in to change notification settings - Fork 0
JWT Expiry Hints
camouflage.nvim detects JSON Web Token (JWT) values among the secrets it has masked, decodes the exp claim, and renders a badge next to the masked line showing how much time is left before the token expires.
The badge is composed with any other check on the same line (such as Have I Been Pwned or Weak Secret Check) through a shared badges renderer, so checks do not visually conflict.
JWT_TOKEN=********* [Google valid 2h]
ID_TOKEN=********** [expires in 30m]
SESSION=*********** [expired 3d ago]
- Every value produced by a parser is offered to the expiry check via the
variable_detectedhook. - The string is checked against a conservative JWT shape (
eyJ...prefix, three url-safe-base64 segments separated by dots). - The header and payload are base64url-decoded with a pure-Lua decoder, then parsed with
vim.json.decode. - The
expclaim (Unix timestamp, seconds) is compared against the current time. - The remaining seconds are classified against configurable thresholds and a badge is written into the checks store. The badges renderer draws one extmark per line from all check results.
- A background timer (configurable interval, default 60s) re-classifies existing badges without re-parsing, so
valid 2hbecomesvalid 1hand thenexpires in 59mas time passes.
No network calls. No signature verification. This is a privacy-friendly, local hint, not a security check.
remaining = exp − now
remaining ≤ 0 → "expired Nd ago" (error / red)
remaining < warn_threshold_seconds → "expires in Nm" (warning / yellow)
warn ≤ remaining < show_threshold_seconds → "valid Nh" (info / Comment)
remaining ≥ show_threshold_seconds → no badge (too far out to be useful)
Defaults: show_threshold_seconds = 86400 (24h), warn_threshold_seconds = 3600 (1h).
require('camouflage').setup({
checks = {
-- Badges layer (shared by pwned + weak_secret + expiry + custom checks)
badges = {
position = 'right_align', -- 'right_align' | 'eol' | 'inline'
separator = ' ', -- between adjacent badges
separator_hl = 'Comment',
},
expiry = {
enabled = true,
show_threshold_seconds = 86400,
warn_threshold_seconds = 3600,
show_provider = true, -- include name from `iss` claim
refresh = {
on_buf_enter = true, -- re-render when a buffer is entered
on_save = true, -- re-render after the file is written
on_change = true, -- re-render after text changes
auto_interval = 60, -- background re-render seconds, 0 disables
},
hl_valid = 'Comment',
hl_warning = 'DiagnosticWarn',
hl_expired = 'DiagnosticError',
},
},
})| Option | Type | Default | Description |
|---|---|---|---|
enabled |
boolean |
true |
Enable/disable the entire expiry check |
show_threshold_seconds |
integer |
86400 |
Only show badge when remaining time is below this |
warn_threshold_seconds |
integer |
3600 |
Switch badge to warning color when below this |
show_provider |
boolean |
true |
Prefix badge with provider name detected from iss
|
refresh.on_buf_enter |
boolean |
true |
Re-render badges when the buffer is entered |
refresh.on_save |
boolean |
true |
Re-render badges after the file is written |
refresh.on_change |
boolean |
true |
Re-render badges after text changes |
refresh.auto_interval |
integer |
60 |
Background timer seconds, 0 turns it off |
hl_valid |
string |
'Comment' |
Highlight when token is valid but within show threshold |
hl_warning |
string |
'DiagnosticWarn' |
Highlight when within warn threshold |
hl_expired |
string |
'DiagnosticError' |
Highlight when expired |
JWTs are typically very long (200+ characters). With virt_text_pos = 'eol' the badge would land far off-screen on most monitors. The default 'right_align' pins the badge to the right edge of the window so it stays visible regardless of line length.
| Position | Behavior |
|---|---|
right_align (default) |
Pinned to the right edge of the window |
eol |
At the end of the line, like classic Neovim virtual text |
inline |
Inserted after the line text, pushing content right |
When show_provider = true, the iss claim is matched against this list. Unknown issuers get no provider tag.
| Provider | Matched on |
|---|---|
accounts.google.com |
|
| Auth0 | *.auth0.com |
| Microsoft |
login.microsoftonline.com, sts.windows.net
|
| GitHub | github.com |
| GitHub Actions | token.actions.githubusercontent.com |
| Cognito | cognito-idp.* |
| Okta | *.okta.com |
| Firebase |
*.firebaseapp.com, securetoken.google.com
|
| Command | Description |
|---|---|
:CamouflageExpiryToggle |
Toggle the expiry check on/off at runtime |
The hook system handles re-detection on every edit, and the background timer keeps badge text fresh, so no manual "check" or "refresh" command is needed.
Decoding a token is done once per value and kept while the value and the config stay the same, so a re-render is only the arithmetic for the time left. The badge never goes stale from that.
Both checks write CheckResult entries into a per-buffer store, keyed by line. The shared badges renderer:
- Composes one extmark per line with all checks'
textjoined byseparator. - Sorts checks in a fixed order:
pwned, thenweak_secret, thenexpiry, then custom checks alphabetically. - Lets the highest-severity check own the sign column and line highlight, since a line can have only one of each (
errorwins overwarning, which wins overinfo).
This is why PASSWORD=password can show the pwned count and [weak: default] side by side without overlapping virtual text.
-
Heuristic detection. A value is treated as a JWT only when it starts with
eyJand has a valid base64url header containing analgfield. False positives are rare in practice, and a non-standard token can be missed. -
No signature verification. The plugin trusts the
expclaim as-is. If you need cryptographic verification, this is not the tool. -
expis the only claim that matters. Tokens without anexpclaim never produce a badge. -
Clock skew. Comparison uses
os.time(). If your local clock is wrong, badges will be wrong by the same amount.
- Have I Been Pwned: sibling check that also renders through the badges layer
- Weak Secret Check: offline quality hints that share the same badges layer
-
Events and Hooks: the
variable_detectedhook that expiry subscribes to - Configuration: full configuration reference