Skip to content

Commit 78e8a8a

Browse files
committed
feat: add initial implementation of JWT abstraction with example usage and documentation
1 parent 1896d93 commit 78e8a8a

1 file changed

Lines changed: 260 additions & 0 deletions

File tree

src/pages/index.astro

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
---
2+
import "../styles/global.css";
3+
import { codeToHtml } from "shiki";
4+
5+
const npmUrl = "https://www.npmjs.com/package/@dax-side/jwt-abstraction";
6+
const githubUrl = "https://github.com/dax-side/jwt-abstraction";
7+
const readmeUrl = "https://github.com/dax-side/jwt-abstraction#readme";
8+
9+
const codeWithout = `import jwt from "jsonwebtoken";
10+
11+
const ACCESS_TTL = "15m";
12+
const REFRESH_TTL = "7d";
13+
14+
const createTokens = (payload) => ({
15+
accessToken: jwt.sign(payload, process.env.JWT_SECRET, {
16+
expiresIn: ACCESS_TTL,
17+
algorithm: "HS256"
18+
}),
19+
refreshToken: jwt.sign(payload, process.env.JWT_REFRESH_SECRET, {
20+
expiresIn: REFRESH_TTL,
21+
algorithm: "HS256"
22+
})
23+
});
24+
25+
const verifyToken = (token, secret) => {
26+
try {
27+
return jwt.verify(token, secret);
28+
} catch (error) {
29+
if (error.name === "TokenExpiredError") {
30+
throw new Error("Token expired");
31+
}
32+
throw new Error("Invalid token");
33+
}
34+
};
35+
36+
const protect = () => (req, res, next) => {
37+
const token = req.headers.authorization?.split(" ")[1];
38+
if (!token) return res.status(401).json({ error: "Missing token" });
39+
40+
try {
41+
req.user = jwt.verify(token, process.env.JWT_SECRET);
42+
next();
43+
} catch (error) {
44+
if (error.name === "TokenExpiredError") {
45+
return res.status(401).json({ error: "Token expired" });
46+
}
47+
res.status(401).json({ error: "Invalid token" });
48+
}
49+
};
50+
51+
const refreshTokens = async (refreshToken) => {
52+
const payload = verifyToken(refreshToken, process.env.JWT_REFRESH_SECRET);
53+
return createTokens({ userId: payload.userId });
54+
};`;
55+
56+
const codeWith = `import { useJwt } from "@dax-side/jwt-abstraction";
57+
58+
const jwt = useJwt({
59+
secret: process.env.JWT_SECRET,
60+
refreshTokenSecret: process.env.JWT_REFRESH_SECRET
61+
});
62+
63+
const tokens = jwt.create({ userId: 123 });
64+
const payload = await jwt.verify(tokens.accessToken);
65+
66+
app.get("/profile", jwt.protect(), (req, res) => {
67+
res.json({ user: req.user });
68+
});`;
69+
70+
const codeSecurity = `const jwt = useJwt({
71+
secret: process.env.JWT_SECRET,
72+
refreshTokenSecret: process.env.JWT_REFRESH_SECRET
73+
});`;
74+
75+
const codeErrors = `import {
76+
TokenExpiredError,
77+
InvalidTokenError,
78+
NoSecretError
79+
} from "@dax-side/jwt-abstraction";
80+
81+
try {
82+
const payload = await jwt.verify(token);
83+
} catch (error) {
84+
if (error instanceof TokenExpiredError) {
85+
// Token expired
86+
} else if (error instanceof InvalidTokenError) {
87+
// Token malformed or tampered with
88+
} else if (error instanceof NoSecretError) {
89+
// Secret not configured
90+
}
91+
}`;
92+
93+
const codeGetStarted = `npm install @dax-side/jwt-abstraction
94+
95+
JWT_SECRET=your-secret`;
96+
97+
const codeWithoutHtml = await codeToHtml(codeWithout, {
98+
lang: "ts",
99+
theme: "github-dark"
100+
});
101+
102+
const codeWithHtml = await codeToHtml(codeWith, {
103+
lang: "ts",
104+
theme: "github-dark"
105+
});
106+
107+
const codeSecurityHtml = await codeToHtml(codeSecurity, {
108+
lang: "ts",
109+
theme: "github-dark"
110+
});
111+
112+
const codeErrorsHtml = await codeToHtml(codeErrors, {
113+
lang: "ts",
114+
theme: "github-dark"
115+
});
116+
117+
const codeGetStartedHtml = await codeToHtml(codeGetStarted, {
118+
lang: "bash",
119+
theme: "github-dark"
120+
});
121+
---
122+
123+
<!doctype html>
124+
<html lang="en">
125+
<head>
126+
<meta charset="utf-8" />
127+
<meta name="viewport" content="width=device-width, initial-scale=1" />
128+
<title>JWT Abstraction - Less JWT boilerplate</title>
129+
<meta
130+
name="description"
131+
content="JWT auth without the boilerplate. Access and refresh tokens, Express middleware, strict TypeScript, and clear errors. Bring your own secret."
132+
/>
133+
</head>
134+
<body>
135+
<main>
136+
<header class="hero">
137+
<div class="kicker">@dax-side/jwt-abstraction</div>
138+
<h1>JWT auth without the boilerplate.</h1>
139+
<p>
140+
Create access and refresh tokens, verify payloads, and protect routes in three
141+
lines. You still provide the secrets.
142+
</p>
143+
<div class="install-row">
144+
<div class="install" id="install-command">npm install @dax-side/jwt-abstraction</div>
145+
<button class="copy-button" type="button" data-copy="install-command">
146+
Copy
147+
</button>
148+
</div>
149+
<img
150+
class="downloads"
151+
src="https://img.shields.io/npm/dw/@dax-side/jwt-abstraction"
152+
alt="npm weekly downloads"
153+
/>
154+
</header>
155+
156+
<section>
157+
<h2>The problem</h2>
158+
<p>JWT auth shows up in every Node.js app. The setup is the same every time:</p>
159+
<ul>
160+
<li>Wire up jsonwebtoken</li>
161+
<li>Configure algorithms and expiries</li>
162+
<li>Create access and refresh tokens</li>
163+
<li>Build Express middleware</li>
164+
<li>Sign, verify, and handle edge cases by hand</li>
165+
<li>Map token errors to responses</li>
166+
</ul>
167+
</section>
168+
169+
<section>
170+
<h2>Side-by-side comparison</h2>
171+
<p>
172+
Without the abstraction, you handle signing, verification, refresh flow, and error
173+
mapping yourself. That is a lot of code to keep consistent.
174+
</p>
175+
<div class="grid-2">
176+
<div class="card">
177+
<div class="badge">Without jwt-abstraction</div>
178+
<div class="code-block" set:html={codeWithoutHtml}></div>
179+
</div>
180+
<div class="card">
181+
<div class="badge">With jwt-abstraction</div>
182+
<div class="code-block" set:html={codeWithHtml}></div>
183+
</div>
184+
</div>
185+
</section>
186+
187+
<section>
188+
<h2>Features</h2>
189+
<ul>
190+
<li>Only dependency is jsonwebtoken</li>
191+
<li>Creates access and refresh token pairs</li>
192+
<li>Separate secrets for access and refresh tokens</li>
193+
<li>Express middleware included</li>
194+
<li>TypeScript support with strict mode</li>
195+
<li>Typed error classes</li>
196+
<li>100% test coverage</li>
197+
</ul>
198+
</section>
199+
200+
<section>
201+
<h2>Security first</h2>
202+
<p>
203+
Secrets are required. If you do not provide one, the package throws
204+
NoSecretError immediately. There are no default secrets.
205+
</p>
206+
<div class="code-block" set:html={codeSecurityHtml}></div>
207+
<p>Safe defaults:</p>
208+
<ul>
209+
<li>Access token TTL: 15 minutes</li>
210+
<li>Refresh token TTL: 7 days</li>
211+
<li>Algorithm: HS256</li>
212+
</ul>
213+
</section>
214+
215+
<section>
216+
<h2>Error handling</h2>
217+
<p>Catch specific error types instead of parsing strings.</p>
218+
<div class="code-block" set:html={codeErrorsHtml}></div>
219+
</section>
220+
221+
<section>
222+
<h2>Get started</h2>
223+
<p>Install the package, set your secret, and wire it into Express.</p>
224+
<div class="code-block" set:html={codeGetStartedHtml}></div>
225+
<p>
226+
Full documentation is in the
227+
<a href={readmeUrl}>README</a>.
228+
</p>
229+
</section>
230+
231+
<footer>
232+
<p>
233+
<a href={npmUrl}>npm</a> · <a href={githubUrl}>GitHub</a> ·
234+
<a href={readmeUrl}>Docs</a>
235+
</p>
236+
</footer>
237+
</main>
238+
<script>
239+
const copyButton = document.querySelector("[data-copy='install-command']");
240+
const installCommand = document.querySelector("#install-command");
241+
242+
if (copyButton && installCommand) {
243+
copyButton.addEventListener("click", async () => {
244+
try {
245+
await navigator.clipboard.writeText(installCommand.textContent ?? "");
246+
copyButton.textContent = "Copied";
247+
setTimeout(() => {
248+
copyButton.textContent = "Copy";
249+
}, 1600);
250+
} catch (error) {
251+
copyButton.textContent = "Failed";
252+
setTimeout(() => {
253+
copyButton.textContent = "Copy";
254+
}, 1600);
255+
}
256+
});
257+
}
258+
</script>
259+
</body>
260+
</html>

0 commit comments

Comments
 (0)