@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.
Files changed (41) hide show
  1. package/.github/workflows/publish-jwt-decoder.yml +40 -0
  2. package/.github/workflows/publish.yml +36 -0
  3. package/LICENSE +21 -0
  4. package/README.md +134 -0
  5. package/dist/components/DecodedView.d.ts +7 -0
  6. package/dist/components/DecodedView.d.ts.map +1 -0
  7. package/dist/components/DecodedView.js +17 -0
  8. package/dist/components/DecodedView.js.map +1 -0
  9. package/dist/components/ExpiryTimer.d.ts +7 -0
  10. package/dist/components/ExpiryTimer.d.ts.map +1 -0
  11. package/dist/components/ExpiryTimer.js +29 -0
  12. package/dist/components/ExpiryTimer.js.map +1 -0
  13. package/dist/components/JWTDecoder.d.ts +2 -0
  14. package/dist/components/JWTDecoder.d.ts.map +1 -0
  15. package/dist/components/JWTDecoder.js +32 -0
  16. package/dist/components/JWTDecoder.js.map +1 -0
  17. package/dist/components/TokenInput.d.ts +7 -0
  18. package/dist/components/TokenInput.d.ts.map +1 -0
  19. package/dist/components/TokenInput.js +27 -0
  20. package/dist/components/TokenInput.js.map +1 -0
  21. package/dist/index.d.ts +7 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +7 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/lib/jwt.d.ts +35 -0
  26. package/dist/lib/jwt.d.ts.map +1 -0
  27. package/dist/lib/jwt.js +79 -0
  28. package/dist/lib/jwt.js.map +1 -0
  29. package/dist/lib/utils.d.ts +21 -0
  30. package/dist/lib/utils.d.ts.map +1 -0
  31. package/dist/lib/utils.js +41 -0
  32. package/dist/lib/utils.js.map +1 -0
  33. package/package.json +39 -0
  34. package/src/components/DecodedView.tsx +59 -0
  35. package/src/components/ExpiryTimer.tsx +110 -0
  36. package/src/components/JWTDecoder.tsx +61 -0
  37. package/src/components/TokenInput.tsx +75 -0
  38. package/src/index.ts +7 -0
  39. package/src/lib/jwt.ts +108 -0
  40. package/src/lib/utils.ts +43 -0
  41. 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
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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,2 @@
1
+ export declare function JWTDecoder(): import("react/jsx-runtime").JSX.Element;
2
+ //# sourceMappingURL=JWTDecoder.d.ts.map
@@ -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"}
@@ -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"}
@@ -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
+ }
@@ -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
+ }