unjwt 0.8.1 → 0.8.2
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/README.md +4 -4
- package/dist/THIRD-PARTY-LICENSES.md.gz +0 -0
- package/dist/_chunks/_codec-jwe.d.mts +105 -22
- package/dist/_chunks/_codec-jws.mjs +306 -232
- package/dist/_chunks/_cookie-attributes.d.mts +12 -0
- package/dist/_chunks/digest.d.mts +8 -0
- package/dist/_chunks/{algorithms.mjs → digest.mjs} +78 -8
- package/dist/_chunks/index.d.mts +311 -268
- package/dist/_chunks/index2.d.mts +2 -1
- package/dist/_chunks/index3.d.mts +1 -0
- package/dist/_chunks/jwe.d.mts +4 -4
- package/dist/_chunks/jwe.mjs +21 -3
- package/dist/_chunks/jwk.mjs +219 -173
- package/dist/_chunks/jws.d.mts +10 -10
- package/dist/_chunks/jws.mjs +18 -6
- package/dist/_chunks/libs/cookie-es.mjs +255 -0
- package/dist/adapters/elysia/index.d.mts +32 -15
- package/dist/adapters/elysia/index.mjs +46 -67
- package/dist/adapters/h3/index.d.mts +135 -0
- package/dist/adapters/h3/index.mjs +119 -0
- package/dist/adapters/h3v1/index.d.mts +100 -27
- package/dist/adapters/h3v1/index.mjs +59 -76
- package/dist/core/jwe.d.mts +2 -2
- package/dist/core/jwk.d.mts +22 -14
- package/dist/core/jwk.mjs +2 -2
- package/dist/core/jws.d.mts +2 -2
- package/dist/core/utils/index.d.mts +2 -1
- package/dist/core/utils/index.mjs +2 -2
- package/dist/index.d.mts +4 -3
- package/dist/index.mjs +3 -3
- package/dist/session/index.d.mts +44 -0
- package/dist/session/index.mjs +111 -0
- package/package.json +37 -18
- package/dist/adapters/h3v2/index.d.mts +0 -65
- package/dist/adapters/h3v2/index.mjs +0 -139
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](https://npmx.dev/package/unjwt)
|
|
5
5
|
[](https://npmx.dev/package/unjwt)
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
A collection of JWT utilities based on the Web Crypto API, in a single zero-dependency package.
|
|
8
8
|
|
|
9
9
|
> **📖 Documentation — [unjwt.s94.dev](https://unjwt.s94.dev)**
|
|
10
10
|
|
|
@@ -13,9 +13,9 @@ Low-level JWT toolkit built on the Web Crypto API. Sign, verify, encrypt, decryp
|
|
|
13
13
|
- **JWS** ([RFC 7515](https://datatracker.ietf.org/doc/html/rfc7515)) — HMAC, RSA, RSA-PSS, ECDSA, EdDSA. Compact + General/Flattened JSON Serialization (multi-signature).
|
|
14
14
|
- **JWE** ([RFC 7516](https://datatracker.ietf.org/doc/html/rfc7516)) — `dir`, AES-KW, AES-GCM-KW, RSA-OAEP, PBES2, ECDH-ES. AES-GCM and AES-CBC-HMAC-SHA2 content encryption. Compact + General/Flattened JSON Serialization (multi-recipient).
|
|
15
15
|
- **JWK** ([RFC 7517](https://datatracker.ietf.org/doc/html/rfc7517)) — generate, import, export, wrap, unwrap. `CryptoKey` ↔ JWK ↔ PEM conversion. ECDH-ES shared-secret derivation. JWK Set lookup and rotation over a set your application fetches.
|
|
16
|
-
- **
|
|
16
|
+
- **Sessions** — cookie-based JWT sessions through [`unjwt/session`](https://unjwt.s94.dev/frameworks/session) on [h3 v2](https://unjwt.s94.dev/frameworks/h3), [Nitro 3](https://unjwt.s94.dev/frameworks/nitro), [Nuxt 4.6+](https://unjwt.s94.dev/frameworks/nuxt), Workers, Bun, Deno and Hono, or on a token in hand; an [Elysia](https://unjwt.s94.dev/frameworks/elysia) plugin; a [driver](https://unjwt.s94.dev/frameworks/drivers) for any other framework.
|
|
17
17
|
- **Runtime-agnostic** — Node 22+, Deno, Bun, Cloudflare Workers, browsers. Anywhere Web Crypto runs.
|
|
18
|
-
- **Zero runtime dependencies** for core; optional peer deps for adapters (`h3`, `
|
|
18
|
+
- **Zero runtime dependencies** for core; optional peer deps for adapters (`h3`, `elysia`).
|
|
19
19
|
|
|
20
20
|
## Install
|
|
21
21
|
|
|
@@ -55,7 +55,7 @@ Full walkthroughs, recipes, and the complete API reference live at **[unjwt.s94.
|
|
|
55
55
|
- [JWS — signing and verifying →](https://unjwt.s94.dev/jwt/jws)
|
|
56
56
|
- [JWE — encrypting and decrypting →](https://unjwt.s94.dev/jwt/jwe)
|
|
57
57
|
- [JWK — key management →](https://unjwt.s94.dev/jwk)
|
|
58
|
-
- [
|
|
58
|
+
- [Sessions on your framework →](https://unjwt.s94.dev/frameworks)
|
|
59
59
|
- [Examples →](https://unjwt.s94.dev/examples) — authentication, JWKS endpoints, refresh tokens, end-to-end encryption, signed receipts.
|
|
60
60
|
|
|
61
61
|
## Development
|
|
Binary file
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { ExpiresIn, JWEAsymmetricPrivateJWK, JWEAsymmetricPublicJWK, JWEDecryptJWK, JWEDecryptOptions, JWEEncryptJWK, JWEEncryptOptions, JWEHeaderParameters,
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
maxAge?: ExpiresIn;
|
|
1
|
+
import { ExpiresIn, JWEAsymmetricPrivateJWK, JWEAsymmetricPublicJWK, JWEDecryptJWK, JWEDecryptOptions, JWEEncryptJWK, JWEEncryptOptions, JWEHeaderParameters, JWKSet, JWK_HMAC, JWK_oct, JWSAsymmetricPrivateJWK, JWSAsymmetricPublicJWK, JWSHeaderParameters, JWSSignOptions, JWSVerifyJWK, JWSVerifyOptions, JWTClaims, StrictOmit } from "./index.mjs";
|
|
2
|
+
interface ExpirableCookieAttributes {
|
|
3
|
+
expires?: Date;
|
|
4
|
+
maxAge?: number;
|
|
6
5
|
}
|
|
7
6
|
/** Session data shape; `any` values let an `interface` (no index signature) satisfy it. */
|
|
8
7
|
export type SessionRecord = Record<string, any>;
|
|
@@ -31,12 +30,28 @@ export interface SessionManager<T extends SessionRecord = SessionClaims, ConfigM
|
|
|
31
30
|
interface SessionBaseConfig<MaxAge extends ExpiresIn | undefined = ExpiresIn | undefined> {
|
|
32
31
|
/** Sets `exp = iat + maxAge`, requires `exp` on incoming tokens, drives cookie expiry; unset never expires. */
|
|
33
32
|
maxAge?: MaxAge;
|
|
34
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* Reissue the session when it is read, needs `maxAge`.
|
|
35
|
+
*
|
|
36
|
+
* - `true`: every read reissues.
|
|
37
|
+
* - a duration: a read reissues once the token is at least that old, so an active session is
|
|
38
|
+
* re-signed at most once per duration. With `maxAge: "1h"` and `rolling: "15m"`, reads in the
|
|
39
|
+
* first 15 minutes change nothing; the first read after that issues a fresh hour. Must be
|
|
40
|
+
* shorter than `maxAge`.
|
|
41
|
+
*
|
|
42
|
+
* A read that cannot write (a read-only context, a response already sent) does not reissue;
|
|
43
|
+
* the next request, carrying the same token, does.
|
|
44
|
+
*
|
|
45
|
+
* @default undefined — the session expires at `iat + maxAge` however often it is read.
|
|
46
|
+
*/
|
|
35
47
|
rolling?: true | ExpiresIn;
|
|
36
48
|
/** Drives the cookie name and default header; prefer `__Host-`; a trailing `.<digits>` is refused. */
|
|
37
49
|
name?: string;
|
|
38
|
-
/**
|
|
39
|
-
|
|
50
|
+
/**
|
|
51
|
+
* Header carrying a bare or `Bearer` token: a header name, `true` for `x-<name>-session` (cookie prefix stripped), or `false` for none.
|
|
52
|
+
* Left unset, a session with cookies still reads `x-<name>-session`, and so does one with `cookie: false`.
|
|
53
|
+
*/
|
|
54
|
+
sessionHeader?: boolean | string;
|
|
40
55
|
/** Session ID (`jti`) generator. Default `crypto.randomUUID`. */
|
|
41
56
|
generateId?: () => string;
|
|
42
57
|
}
|
|
@@ -44,9 +59,9 @@ interface SessionBaseConfig<MaxAge extends ExpiresIn | undefined = ExpiresIn | u
|
|
|
44
59
|
* Lifecycle hooks; `onRead`, `onExpire` and `onError` are mutually exclusive per incoming token.
|
|
45
60
|
* `session` is the live manager, so a hook can `update()` or `clear()` in place; `oldSession` is a snapshot.
|
|
46
61
|
*/
|
|
47
|
-
interface SessionHooks<TSession, TManager,
|
|
62
|
+
interface SessionHooks<TSession, TManager, TArgs extends Record<string, unknown>, TConfig> {
|
|
48
63
|
/** Fires once per request, when the session is read from its token. */
|
|
49
|
-
onRead?: (args:
|
|
64
|
+
onRead?: (args: TArgs & {
|
|
50
65
|
session: TManager & {
|
|
51
66
|
id: string;
|
|
52
67
|
token: string;
|
|
@@ -54,7 +69,7 @@ interface SessionHooks<TSession, TManager, TCarrier extends Record<string, unkno
|
|
|
54
69
|
config: TConfig;
|
|
55
70
|
}) => void | Promise<void>;
|
|
56
71
|
/** Fires after a token is issued and its cookie written, by `update()` or by `rolling`; a throw rejects without rollback. */
|
|
57
|
-
onUpdate?: (args:
|
|
72
|
+
onUpdate?: (args: TArgs & {
|
|
58
73
|
session: TManager & {
|
|
59
74
|
id: string;
|
|
60
75
|
token: string;
|
|
@@ -65,12 +80,12 @@ interface SessionHooks<TSession, TManager, TCarrier extends Record<string, unkno
|
|
|
65
80
|
config: TConfig;
|
|
66
81
|
}) => void | Promise<void>;
|
|
67
82
|
/** Fires when the session is cleared. */
|
|
68
|
-
onClear?: (args:
|
|
83
|
+
onClear?: (args: TArgs & {
|
|
69
84
|
oldSession: TSession | undefined;
|
|
70
85
|
config: TConfig;
|
|
71
86
|
}) => void | Promise<void>;
|
|
72
87
|
/** Fires when the incoming token is expired. Receives the expired token's decoded claims. */
|
|
73
|
-
onExpire?: (args:
|
|
88
|
+
onExpire?: (args: TArgs & {
|
|
74
89
|
session: {
|
|
75
90
|
id: string | undefined;
|
|
76
91
|
createdAt: number | undefined;
|
|
@@ -81,14 +96,17 @@ interface SessionHooks<TSession, TManager, TCarrier extends Record<string, unkno
|
|
|
81
96
|
config: TConfig;
|
|
82
97
|
}) => void | Promise<void>;
|
|
83
98
|
/** Fires on read-path failures other than expiry, and on write-path issue failures. */
|
|
84
|
-
onError?: (args:
|
|
99
|
+
onError?: (args: TArgs & {
|
|
85
100
|
session: TManager;
|
|
86
101
|
error: any;
|
|
87
102
|
config: TConfig;
|
|
88
103
|
}) => void | Promise<void>;
|
|
89
104
|
}
|
|
90
|
-
/**
|
|
91
|
-
|
|
105
|
+
/** Per-issue overrides for a single `update()` call. */
|
|
106
|
+
export interface SessionIssueOptions {
|
|
107
|
+
/** Lifetime for this issue only, overriding the config's `maxAge`. */
|
|
108
|
+
maxAge?: ExpiresIn;
|
|
109
|
+
}
|
|
92
110
|
/** In-request session state, mirroring the claims of the underlying token. */
|
|
93
111
|
export interface SessionState<T extends SessionRecord, MaxAge extends ExpiresIn | undefined> {
|
|
94
112
|
/** JWT `jti`; `undefined` until the session is persisted. */
|
|
@@ -99,7 +117,72 @@ export interface SessionState<T extends SessionRecord, MaxAge extends ExpiresIn
|
|
|
99
117
|
expiresAt: MaxAge extends ExpiresIn ? number : number | undefined;
|
|
100
118
|
data: SessionData<T>;
|
|
101
119
|
token: string | undefined;
|
|
102
|
-
|
|
120
|
+
}
|
|
121
|
+
/** Session state read back from a token: always carries its `id` and `createdAt`. */
|
|
122
|
+
export type OpenedSessionState<T extends SessionRecord, MaxAge extends ExpiresIn | undefined> = Pick<SessionState<T, MaxAge>, "id" | "createdAt" | "expiresAt" | "data"> & {
|
|
123
|
+
id: string;
|
|
124
|
+
createdAt: number;
|
|
125
|
+
};
|
|
126
|
+
/** One session, defined once from its config and used on any request. */
|
|
127
|
+
export interface SessionDefinition<T extends SessionRecord = SessionClaims, MaxAge extends ExpiresIn | undefined = ExpiresIn | undefined, TContext = unknown> {
|
|
128
|
+
/** Reads the request's session once and returns its manager; later calls on the same request share that state. */
|
|
129
|
+
use: (context: TContext) => Promise<SessionManager<T, MaxAge>>;
|
|
130
|
+
/** The token the request carries (this response's own write first), unopened. */
|
|
131
|
+
token: (context: TContext) => string | undefined;
|
|
132
|
+
/** Mints a token for `data` with a fresh `jti` and `iat`, and `exp` from `maxAge` (`options.maxAge` overrides it); reads and writes nothing. */
|
|
133
|
+
issue: (data?: Partial<SessionData<T>>, options?: SessionIssueOptions) => Promise<string>;
|
|
134
|
+
/** Opens a token into session state, checking `typ`, pinned algorithms, `jti`/`iat` and `exp` when `maxAge` is set; the key-lookup hook receives no request. */
|
|
135
|
+
open: (token: string) => Promise<OpenedSessionState<T, MaxAge>>;
|
|
136
|
+
}
|
|
137
|
+
/** A web-standard request event: reads, and writes while `res` is there. */
|
|
138
|
+
export interface SessionRequestEvent {
|
|
139
|
+
readonly req: Request;
|
|
140
|
+
/** The response being prepared; without it the session is read-only. */
|
|
141
|
+
readonly res?: {
|
|
142
|
+
readonly headers: Headers;
|
|
143
|
+
};
|
|
144
|
+
/** The framework's per-request state, where `sessions` is kept; without it state is keyed on `req`. */
|
|
145
|
+
readonly context?: Record<string, unknown>;
|
|
146
|
+
}
|
|
147
|
+
/** A request by its headers: a `Request` itself, a WebSocket upgrade's request, a peer's `request` with its `context`. */
|
|
148
|
+
export interface SessionRequestHeaders {
|
|
149
|
+
readonly headers: Headers;
|
|
150
|
+
/** The response being prepared, such as the headers a WebSocket `upgrade` returns; without it the session is read-only. */
|
|
151
|
+
readonly res?: {
|
|
152
|
+
readonly headers: Headers;
|
|
153
|
+
};
|
|
154
|
+
/** Per-request state shared with the framework; without it state is keyed on this object. */
|
|
155
|
+
readonly context?: Record<string, unknown>;
|
|
156
|
+
}
|
|
157
|
+
/** A token handed in directly; sending a re-issued one is the caller's job. */
|
|
158
|
+
export interface SessionToken {
|
|
159
|
+
token: string | undefined;
|
|
160
|
+
}
|
|
161
|
+
/** What `use()` takes: a request event, a request by its headers, or a token in hand. */
|
|
162
|
+
export type SessionContext = SessionRequestEvent | SessionRequestHeaders | SessionToken;
|
|
163
|
+
/** The cookies of one request and its response, as a framework exposes them. */
|
|
164
|
+
export interface CookieJar<A extends ExpirableCookieAttributes = ExpirableCookieAttributes> {
|
|
165
|
+
/** The request's cookie. */
|
|
166
|
+
read(name: string): string | undefined;
|
|
167
|
+
/** The value this response already assigned to `name` (`""` once deleted), when the framework exposes it. */
|
|
168
|
+
written?(name: string): string | undefined;
|
|
169
|
+
/** Absent when this context cannot take a `Set-Cookie` (a headers-only event). */
|
|
170
|
+
write?(name: string, value: string, attrs: A): void;
|
|
171
|
+
}
|
|
172
|
+
/** The primitives a framework supplies; the session core builds the cookie and header transports over them. */
|
|
173
|
+
export interface SessionDriver<TContext> {
|
|
174
|
+
/** Message prefix for every error the adapter emits. */
|
|
175
|
+
prefix: string;
|
|
176
|
+
/** The per-request record session state and the session-name registry live in. */
|
|
177
|
+
state(context: TContext): Record<string, unknown>;
|
|
178
|
+
header(context: TContext, name: string): string | null | undefined;
|
|
179
|
+
cookies(context: TContext): CookieJar;
|
|
180
|
+
/** Platform signal that the response can still change (Node `headersSent`); absent = trust the jar. */
|
|
181
|
+
writable?(context: TContext): boolean;
|
|
182
|
+
/** A token the context carries directly, when it does: no cookie or header is consulted. */
|
|
183
|
+
handed?(context: TContext): {
|
|
184
|
+
token: string | undefined;
|
|
185
|
+
} | undefined;
|
|
103
186
|
}
|
|
104
187
|
/** HS* `oct` JWK, `CryptoKey` (requires pinned `alg`), or a pair: private signs, public JWK/list/set verifies. */
|
|
105
188
|
type JWSSessionKey = CryptoKey | JWK_oct<JWK_HMAC> | {
|
|
@@ -127,10 +210,10 @@ export interface JWSSessionConfig<MaxAge extends ExpiresIn | undefined = Expires
|
|
|
127
210
|
jws?: JWSSessionOptions;
|
|
128
211
|
}
|
|
129
212
|
/** JWS session hooks, plus the verify key lookup. */
|
|
130
|
-
export interface JWSSessionHooks<TSession, TManager,
|
|
213
|
+
export interface JWSSessionHooks<TSession, TManager, TArgs extends Record<string, unknown>, TConfig> extends SessionHooks<TSession, TManager, TArgs, TConfig> {
|
|
131
214
|
/** Resolve the verification key(s) for the incoming token from its protected header (e.g. by `kid`). */
|
|
132
|
-
onVerifyKeyLookup?: (args:
|
|
133
|
-
header:
|
|
215
|
+
onVerifyKeyLookup?: (args: Partial<TArgs> & {
|
|
216
|
+
header: JWSHeaderParameters;
|
|
134
217
|
config: TConfig;
|
|
135
218
|
}) => JWKSet | JWSVerifyJWK | Promise<JWKSet | JWSVerifyJWK>;
|
|
136
219
|
}
|
|
@@ -160,9 +243,9 @@ export interface JWESessionConfig<MaxAge extends ExpiresIn | undefined = Expires
|
|
|
160
243
|
jwe?: JWESessionOptions;
|
|
161
244
|
}
|
|
162
245
|
/** JWE session hooks, plus the unseal key lookup. */
|
|
163
|
-
export interface JWESessionHooks<TSession, TManager,
|
|
246
|
+
export interface JWESessionHooks<TSession, TManager, TArgs extends Record<string, unknown>, TConfig> extends SessionHooks<TSession, TManager, TArgs, TConfig> {
|
|
164
247
|
/** Resolve the decryption key for the incoming token from its protected header (e.g. by `kid`). */
|
|
165
|
-
onUnsealKeyLookup?: (args:
|
|
248
|
+
onUnsealKeyLookup?: (args: Partial<TArgs> & {
|
|
166
249
|
header: JWEHeaderParameters;
|
|
167
250
|
config: TConfig;
|
|
168
251
|
}) => JWEDecryptJWK | Promise<JWEDecryptJWK>;
|