@rashidv/jwt-decoder 1.0.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/.github/workflows/publish-jwt-decoder.yml +40 -0
- package/.github/workflows/publish.yml +36 -0
- package/LICENSE +21 -0
- package/README.md +134 -0
- package/dist/components/DecodedView.d.ts +7 -0
- package/dist/components/DecodedView.d.ts.map +1 -0
- package/dist/components/DecodedView.js +17 -0
- package/dist/components/DecodedView.js.map +1 -0
- package/dist/components/ExpiryTimer.d.ts +7 -0
- package/dist/components/ExpiryTimer.d.ts.map +1 -0
- package/dist/components/ExpiryTimer.js +29 -0
- package/dist/components/ExpiryTimer.js.map +1 -0
- package/dist/components/JWTDecoder.d.ts +2 -0
- package/dist/components/JWTDecoder.d.ts.map +1 -0
- package/dist/components/JWTDecoder.js +32 -0
- package/dist/components/JWTDecoder.js.map +1 -0
- package/dist/components/TokenInput.d.ts +7 -0
- package/dist/components/TokenInput.d.ts.map +1 -0
- package/dist/components/TokenInput.js +27 -0
- package/dist/components/TokenInput.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/jwt.d.ts +35 -0
- package/dist/lib/jwt.d.ts.map +1 -0
- package/dist/lib/jwt.js +79 -0
- package/dist/lib/jwt.js.map +1 -0
- package/dist/lib/utils.d.ts +21 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +41 -0
- package/dist/lib/utils.js.map +1 -0
- package/package.json +39 -0
- package/src/components/DecodedView.tsx +59 -0
- package/src/components/ExpiryTimer.tsx +110 -0
- package/src/components/JWTDecoder.tsx +61 -0
- package/src/components/TokenInput.tsx +75 -0
- package/src/index.ts +7 -0
- package/src/lib/jwt.ts +108 -0
- package/src/lib/utils.ts +43 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
name: Publish JWT Decoder to npm
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "jwt-decoder-v1.0.0"
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
publish:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
|
|
12
|
+
defaults:
|
|
13
|
+
run:
|
|
14
|
+
working-directory: packages/jwt-decoder
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- name: Checkout code
|
|
18
|
+
uses: actions/checkout@v4
|
|
19
|
+
|
|
20
|
+
- name: Setup pnpm
|
|
21
|
+
uses: pnpm/action-setup@v2
|
|
22
|
+
with:
|
|
23
|
+
version: 10
|
|
24
|
+
|
|
25
|
+
- name: Setup Node.js
|
|
26
|
+
uses: actions/setup-node@v4
|
|
27
|
+
with:
|
|
28
|
+
node-version: "20"
|
|
29
|
+
registry-url: "https://registry.npmjs.org"
|
|
30
|
+
|
|
31
|
+
- name: Install dependencies
|
|
32
|
+
run: pnpm install
|
|
33
|
+
|
|
34
|
+
- name: Build package
|
|
35
|
+
run: pnpm build
|
|
36
|
+
|
|
37
|
+
- name: Publish to npm
|
|
38
|
+
run: npm publish --access public
|
|
39
|
+
env:
|
|
40
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
name: Publish to npm
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- 'v*' # Triggers on tags like v1.0.0, v1.0.1, etc.
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
publish:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
|
|
12
|
+
steps:
|
|
13
|
+
- name: Checkout code
|
|
14
|
+
uses: actions/checkout@v4
|
|
15
|
+
|
|
16
|
+
- name: Setup pnpm
|
|
17
|
+
uses: pnpm/action-setup@v2
|
|
18
|
+
with:
|
|
19
|
+
version: 10
|
|
20
|
+
|
|
21
|
+
- name: Setup Node.js
|
|
22
|
+
uses: actions/setup-node@v4
|
|
23
|
+
with:
|
|
24
|
+
node-version: '20'
|
|
25
|
+
registry-url: 'https://registry.npmjs.org'
|
|
26
|
+
|
|
27
|
+
- name: Install dependencies
|
|
28
|
+
run: pnpm install
|
|
29
|
+
|
|
30
|
+
- name: Build package
|
|
31
|
+
run: pnpm build
|
|
32
|
+
|
|
33
|
+
- name: Publish to npm
|
|
34
|
+
run: npm publish --access public
|
|
35
|
+
env:
|
|
36
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Muhammed Rashid
|
|
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,134 @@
|
|
|
1
|
+
# JWT Decoder & Expiry Checker
|
|
2
|
+
|
|
3
|
+
> Free, open-source JWT decoder with live expiry countdown. Better UX than jwt.io.
|
|
4
|
+
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
|
|
7
|
+
## 🎯 Features
|
|
8
|
+
|
|
9
|
+
- **Decode JWT Tokens** - Instantly decode header and payload
|
|
10
|
+
- **Live Countdown** - Real-time expiry countdown timer
|
|
11
|
+
- **Timezone Support** - Shows expiry in your local timezone
|
|
12
|
+
- **Visual Indicators** - Color-coded status (valid/expired/expiring soon)
|
|
13
|
+
- **Privacy First** - All decoding happens in your browser
|
|
14
|
+
- **Copy to Clipboard** - Quick copy for header/payload
|
|
15
|
+
- **Zero Dependencies** - Lightweight and fast
|
|
16
|
+
|
|
17
|
+
## 🚀 Demo
|
|
18
|
+
|
|
19
|
+
Try it live: [https://rashidv.dev/tools/jwt-decoder](https://rashidv.dev/tools/jwt-decoder)
|
|
20
|
+
|
|
21
|
+
## 📦 Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# npm
|
|
25
|
+
npm install @rashidv/jwt-decoder
|
|
26
|
+
|
|
27
|
+
# pnpm
|
|
28
|
+
pnpm add @rashidv/jwt-decoder
|
|
29
|
+
|
|
30
|
+
# yarn
|
|
31
|
+
yarn add @rashidv/jwt-decoder
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## 🔧 Usage
|
|
35
|
+
|
|
36
|
+
### As a React Component
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
import { JWTDecoder } from '@rashidv/jwt-decoder';
|
|
40
|
+
|
|
41
|
+
export default function MyPage() {
|
|
42
|
+
return (
|
|
43
|
+
<div>
|
|
44
|
+
<h1>JWT Decoder</h1>
|
|
45
|
+
<JWTDecoder />
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Using Individual Components
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
import { TokenInput, DecodedView, ExpiryTimer } from '@rashidv/jwt-decoder';
|
|
55
|
+
import { decodeToken, validateToken } from '@rashidv/jwt-decoder';
|
|
56
|
+
|
|
57
|
+
// Your custom implementation
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## 🎨 Styling
|
|
61
|
+
|
|
62
|
+
The components use Tailwind CSS classes. Make sure you have Tailwind CSS configured in your project.
|
|
63
|
+
|
|
64
|
+
## 📖 API
|
|
65
|
+
|
|
66
|
+
### `JWTDecoder`
|
|
67
|
+
|
|
68
|
+
Main component that includes all functionality.
|
|
69
|
+
|
|
70
|
+
### `TokenInput`
|
|
71
|
+
|
|
72
|
+
Input component for pasting JWT tokens.
|
|
73
|
+
|
|
74
|
+
**Props:**
|
|
75
|
+
- `onTokenChange: (token: string) => void` - Callback when token changes
|
|
76
|
+
- `error?: string` - Error message to display
|
|
77
|
+
|
|
78
|
+
### `DecodedView`
|
|
79
|
+
|
|
80
|
+
Displays decoded header and payload.
|
|
81
|
+
|
|
82
|
+
**Props:**
|
|
83
|
+
- `decoded: DecodedJWT` - Decoded JWT object
|
|
84
|
+
|
|
85
|
+
### `ExpiryTimer`
|
|
86
|
+
|
|
87
|
+
Shows expiry status and countdown.
|
|
88
|
+
|
|
89
|
+
**Props:**
|
|
90
|
+
- `validation: JWTValidation` - Validation result
|
|
91
|
+
|
|
92
|
+
### Utility Functions
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
// Decode a JWT token
|
|
96
|
+
const decoded = decodeToken(token);
|
|
97
|
+
|
|
98
|
+
// Validate and check expiration
|
|
99
|
+
const validation = validateToken(decoded);
|
|
100
|
+
|
|
101
|
+
// Get time remaining
|
|
102
|
+
const timeRemaining = getTimeRemaining(validation.expiresAt);
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## 🤝 Contributing
|
|
106
|
+
|
|
107
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
108
|
+
|
|
109
|
+
1. Fork the repository
|
|
110
|
+
2. Create your feature branch (`git checkout -b feature/AmazingFeature`)
|
|
111
|
+
3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
|
|
112
|
+
4. Push to the branch (`git push origin feature/AmazingFeature`)
|
|
113
|
+
5. Open a Pull Request
|
|
114
|
+
|
|
115
|
+
## 📝 License
|
|
116
|
+
|
|
117
|
+
MIT © [Muhammed Rashid](https://rashidv.dev)
|
|
118
|
+
|
|
119
|
+
## 🔗 Links
|
|
120
|
+
|
|
121
|
+
- **Live Demo:** [rashidv.dev/tools/jwt-decoder](https://rashidv.dev/tools/jwt-decoder)
|
|
122
|
+
- **GitHub:** [github.com/rashidrashiii/jwt-decoder](https://github.com/rashidrashiii/jwt-decoder)
|
|
123
|
+
- **npm:** [@rashidv/jwt-decoder](https://www.npmjs.com/package/@rashidv/jwt-decoder)
|
|
124
|
+
|
|
125
|
+
## 👨💻 Author
|
|
126
|
+
|
|
127
|
+
**Muhammed Rashid**
|
|
128
|
+
- Website: [rashidv.dev](https://rashidv.dev)
|
|
129
|
+
- GitHub: [@rashidrashiii](https://github.com/rashidrashiii)
|
|
130
|
+
- LinkedIn: [muhammed-rashid-v](https://linkedin.com/in/muhammed-rashid-v)
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
Made with ❤️ by [Rashid](https://rashidv.dev)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { DecodedJWT } from '../lib/jwt';
|
|
2
|
+
interface DecodedViewProps {
|
|
3
|
+
decoded: DecodedJWT;
|
|
4
|
+
}
|
|
5
|
+
export declare function DecodedView({ decoded }: DecodedViewProps): import("react/jsx-runtime").JSX.Element;
|
|
6
|
+
export {};
|
|
7
|
+
//# sourceMappingURL=DecodedView.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DecodedView.d.ts","sourceRoot":"","sources":["../../src/components/DecodedView.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAG7C,UAAU,gBAAgB;IACxB,OAAO,EAAE,UAAU,CAAC;CACrB;AAED,wBAAgB,WAAW,CAAC,EAAE,OAAO,EAAE,EAAE,gBAAgB,2CAgDxD"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { formatJSON, copyToClipboard } from '../lib/utils';
|
|
5
|
+
export function DecodedView({ decoded }) {
|
|
6
|
+
const [copiedSection, setCopiedSection] = useState(null);
|
|
7
|
+
const handleCopy = async (section) => {
|
|
8
|
+
const data = section === 'header' ? decoded.header : decoded.payload;
|
|
9
|
+
const success = await copyToClipboard(formatJSON(data));
|
|
10
|
+
if (success) {
|
|
11
|
+
setCopiedSection(section);
|
|
12
|
+
setTimeout(() => setCopiedSection(null), 2000);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
return (_jsxs("div", { className: "grid md:grid-cols-2 gap-4", children: [_jsxs("div", { className: "rounded-lg bg-white/5 border border-white/10 p-4", children: [_jsxs("div", { className: "flex items-center justify-between mb-3", children: [_jsx("h3", { className: "text-sm font-semibold text-gray-300", children: "Header" }), _jsx("button", { onClick: () => handleCopy('header'), className: "text-xs px-3 py-1 rounded bg-white/5 hover:bg-white/10 text-gray-300 transition-colors", children: copiedSection === 'header' ? '✓ Copied' : 'Copy' })] }), _jsx("pre", { className: "text-xs text-gray-300 overflow-x-auto", children: _jsx("code", { children: formatJSON(decoded.header) }) })] }), _jsxs("div", { className: "rounded-lg bg-white/5 border border-white/10 p-4", children: [_jsxs("div", { className: "flex items-center justify-between mb-3", children: [_jsx("h3", { className: "text-sm font-semibold text-gray-300", children: "Payload" }), _jsx("button", { onClick: () => handleCopy('payload'), className: "text-xs px-3 py-1 rounded bg-white/5 hover:bg-white/10 text-gray-300 transition-colors", children: copiedSection === 'payload' ? '✓ Copied' : 'Copy' })] }), _jsx("pre", { className: "text-xs text-gray-300 overflow-x-auto", children: _jsx("code", { children: formatJSON(decoded.payload) }) })] })] }));
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=DecodedView.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DecodedView.js","sourceRoot":"","sources":["../../src/components/DecodedView.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAEjC,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAM3D,MAAM,UAAU,WAAW,CAAC,EAAE,OAAO,EAAoB;IACvD,MAAM,CAAC,aAAa,EAAE,gBAAgB,CAAC,GAAG,QAAQ,CAA8B,IAAI,CAAC,CAAC;IAEtF,MAAM,UAAU,GAAG,KAAK,EAAE,OAA6B,EAAE,EAAE;QACzD,MAAM,IAAI,GAAG,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC;QACrE,MAAM,OAAO,GAAG,MAAM,eAAe,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC;QAExD,IAAI,OAAO,EAAE,CAAC;YACZ,gBAAgB,CAAC,OAAO,CAAC,CAAC;YAC1B,UAAU,CAAC,GAAG,EAAE,CAAC,gBAAgB,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC,CAAC;QACjD,CAAC;IACH,CAAC,CAAC;IAEF,OAAO,CACL,eAAK,SAAS,EAAC,2BAA2B,aAExC,eAAK,SAAS,EAAC,kDAAkD,aAC/D,eAAK,SAAS,EAAC,wCAAwC,aACrD,aAAI,SAAS,EAAC,qCAAqC,uBAAY,EAC/D,iBACE,OAAO,EAAE,GAAG,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EACnC,SAAS,EAAC,wFAAwF,YAEjG,aAAa,KAAK,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,MAAM,GAC1C,IACL,EACN,cAAK,SAAS,EAAC,uCAAuC,YACpD,yBAAO,UAAU,CAAC,OAAO,CAAC,MAAM,CAAC,GAAQ,GACrC,IACF,EAGN,eAAK,SAAS,EAAC,kDAAkD,aAC/D,eAAK,SAAS,EAAC,wCAAwC,aACrD,aAAI,SAAS,EAAC,qCAAqC,wBAAa,EAChE,iBACE,OAAO,EAAE,GAAG,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,EACpC,SAAS,EAAC,wFAAwF,YAEjG,aAAa,KAAK,SAAS,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,MAAM,GAC3C,IACL,EACN,cAAK,SAAS,EAAC,uCAAuC,YACpD,yBAAO,UAAU,CAAC,OAAO,CAAC,OAAO,CAAC,GAAQ,GACtC,IACF,IACF,CACP,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { JWTValidation } from '../lib/jwt';
|
|
2
|
+
interface ExpiryTimerProps {
|
|
3
|
+
validation: JWTValidation;
|
|
4
|
+
}
|
|
5
|
+
export declare function ExpiryTimer({ validation }: ExpiryTimerProps): import("react/jsx-runtime").JSX.Element;
|
|
6
|
+
export {};
|
|
7
|
+
//# sourceMappingURL=ExpiryTimer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExpiryTimer.d.ts","sourceRoot":"","sources":["../../src/components/ExpiryTimer.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAIhD,UAAU,gBAAgB;IACxB,UAAU,EAAE,aAAa,CAAC;CAC3B;AAED,wBAAgB,WAAW,CAAC,EAAE,UAAU,EAAE,EAAE,gBAAgB,2CAkG3D"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
import { getTimeRemaining, formatTimeRemaining } from '../lib/jwt';
|
|
5
|
+
import { formatDate, formatRelativeTime } from '../lib/utils';
|
|
6
|
+
export function ExpiryTimer({ validation }) {
|
|
7
|
+
const [timeRemaining, setTimeRemaining] = useState(null);
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
if (!validation.expiresAt)
|
|
10
|
+
return;
|
|
11
|
+
const updateTimer = () => {
|
|
12
|
+
setTimeRemaining(getTimeRemaining(validation.expiresAt));
|
|
13
|
+
};
|
|
14
|
+
updateTimer();
|
|
15
|
+
const interval = setInterval(updateTimer, 1000);
|
|
16
|
+
return () => clearInterval(interval);
|
|
17
|
+
}, [validation.expiresAt]);
|
|
18
|
+
if (!validation.expiresAt) {
|
|
19
|
+
return (_jsx("div", { className: "rounded-lg bg-yellow-500/10 border border-yellow-500/20 p-4", children: _jsx("p", { className: "text-sm text-yellow-400", children: "\u26A0\uFE0F No expiration claim (exp) found in token" }) }));
|
|
20
|
+
}
|
|
21
|
+
const isExpired = validation.isExpired;
|
|
22
|
+
const isExpiringSoon = timeRemaining && timeRemaining.total > 0 && timeRemaining.total < 5 * 60 * 1000; // 5 minutes
|
|
23
|
+
return (_jsx("div", { className: `rounded-lg border p-4 ${isExpired
|
|
24
|
+
? 'bg-red-500/10 border-red-500/20'
|
|
25
|
+
: isExpiringSoon
|
|
26
|
+
? 'bg-yellow-500/10 border-yellow-500/20'
|
|
27
|
+
: 'bg-green-500/10 border-green-500/20'}`, children: _jsxs("div", { className: "space-y-3", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-2xl", children: isExpired ? '🔴' : isExpiringSoon ? '🟡' : '🟢' }), _jsxs("div", { children: [_jsx("p", { className: `text-sm font-semibold ${isExpired ? 'text-red-400' : isExpiringSoon ? 'text-yellow-400' : 'text-green-400'}`, children: isExpired ? 'Expired' : isExpiringSoon ? 'Expiring Soon' : 'Valid' }), _jsx("p", { className: "text-xs text-gray-400", children: isExpired ? 'This token is no longer valid' : 'Token is currently valid' })] })] }), !isExpired && timeRemaining && (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-lg", children: "\u23F1\uFE0F" }), _jsxs("div", { children: [_jsx("p", { className: "text-sm font-semibold text-white", children: formatTimeRemaining(timeRemaining) }), _jsx("p", { className: "text-xs text-gray-400", children: "Time remaining" })] })] })), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-lg", children: "\uD83D\uDCC5" }), _jsxs("div", { children: [_jsx("p", { className: "text-sm font-semibold text-white", children: formatDate(validation.expiresAt) }), _jsxs("p", { className: "text-xs text-gray-400", children: [isExpired ? 'Expired' : 'Expires', " ", formatRelativeTime(validation.expiresAt)] })] })] }), validation.issuedAt && (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-lg", children: "\uD83D\uDD50" }), _jsxs("div", { children: [_jsx("p", { className: "text-sm font-semibold text-white", children: formatDate(validation.issuedAt) }), _jsxs("p", { className: "text-xs text-gray-400", children: ["Issued ", formatRelativeTime(validation.issuedAt)] })] })] }))] }) }));
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=ExpiryTimer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExpiryTimer.js","sourceRoot":"","sources":["../../src/components/ExpiryTimer.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"JWTDecoder.d.ts","sourceRoot":"","sources":["../../src/components/JWTDecoder.tsx"],"names":[],"mappings":"AAQA,wBAAgB,UAAU,4CAoDzB"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { TokenInput } from './TokenInput';
|
|
5
|
+
import { DecodedView } from './DecodedView';
|
|
6
|
+
import { ExpiryTimer } from './ExpiryTimer';
|
|
7
|
+
import { decodeToken, validateToken } from '../lib/jwt';
|
|
8
|
+
export function JWTDecoder() {
|
|
9
|
+
const [decoded, setDecoded] = useState(null);
|
|
10
|
+
const [validation, setValidation] = useState(null);
|
|
11
|
+
const [error, setError] = useState('');
|
|
12
|
+
const handleTokenChange = (token) => {
|
|
13
|
+
if (!token.trim()) {
|
|
14
|
+
setDecoded(null);
|
|
15
|
+
setValidation(null);
|
|
16
|
+
setError('');
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const decodedToken = decodeToken(token);
|
|
20
|
+
if (!decodedToken) {
|
|
21
|
+
setError('Invalid JWT token. Please check the format.');
|
|
22
|
+
setDecoded(null);
|
|
23
|
+
setValidation(null);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
setError('');
|
|
27
|
+
setDecoded(decodedToken);
|
|
28
|
+
setValidation(validateToken(decodedToken));
|
|
29
|
+
};
|
|
30
|
+
return (_jsxs("div", { className: "w-full max-w-6xl mx-auto space-y-6", children: [_jsx(TokenInput, { onTokenChange: handleTokenChange, error: error }), decoded && validation && (_jsxs("div", { className: "space-y-6 animate-fade-in", children: [_jsx(ExpiryTimer, { validation: validation }), _jsx(DecodedView, { decoded: decoded })] })), !decoded && !error && (_jsxs("div", { className: "text-center py-12 text-gray-500", children: [_jsx("p", { className: "text-lg mb-2", children: "\uD83D\uDC46 Paste a JWT token above to decode it" }), _jsx("p", { className: "text-sm", children: "We'll show you the header, payload, and expiration status" })] }))] }));
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=JWTDecoder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"JWTDecoder.js","sourceRoot":"","sources":["../../src/components/JWTDecoder.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AACjC,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,WAAW,EAAE,aAAa,EAAuC,MAAM,YAAY,CAAC;AAE7F,MAAM,UAAU,UAAU;IACxB,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,QAAQ,CAAoB,IAAI,CAAC,CAAC;IAChE,MAAM,CAAC,UAAU,EAAE,aAAa,CAAC,GAAG,QAAQ,CAAuB,IAAI,CAAC,CAAC;IACzE,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAS,EAAE,CAAC,CAAC;IAE/C,MAAM,iBAAiB,GAAG,CAAC,KAAa,EAAE,EAAE;QAC1C,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,EAAE,CAAC;YAClB,UAAU,CAAC,IAAI,CAAC,CAAC;YACjB,aAAa,CAAC,IAAI,CAAC,CAAC;YACpB,QAAQ,CAAC,EAAE,CAAC,CAAC;YACb,OAAO;QACT,CAAC;QAED,MAAM,YAAY,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;QAExC,IAAI,CAAC,YAAY,EAAE,CAAC;YAClB,QAAQ,CAAC,6CAA6C,CAAC,CAAC;YACxD,UAAU,CAAC,IAAI,CAAC,CAAC;YACjB,aAAa,CAAC,IAAI,CAAC,CAAC;YACpB,OAAO;QACT,CAAC;QAED,QAAQ,CAAC,EAAE,CAAC,CAAC;QACb,UAAU,CAAC,YAAY,CAAC,CAAC;QACzB,aAAa,CAAC,aAAa,CAAC,YAAY,CAAC,CAAC,CAAC;IAC7C,CAAC,CAAC;IAEF,OAAO,CACL,eAAK,SAAS,EAAC,oCAAoC,aAEjD,KAAC,UAAU,IAAC,aAAa,EAAE,iBAAiB,EAAE,KAAK,EAAE,KAAK,GAAI,EAG7D,OAAO,IAAI,UAAU,IAAI,CACxB,eAAK,SAAS,EAAC,2BAA2B,aAExC,KAAC,WAAW,IAAC,UAAU,EAAE,UAAU,GAAI,EAGvC,KAAC,WAAW,IAAC,OAAO,EAAE,OAAO,GAAI,IAC7B,CACP,EAGA,CAAC,OAAO,IAAI,CAAC,KAAK,IAAI,CACrB,eAAK,SAAS,EAAC,iCAAiC,aAC9C,YAAG,SAAS,EAAC,cAAc,kEAA4C,EACvE,YAAG,SAAS,EAAC,SAAS,0EAA8D,IAChF,CACP,IACG,CACP,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
interface TokenInputProps {
|
|
2
|
+
onTokenChange: (token: string) => void;
|
|
3
|
+
error?: string;
|
|
4
|
+
}
|
|
5
|
+
export declare function TokenInput({ onTokenChange, error }: TokenInputProps): import("react/jsx-runtime").JSX.Element;
|
|
6
|
+
export {};
|
|
7
|
+
//# sourceMappingURL=TokenInput.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TokenInput.d.ts","sourceRoot":"","sources":["../../src/components/TokenInput.tsx"],"names":[],"mappings":"AAIA,UAAU,eAAe;IACvB,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,UAAU,CAAC,EAAE,aAAa,EAAE,KAAK,EAAE,EAAE,eAAe,2CAiEnE"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
export function TokenInput({ onTokenChange, error }) {
|
|
5
|
+
const [value, setValue] = useState('');
|
|
6
|
+
const handleChange = (e) => {
|
|
7
|
+
const newValue = e.target.value;
|
|
8
|
+
setValue(newValue);
|
|
9
|
+
onTokenChange(newValue);
|
|
10
|
+
};
|
|
11
|
+
const handlePaste = async () => {
|
|
12
|
+
try {
|
|
13
|
+
const text = await navigator.clipboard.readText();
|
|
14
|
+
setValue(text);
|
|
15
|
+
onTokenChange(text);
|
|
16
|
+
}
|
|
17
|
+
catch (err) {
|
|
18
|
+
console.error('Failed to read clipboard:', err);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
const handleClear = () => {
|
|
22
|
+
setValue('');
|
|
23
|
+
onTokenChange('');
|
|
24
|
+
};
|
|
25
|
+
return (_jsxs("div", { className: "w-full", children: [_jsxs("div", { className: "flex items-center justify-between mb-2", children: [_jsx("label", { htmlFor: "jwt-input", className: "text-sm font-medium text-gray-300", children: "JWT Token" }), _jsxs("div", { className: "flex gap-2", children: [_jsx("button", { onClick: handlePaste, className: "text-xs px-3 py-1 rounded bg-white/5 hover:bg-white/10 text-gray-300 transition-colors", children: "Paste" }), value && (_jsx("button", { onClick: handleClear, className: "text-xs px-3 py-1 rounded bg-white/5 hover:bg-white/10 text-gray-300 transition-colors", children: "Clear" }))] })] }), _jsx("textarea", { id: "jwt-input", value: value, onChange: handleChange, placeholder: "Paste your JWT token here...", className: `w-full h-32 px-4 py-3 rounded-lg bg-white/5 border ${error ? 'border-red-500/50' : 'border-white/10'} text-white placeholder-gray-500 focus:outline-none focus:border-primary/50 transition-colors resize-none font-mono text-sm` }), error && (_jsx("p", { className: "mt-2 text-sm text-red-400", children: error }))] }));
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=TokenInput.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TokenInput.js","sourceRoot":"","sources":["../../src/components/TokenInput.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAOjC,MAAM,UAAU,UAAU,CAAC,EAAE,aAAa,EAAE,KAAK,EAAmB;IAClE,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAC;IAEvC,MAAM,YAAY,GAAG,CAAC,CAAyC,EAAE,EAAE;QACjE,MAAM,QAAQ,GAAG,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;QAChC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QACnB,aAAa,CAAC,QAAQ,CAAC,CAAC;IAC1B,CAAC,CAAC;IAEF,MAAM,WAAW,GAAG,KAAK,IAAI,EAAE;QAC7B,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,MAAM,SAAS,CAAC,SAAS,CAAC,QAAQ,EAAE,CAAC;YAClD,QAAQ,CAAC,IAAI,CAAC,CAAC;YACf,aAAa,CAAC,IAAI,CAAC,CAAC;QACtB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,CAAC,KAAK,CAAC,2BAA2B,EAAE,GAAG,CAAC,CAAC;QAClD,CAAC;IACH,CAAC,CAAC;IAEF,MAAM,WAAW,GAAG,GAAG,EAAE;QACvB,QAAQ,CAAC,EAAE,CAAC,CAAC;QACb,aAAa,CAAC,EAAE,CAAC,CAAC;IACpB,CAAC,CAAC;IAEF,OAAO,CACL,eAAK,SAAS,EAAC,QAAQ,aACrB,eAAK,SAAS,EAAC,wCAAwC,aACrD,gBAAO,OAAO,EAAC,WAAW,EAAC,SAAS,EAAC,mCAAmC,0BAEhE,EACR,eAAK,SAAS,EAAC,YAAY,aACzB,iBACE,OAAO,EAAE,WAAW,EACpB,SAAS,EAAC,wFAAwF,sBAG3F,EACR,KAAK,IAAI,CACR,iBACE,OAAO,EAAE,WAAW,EACpB,SAAS,EAAC,wFAAwF,sBAG3F,CACV,IACG,IACF,EAEN,mBACE,EAAE,EAAC,WAAW,EACd,KAAK,EAAE,KAAK,EACZ,QAAQ,EAAE,YAAY,EACtB,WAAW,EAAC,8BAA8B,EAC1C,SAAS,EAAE,sDACT,KAAK,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,iBAChC,6HAA6H,GAC7H,EAED,KAAK,IAAI,CACR,YAAG,SAAS,EAAC,2BAA2B,YACrC,KAAK,GACJ,CACL,IACG,CACP,CAAC;AACJ,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { JWTDecoder } from './components/JWTDecoder';
|
|
2
|
+
export { TokenInput } from './components/TokenInput';
|
|
3
|
+
export { DecodedView } from './components/DecodedView';
|
|
4
|
+
export { ExpiryTimer } from './components/ExpiryTimer';
|
|
5
|
+
export * from './lib/jwt';
|
|
6
|
+
export * from './lib/utils';
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AACrD,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AACrD,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvD,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAEvD,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { JWTDecoder } from './components/JWTDecoder';
|
|
2
|
+
export { TokenInput } from './components/TokenInput';
|
|
3
|
+
export { DecodedView } from './components/DecodedView';
|
|
4
|
+
export { ExpiryTimer } from './components/ExpiryTimer';
|
|
5
|
+
export * from './lib/jwt';
|
|
6
|
+
export * from './lib/utils';
|
|
7
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AACrD,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AACrD,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvD,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAEvD,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export interface DecodedJWT {
|
|
2
|
+
header: Record<string, any>;
|
|
3
|
+
payload: Record<string, any>;
|
|
4
|
+
raw: string;
|
|
5
|
+
}
|
|
6
|
+
export interface JWTValidation {
|
|
7
|
+
isValid: boolean;
|
|
8
|
+
isExpired: boolean;
|
|
9
|
+
expiresAt?: Date;
|
|
10
|
+
issuedAt?: Date;
|
|
11
|
+
error?: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Decodes a JWT token into header and payload
|
|
15
|
+
*/
|
|
16
|
+
export declare function decodeToken(token: string): DecodedJWT | null;
|
|
17
|
+
/**
|
|
18
|
+
* Validates JWT and checks expiration
|
|
19
|
+
*/
|
|
20
|
+
export declare function validateToken(decoded: DecodedJWT): JWTValidation;
|
|
21
|
+
/**
|
|
22
|
+
* Calculates time remaining until expiration
|
|
23
|
+
*/
|
|
24
|
+
export declare function getTimeRemaining(expiresAt: Date): {
|
|
25
|
+
total: number;
|
|
26
|
+
days: number;
|
|
27
|
+
hours: number;
|
|
28
|
+
minutes: number;
|
|
29
|
+
seconds: number;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Formats time remaining as human-readable string
|
|
33
|
+
*/
|
|
34
|
+
export declare function formatTimeRemaining(time: ReturnType<typeof getTimeRemaining>): string;
|
|
35
|
+
//# sourceMappingURL=jwt.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jwt.d.ts","sourceRoot":"","sources":["../../src/lib/jwt.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC5B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC7B,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,IAAI,CAAC;IACjB,QAAQ,CAAC,EAAE,IAAI,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAqB5D;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,UAAU,GAAG,aAAa,CAuBhE;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,IAAI,GAAG;IACjD,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;CACjB,CAaA;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,UAAU,CAAC,OAAO,gBAAgB,CAAC,GAAG,MAAM,CAUrF"}
|
package/dist/lib/jwt.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { decodeJwt, decodeProtectedHeader } from 'jose';
|
|
2
|
+
/**
|
|
3
|
+
* Decodes a JWT token into header and payload
|
|
4
|
+
*/
|
|
5
|
+
export function decodeToken(token) {
|
|
6
|
+
try {
|
|
7
|
+
const trimmedToken = token.trim();
|
|
8
|
+
// Basic validation
|
|
9
|
+
if (!trimmedToken || trimmedToken.split('.').length !== 3) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
const header = decodeProtectedHeader(trimmedToken);
|
|
13
|
+
const payload = decodeJwt(trimmedToken);
|
|
14
|
+
return {
|
|
15
|
+
header,
|
|
16
|
+
payload,
|
|
17
|
+
raw: trimmedToken,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
console.error('Failed to decode JWT:', error);
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Validates JWT and checks expiration
|
|
27
|
+
*/
|
|
28
|
+
export function validateToken(decoded) {
|
|
29
|
+
const { payload } = decoded;
|
|
30
|
+
const now = Math.floor(Date.now() / 1000); // Current time in seconds
|
|
31
|
+
// Check if token has expiration
|
|
32
|
+
if (!payload.exp) {
|
|
33
|
+
return {
|
|
34
|
+
isValid: true,
|
|
35
|
+
isExpired: false,
|
|
36
|
+
error: 'No expiration claim (exp) found',
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
const expiresAt = new Date(payload.exp * 1000);
|
|
40
|
+
const issuedAt = payload.iat ? new Date(payload.iat * 1000) : undefined;
|
|
41
|
+
const isExpired = payload.exp < now;
|
|
42
|
+
return {
|
|
43
|
+
isValid: !isExpired,
|
|
44
|
+
isExpired,
|
|
45
|
+
expiresAt,
|
|
46
|
+
issuedAt,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Calculates time remaining until expiration
|
|
51
|
+
*/
|
|
52
|
+
export function getTimeRemaining(expiresAt) {
|
|
53
|
+
const total = expiresAt.getTime() - Date.now();
|
|
54
|
+
if (total <= 0) {
|
|
55
|
+
return { total: 0, days: 0, hours: 0, minutes: 0, seconds: 0 };
|
|
56
|
+
}
|
|
57
|
+
const seconds = Math.floor((total / 1000) % 60);
|
|
58
|
+
const minutes = Math.floor((total / 1000 / 60) % 60);
|
|
59
|
+
const hours = Math.floor((total / (1000 * 60 * 60)) % 24);
|
|
60
|
+
const days = Math.floor(total / (1000 * 60 * 60 * 24));
|
|
61
|
+
return { total, days, hours, minutes, seconds };
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Formats time remaining as human-readable string
|
|
65
|
+
*/
|
|
66
|
+
export function formatTimeRemaining(time) {
|
|
67
|
+
const { days, hours, minutes, seconds } = time;
|
|
68
|
+
const parts = [];
|
|
69
|
+
if (days > 0)
|
|
70
|
+
parts.push(`${days}d`);
|
|
71
|
+
if (hours > 0)
|
|
72
|
+
parts.push(`${hours}h`);
|
|
73
|
+
if (minutes > 0)
|
|
74
|
+
parts.push(`${minutes}m`);
|
|
75
|
+
if (seconds > 0 || parts.length === 0)
|
|
76
|
+
parts.push(`${seconds}s`);
|
|
77
|
+
return parts.join(' ');
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=jwt.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jwt.js","sourceRoot":"","sources":["../../src/lib/jwt.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formats a date in a readable format
|
|
3
|
+
*/
|
|
4
|
+
export declare function formatDate(date: Date): string;
|
|
5
|
+
/**
|
|
6
|
+
* Formats a date as relative time (e.g., "2 hours ago")
|
|
7
|
+
*/
|
|
8
|
+
export declare function formatRelativeTime(date: Date): string;
|
|
9
|
+
/**
|
|
10
|
+
* Copies text to clipboard
|
|
11
|
+
*/
|
|
12
|
+
export declare function copyToClipboard(text: string): Promise<boolean>;
|
|
13
|
+
/**
|
|
14
|
+
* Formats JSON with syntax highlighting
|
|
15
|
+
*/
|
|
16
|
+
export declare function formatJSON(obj: any): string;
|
|
17
|
+
/**
|
|
18
|
+
* Truncates text with ellipsis
|
|
19
|
+
*/
|
|
20
|
+
export declare function truncate(text: string, maxLength: number): string;
|
|
21
|
+
//# sourceMappingURL=utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/lib/utils.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,CAE7C;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,CAErD;AAED;;GAEG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAQpE;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,GAAG,GAAG,MAAM,CAE3C;AAED;;GAEG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAGhE"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { format, formatDistanceToNow } from 'date-fns';
|
|
2
|
+
/**
|
|
3
|
+
* Formats a date in a readable format
|
|
4
|
+
*/
|
|
5
|
+
export function formatDate(date) {
|
|
6
|
+
return format(date, 'MMM dd, yyyy \'at\' h:mm a');
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Formats a date as relative time (e.g., "2 hours ago")
|
|
10
|
+
*/
|
|
11
|
+
export function formatRelativeTime(date) {
|
|
12
|
+
return formatDistanceToNow(date, { addSuffix: true });
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Copies text to clipboard
|
|
16
|
+
*/
|
|
17
|
+
export async function copyToClipboard(text) {
|
|
18
|
+
try {
|
|
19
|
+
await navigator.clipboard.writeText(text);
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
console.error('Failed to copy to clipboard:', error);
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Formats JSON with syntax highlighting
|
|
29
|
+
*/
|
|
30
|
+
export function formatJSON(obj) {
|
|
31
|
+
return JSON.stringify(obj, null, 2);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Truncates text with ellipsis
|
|
35
|
+
*/
|
|
36
|
+
export function truncate(text, maxLength) {
|
|
37
|
+
if (text.length <= maxLength)
|
|
38
|
+
return text;
|
|
39
|
+
return text.slice(0, maxLength) + '...';
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../../src/lib/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;AAEvD;;GAEG;AACH,MAAM,UAAU,UAAU,CAAC,IAAU;IACnC,OAAO,MAAM,CAAC,IAAI,EAAE,4BAA4B,CAAC,CAAC;AACpD,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAU;IAC3C,OAAO,mBAAmB,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;AACxD,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,IAAY;IAChD,IAAI,CAAC;QACH,MAAM,SAAS,CAAC,SAAS,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;QAC1C,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CAAC,8BAA8B,EAAE,KAAK,CAAC,CAAC;QACrD,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,UAAU,CAAC,GAAQ;IACjC,OAAO,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;AACtC,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,QAAQ,CAAC,IAAY,EAAE,SAAiB;IACtD,IAAI,IAAI,CAAC,MAAM,IAAI,SAAS;QAAE,OAAO,IAAI,CAAC;IAC1C,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,SAAS,CAAC,GAAG,KAAK,CAAC;AAC1C,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rashidv/jwt-decoder",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Free, open-source JWT decoder with expiry checker and live countdown",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"dev": "tsc --watch"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"jwt",
|
|
13
|
+
"decoder",
|
|
14
|
+
"expiry",
|
|
15
|
+
"validator",
|
|
16
|
+
"json-web-token",
|
|
17
|
+
"authentication"
|
|
18
|
+
],
|
|
19
|
+
"author": "Muhammed Rashid <rashidv.dev@gmail.com>",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "https://github.com/rashidrashiii/jwt-decoder"
|
|
24
|
+
},
|
|
25
|
+
"homepage": "https://rashidv.dev/tools/jwt-decoder",
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"date-fns": "^4.1.0",
|
|
28
|
+
"jose": "^5.10.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "^22.10.5",
|
|
32
|
+
"@types/react": "^18.3.18",
|
|
33
|
+
"react": "^19.0.0",
|
|
34
|
+
"typescript": "^5.7.2"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"react": "^18.0.0 || ^19.0.0"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import type { DecodedJWT } from '../lib/jwt';
|
|
5
|
+
import { formatJSON, copyToClipboard } from '../lib/utils';
|
|
6
|
+
|
|
7
|
+
interface DecodedViewProps {
|
|
8
|
+
decoded: DecodedJWT;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function DecodedView({ decoded }: DecodedViewProps) {
|
|
12
|
+
const [copiedSection, setCopiedSection] = useState<'header' | 'payload' | null>(null);
|
|
13
|
+
|
|
14
|
+
const handleCopy = async (section: 'header' | 'payload') => {
|
|
15
|
+
const data = section === 'header' ? decoded.header : decoded.payload;
|
|
16
|
+
const success = await copyToClipboard(formatJSON(data));
|
|
17
|
+
|
|
18
|
+
if (success) {
|
|
19
|
+
setCopiedSection(section);
|
|
20
|
+
setTimeout(() => setCopiedSection(null), 2000);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div className="grid md:grid-cols-2 gap-4">
|
|
26
|
+
{/* Header */}
|
|
27
|
+
<div className="rounded-lg bg-white/5 border border-white/10 p-4">
|
|
28
|
+
<div className="flex items-center justify-between mb-3">
|
|
29
|
+
<h3 className="text-sm font-semibold text-gray-300">Header</h3>
|
|
30
|
+
<button
|
|
31
|
+
onClick={() => handleCopy('header')}
|
|
32
|
+
className="text-xs px-3 py-1 rounded bg-white/5 hover:bg-white/10 text-gray-300 transition-colors"
|
|
33
|
+
>
|
|
34
|
+
{copiedSection === 'header' ? '✓ Copied' : 'Copy'}
|
|
35
|
+
</button>
|
|
36
|
+
</div>
|
|
37
|
+
<pre className="text-xs text-gray-300 overflow-x-auto">
|
|
38
|
+
<code>{formatJSON(decoded.header)}</code>
|
|
39
|
+
</pre>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
{/* Payload */}
|
|
43
|
+
<div className="rounded-lg bg-white/5 border border-white/10 p-4">
|
|
44
|
+
<div className="flex items-center justify-between mb-3">
|
|
45
|
+
<h3 className="text-sm font-semibold text-gray-300">Payload</h3>
|
|
46
|
+
<button
|
|
47
|
+
onClick={() => handleCopy('payload')}
|
|
48
|
+
className="text-xs px-3 py-1 rounded bg-white/5 hover:bg-white/10 text-gray-300 transition-colors"
|
|
49
|
+
>
|
|
50
|
+
{copiedSection === 'payload' ? '✓ Copied' : 'Copy'}
|
|
51
|
+
</button>
|
|
52
|
+
</div>
|
|
53
|
+
<pre className="text-xs text-gray-300 overflow-x-auto">
|
|
54
|
+
<code>{formatJSON(decoded.payload)}</code>
|
|
55
|
+
</pre>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
import type { JWTValidation } from '../lib/jwt';
|
|
5
|
+
import { getTimeRemaining, formatTimeRemaining } from '../lib/jwt';
|
|
6
|
+
import { formatDate, formatRelativeTime } from '../lib/utils';
|
|
7
|
+
|
|
8
|
+
interface ExpiryTimerProps {
|
|
9
|
+
validation: JWTValidation;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function ExpiryTimer({ validation }: ExpiryTimerProps) {
|
|
13
|
+
const [timeRemaining, setTimeRemaining] = useState<ReturnType<typeof getTimeRemaining> | null>(null);
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
if (!validation.expiresAt) return;
|
|
17
|
+
|
|
18
|
+
const updateTimer = () => {
|
|
19
|
+
setTimeRemaining(getTimeRemaining(validation.expiresAt!));
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
updateTimer();
|
|
23
|
+
const interval = setInterval(updateTimer, 1000);
|
|
24
|
+
|
|
25
|
+
return () => clearInterval(interval);
|
|
26
|
+
}, [validation.expiresAt]);
|
|
27
|
+
|
|
28
|
+
if (!validation.expiresAt) {
|
|
29
|
+
return (
|
|
30
|
+
<div className="rounded-lg bg-yellow-500/10 border border-yellow-500/20 p-4">
|
|
31
|
+
<p className="text-sm text-yellow-400">
|
|
32
|
+
⚠️ No expiration claim (exp) found in token
|
|
33
|
+
</p>
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const isExpired = validation.isExpired;
|
|
39
|
+
const isExpiringSoon = timeRemaining && timeRemaining.total > 0 && timeRemaining.total < 5 * 60 * 1000; // 5 minutes
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div className={`rounded-lg border p-4 ${
|
|
43
|
+
isExpired
|
|
44
|
+
? 'bg-red-500/10 border-red-500/20'
|
|
45
|
+
: isExpiringSoon
|
|
46
|
+
? 'bg-yellow-500/10 border-yellow-500/20'
|
|
47
|
+
: 'bg-green-500/10 border-green-500/20'
|
|
48
|
+
}`}>
|
|
49
|
+
<div className="space-y-3">
|
|
50
|
+
{/* Status */}
|
|
51
|
+
<div className="flex items-center gap-2">
|
|
52
|
+
<span className="text-2xl">
|
|
53
|
+
{isExpired ? '🔴' : isExpiringSoon ? '🟡' : '🟢'}
|
|
54
|
+
</span>
|
|
55
|
+
<div>
|
|
56
|
+
<p className={`text-sm font-semibold ${
|
|
57
|
+
isExpired ? 'text-red-400' : isExpiringSoon ? 'text-yellow-400' : 'text-green-400'
|
|
58
|
+
}`}>
|
|
59
|
+
{isExpired ? 'Expired' : isExpiringSoon ? 'Expiring Soon' : 'Valid'}
|
|
60
|
+
</p>
|
|
61
|
+
<p className="text-xs text-gray-400">
|
|
62
|
+
{isExpired ? 'This token is no longer valid' : 'Token is currently valid'}
|
|
63
|
+
</p>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
{/* Time Remaining */}
|
|
68
|
+
{!isExpired && timeRemaining && (
|
|
69
|
+
<div className="flex items-center gap-2">
|
|
70
|
+
<span className="text-lg">⏱️</span>
|
|
71
|
+
<div>
|
|
72
|
+
<p className="text-sm font-semibold text-white">
|
|
73
|
+
{formatTimeRemaining(timeRemaining)}
|
|
74
|
+
</p>
|
|
75
|
+
<p className="text-xs text-gray-400">Time remaining</p>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
)}
|
|
79
|
+
|
|
80
|
+
{/* Expiry Date */}
|
|
81
|
+
<div className="flex items-center gap-2">
|
|
82
|
+
<span className="text-lg">📅</span>
|
|
83
|
+
<div>
|
|
84
|
+
<p className="text-sm font-semibold text-white">
|
|
85
|
+
{formatDate(validation.expiresAt)}
|
|
86
|
+
</p>
|
|
87
|
+
<p className="text-xs text-gray-400">
|
|
88
|
+
{isExpired ? 'Expired' : 'Expires'} {formatRelativeTime(validation.expiresAt)}
|
|
89
|
+
</p>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
{/* Issued At */}
|
|
94
|
+
{validation.issuedAt && (
|
|
95
|
+
<div className="flex items-center gap-2">
|
|
96
|
+
<span className="text-lg">🕐</span>
|
|
97
|
+
<div>
|
|
98
|
+
<p className="text-sm font-semibold text-white">
|
|
99
|
+
{formatDate(validation.issuedAt)}
|
|
100
|
+
</p>
|
|
101
|
+
<p className="text-xs text-gray-400">
|
|
102
|
+
Issued {formatRelativeTime(validation.issuedAt)}
|
|
103
|
+
</p>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
)}
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { TokenInput } from './TokenInput';
|
|
5
|
+
import { DecodedView } from './DecodedView';
|
|
6
|
+
import { ExpiryTimer } from './ExpiryTimer';
|
|
7
|
+
import { decodeToken, validateToken, type DecodedJWT, type JWTValidation } from '../lib/jwt';
|
|
8
|
+
|
|
9
|
+
export function JWTDecoder() {
|
|
10
|
+
const [decoded, setDecoded] = useState<DecodedJWT | null>(null);
|
|
11
|
+
const [validation, setValidation] = useState<JWTValidation | null>(null);
|
|
12
|
+
const [error, setError] = useState<string>('');
|
|
13
|
+
|
|
14
|
+
const handleTokenChange = (token: string) => {
|
|
15
|
+
if (!token.trim()) {
|
|
16
|
+
setDecoded(null);
|
|
17
|
+
setValidation(null);
|
|
18
|
+
setError('');
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const decodedToken = decodeToken(token);
|
|
23
|
+
|
|
24
|
+
if (!decodedToken) {
|
|
25
|
+
setError('Invalid JWT token. Please check the format.');
|
|
26
|
+
setDecoded(null);
|
|
27
|
+
setValidation(null);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
setError('');
|
|
32
|
+
setDecoded(decodedToken);
|
|
33
|
+
setValidation(validateToken(decodedToken));
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div className="w-full max-w-6xl mx-auto space-y-6">
|
|
38
|
+
{/* Token Input */}
|
|
39
|
+
<TokenInput onTokenChange={handleTokenChange} error={error} />
|
|
40
|
+
|
|
41
|
+
{/* Results */}
|
|
42
|
+
{decoded && validation && (
|
|
43
|
+
<div className="space-y-6 animate-fade-in">
|
|
44
|
+
{/* Expiry Timer */}
|
|
45
|
+
<ExpiryTimer validation={validation} />
|
|
46
|
+
|
|
47
|
+
{/* Decoded Token */}
|
|
48
|
+
<DecodedView decoded={decoded} />
|
|
49
|
+
</div>
|
|
50
|
+
)}
|
|
51
|
+
|
|
52
|
+
{/* Empty State */}
|
|
53
|
+
{!decoded && !error && (
|
|
54
|
+
<div className="text-center py-12 text-gray-500">
|
|
55
|
+
<p className="text-lg mb-2">👆 Paste a JWT token above to decode it</p>
|
|
56
|
+
<p className="text-sm">We'll show you the header, payload, and expiration status</p>
|
|
57
|
+
</div>
|
|
58
|
+
)}
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
|
|
5
|
+
interface TokenInputProps {
|
|
6
|
+
onTokenChange: (token: string) => void;
|
|
7
|
+
error?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function TokenInput({ onTokenChange, error }: TokenInputProps) {
|
|
11
|
+
const [value, setValue] = useState('');
|
|
12
|
+
|
|
13
|
+
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
14
|
+
const newValue = e.target.value;
|
|
15
|
+
setValue(newValue);
|
|
16
|
+
onTokenChange(newValue);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const handlePaste = async () => {
|
|
20
|
+
try {
|
|
21
|
+
const text = await navigator.clipboard.readText();
|
|
22
|
+
setValue(text);
|
|
23
|
+
onTokenChange(text);
|
|
24
|
+
} catch (err) {
|
|
25
|
+
console.error('Failed to read clipboard:', err);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const handleClear = () => {
|
|
30
|
+
setValue('');
|
|
31
|
+
onTokenChange('');
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div className="w-full">
|
|
36
|
+
<div className="flex items-center justify-between mb-2">
|
|
37
|
+
<label htmlFor="jwt-input" className="text-sm font-medium text-gray-300">
|
|
38
|
+
JWT Token
|
|
39
|
+
</label>
|
|
40
|
+
<div className="flex gap-2">
|
|
41
|
+
<button
|
|
42
|
+
onClick={handlePaste}
|
|
43
|
+
className="text-xs px-3 py-1 rounded bg-white/5 hover:bg-white/10 text-gray-300 transition-colors"
|
|
44
|
+
>
|
|
45
|
+
Paste
|
|
46
|
+
</button>
|
|
47
|
+
{value && (
|
|
48
|
+
<button
|
|
49
|
+
onClick={handleClear}
|
|
50
|
+
className="text-xs px-3 py-1 rounded bg-white/5 hover:bg-white/10 text-gray-300 transition-colors"
|
|
51
|
+
>
|
|
52
|
+
Clear
|
|
53
|
+
</button>
|
|
54
|
+
)}
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<textarea
|
|
59
|
+
id="jwt-input"
|
|
60
|
+
value={value}
|
|
61
|
+
onChange={handleChange}
|
|
62
|
+
placeholder="Paste your JWT token here..."
|
|
63
|
+
className={`w-full h-32 px-4 py-3 rounded-lg bg-white/5 border ${
|
|
64
|
+
error ? 'border-red-500/50' : 'border-white/10'
|
|
65
|
+
} text-white placeholder-gray-500 focus:outline-none focus:border-primary/50 transition-colors resize-none font-mono text-sm`}
|
|
66
|
+
/>
|
|
67
|
+
|
|
68
|
+
{error && (
|
|
69
|
+
<p className="mt-2 text-sm text-red-400">
|
|
70
|
+
{error}
|
|
71
|
+
</p>
|
|
72
|
+
)}
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { JWTDecoder } from './components/JWTDecoder';
|
|
2
|
+
export { TokenInput } from './components/TokenInput';
|
|
3
|
+
export { DecodedView } from './components/DecodedView';
|
|
4
|
+
export { ExpiryTimer } from './components/ExpiryTimer';
|
|
5
|
+
|
|
6
|
+
export * from './lib/jwt';
|
|
7
|
+
export * from './lib/utils';
|
package/src/lib/jwt.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { decodeJwt, decodeProtectedHeader } from 'jose';
|
|
2
|
+
|
|
3
|
+
export interface DecodedJWT {
|
|
4
|
+
header: Record<string, any>;
|
|
5
|
+
payload: Record<string, any>;
|
|
6
|
+
raw: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface JWTValidation {
|
|
10
|
+
isValid: boolean;
|
|
11
|
+
isExpired: boolean;
|
|
12
|
+
expiresAt?: Date;
|
|
13
|
+
issuedAt?: Date;
|
|
14
|
+
error?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Decodes a JWT token into header and payload
|
|
19
|
+
*/
|
|
20
|
+
export function decodeToken(token: string): DecodedJWT | null {
|
|
21
|
+
try {
|
|
22
|
+
const trimmedToken = token.trim();
|
|
23
|
+
|
|
24
|
+
// Basic validation
|
|
25
|
+
if (!trimmedToken || trimmedToken.split('.').length !== 3) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const header = decodeProtectedHeader(trimmedToken);
|
|
30
|
+
const payload = decodeJwt(trimmedToken);
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
header,
|
|
34
|
+
payload,
|
|
35
|
+
raw: trimmedToken,
|
|
36
|
+
};
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.error('Failed to decode JWT:', error);
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Validates JWT and checks expiration
|
|
45
|
+
*/
|
|
46
|
+
export function validateToken(decoded: DecodedJWT): JWTValidation {
|
|
47
|
+
const { payload } = decoded;
|
|
48
|
+
const now = Math.floor(Date.now() / 1000); // Current time in seconds
|
|
49
|
+
|
|
50
|
+
// Check if token has expiration
|
|
51
|
+
if (!payload.exp) {
|
|
52
|
+
return {
|
|
53
|
+
isValid: true,
|
|
54
|
+
isExpired: false,
|
|
55
|
+
error: 'No expiration claim (exp) found',
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const expiresAt = new Date(payload.exp * 1000);
|
|
60
|
+
const issuedAt = payload.iat ? new Date(payload.iat * 1000) : undefined;
|
|
61
|
+
const isExpired = payload.exp < now;
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
isValid: !isExpired,
|
|
65
|
+
isExpired,
|
|
66
|
+
expiresAt,
|
|
67
|
+
issuedAt,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Calculates time remaining until expiration
|
|
73
|
+
*/
|
|
74
|
+
export function getTimeRemaining(expiresAt: Date): {
|
|
75
|
+
total: number;
|
|
76
|
+
days: number;
|
|
77
|
+
hours: number;
|
|
78
|
+
minutes: number;
|
|
79
|
+
seconds: number;
|
|
80
|
+
} {
|
|
81
|
+
const total = expiresAt.getTime() - Date.now();
|
|
82
|
+
|
|
83
|
+
if (total <= 0) {
|
|
84
|
+
return { total: 0, days: 0, hours: 0, minutes: 0, seconds: 0 };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const seconds = Math.floor((total / 1000) % 60);
|
|
88
|
+
const minutes = Math.floor((total / 1000 / 60) % 60);
|
|
89
|
+
const hours = Math.floor((total / (1000 * 60 * 60)) % 24);
|
|
90
|
+
const days = Math.floor(total / (1000 * 60 * 60 * 24));
|
|
91
|
+
|
|
92
|
+
return { total, days, hours, minutes, seconds };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Formats time remaining as human-readable string
|
|
97
|
+
*/
|
|
98
|
+
export function formatTimeRemaining(time: ReturnType<typeof getTimeRemaining>): string {
|
|
99
|
+
const { days, hours, minutes, seconds } = time;
|
|
100
|
+
|
|
101
|
+
const parts: string[] = [];
|
|
102
|
+
if (days > 0) parts.push(`${days}d`);
|
|
103
|
+
if (hours > 0) parts.push(`${hours}h`);
|
|
104
|
+
if (minutes > 0) parts.push(`${minutes}m`);
|
|
105
|
+
if (seconds > 0 || parts.length === 0) parts.push(`${seconds}s`);
|
|
106
|
+
|
|
107
|
+
return parts.join(' ');
|
|
108
|
+
}
|
package/src/lib/utils.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { format, formatDistanceToNow } from 'date-fns';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Formats a date in a readable format
|
|
5
|
+
*/
|
|
6
|
+
export function formatDate(date: Date): string {
|
|
7
|
+
return format(date, 'MMM dd, yyyy \'at\' h:mm a');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Formats a date as relative time (e.g., "2 hours ago")
|
|
12
|
+
*/
|
|
13
|
+
export function formatRelativeTime(date: Date): string {
|
|
14
|
+
return formatDistanceToNow(date, { addSuffix: true });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Copies text to clipboard
|
|
19
|
+
*/
|
|
20
|
+
export async function copyToClipboard(text: string): Promise<boolean> {
|
|
21
|
+
try {
|
|
22
|
+
await navigator.clipboard.writeText(text);
|
|
23
|
+
return true;
|
|
24
|
+
} catch (error) {
|
|
25
|
+
console.error('Failed to copy to clipboard:', error);
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Formats JSON with syntax highlighting
|
|
32
|
+
*/
|
|
33
|
+
export function formatJSON(obj: any): string {
|
|
34
|
+
return JSON.stringify(obj, null, 2);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Truncates text with ellipsis
|
|
39
|
+
*/
|
|
40
|
+
export function truncate(text: string, maxLength: number): string {
|
|
41
|
+
if (text.length <= maxLength) return text;
|
|
42
|
+
return text.slice(0, maxLength) + '...';
|
|
43
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"lib": ["ES2022", "DOM"],
|
|
6
|
+
"jsx": "react-jsx",
|
|
7
|
+
"moduleResolution": "bundler",
|
|
8
|
+
"outDir": "./dist",
|
|
9
|
+
"rootDir": "./src",
|
|
10
|
+
"strict": true,
|
|
11
|
+
"esModuleInterop": true,
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"forceConsistentCasingInFileNames": true,
|
|
14
|
+
"declaration": true,
|
|
15
|
+
"declarationMap": true,
|
|
16
|
+
"sourceMap": true,
|
|
17
|
+
"resolveJsonModule": true
|
|
18
|
+
},
|
|
19
|
+
"include": ["src/**/*"],
|
|
20
|
+
"exclude": ["node_modules", "dist"]
|
|
21
|
+
}
|