@lacspace/jwt 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/LICENSE +21 -0
- package/README.md +80 -0
- package/dist/index.cjs +97 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +55 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.js +90 -0
- package/dist/index.js.map +1 -0
- package/package.json +21 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Lacspace
|
|
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,80 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# @lacspace/jwt
|
|
4
|
+
|
|
5
|
+
**JSON Web Tokens (HMAC) + secure random tokens — done safely.**
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/@lacspace/jwt)
|
|
8
|
+
[](https://packagephobia.com/result?p=@lacspace/jwt)
|
|
9
|
+
[](https://bundlephobia.com/package/@lacspace/jwt)
|
|
10
|
+
[](https://www.npmjs.com/package/@lacspace/jwt)
|
|
11
|
+
[](https://github.com/lacspace/npm-packages/blob/main/LICENSE)
|
|
12
|
+
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
> Sign and verify **HS256 / HS384 / HS512** JWTs over Web Crypto with strict expiry / not-before / issuer / audience checks and constant-time signature comparison. Isomorphic — runs on edge and workers where the `jsonwebtoken` package can't. Plus opaque and CSRF tokens.
|
|
16
|
+
|
|
17
|
+
- 🎟️ `sign` / `verify` / `decode` with typed claims
|
|
18
|
+
- ⏱️ `exp` / `nbf` / `iat`, `issuer`, `audience`, `clockTolerance`
|
|
19
|
+
- 🧯 Typed `JwtError` with a `code` (`expired`, `signature`, `audience`…)
|
|
20
|
+
- 🎲 `randomToken` / `csrfToken`
|
|
21
|
+
- ⚡ Zero deps (bar `@lacspace/crypto`) · 🌍 isomorphic · fully typed
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install @lacspace/jwt
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { sign, verify, JwtError } from "@lacspace/jwt";
|
|
33
|
+
|
|
34
|
+
const token = await sign({ sub: "user_1", role: "admin" }, process.env.JWT_SECRET!, {
|
|
35
|
+
expiresIn: 3600, // seconds
|
|
36
|
+
issuer: "lacspace",
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const payload = await verify(token, process.env.JWT_SECRET!, { issuer: "lacspace" });
|
|
41
|
+
payload.sub; // "user_1"
|
|
42
|
+
} catch (e) {
|
|
43
|
+
if (e instanceof JwtError) console.log(e.code); // "expired" | "signature" | …
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Opaque & CSRF tokens
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import { randomToken, csrfToken } from "@lacspace/jwt";
|
|
51
|
+
|
|
52
|
+
randomToken(); // 43-char URL-safe (32 bytes) — session ids, reset tokens
|
|
53
|
+
csrfToken(); // CSRF token
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## API
|
|
57
|
+
|
|
58
|
+
| Export | Description |
|
|
59
|
+
| --- | --- |
|
|
60
|
+
| `sign(payload, secret, opts?)` | `algorithm`, `expiresIn`, `issuer`, `audience`, `subject` |
|
|
61
|
+
| `verify(token, secret, opts?)` | throws `JwtError`; checks sig + claims |
|
|
62
|
+
| `decode(token)` | header + payload, **no** verification |
|
|
63
|
+
| `randomToken(bytes?)` / `csrfToken()` | secure random tokens |
|
|
64
|
+
|
|
65
|
+
## The Lacspace Security Kit
|
|
66
|
+
|
|
67
|
+
| Package | For |
|
|
68
|
+
| --- | --- |
|
|
69
|
+
| [`@lacspace/crypto`](https://www.npmjs.com/package/@lacspace/crypto) | AES encryption & hashing |
|
|
70
|
+
| [`@lacspace/password`](https://www.npmjs.com/package/@lacspace/password) | Password hashing |
|
|
71
|
+
| **`@lacspace/jwt`** | JWTs & tokens (this package) |
|
|
72
|
+
| [`@lacspace/apikey`](https://www.npmjs.com/package/@lacspace/apikey) | API keys |
|
|
73
|
+
| [`@lacspace/otp`](https://www.npmjs.com/package/@lacspace/otp) | TOTP/HOTP 2FA |
|
|
74
|
+
| [`@lacspace/webauthn`](https://www.npmjs.com/package/@lacspace/webauthn) | Passkeys / biometric |
|
|
75
|
+
| [`@lacspace/mfa`](https://www.npmjs.com/package/@lacspace/mfa) | 2FA/3FA orchestration |
|
|
76
|
+
| [`@lacspace/lock`](https://www.npmjs.com/package/@lacspace/lock) | Account lockout |
|
|
77
|
+
| [`@lacspace/headers`](https://www.npmjs.com/package/@lacspace/headers) | Secure headers / CSP |
|
|
78
|
+
| [`@lacspace/redact`](https://www.npmjs.com/package/@lacspace/redact) | Log redaction |
|
|
79
|
+
|
|
80
|
+
<div align="center"><sub>Built with care by <a href="https://lacspace.com">Lacspace</a> · MIT licensed · <a href="https://github.com/lacspace/npm-packages">source</a></sub></div>
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var crypto = require('@lacspace/crypto');
|
|
4
|
+
|
|
5
|
+
// src/index.ts
|
|
6
|
+
var ALG_HASH = {
|
|
7
|
+
HS256: "SHA-256",
|
|
8
|
+
HS384: "SHA-384",
|
|
9
|
+
HS512: "SHA-512"
|
|
10
|
+
};
|
|
11
|
+
var JwtError = class extends Error {
|
|
12
|
+
constructor(message, code) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.code = code;
|
|
15
|
+
this.name = "JwtError";
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
var enc = new TextEncoder();
|
|
19
|
+
var dec = new TextDecoder();
|
|
20
|
+
function b64urlJson(obj) {
|
|
21
|
+
return crypto.toBase64url(enc.encode(JSON.stringify(obj)));
|
|
22
|
+
}
|
|
23
|
+
function nowSec() {
|
|
24
|
+
return Math.floor(Date.now() / 1e3);
|
|
25
|
+
}
|
|
26
|
+
async function sign(payload, secret, opts = {}) {
|
|
27
|
+
const alg = opts.algorithm ?? "HS256";
|
|
28
|
+
const iat = nowSec();
|
|
29
|
+
const body = { iat, ...payload };
|
|
30
|
+
if (opts.expiresIn !== void 0) body.exp = iat + opts.expiresIn;
|
|
31
|
+
if (opts.issuer) body.iss = opts.issuer;
|
|
32
|
+
if (opts.audience) body.aud = opts.audience;
|
|
33
|
+
if (opts.subject) body.sub = opts.subject;
|
|
34
|
+
const signingInput = `${b64urlJson({ alg, typ: "JWT" })}.${b64urlJson(body)}`;
|
|
35
|
+
const sig = await crypto.hmac(secret, signingInput, ALG_HASH[alg]);
|
|
36
|
+
return `${signingInput}.${crypto.toBase64url(sig)}`;
|
|
37
|
+
}
|
|
38
|
+
function decode(token) {
|
|
39
|
+
const parts = token.split(".");
|
|
40
|
+
if (parts.length !== 3) throw new JwtError("malformed token", "malformed");
|
|
41
|
+
return {
|
|
42
|
+
header: JSON.parse(dec.decode(crypto.fromBase64url(parts[0]))),
|
|
43
|
+
payload: JSON.parse(dec.decode(crypto.fromBase64url(parts[1])))
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
async function verify(token, secret, opts = {}) {
|
|
47
|
+
const parts = token.split(".");
|
|
48
|
+
if (parts.length !== 3) throw new JwtError("malformed token", "malformed");
|
|
49
|
+
const [h, p, s] = parts;
|
|
50
|
+
let header;
|
|
51
|
+
try {
|
|
52
|
+
header = JSON.parse(dec.decode(crypto.fromBase64url(h)));
|
|
53
|
+
} catch {
|
|
54
|
+
throw new JwtError("malformed header", "malformed");
|
|
55
|
+
}
|
|
56
|
+
const alg = header.alg;
|
|
57
|
+
if (!alg || !(alg in ALG_HASH)) throw new JwtError(`unsupported algorithm ${alg}`, "algorithm");
|
|
58
|
+
if (opts.algorithms && !opts.algorithms.includes(alg))
|
|
59
|
+
throw new JwtError(`algorithm ${alg} not allowed`, "algorithm");
|
|
60
|
+
const expected = await crypto.hmac(secret, `${h}.${p}`, ALG_HASH[alg]);
|
|
61
|
+
if (!crypto.constantTimeEqual(expected, crypto.fromBase64url(s)))
|
|
62
|
+
throw new JwtError("signature verification failed", "signature");
|
|
63
|
+
let payload;
|
|
64
|
+
try {
|
|
65
|
+
payload = JSON.parse(dec.decode(crypto.fromBase64url(p)));
|
|
66
|
+
} catch {
|
|
67
|
+
throw new JwtError("malformed payload", "malformed");
|
|
68
|
+
}
|
|
69
|
+
const now = nowSec();
|
|
70
|
+
const skew = opts.clockTolerance ?? 0;
|
|
71
|
+
if (payload.exp !== void 0 && now > payload.exp + skew)
|
|
72
|
+
throw new JwtError("token expired", "expired");
|
|
73
|
+
if (payload.nbf !== void 0 && now + skew < payload.nbf)
|
|
74
|
+
throw new JwtError("token not yet active", "not_active");
|
|
75
|
+
if (opts.issuer && payload.iss !== opts.issuer)
|
|
76
|
+
throw new JwtError("issuer mismatch", "issuer");
|
|
77
|
+
if (opts.audience) {
|
|
78
|
+
const aud = Array.isArray(payload.aud) ? payload.aud : payload.aud ? [payload.aud] : [];
|
|
79
|
+
if (!aud.includes(opts.audience)) throw new JwtError("audience mismatch", "audience");
|
|
80
|
+
}
|
|
81
|
+
return payload;
|
|
82
|
+
}
|
|
83
|
+
function randomToken(bytes = 32) {
|
|
84
|
+
return crypto.toBase64url(crypto.randomBytes(bytes));
|
|
85
|
+
}
|
|
86
|
+
function csrfToken() {
|
|
87
|
+
return randomToken(32);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
exports.JwtError = JwtError;
|
|
91
|
+
exports.csrfToken = csrfToken;
|
|
92
|
+
exports.decode = decode;
|
|
93
|
+
exports.randomToken = randomToken;
|
|
94
|
+
exports.sign = sign;
|
|
95
|
+
exports.verify = verify;
|
|
96
|
+
//# sourceMappingURL=index.cjs.map
|
|
97
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":["toBase64url","hmac","fromBase64url","constantTimeEqual","randomBytes"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * @lacspace/jwt\n * JSON Web Tokens (HMAC) + secure random tokens — done safely.\n *\n * Sign and verify HS256/HS384/HS512 JWTs over Web Crypto (isomorphic), with\n * strict expiry / not-before / issuer / audience checks and constant-time\n * signature comparison. Plus opaque secure tokens and CSRF tokens.\n *\n * Zero dependencies (bar @lacspace/crypto) · isomorphic · fully typed.\n */\n\nimport { hmac, toBase64url, fromBase64url, constantTimeEqual, randomBytes, type HashAlgorithm } from \"@lacspace/crypto\";\n\nexport type Algorithm = \"HS256\" | \"HS384\" | \"HS512\";\nconst ALG_HASH: Record<Algorithm, HashAlgorithm> = {\n HS256: \"SHA-256\",\n HS384: \"SHA-384\",\n HS512: \"SHA-512\",\n};\n\nexport class JwtError extends Error {\n constructor(\n message: string,\n public code:\n | \"malformed\"\n | \"signature\"\n | \"expired\"\n | \"not_active\"\n | \"issuer\"\n | \"audience\"\n | \"algorithm\",\n ) {\n super(message);\n this.name = \"JwtError\";\n }\n}\n\nexport interface JwtPayload {\n iss?: string;\n sub?: string;\n aud?: string | string[];\n exp?: number;\n nbf?: number;\n iat?: number;\n jti?: string;\n [key: string]: unknown;\n}\n\nexport interface SignOptions {\n algorithm?: Algorithm;\n /** Seconds from now until expiry, e.g. 3600. */\n expiresIn?: number;\n issuer?: string;\n audience?: string | string[];\n subject?: string;\n}\n\nconst enc = new TextEncoder();\nconst dec = new TextDecoder();\n\nfunction b64urlJson(obj: unknown): string {\n return toBase64url(enc.encode(JSON.stringify(obj)));\n}\n\nfunction nowSec(): number {\n return Math.floor(Date.now() / 1000);\n}\n\n/** Sign a JWT. */\nexport async function sign(\n payload: JwtPayload,\n secret: string | Uint8Array,\n opts: SignOptions = {},\n): Promise<string> {\n const alg = opts.algorithm ?? \"HS256\";\n const iat = nowSec();\n const body: JwtPayload = { iat, ...payload };\n if (opts.expiresIn !== undefined) body.exp = iat + opts.expiresIn;\n if (opts.issuer) body.iss = opts.issuer;\n if (opts.audience) body.aud = opts.audience;\n if (opts.subject) body.sub = opts.subject;\n\n const signingInput = `${b64urlJson({ alg, typ: \"JWT\" })}.${b64urlJson(body)}`;\n const sig = await hmac(secret, signingInput, ALG_HASH[alg]);\n return `${signingInput}.${toBase64url(sig)}`;\n}\n\nexport interface VerifyOptions {\n algorithms?: Algorithm[];\n issuer?: string;\n audience?: string;\n /** Allowed clock skew in seconds. Default 0. */\n clockTolerance?: number;\n}\n\n/** Decode a JWT without verifying (never trust the result for auth). */\nexport function decode(token: string): { header: Record<string, unknown>; payload: JwtPayload } {\n const parts = token.split(\".\");\n if (parts.length !== 3) throw new JwtError(\"malformed token\", \"malformed\");\n return {\n header: JSON.parse(dec.decode(fromBase64url(parts[0]!))),\n payload: JSON.parse(dec.decode(fromBase64url(parts[1]!))),\n };\n}\n\n/** Verify a JWT's signature and claims. Throws {@link JwtError} on any failure. */\nexport async function verify<T extends JwtPayload = JwtPayload>(\n token: string,\n secret: string | Uint8Array,\n opts: VerifyOptions = {},\n): Promise<T> {\n const parts = token.split(\".\");\n if (parts.length !== 3) throw new JwtError(\"malformed token\", \"malformed\");\n const [h, p, s] = parts as [string, string, string];\n\n let header: { alg?: Algorithm };\n try {\n header = JSON.parse(dec.decode(fromBase64url(h)));\n } catch {\n throw new JwtError(\"malformed header\", \"malformed\");\n }\n const alg = header.alg;\n if (!alg || !(alg in ALG_HASH)) throw new JwtError(`unsupported algorithm ${alg}`, \"algorithm\");\n if (opts.algorithms && !opts.algorithms.includes(alg))\n throw new JwtError(`algorithm ${alg} not allowed`, \"algorithm\");\n\n const expected = await hmac(secret, `${h}.${p}`, ALG_HASH[alg]);\n if (!constantTimeEqual(expected, fromBase64url(s)))\n throw new JwtError(\"signature verification failed\", \"signature\");\n\n let payload: T;\n try {\n payload = JSON.parse(dec.decode(fromBase64url(p)));\n } catch {\n throw new JwtError(\"malformed payload\", \"malformed\");\n }\n\n const now = nowSec();\n const skew = opts.clockTolerance ?? 0;\n if (payload.exp !== undefined && now > payload.exp + skew)\n throw new JwtError(\"token expired\", \"expired\");\n if (payload.nbf !== undefined && now + skew < payload.nbf)\n throw new JwtError(\"token not yet active\", \"not_active\");\n if (opts.issuer && payload.iss !== opts.issuer)\n throw new JwtError(\"issuer mismatch\", \"issuer\");\n if (opts.audience) {\n const aud = Array.isArray(payload.aud) ? payload.aud : payload.aud ? [payload.aud] : [];\n if (!aud.includes(opts.audience)) throw new JwtError(\"audience mismatch\", \"audience\");\n }\n return payload;\n}\n\n/* ------------------------------ opaque tokens ------------------------------ */\n\n/** A cryptographically-random URL-safe token (default 32 bytes → 43 chars). */\nexport function randomToken(bytes = 32): string {\n return toBase64url(randomBytes(bytes));\n}\n\n/** A CSRF token (alias of a 32-byte random token). */\nexport function csrfToken(): string {\n return randomToken(32);\n}\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @lacspace/jwt
|
|
3
|
+
* JSON Web Tokens (HMAC) + secure random tokens — done safely.
|
|
4
|
+
*
|
|
5
|
+
* Sign and verify HS256/HS384/HS512 JWTs over Web Crypto (isomorphic), with
|
|
6
|
+
* strict expiry / not-before / issuer / audience checks and constant-time
|
|
7
|
+
* signature comparison. Plus opaque secure tokens and CSRF tokens.
|
|
8
|
+
*
|
|
9
|
+
* Zero dependencies (bar @lacspace/crypto) · isomorphic · fully typed.
|
|
10
|
+
*/
|
|
11
|
+
type Algorithm = "HS256" | "HS384" | "HS512";
|
|
12
|
+
declare class JwtError extends Error {
|
|
13
|
+
code: "malformed" | "signature" | "expired" | "not_active" | "issuer" | "audience" | "algorithm";
|
|
14
|
+
constructor(message: string, code: "malformed" | "signature" | "expired" | "not_active" | "issuer" | "audience" | "algorithm");
|
|
15
|
+
}
|
|
16
|
+
interface JwtPayload {
|
|
17
|
+
iss?: string;
|
|
18
|
+
sub?: string;
|
|
19
|
+
aud?: string | string[];
|
|
20
|
+
exp?: number;
|
|
21
|
+
nbf?: number;
|
|
22
|
+
iat?: number;
|
|
23
|
+
jti?: string;
|
|
24
|
+
[key: string]: unknown;
|
|
25
|
+
}
|
|
26
|
+
interface SignOptions {
|
|
27
|
+
algorithm?: Algorithm;
|
|
28
|
+
/** Seconds from now until expiry, e.g. 3600. */
|
|
29
|
+
expiresIn?: number;
|
|
30
|
+
issuer?: string;
|
|
31
|
+
audience?: string | string[];
|
|
32
|
+
subject?: string;
|
|
33
|
+
}
|
|
34
|
+
/** Sign a JWT. */
|
|
35
|
+
declare function sign(payload: JwtPayload, secret: string | Uint8Array, opts?: SignOptions): Promise<string>;
|
|
36
|
+
interface VerifyOptions {
|
|
37
|
+
algorithms?: Algorithm[];
|
|
38
|
+
issuer?: string;
|
|
39
|
+
audience?: string;
|
|
40
|
+
/** Allowed clock skew in seconds. Default 0. */
|
|
41
|
+
clockTolerance?: number;
|
|
42
|
+
}
|
|
43
|
+
/** Decode a JWT without verifying (never trust the result for auth). */
|
|
44
|
+
declare function decode(token: string): {
|
|
45
|
+
header: Record<string, unknown>;
|
|
46
|
+
payload: JwtPayload;
|
|
47
|
+
};
|
|
48
|
+
/** Verify a JWT's signature and claims. Throws {@link JwtError} on any failure. */
|
|
49
|
+
declare function verify<T extends JwtPayload = JwtPayload>(token: string, secret: string | Uint8Array, opts?: VerifyOptions): Promise<T>;
|
|
50
|
+
/** A cryptographically-random URL-safe token (default 32 bytes → 43 chars). */
|
|
51
|
+
declare function randomToken(bytes?: number): string;
|
|
52
|
+
/** A CSRF token (alias of a 32-byte random token). */
|
|
53
|
+
declare function csrfToken(): string;
|
|
54
|
+
|
|
55
|
+
export { type Algorithm, JwtError, type JwtPayload, type SignOptions, type VerifyOptions, csrfToken, decode, randomToken, sign, verify };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @lacspace/jwt
|
|
3
|
+
* JSON Web Tokens (HMAC) + secure random tokens — done safely.
|
|
4
|
+
*
|
|
5
|
+
* Sign and verify HS256/HS384/HS512 JWTs over Web Crypto (isomorphic), with
|
|
6
|
+
* strict expiry / not-before / issuer / audience checks and constant-time
|
|
7
|
+
* signature comparison. Plus opaque secure tokens and CSRF tokens.
|
|
8
|
+
*
|
|
9
|
+
* Zero dependencies (bar @lacspace/crypto) · isomorphic · fully typed.
|
|
10
|
+
*/
|
|
11
|
+
type Algorithm = "HS256" | "HS384" | "HS512";
|
|
12
|
+
declare class JwtError extends Error {
|
|
13
|
+
code: "malformed" | "signature" | "expired" | "not_active" | "issuer" | "audience" | "algorithm";
|
|
14
|
+
constructor(message: string, code: "malformed" | "signature" | "expired" | "not_active" | "issuer" | "audience" | "algorithm");
|
|
15
|
+
}
|
|
16
|
+
interface JwtPayload {
|
|
17
|
+
iss?: string;
|
|
18
|
+
sub?: string;
|
|
19
|
+
aud?: string | string[];
|
|
20
|
+
exp?: number;
|
|
21
|
+
nbf?: number;
|
|
22
|
+
iat?: number;
|
|
23
|
+
jti?: string;
|
|
24
|
+
[key: string]: unknown;
|
|
25
|
+
}
|
|
26
|
+
interface SignOptions {
|
|
27
|
+
algorithm?: Algorithm;
|
|
28
|
+
/** Seconds from now until expiry, e.g. 3600. */
|
|
29
|
+
expiresIn?: number;
|
|
30
|
+
issuer?: string;
|
|
31
|
+
audience?: string | string[];
|
|
32
|
+
subject?: string;
|
|
33
|
+
}
|
|
34
|
+
/** Sign a JWT. */
|
|
35
|
+
declare function sign(payload: JwtPayload, secret: string | Uint8Array, opts?: SignOptions): Promise<string>;
|
|
36
|
+
interface VerifyOptions {
|
|
37
|
+
algorithms?: Algorithm[];
|
|
38
|
+
issuer?: string;
|
|
39
|
+
audience?: string;
|
|
40
|
+
/** Allowed clock skew in seconds. Default 0. */
|
|
41
|
+
clockTolerance?: number;
|
|
42
|
+
}
|
|
43
|
+
/** Decode a JWT without verifying (never trust the result for auth). */
|
|
44
|
+
declare function decode(token: string): {
|
|
45
|
+
header: Record<string, unknown>;
|
|
46
|
+
payload: JwtPayload;
|
|
47
|
+
};
|
|
48
|
+
/** Verify a JWT's signature and claims. Throws {@link JwtError} on any failure. */
|
|
49
|
+
declare function verify<T extends JwtPayload = JwtPayload>(token: string, secret: string | Uint8Array, opts?: VerifyOptions): Promise<T>;
|
|
50
|
+
/** A cryptographically-random URL-safe token (default 32 bytes → 43 chars). */
|
|
51
|
+
declare function randomToken(bytes?: number): string;
|
|
52
|
+
/** A CSRF token (alias of a 32-byte random token). */
|
|
53
|
+
declare function csrfToken(): string;
|
|
54
|
+
|
|
55
|
+
export { type Algorithm, JwtError, type JwtPayload, type SignOptions, type VerifyOptions, csrfToken, decode, randomToken, sign, verify };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { hmac, toBase64url, fromBase64url, constantTimeEqual, randomBytes } from '@lacspace/crypto';
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
var ALG_HASH = {
|
|
5
|
+
HS256: "SHA-256",
|
|
6
|
+
HS384: "SHA-384",
|
|
7
|
+
HS512: "SHA-512"
|
|
8
|
+
};
|
|
9
|
+
var JwtError = class extends Error {
|
|
10
|
+
constructor(message, code) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.code = code;
|
|
13
|
+
this.name = "JwtError";
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
var enc = new TextEncoder();
|
|
17
|
+
var dec = new TextDecoder();
|
|
18
|
+
function b64urlJson(obj) {
|
|
19
|
+
return toBase64url(enc.encode(JSON.stringify(obj)));
|
|
20
|
+
}
|
|
21
|
+
function nowSec() {
|
|
22
|
+
return Math.floor(Date.now() / 1e3);
|
|
23
|
+
}
|
|
24
|
+
async function sign(payload, secret, opts = {}) {
|
|
25
|
+
const alg = opts.algorithm ?? "HS256";
|
|
26
|
+
const iat = nowSec();
|
|
27
|
+
const body = { iat, ...payload };
|
|
28
|
+
if (opts.expiresIn !== void 0) body.exp = iat + opts.expiresIn;
|
|
29
|
+
if (opts.issuer) body.iss = opts.issuer;
|
|
30
|
+
if (opts.audience) body.aud = opts.audience;
|
|
31
|
+
if (opts.subject) body.sub = opts.subject;
|
|
32
|
+
const signingInput = `${b64urlJson({ alg, typ: "JWT" })}.${b64urlJson(body)}`;
|
|
33
|
+
const sig = await hmac(secret, signingInput, ALG_HASH[alg]);
|
|
34
|
+
return `${signingInput}.${toBase64url(sig)}`;
|
|
35
|
+
}
|
|
36
|
+
function decode(token) {
|
|
37
|
+
const parts = token.split(".");
|
|
38
|
+
if (parts.length !== 3) throw new JwtError("malformed token", "malformed");
|
|
39
|
+
return {
|
|
40
|
+
header: JSON.parse(dec.decode(fromBase64url(parts[0]))),
|
|
41
|
+
payload: JSON.parse(dec.decode(fromBase64url(parts[1])))
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
async function verify(token, secret, opts = {}) {
|
|
45
|
+
const parts = token.split(".");
|
|
46
|
+
if (parts.length !== 3) throw new JwtError("malformed token", "malformed");
|
|
47
|
+
const [h, p, s] = parts;
|
|
48
|
+
let header;
|
|
49
|
+
try {
|
|
50
|
+
header = JSON.parse(dec.decode(fromBase64url(h)));
|
|
51
|
+
} catch {
|
|
52
|
+
throw new JwtError("malformed header", "malformed");
|
|
53
|
+
}
|
|
54
|
+
const alg = header.alg;
|
|
55
|
+
if (!alg || !(alg in ALG_HASH)) throw new JwtError(`unsupported algorithm ${alg}`, "algorithm");
|
|
56
|
+
if (opts.algorithms && !opts.algorithms.includes(alg))
|
|
57
|
+
throw new JwtError(`algorithm ${alg} not allowed`, "algorithm");
|
|
58
|
+
const expected = await hmac(secret, `${h}.${p}`, ALG_HASH[alg]);
|
|
59
|
+
if (!constantTimeEqual(expected, fromBase64url(s)))
|
|
60
|
+
throw new JwtError("signature verification failed", "signature");
|
|
61
|
+
let payload;
|
|
62
|
+
try {
|
|
63
|
+
payload = JSON.parse(dec.decode(fromBase64url(p)));
|
|
64
|
+
} catch {
|
|
65
|
+
throw new JwtError("malformed payload", "malformed");
|
|
66
|
+
}
|
|
67
|
+
const now = nowSec();
|
|
68
|
+
const skew = opts.clockTolerance ?? 0;
|
|
69
|
+
if (payload.exp !== void 0 && now > payload.exp + skew)
|
|
70
|
+
throw new JwtError("token expired", "expired");
|
|
71
|
+
if (payload.nbf !== void 0 && now + skew < payload.nbf)
|
|
72
|
+
throw new JwtError("token not yet active", "not_active");
|
|
73
|
+
if (opts.issuer && payload.iss !== opts.issuer)
|
|
74
|
+
throw new JwtError("issuer mismatch", "issuer");
|
|
75
|
+
if (opts.audience) {
|
|
76
|
+
const aud = Array.isArray(payload.aud) ? payload.aud : payload.aud ? [payload.aud] : [];
|
|
77
|
+
if (!aud.includes(opts.audience)) throw new JwtError("audience mismatch", "audience");
|
|
78
|
+
}
|
|
79
|
+
return payload;
|
|
80
|
+
}
|
|
81
|
+
function randomToken(bytes = 32) {
|
|
82
|
+
return toBase64url(randomBytes(bytes));
|
|
83
|
+
}
|
|
84
|
+
function csrfToken() {
|
|
85
|
+
return randomToken(32);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export { JwtError, csrfToken, decode, randomToken, sign, verify };
|
|
89
|
+
//# sourceMappingURL=index.js.map
|
|
90
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @lacspace/jwt\n * JSON Web Tokens (HMAC) + secure random tokens — done safely.\n *\n * Sign and verify HS256/HS384/HS512 JWTs over Web Crypto (isomorphic), with\n * strict expiry / not-before / issuer / audience checks and constant-time\n * signature comparison. Plus opaque secure tokens and CSRF tokens.\n *\n * Zero dependencies (bar @lacspace/crypto) · isomorphic · fully typed.\n */\n\nimport { hmac, toBase64url, fromBase64url, constantTimeEqual, randomBytes, type HashAlgorithm } from \"@lacspace/crypto\";\n\nexport type Algorithm = \"HS256\" | \"HS384\" | \"HS512\";\nconst ALG_HASH: Record<Algorithm, HashAlgorithm> = {\n HS256: \"SHA-256\",\n HS384: \"SHA-384\",\n HS512: \"SHA-512\",\n};\n\nexport class JwtError extends Error {\n constructor(\n message: string,\n public code:\n | \"malformed\"\n | \"signature\"\n | \"expired\"\n | \"not_active\"\n | \"issuer\"\n | \"audience\"\n | \"algorithm\",\n ) {\n super(message);\n this.name = \"JwtError\";\n }\n}\n\nexport interface JwtPayload {\n iss?: string;\n sub?: string;\n aud?: string | string[];\n exp?: number;\n nbf?: number;\n iat?: number;\n jti?: string;\n [key: string]: unknown;\n}\n\nexport interface SignOptions {\n algorithm?: Algorithm;\n /** Seconds from now until expiry, e.g. 3600. */\n expiresIn?: number;\n issuer?: string;\n audience?: string | string[];\n subject?: string;\n}\n\nconst enc = new TextEncoder();\nconst dec = new TextDecoder();\n\nfunction b64urlJson(obj: unknown): string {\n return toBase64url(enc.encode(JSON.stringify(obj)));\n}\n\nfunction nowSec(): number {\n return Math.floor(Date.now() / 1000);\n}\n\n/** Sign a JWT. */\nexport async function sign(\n payload: JwtPayload,\n secret: string | Uint8Array,\n opts: SignOptions = {},\n): Promise<string> {\n const alg = opts.algorithm ?? \"HS256\";\n const iat = nowSec();\n const body: JwtPayload = { iat, ...payload };\n if (opts.expiresIn !== undefined) body.exp = iat + opts.expiresIn;\n if (opts.issuer) body.iss = opts.issuer;\n if (opts.audience) body.aud = opts.audience;\n if (opts.subject) body.sub = opts.subject;\n\n const signingInput = `${b64urlJson({ alg, typ: \"JWT\" })}.${b64urlJson(body)}`;\n const sig = await hmac(secret, signingInput, ALG_HASH[alg]);\n return `${signingInput}.${toBase64url(sig)}`;\n}\n\nexport interface VerifyOptions {\n algorithms?: Algorithm[];\n issuer?: string;\n audience?: string;\n /** Allowed clock skew in seconds. Default 0. */\n clockTolerance?: number;\n}\n\n/** Decode a JWT without verifying (never trust the result for auth). */\nexport function decode(token: string): { header: Record<string, unknown>; payload: JwtPayload } {\n const parts = token.split(\".\");\n if (parts.length !== 3) throw new JwtError(\"malformed token\", \"malformed\");\n return {\n header: JSON.parse(dec.decode(fromBase64url(parts[0]!))),\n payload: JSON.parse(dec.decode(fromBase64url(parts[1]!))),\n };\n}\n\n/** Verify a JWT's signature and claims. Throws {@link JwtError} on any failure. */\nexport async function verify<T extends JwtPayload = JwtPayload>(\n token: string,\n secret: string | Uint8Array,\n opts: VerifyOptions = {},\n): Promise<T> {\n const parts = token.split(\".\");\n if (parts.length !== 3) throw new JwtError(\"malformed token\", \"malformed\");\n const [h, p, s] = parts as [string, string, string];\n\n let header: { alg?: Algorithm };\n try {\n header = JSON.parse(dec.decode(fromBase64url(h)));\n } catch {\n throw new JwtError(\"malformed header\", \"malformed\");\n }\n const alg = header.alg;\n if (!alg || !(alg in ALG_HASH)) throw new JwtError(`unsupported algorithm ${alg}`, \"algorithm\");\n if (opts.algorithms && !opts.algorithms.includes(alg))\n throw new JwtError(`algorithm ${alg} not allowed`, \"algorithm\");\n\n const expected = await hmac(secret, `${h}.${p}`, ALG_HASH[alg]);\n if (!constantTimeEqual(expected, fromBase64url(s)))\n throw new JwtError(\"signature verification failed\", \"signature\");\n\n let payload: T;\n try {\n payload = JSON.parse(dec.decode(fromBase64url(p)));\n } catch {\n throw new JwtError(\"malformed payload\", \"malformed\");\n }\n\n const now = nowSec();\n const skew = opts.clockTolerance ?? 0;\n if (payload.exp !== undefined && now > payload.exp + skew)\n throw new JwtError(\"token expired\", \"expired\");\n if (payload.nbf !== undefined && now + skew < payload.nbf)\n throw new JwtError(\"token not yet active\", \"not_active\");\n if (opts.issuer && payload.iss !== opts.issuer)\n throw new JwtError(\"issuer mismatch\", \"issuer\");\n if (opts.audience) {\n const aud = Array.isArray(payload.aud) ? payload.aud : payload.aud ? [payload.aud] : [];\n if (!aud.includes(opts.audience)) throw new JwtError(\"audience mismatch\", \"audience\");\n }\n return payload;\n}\n\n/* ------------------------------ opaque tokens ------------------------------ */\n\n/** A cryptographically-random URL-safe token (default 32 bytes → 43 chars). */\nexport function randomToken(bytes = 32): string {\n return toBase64url(randomBytes(bytes));\n}\n\n/** A CSRF token (alias of a 32-byte random token). */\nexport function csrfToken(): string {\n return randomToken(32);\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lacspace/jwt",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "JSON Web Tokens (HS256/384/512) with strict expiry/issuer/audience checks + secure random & CSRF tokens. Isomorphic over Web Crypto — Node, edge, browser.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": { ".": { "import": { "types": "./dist/index.d.ts", "default": "./dist/index.js" }, "require": { "types": "./dist/index.d.cts", "default": "./dist/index.cjs" } } },
|
|
10
|
+
"files": ["dist"],
|
|
11
|
+
"sideEffects": false,
|
|
12
|
+
"scripts": { "build": "tsup", "prepublishOnly": "npm run build" },
|
|
13
|
+
"keywords": ["jwt", "jsonwebtoken", "hs256", "token", "auth", "csrf", "web-crypto", "edge", "typescript"],
|
|
14
|
+
"author": "Lacspace <contact@lacspace.com>",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"homepage": "https://lacspace.com/packages",
|
|
17
|
+
"repository": { "type": "git", "url": "git+https://github.com/lacspace/npm-packages.git", "directory": "jwt" },
|
|
18
|
+
"bugs": { "url": "https://github.com/lacspace/npm-packages/issues" },
|
|
19
|
+
"engines": { "node": ">=18" },
|
|
20
|
+
"dependencies": { "@lacspace/crypto": "^1.0.0" }
|
|
21
|
+
}
|