jwtpeek 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +92 -0
- package/bin/cli.js +137 -0
- package/package.json +44 -0
- package/src/core.js +159 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 jwtpeek contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# jwtpeek
|
|
2
|
+
|
|
3
|
+
**Decode and inspect a JWT in your terminal.** See the header, the payload, and
|
|
4
|
+
a human-readable expiry verdict — without pasting your token into a website.
|
|
5
|
+
**Zero dependencies, fully offline, nothing uploaded.**
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx jwtpeek eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
Header
|
|
13
|
+
{
|
|
14
|
+
"alg": "HS256",
|
|
15
|
+
"typ": "JWT"
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
Payload
|
|
19
|
+
{
|
|
20
|
+
"sub": "1234567890",
|
|
21
|
+
"name": "John Doe",
|
|
22
|
+
"iat": 1516239022
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
Claims
|
|
26
|
+
issued 2018-01-18 01:30:22 UTC 8y 150d ago
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Why
|
|
30
|
+
|
|
31
|
+
Debugging auth means constantly asking *"what's actually in this token, and has
|
|
32
|
+
it expired?"* The usual answers are bad: paste it into **jwt.io** (your token —
|
|
33
|
+
often a live credential — now lives in a browser tab and maybe a third party's
|
|
34
|
+
logs), or hand-assemble a pipeline nobody remembers:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
echo "$TOKEN" | cut -d. -f2 | base64 -d 2>/dev/null | python -m json.tool # and base64 -d vs -D differs across macOS/Linux…
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`jwtpeek` is one command. It runs entirely on your machine, makes no network
|
|
41
|
+
requests, and prints the time claims (`exp`, `iat`, `nbf`, …) as real dates plus
|
|
42
|
+
"expires in 3h 21m" / "EXPIRED 2d ago".
|
|
43
|
+
|
|
44
|
+
## Decode, not verify — read this
|
|
45
|
+
|
|
46
|
+
> **jwtpeek never checks the signature.** It shows you what a token *says*, not
|
|
47
|
+
> whether it's authentic. Anyone can forge a token that decodes cleanly. Never
|
|
48
|
+
> trust decoded contents for an authorization decision — verify the signature
|
|
49
|
+
> against the issuer's key first (that needs a secret/public key, which is out
|
|
50
|
+
> of scope for a decoder).
|
|
51
|
+
|
|
52
|
+
## Usage
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
jwtpeek <token> # header, payload & expiry (human-readable)
|
|
56
|
+
jwtpeek <token> --json # {header, payload, signature, expired, notYetValid}
|
|
57
|
+
jwtpeek <token> --header # only the decoded header (JSON)
|
|
58
|
+
jwtpeek <token> --payload # only the decoded payload (JSON)
|
|
59
|
+
|
|
60
|
+
pbpaste | jwtpeek # read the token from stdin
|
|
61
|
+
echo "$AUTH_HEADER" | jwtpeek # a leading "Bearer "/"Authorization:" is stripped
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Pipe-friendly: the decoded output goes to **stdout**, the "not verified" safety
|
|
65
|
+
note goes to **stderr**, so `jwtpeek "$t" --json | jq .payload` stays clean.
|
|
66
|
+
|
|
67
|
+
### Scripting with the exit code
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
0 decoded OK and not expired (or no exp claim)
|
|
71
|
+
1 decoded OK but the token is expired
|
|
72
|
+
2 not a valid JWT (decode error)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
jwtpeek "$TOKEN" >/dev/null 2>&1 && echo "still valid" || echo "expired or invalid"
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Install
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
npm install -g jwtpeek # then: jwtpeek <token>
|
|
83
|
+
# or just run it once:
|
|
84
|
+
npx jwtpeek <token>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Requires Node ≥ 18. There is also a byte-for-byte Python port:
|
|
88
|
+
`pip install jwtpeek` ([jwtpeek-py](https://github.com/jjdoor/jwtpeek-py)).
|
|
89
|
+
|
|
90
|
+
## License
|
|
91
|
+
|
|
92
|
+
MIT
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const core = require('../src/core.js');
|
|
6
|
+
const VERSION = require('../package.json').version;
|
|
7
|
+
|
|
8
|
+
// ----- tiny color helpers (no dep) -----
|
|
9
|
+
const useColor = process.stdout.isTTY && !process.env.NO_COLOR && !process.argv.includes('--no-color');
|
|
10
|
+
const col = (c, s) => (useColor ? `\x1b[${c}m${s}\x1b[0m` : s);
|
|
11
|
+
const red = (s) => col('31', s), green = (s) => col('32', s), yellow = (s) => col('33', s),
|
|
12
|
+
dim = (s) => col('2', s), bold = (s) => col('1', s), cyan = (s) => col('36', s);
|
|
13
|
+
|
|
14
|
+
const HELP = `${bold('jwtpeek')} — decode & inspect a JWT in your terminal. Decodes only, never verifies.
|
|
15
|
+
|
|
16
|
+
${bold('Usage')}
|
|
17
|
+
jwtpeek <token> Decode a token and show header, payload & expiry
|
|
18
|
+
jwtpeek <token> --json Machine-readable {header, payload, signature, ...}
|
|
19
|
+
jwtpeek <token> --header Print only the decoded header (JSON)
|
|
20
|
+
jwtpeek <token> --payload Print only the decoded payload (JSON)
|
|
21
|
+
pbpaste | jwtpeek Read the token from stdin (pipe)
|
|
22
|
+
echo "$AUTH" | jwtpeek "Bearer "/"Authorization:" prefixes are stripped
|
|
23
|
+
|
|
24
|
+
${bold('Options')}
|
|
25
|
+
--json Full structure as JSON (stdout stays pure; notes go to stderr)
|
|
26
|
+
--header Only the header object
|
|
27
|
+
--payload Only the payload object
|
|
28
|
+
--no-color Disable ANSI colors
|
|
29
|
+
-v, --version
|
|
30
|
+
-h, --help
|
|
31
|
+
|
|
32
|
+
${bold('Exit')} 0 valid (or no exp) · 1 expired · 2 decode error
|
|
33
|
+
`;
|
|
34
|
+
|
|
35
|
+
function fail(msg) {
|
|
36
|
+
process.stderr.write(red(`jwtpeek: ${msg}\n`));
|
|
37
|
+
process.exit(2);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function readStdin() {
|
|
41
|
+
try { return fs.readFileSync(0, 'utf8'); } catch { return ''; }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// "2018-01-18T02:30:22.000Z" -> "2018-01-18 02:30:22 UTC"
|
|
45
|
+
function isoUTC(ms) {
|
|
46
|
+
return new Date(ms).toISOString().replace('T', ' ').replace(/\.\d+Z$/, '') + ' UTC';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function indentJSON(obj) {
|
|
50
|
+
return JSON.stringify(obj, null, 2).split('\n').map((l) => ' ' + l).join('\n');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function claimVerdict(key, ms, exp, nowMs) {
|
|
54
|
+
if (key === 'exp') {
|
|
55
|
+
return exp.expired
|
|
56
|
+
? red(`EXPIRED ${core.formatDuration(nowMs - ms)} ago`)
|
|
57
|
+
: green(`expires in ${core.formatDuration(ms - nowMs)}`);
|
|
58
|
+
}
|
|
59
|
+
if (key === 'nbf') {
|
|
60
|
+
return nowMs < ms
|
|
61
|
+
? yellow(`not valid for ${core.formatDuration(ms - nowMs)}`)
|
|
62
|
+
: dim(`valid since ${core.formatDuration(nowMs - ms)} ago`);
|
|
63
|
+
}
|
|
64
|
+
return ms <= nowMs
|
|
65
|
+
? dim(`${core.formatDuration(nowMs - ms)} ago`)
|
|
66
|
+
: dim(`in ${core.formatDuration(ms - nowMs)}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function printHuman(decoded, exp, nowMs) {
|
|
70
|
+
const out = [bold('Header'), indentJSON(decoded.header), '',
|
|
71
|
+
bold('Payload'), indentJSON(decoded.payload)];
|
|
72
|
+
|
|
73
|
+
const rows = [];
|
|
74
|
+
for (const [key, label] of core.TIME_CLAIMS) {
|
|
75
|
+
if (!(key in decoded.payload)) continue;
|
|
76
|
+
const ms = core.normalizeEpochMs(decoded.payload[key]);
|
|
77
|
+
if (ms == null) continue;
|
|
78
|
+
rows.push({ key, label, ms });
|
|
79
|
+
}
|
|
80
|
+
if (rows.length) {
|
|
81
|
+
out.push('', bold('Claims'));
|
|
82
|
+
const w = Math.max(...rows.map((r) => r.label.length));
|
|
83
|
+
for (const r of rows) {
|
|
84
|
+
out.push(` ${cyan(r.label.padEnd(w))} ${dim(isoUTC(r.ms))} ${claimVerdict(r.key, r.ms, exp, nowMs)}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
process.stdout.write(out.join('\n') + '\n');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function main() {
|
|
91
|
+
const argv = process.argv.slice(2);
|
|
92
|
+
if (argv.includes('-h') || argv.includes('--help')) { process.stdout.write(HELP); process.exit(0); }
|
|
93
|
+
if (argv.includes('-v') || argv.includes('--version')) { process.stdout.write(VERSION + '\n'); process.exit(0); }
|
|
94
|
+
if (argv.length === 0 && process.stdin.isTTY) { process.stdout.write(HELP); process.exit(0); }
|
|
95
|
+
|
|
96
|
+
const flags = new Set(argv.filter((a) => a.startsWith('-')));
|
|
97
|
+
const positional = argv.filter((a) => !a.startsWith('-'));
|
|
98
|
+
|
|
99
|
+
let raw = positional[0];
|
|
100
|
+
if (!raw) {
|
|
101
|
+
if (process.stdin.isTTY) fail('no token given (pass it as an argument or pipe it in)');
|
|
102
|
+
raw = readStdin();
|
|
103
|
+
}
|
|
104
|
+
const token = core.stripToken(raw);
|
|
105
|
+
if (!token) fail('no token given (pass it as an argument or pipe it in)');
|
|
106
|
+
|
|
107
|
+
let decoded;
|
|
108
|
+
try { decoded = core.decodeJwt(token); }
|
|
109
|
+
catch (e) { fail(e.message); }
|
|
110
|
+
|
|
111
|
+
const nowMs = Date.now();
|
|
112
|
+
const exp = core.evaluateExpiry(decoded.payload, nowMs);
|
|
113
|
+
|
|
114
|
+
if (flags.has('--header')) {
|
|
115
|
+
process.stdout.write(JSON.stringify(decoded.header, null, 2) + '\n');
|
|
116
|
+
} else if (flags.has('--payload')) {
|
|
117
|
+
process.stdout.write(JSON.stringify(decoded.payload, null, 2) + '\n');
|
|
118
|
+
} else if (flags.has('--json')) {
|
|
119
|
+
process.stdout.write(JSON.stringify({
|
|
120
|
+
header: decoded.header,
|
|
121
|
+
payload: decoded.payload,
|
|
122
|
+
signature: decoded.signature,
|
|
123
|
+
expired: exp.expired,
|
|
124
|
+
notYetValid: exp.notYetValid,
|
|
125
|
+
}, null, 2) + '\n');
|
|
126
|
+
} else {
|
|
127
|
+
printHuman(decoded, exp, nowMs);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Safety note always to stderr so it never pollutes stdout / pipes.
|
|
131
|
+
process.stderr.write(dim('note: signature NOT verified — jwtpeek decodes only\n'));
|
|
132
|
+
if (exp.notYetValid) process.stderr.write(yellow('warning: token is not valid yet (nbf is in the future)\n'));
|
|
133
|
+
|
|
134
|
+
process.exit(exp.expired ? 1 : 0);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "jwtpeek",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Decode & inspect JWT tokens in your terminal — header, payload, and human-readable expiry. Zero dependencies, fully offline, nothing uploaded.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"jwtpeek": "bin/cli.js"
|
|
7
|
+
},
|
|
8
|
+
"main": "src/core.js",
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "node --test"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"jwt",
|
|
14
|
+
"jwt-decode",
|
|
15
|
+
"json-web-token",
|
|
16
|
+
"jwt-cli",
|
|
17
|
+
"token",
|
|
18
|
+
"decode",
|
|
19
|
+
"auth",
|
|
20
|
+
"oauth",
|
|
21
|
+
"oidc",
|
|
22
|
+
"cli",
|
|
23
|
+
"devtools",
|
|
24
|
+
"debug"
|
|
25
|
+
],
|
|
26
|
+
"files": [
|
|
27
|
+
"bin",
|
|
28
|
+
"src",
|
|
29
|
+
"README.md",
|
|
30
|
+
"LICENSE"
|
|
31
|
+
],
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=18"
|
|
34
|
+
},
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "git+https://github.com/jjdoor/jwtpeek.git"
|
|
39
|
+
},
|
|
40
|
+
"homepage": "https://github.com/jjdoor/jwtpeek#readme",
|
|
41
|
+
"bugs": {
|
|
42
|
+
"url": "https://github.com/jjdoor/jwtpeek/issues"
|
|
43
|
+
}
|
|
44
|
+
}
|
package/src/core.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* jwtpeek core — pure JWT decoding + claim classification.
|
|
5
|
+
*
|
|
6
|
+
* No fs, no clock, no network: every function takes its inputs explicitly so
|
|
7
|
+
* this and the Python port behave identically and stay unit-testable.
|
|
8
|
+
*
|
|
9
|
+
* IMPORTANT: jwtpeek DECODES, it does not VERIFY. A JWT's signature is what
|
|
10
|
+
* proves the payload wasn't tampered with; we never check it. Treat decoded
|
|
11
|
+
* contents as untrusted unless you've verified the signature with the issuer's
|
|
12
|
+
* key.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const UNITS = { y: 31536000000, d: 86400000, h: 3600000, m: 60000, s: 1000 };
|
|
16
|
+
|
|
17
|
+
// Registered date claims (RFC 7519 + common OIDC) mapped to a display label.
|
|
18
|
+
// This is also the print order; `exp` is last so the validity verdict lands
|
|
19
|
+
// as the kicker.
|
|
20
|
+
const TIME_CLAIMS = [
|
|
21
|
+
['iat', 'issued'],
|
|
22
|
+
['nbf', 'not before'],
|
|
23
|
+
['auth_time', 'auth time'],
|
|
24
|
+
['updated_at', 'updated'],
|
|
25
|
+
['exp', 'expires'],
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Strip the noise that creeps in when a token is copy-pasted: surrounding
|
|
30
|
+
* quotes, an "Authorization:"/"Bearer " prefix, and any wrapping whitespace
|
|
31
|
+
* (including the line breaks a wrapped terminal paste introduces).
|
|
32
|
+
*
|
|
33
|
+
* @param {*} input
|
|
34
|
+
* @returns {string}
|
|
35
|
+
*/
|
|
36
|
+
function stripToken(input) {
|
|
37
|
+
let s = String(input == null ? '' : input).trim();
|
|
38
|
+
s = s.replace(/^['"]+|['"]+$/g, '');
|
|
39
|
+
s = s.replace(/^Authorization:\s*/i, '');
|
|
40
|
+
s = s.replace(/^Bearer\s+/i, '');
|
|
41
|
+
s = s.replace(/\s+/g, '');
|
|
42
|
+
return s;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Decode one base64url segment to a UTF-8 string. Throws on non-base64url so
|
|
47
|
+
* callers can report a precise error instead of silently mangling bytes.
|
|
48
|
+
*
|
|
49
|
+
* @param {string} seg
|
|
50
|
+
* @returns {string}
|
|
51
|
+
*/
|
|
52
|
+
function b64urlToString(seg) {
|
|
53
|
+
if (!/^[A-Za-z0-9_-]+$/.test(seg)) throw new Error('not base64url');
|
|
54
|
+
let b64 = seg.replace(/-/g, '+').replace(/_/g, '/');
|
|
55
|
+
while (b64.length % 4) b64 += '=';
|
|
56
|
+
return Buffer.from(b64, 'base64').toString('utf8');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function parseSegment(seg, which) {
|
|
60
|
+
if (!seg) throw new Error(`${which} segment is empty`);
|
|
61
|
+
let json;
|
|
62
|
+
try { json = b64urlToString(seg); }
|
|
63
|
+
catch { throw new Error(`${which} is not valid base64url`); }
|
|
64
|
+
let obj;
|
|
65
|
+
try { obj = JSON.parse(json); }
|
|
66
|
+
catch { throw new Error(`${which} is not valid JSON`); }
|
|
67
|
+
if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) {
|
|
68
|
+
throw new Error(`${which} is not a JSON object`);
|
|
69
|
+
}
|
|
70
|
+
return obj;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Decode a JWT into { header, payload, signature }. Accepts a 2-part unsecured
|
|
75
|
+
* token (alg: none) as well as the usual 3-part form. Throws an Error with a
|
|
76
|
+
* human message on anything that isn't a structurally valid JWT.
|
|
77
|
+
*
|
|
78
|
+
* Does NOT verify the signature — see the module note.
|
|
79
|
+
*
|
|
80
|
+
* @param {string} token
|
|
81
|
+
* @returns {{ header: object, payload: object, signature: string }}
|
|
82
|
+
*/
|
|
83
|
+
function decodeJwt(token) {
|
|
84
|
+
const t = String(token == null ? '' : token).trim();
|
|
85
|
+
if (!t) throw new Error('empty token');
|
|
86
|
+
const parts = t.split('.');
|
|
87
|
+
if (parts.length < 2 || parts.length > 3) {
|
|
88
|
+
throw new Error(`not a JWT: expected 2 or 3 dot-separated parts, got ${parts.length}`);
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
header: parseSegment(parts[0], 'header'),
|
|
92
|
+
payload: parseSegment(parts[1], 'payload'),
|
|
93
|
+
signature: parts.length === 3 ? parts[2] : '',
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Normalize a JWT NumericDate to epoch milliseconds. Per RFC 7519 these are
|
|
99
|
+
* *seconds*, but some issuers wrongly emit milliseconds — detect that (a real
|
|
100
|
+
* seconds value won't reach 1e12 until the year 5138) and pass it through.
|
|
101
|
+
* Returns null for anything that isn't a finite number.
|
|
102
|
+
*
|
|
103
|
+
* @param {*} value
|
|
104
|
+
* @returns {number|null}
|
|
105
|
+
*/
|
|
106
|
+
function normalizeEpochMs(value) {
|
|
107
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) return null;
|
|
108
|
+
return value >= 1e12 ? Math.round(value) : Math.round(value * 1000);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Evaluate validity windows against a supplied `nowMs` (no clock in here).
|
|
113
|
+
* `expired`/`notYetValid` are null when the corresponding claim is absent.
|
|
114
|
+
*
|
|
115
|
+
* @param {object} payload
|
|
116
|
+
* @param {number} nowMs
|
|
117
|
+
* @returns {{ expMs:number|null, nbfMs:number|null, expired:boolean|null, notYetValid:boolean|null }}
|
|
118
|
+
*/
|
|
119
|
+
function evaluateExpiry(payload, nowMs) {
|
|
120
|
+
const expMs = normalizeEpochMs(payload && payload.exp);
|
|
121
|
+
const nbfMs = normalizeEpochMs(payload && payload.nbf);
|
|
122
|
+
return {
|
|
123
|
+
expMs,
|
|
124
|
+
nbfMs,
|
|
125
|
+
expired: expMs == null ? null : nowMs >= expMs,
|
|
126
|
+
notYetValid: nbfMs == null ? null : nowMs < nbfMs,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Compact, two-unit-max human span, e.g. "1d 3h", "7y 2d", "45s". Sign is
|
|
132
|
+
* ignored (callers phrase "ago"/"in" themselves).
|
|
133
|
+
*
|
|
134
|
+
* @param {number} ms
|
|
135
|
+
* @returns {string}
|
|
136
|
+
*/
|
|
137
|
+
function formatDuration(ms) {
|
|
138
|
+
ms = Math.abs(Math.round(Number(ms) || 0));
|
|
139
|
+
if (ms < 1000) return '0s';
|
|
140
|
+
const order = [['y', UNITS.y], ['d', UNITS.d], ['h', UNITS.h], ['m', UNITS.m], ['s', UNITS.s]];
|
|
141
|
+
const parts = [];
|
|
142
|
+
let rem = ms;
|
|
143
|
+
for (const [label, size] of order) {
|
|
144
|
+
if (rem >= size) {
|
|
145
|
+
const n = Math.floor(rem / size);
|
|
146
|
+
rem -= n * size;
|
|
147
|
+
parts.push(`${n}${label}`);
|
|
148
|
+
if (parts.length === 2) break;
|
|
149
|
+
} else if (parts.length) {
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return parts.length ? parts.join(' ') : '0s';
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
module.exports = {
|
|
157
|
+
UNITS, TIME_CLAIMS, stripToken, b64urlToString, decodeJwt,
|
|
158
|
+
normalizeEpochMs, evaluateExpiry, formatDuration,
|
|
159
|
+
};
|