@dws-std/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.md +21 -0
- package/README.md +109 -0
- package/dist/constant/jwt-error-keys.d.ts +7 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +68 -0
- package/dist/jwt.d.ts +29 -0
- package/package.json +55 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dominus Web Services (DWS)
|
|
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,109 @@
|
|
|
1
|
+
# 🔐 DWS JWT
|
|
2
|
+
|
|
3
|
+
Signing and verifying JWTs shouldn't require boilerplate.
|
|
4
|
+
`@dws-std/jwt` wraps [jose](https://github.com/panva/jose) with sane defaults — HS256, standard claims pre-filled, human-readable expiration - so you can focus on what matters instead of re-reading the JWT spec.
|
|
5
|
+
|
|
6
|
+
## 📌 Table of Contents
|
|
7
|
+
|
|
8
|
+
- [Installation](#-installation)
|
|
9
|
+
- [Usage](#-usage)
|
|
10
|
+
- [signJWT](#signjwt)
|
|
11
|
+
- [verifyJWT](#verifyjwt)
|
|
12
|
+
- [Error handling](#-error-handling)
|
|
13
|
+
- [License](#-license)
|
|
14
|
+
|
|
15
|
+
## 🔧 Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
bun add @dws-std/jwt
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## ⚙️ Usage
|
|
22
|
+
|
|
23
|
+
### `signJWT`
|
|
24
|
+
|
|
25
|
+
Signs a payload and returns a JWT string.
|
|
26
|
+
All standard claims (`iss`, `sub`, `aud`, `jti`, `nbf`, `iat`, `exp`) are included automatically - just pass your custom data and let the function handle the rest.
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { signJWT } from '@dws-std/jwt';
|
|
30
|
+
|
|
31
|
+
// Default expiration: 15 minutes
|
|
32
|
+
const token = await signJWT(secret, { userId: 42, role: 'admin' });
|
|
33
|
+
|
|
34
|
+
// Numeric offset in seconds
|
|
35
|
+
const token = await signJWT(secret, { userId: 42 }, 3600);
|
|
36
|
+
|
|
37
|
+
// Date object
|
|
38
|
+
const token = await signJWT(secret, { userId: 42 }, new Date(Date.now() + 3600_000));
|
|
39
|
+
|
|
40
|
+
// Human-readable — powered by @dws-std/common
|
|
41
|
+
const token = await signJWT(secret, { userId: 42 }, '1 hour');
|
|
42
|
+
const token = await signJWT(secret, { userId: 42 }, '30 minutes');
|
|
43
|
+
const token = await signJWT(secret, { userId: 42 }, '7 days');
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The secret must be at least 32 characters long (HS256 requirement). Any shorter and it throws immediately — better to catch it at startup than in production.
|
|
47
|
+
|
|
48
|
+
Default claims can be overridden by providing them in the payload:
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
const token = await signJWT(secret, {
|
|
52
|
+
iss: 'my-service',
|
|
53
|
+
sub: 'user-123',
|
|
54
|
+
aud: ['my-api'],
|
|
55
|
+
userId: 42
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### `verifyJWT`
|
|
60
|
+
|
|
61
|
+
Verifies a token and returns the decoded payload. Throws if anything is wrong.
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import { verifyJWT } from '@dws-std/jwt';
|
|
65
|
+
|
|
66
|
+
const { payload } = await verifyJWT(token, secret);
|
|
67
|
+
console.log(payload.userId);
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Optionally validate `iss` and `aud` claims:
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
const { payload } = await verifyJWT(token, secret, {
|
|
74
|
+
issuer: 'my-service',
|
|
75
|
+
audience: 'my-api'
|
|
76
|
+
});
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## 🚨 Error handling
|
|
80
|
+
|
|
81
|
+
All errors are `Exception` instances from `@dws-std/error` with a `key` you can check:
|
|
82
|
+
|
|
83
|
+
| Key | When |
|
|
84
|
+
| ----------------------- | --------------------------------------------------------------- |
|
|
85
|
+
| `jwt.secret_too_weak` | Secret is shorter than 32 characters |
|
|
86
|
+
| `jwt.expiration_passed` | Expiration is in the past or equals now |
|
|
87
|
+
| `jwt.sign_error` | jose failed to sign the token |
|
|
88
|
+
| `jwt.token_expired` | Token is valid but past its expiry date |
|
|
89
|
+
| `jwt.unauthorized` | Invalid signature, malformed token, or claim validation failure |
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
import { Exception } from '@dws-std/error';
|
|
93
|
+
import { JWT_ERROR_KEYS, verifyJWT } from '@dws-std/jwt';
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const { payload } = await verifyJWT(token, secret);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
if (error instanceof Exception) {
|
|
99
|
+
if (error.key === JWT_ERROR_KEYS.JWT_EXPIRED) {
|
|
100
|
+
// Token expired — trigger a refresh
|
|
101
|
+
}
|
|
102
|
+
// Everything else is unauthorized
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## ⚖️ License
|
|
108
|
+
|
|
109
|
+
MIT - Feel free to use it.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const JWT_ERROR_KEYS: {
|
|
2
|
+
readonly JWT_SECRET_TOO_WEAK: "jwt.secret_too_weak";
|
|
3
|
+
readonly JWT_EXPIRATION_PASSED: "jwt.expiration_passed";
|
|
4
|
+
readonly JWT_SIGN_ERROR: "jwt.sign_error";
|
|
5
|
+
readonly JWT_EXPIRED: "jwt.token_expired";
|
|
6
|
+
readonly JWT_UNAUTHORIZED: "jwt.unauthorized";
|
|
7
|
+
};
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/constant/jwt-error-keys.ts
|
|
3
|
+
var JWT_ERROR_KEYS = {
|
|
4
|
+
JWT_SECRET_TOO_WEAK: "jwt.secret_too_weak",
|
|
5
|
+
JWT_EXPIRATION_PASSED: "jwt.expiration_passed",
|
|
6
|
+
JWT_SIGN_ERROR: "jwt.sign_error",
|
|
7
|
+
JWT_EXPIRED: "jwt.token_expired",
|
|
8
|
+
JWT_UNAUTHORIZED: "jwt.unauthorized"
|
|
9
|
+
};
|
|
10
|
+
// src/jwt.ts
|
|
11
|
+
import { parseHumanTimeToSeconds } from "@dws-std/common";
|
|
12
|
+
import { Exception } from "@dws-std/error";
|
|
13
|
+
import { SignJWT, errors, jwtVerify } from "jose";
|
|
14
|
+
var _textEncoder = new TextEncoder;
|
|
15
|
+
var signJWT = (secret, payload, expiration = 60 * 15) => {
|
|
16
|
+
if (secret.length < 32)
|
|
17
|
+
throw new Exception("Secret key must be at least 32 characters long", {
|
|
18
|
+
key: JWT_ERROR_KEYS.JWT_SECRET_TOO_WEAK,
|
|
19
|
+
cause: { providedLength: secret.length }
|
|
20
|
+
});
|
|
21
|
+
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
22
|
+
const exp = expiration instanceof Date ? Math.floor(expiration.getTime() / 1000) : typeof expiration === "number" ? nowSeconds + expiration : nowSeconds + parseHumanTimeToSeconds(expiration);
|
|
23
|
+
if (exp <= nowSeconds)
|
|
24
|
+
throw new Exception("Expiration time must be in the future", {
|
|
25
|
+
key: JWT_ERROR_KEYS.JWT_EXPIRATION_PASSED,
|
|
26
|
+
cause: { providedExpiration: expiration }
|
|
27
|
+
});
|
|
28
|
+
const finalPayload = {
|
|
29
|
+
iss: "DWS-Issuer",
|
|
30
|
+
sub: "",
|
|
31
|
+
aud: ["DWS-Audience"],
|
|
32
|
+
jti: Bun.randomUUIDv7(),
|
|
33
|
+
nbf: nowSeconds,
|
|
34
|
+
iat: nowSeconds,
|
|
35
|
+
exp,
|
|
36
|
+
...payload
|
|
37
|
+
};
|
|
38
|
+
try {
|
|
39
|
+
return new SignJWT(finalPayload).setProtectedHeader({ alg: "HS256", typ: "JWT" }).sign(_textEncoder.encode(secret));
|
|
40
|
+
} catch (error) {
|
|
41
|
+
throw new Exception("Failed to sign JWT", {
|
|
42
|
+
key: JWT_ERROR_KEYS.JWT_SIGN_ERROR,
|
|
43
|
+
cause: error
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
var verifyJWT = async (token, secret, options) => {
|
|
48
|
+
try {
|
|
49
|
+
return await jwtVerify(token, _textEncoder.encode(secret), {
|
|
50
|
+
algorithms: ["HS256"],
|
|
51
|
+
...options?.issuer && { issuer: options.issuer },
|
|
52
|
+
...options?.audience && { audience: options.audience }
|
|
53
|
+
});
|
|
54
|
+
} catch (error) {
|
|
55
|
+
if (error instanceof errors.JWTExpired)
|
|
56
|
+
throw new Exception("JWT token has expired", {
|
|
57
|
+
key: JWT_ERROR_KEYS.JWT_EXPIRED
|
|
58
|
+
});
|
|
59
|
+
throw new Exception("Unauthorized", {
|
|
60
|
+
key: JWT_ERROR_KEYS.JWT_UNAUTHORIZED
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
export {
|
|
65
|
+
verifyJWT,
|
|
66
|
+
signJWT,
|
|
67
|
+
JWT_ERROR_KEYS
|
|
68
|
+
};
|
package/dist/jwt.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type JWTPayload, type JWTVerifyResult } from 'jose';
|
|
2
|
+
export interface VerifyOptions {
|
|
3
|
+
issuer?: string;
|
|
4
|
+
audience?: string | string[];
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Signs a JWT with the given payload and expiration
|
|
8
|
+
*
|
|
9
|
+
* @param secret - The secret key used for HS256 signing (minimum 32 characters)
|
|
10
|
+
* @param payload - The JWT payload claims
|
|
11
|
+
* @param expiration - Token expiration as seconds offset, Date, or human-readable string (default: 15 minutes)
|
|
12
|
+
*
|
|
13
|
+
* @throws ({@link Exception}) – If secret is too short, expiration is in the past, or signing fails
|
|
14
|
+
*
|
|
15
|
+
* @returns A Promise resolving to the signed JWT string
|
|
16
|
+
*/
|
|
17
|
+
export declare const signJWT: (secret: string, payload: JWTPayload, expiration?: number | string | Date) => Promise<string>;
|
|
18
|
+
/**
|
|
19
|
+
* Verifies a JWT token and throws Exception on failure
|
|
20
|
+
*
|
|
21
|
+
* @param token - The JWT token string to verify
|
|
22
|
+
* @param secret - The secret key used for HS256 verification
|
|
23
|
+
* @param options - Optional verification options for issuer/audience validation
|
|
24
|
+
*
|
|
25
|
+
* @throws ({@link Exception}) – 401 if token is expired, invalid signature, malformed, or claim validation fails
|
|
26
|
+
*
|
|
27
|
+
* @returns The verification result with payload and protected header
|
|
28
|
+
*/
|
|
29
|
+
export declare const verifyJWT: (token: string, secret: string, options?: VerifyOptions) => Promise<JWTVerifyResult>;
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dws-std/jwt",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "JWT utilities for Dominus Web Services (DWS) projects.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"bun",
|
|
7
|
+
"dws",
|
|
8
|
+
"jwt",
|
|
9
|
+
"types",
|
|
10
|
+
"utilities"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"author": "Dominus Web Services (DWS)",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://github.com/Dominus-Web-Service/std",
|
|
17
|
+
"directory": "packages/common"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
22
|
+
"type": "module",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"default": "./dist/index.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "bun builder.ts",
|
|
31
|
+
"docs": "bunx typedoc --tsconfig tsconfig.build.json",
|
|
32
|
+
"fmt:check": "oxfmt --check",
|
|
33
|
+
"fmt": "oxfmt",
|
|
34
|
+
"lint:fix": "oxlint --type-aware --type-check --fix ./src",
|
|
35
|
+
"lint:github": "oxlint --type-aware --type-check --format=github ./src",
|
|
36
|
+
"lint": "oxlint --type-aware --type-check ./src",
|
|
37
|
+
"test": "bun test --pass-with-no-tests --coverage"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@dws-std/common": "^1.0.0",
|
|
41
|
+
"@dws-std/error": "^2.1.0",
|
|
42
|
+
"jose": "^6.2.2"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/bun": "^1.3.11",
|
|
46
|
+
"oxfmt": "0.41.0",
|
|
47
|
+
"oxlint": "1.56.0",
|
|
48
|
+
"oxlint-tsgolint": "0.17.0",
|
|
49
|
+
"typescript": "^5.9.3"
|
|
50
|
+
},
|
|
51
|
+
"peerDependencies": {
|
|
52
|
+
"@dws-std/common": "^1.0.0",
|
|
53
|
+
"@dws-std/error": "^2.1.0"
|
|
54
|
+
}
|
|
55
|
+
}
|