@lacspace/jwt 1.0.2 → 1.2.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/README.md CHANGED
@@ -77,6 +77,44 @@ csrfToken(); // CSRF token
77
77
  | [`@lacspace/headers`](https://www.npmjs.com/package/@lacspace/headers) | Secure headers / CSP |
78
78
  | [`@lacspace/redact`](https://www.npmjs.com/package/@lacspace/redact) | Log redaction |
79
79
 
80
+ ## New in 1.1 — framework adapters & cookies
81
+
82
+ ```ts
83
+ import { authenticate, extractBearer, toAuthCookie, clearAuthCookie, expressJwt } from "@lacspace/jwt";
84
+
85
+ // Verify straight from a Fetch Request / Next route / Node req (Bearer or cookie)
86
+ const payload = await authenticate(req, secret); // throws JwtError on failure
87
+ const payload2 = await authenticate(req, secret, { cookieName: "session" });
88
+
89
+ // Hardened auth cookie (HttpOnly + Secure + SameSite by default)
90
+ res.headers.set("Set-Cookie", toAuthCookie(token, { name: "session", maxAge: 3600 }));
91
+ res.headers.set("Set-Cookie", clearAuthCookie({ name: "session" })); // logout
92
+
93
+ // Express: verifies the token → req.user, else 401
94
+ app.use(expressJwt(secret));
95
+ ```
96
+
97
+ ## New in 1.2 — RS256/ES256, JWKS & refresh tokens
98
+
99
+ ```ts
100
+ import { sign, verify, importPkcs8, importSpki, createRemoteJWKS, issueTokenPair, rotateRefreshToken } from "@lacspace/jwt";
101
+
102
+ // Asymmetric: sign with a private key, verify with the public key (clients can't forge)
103
+ const priv = await importPkcs8(PRIVATE_PEM, "ES256");
104
+ const token = await sign({ sub: "u1" }, priv, { algorithm: "ES256", keyId: "k1" });
105
+
106
+ // Consume 3rd-party OIDC tokens (Auth0/Cognito/Google) via cached JWKS
107
+ const jwks = createRemoteJWKS("https://issuer/.well-known/jwks.json");
108
+ const claims = await verify(idToken, jwks, { algorithms: ["RS256"] });
109
+
110
+ // Refresh-token flow with rotation + reuse detection
111
+ const { accessToken, refreshToken, refreshJti } = await issueTokenPair({ sub: "u1" }, secret);
112
+ const next = await rotateRefreshToken(refreshToken, secret, { isUsed: (jti) => store.has(jti) });
113
+ store.add(next.usedJti); // mark the old one spent
114
+ ```
115
+
116
+ Plus `audience: string[]` matching and `importJwk()`.
117
+
80
118
  ## Licensing
81
119
 
82
120
  This package is **free** under the **[Lacspace Free Licence](https://lacspace.com/licenses/lacspace-free-1.0)** — MIT-equivalent freedoms. Use it in personal and commercial projects at no cost; just keep the notice.
package/dist/index.cjs CHANGED
@@ -6,7 +6,13 @@ var crypto = require('@lacspace/crypto');
6
6
  var ALG_HASH = {
7
7
  HS256: "SHA-256",
8
8
  HS384: "SHA-384",
9
- HS512: "SHA-512"
9
+ HS512: "SHA-512",
10
+ RS256: "SHA-256",
11
+ RS384: "SHA-384",
12
+ RS512: "SHA-512",
13
+ ES256: "SHA-256",
14
+ ES384: "SHA-384",
15
+ ES512: "SHA-512"
10
16
  };
11
17
  var JwtError = class extends Error {
12
18
  constructor(message, code) {
@@ -15,6 +21,41 @@ var JwtError = class extends Error {
15
21
  this.name = "JwtError";
16
22
  }
17
23
  };
24
+ function getSubtle() {
25
+ const c = globalThis.crypto;
26
+ if (!c || !c.subtle) throw new JwtError("Web Crypto API unavailable for asymmetric JWT", "algorithm");
27
+ return c.subtle;
28
+ }
29
+ function b64ToBytes(b64) {
30
+ if (typeof atob !== "undefined") {
31
+ const bin = atob(b64);
32
+ const out = new Uint8Array(bin.length);
33
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
34
+ return out;
35
+ }
36
+ return new Uint8Array(Buffer.from(b64, "base64"));
37
+ }
38
+ function subtleSignAlgo(alg) {
39
+ if (alg.startsWith("RS")) return { name: "RSASSA-PKCS1-v1_5" };
40
+ if (alg.startsWith("ES")) return { name: "ECDSA", hash: ALG_HASH[alg] };
41
+ throw new JwtError(`not an asymmetric algorithm: ${alg}`, "algorithm");
42
+ }
43
+ function subtleImportAlgo(alg) {
44
+ if (alg.startsWith("RS")) return { name: "RSASSA-PKCS1-v1_5", hash: ALG_HASH[alg] };
45
+ const curve = alg === "ES256" ? "P-256" : alg === "ES384" ? "P-384" : "P-521";
46
+ return { name: "ECDSA", namedCurve: curve };
47
+ }
48
+ async function signBytes(alg, key, input) {
49
+ if (alg.startsWith("HS")) {
50
+ if (typeof key !== "string" && !(key instanceof Uint8Array))
51
+ throw new JwtError("HMAC algorithms need a string/bytes secret", "algorithm");
52
+ return crypto.hmac(key, input, ALG_HASH[alg]);
53
+ }
54
+ if (!(typeof CryptoKey !== "undefined" && key instanceof CryptoKey))
55
+ throw new JwtError(`${alg} needs a CryptoKey \u2014 use importPkcs8 / importJwk`, "algorithm");
56
+ const sig = await getSubtle().sign(subtleSignAlgo(alg), key, enc.encode(input));
57
+ return new Uint8Array(sig);
58
+ }
18
59
  var enc = new TextEncoder();
19
60
  var dec = new TextDecoder();
20
61
  function b64urlJson(obj) {
@@ -31,8 +72,10 @@ async function sign(payload, secret, opts = {}) {
31
72
  if (opts.issuer) body.iss = opts.issuer;
32
73
  if (opts.audience) body.aud = opts.audience;
33
74
  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]);
75
+ const header = { alg, typ: "JWT" };
76
+ if (opts.keyId) header.kid = opts.keyId;
77
+ const signingInput = `${b64urlJson(header)}.${b64urlJson(body)}`;
78
+ const sig = await signBytes(alg, secret, signingInput);
36
79
  return `${signingInput}.${crypto.toBase64url(sig)}`;
37
80
  }
38
81
  function decode(token) {
@@ -43,7 +86,7 @@ function decode(token) {
43
86
  payload: JSON.parse(dec.decode(crypto.fromBase64url(parts[1])))
44
87
  };
45
88
  }
46
- async function verify(token, secret, opts = {}) {
89
+ async function verify(token, key, opts = {}) {
47
90
  const parts = token.split(".");
48
91
  if (parts.length !== 3) throw new JwtError("malformed token", "malformed");
49
92
  const [h, p, s] = parts;
@@ -57,9 +100,25 @@ async function verify(token, secret, opts = {}) {
57
100
  if (!alg || !(alg in ALG_HASH)) throw new JwtError(`unsupported algorithm ${alg}`, "algorithm");
58
101
  if (opts.algorithms && !opts.algorithms.includes(alg))
59
102
  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");
103
+ const resolved = typeof key === "function" ? await key({ alg, kid: header.kid }) : key;
104
+ const signingInput = `${h}.${p}`;
105
+ const sig = crypto.fromBase64url(s);
106
+ let valid;
107
+ if (alg.startsWith("HS")) {
108
+ if (typeof resolved !== "string" && !(resolved instanceof Uint8Array))
109
+ throw new JwtError("HMAC algorithms need a string/bytes secret", "algorithm");
110
+ valid = crypto.constantTimeEqual(await crypto.hmac(resolved, signingInput, ALG_HASH[alg]), sig);
111
+ } else {
112
+ if (!(typeof CryptoKey !== "undefined" && resolved instanceof CryptoKey))
113
+ throw new JwtError(`${alg} needs a CryptoKey \u2014 use importSpki / importJwk / createRemoteJWKS`, "algorithm");
114
+ valid = await getSubtle().verify(
115
+ subtleSignAlgo(alg),
116
+ resolved,
117
+ sig,
118
+ enc.encode(signingInput)
119
+ );
120
+ }
121
+ if (!valid) throw new JwtError("signature verification failed", "signature");
63
122
  let payload;
64
123
  try {
65
124
  payload = JSON.parse(dec.decode(crypto.fromBase64url(p)));
@@ -75,23 +134,158 @@ async function verify(token, secret, opts = {}) {
75
134
  if (opts.issuer && payload.iss !== opts.issuer)
76
135
  throw new JwtError("issuer mismatch", "issuer");
77
136
  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");
137
+ const want = Array.isArray(opts.audience) ? opts.audience : [opts.audience];
138
+ const have = Array.isArray(payload.aud) ? payload.aud : payload.aud ? [payload.aud] : [];
139
+ if (!want.some((a) => have.includes(a))) throw new JwtError("audience mismatch", "audience");
80
140
  }
81
141
  return payload;
82
142
  }
143
+ function pemToDer(pem) {
144
+ const b64 = pem.replace(/-----BEGIN [^-]+-----/, "").replace(/-----END [^-]+-----/, "").replace(/\s+/g, "");
145
+ return b64ToBytes(b64);
146
+ }
147
+ function importPkcs8(pem, alg) {
148
+ return getSubtle().importKey("pkcs8", pemToDer(pem), subtleImportAlgo(alg), false, ["sign"]);
149
+ }
150
+ function importSpki(pem, alg) {
151
+ return getSubtle().importKey("spki", pemToDer(pem), subtleImportAlgo(alg), false, ["verify"]);
152
+ }
153
+ function importJwk(jwk, alg) {
154
+ const usage = jwk.d ? ["sign"] : ["verify"];
155
+ return getSubtle().importKey("jwk", jwk, subtleImportAlgo(alg), false, usage);
156
+ }
157
+ function createRemoteJWKS(url, opts = {}) {
158
+ const ttl = opts.cacheMaxAgeMs ?? 6e5;
159
+ const f = opts.fetch ?? (typeof fetch !== "undefined" ? fetch.bind(globalThis) : void 0);
160
+ if (!f) throw new JwtError("no global fetch \u2014 pass opts.fetch", "signature");
161
+ let cache = null;
162
+ const imported = /* @__PURE__ */ new Map();
163
+ return async ({ alg, kid }) => {
164
+ if (!cache || Date.now() - cache.at > ttl) {
165
+ const res = await f(url);
166
+ if (!res.ok) throw new JwtError(`JWKS fetch failed: ${res.status}`, "signature");
167
+ const data = await res.json();
168
+ cache = { keys: data.keys ?? [], at: Date.now() };
169
+ imported.clear();
170
+ }
171
+ const jwk = kid ? cache.keys.find((k) => k.kid === kid) : cache.keys[0];
172
+ if (!jwk) throw new JwtError(`no JWK found for kid "${kid}"`, "signature");
173
+ const keyAlg = jwk.alg ?? alg;
174
+ const cacheKey = `${keyAlg}:${kid ?? ""}`;
175
+ let ck = imported.get(cacheKey);
176
+ if (!ck) {
177
+ ck = await importJwk(jwk, keyAlg);
178
+ imported.set(cacheKey, ck);
179
+ }
180
+ return ck;
181
+ };
182
+ }
183
+ async function issueTokenPair(payload, secret, opts = {}) {
184
+ const subject = opts.subject ?? payload.sub;
185
+ const common = { algorithm: opts.algorithm, issuer: opts.issuer, audience: opts.audience, subject, keyId: opts.keyId };
186
+ const accessToken = await sign(payload, secret, { ...common, expiresIn: opts.accessTtl ?? 900 });
187
+ const refreshJti = randomToken(16);
188
+ const refreshToken = await sign({ typ: "refresh", jti: refreshJti }, secret, {
189
+ ...common,
190
+ expiresIn: opts.refreshTtl ?? 2592e3
191
+ });
192
+ return { accessToken, refreshToken, refreshJti };
193
+ }
194
+ async function verifyRefreshToken(token, key, opts = {}) {
195
+ const payload = await verify(token, key, opts);
196
+ if (payload.typ !== "refresh") throw new JwtError("not a refresh token", "malformed");
197
+ return payload;
198
+ }
199
+ async function rotateRefreshToken(refreshToken, secret, opts = {}) {
200
+ const claims = await verifyRefreshToken(refreshToken, secret, opts);
201
+ const jti = claims.jti;
202
+ if (jti && opts.isUsed && await opts.isUsed(jti))
203
+ throw new JwtError("refresh token reuse detected", "reuse");
204
+ const pair = await issueTokenPair(opts.payload ?? { sub: claims.sub }, secret, {
205
+ ...opts,
206
+ subject: opts.subject ?? claims.sub
207
+ });
208
+ return { ...pair, usedJti: jti };
209
+ }
83
210
  function randomToken(bytes = 32) {
84
211
  return crypto.toBase64url(crypto.randomBytes(bytes));
85
212
  }
86
213
  function csrfToken() {
87
214
  return randomToken(32);
88
215
  }
216
+ function readHeader(src, name) {
217
+ const lower = name.toLowerCase();
218
+ const h = src.headers ?? src;
219
+ if (h && typeof h.get === "function") return h.get(name) ?? void 0;
220
+ const rec = h;
221
+ const v = rec[name] ?? rec[lower];
222
+ return Array.isArray(v) ? v[0] : v ?? void 0;
223
+ }
224
+ function extractBearer(src, opts = {}) {
225
+ const auth = readHeader(src, "authorization");
226
+ if (auth) {
227
+ const m = /^Bearer\s+(.+)$/i.exec(auth.trim());
228
+ if (m) return m[1];
229
+ }
230
+ if (opts.cookieName) {
231
+ const cookie = readHeader(src, "cookie");
232
+ if (cookie) {
233
+ const found = cookie.split(/;\s*/).find((c) => c.startsWith(`${opts.cookieName}=`));
234
+ if (found) return decodeURIComponent(found.slice(opts.cookieName.length + 1));
235
+ }
236
+ }
237
+ return void 0;
238
+ }
239
+ async function authenticate(src, secret, opts = {}) {
240
+ const token = extractBearer(src, { cookieName: opts.cookieName });
241
+ if (!token) throw new JwtError("no bearer token", "malformed");
242
+ return verify(token, secret, opts);
243
+ }
244
+ function toAuthCookie(token, opts = {}) {
245
+ const name = opts.name ?? "token";
246
+ const parts = [`${name}=${encodeURIComponent(token)}`, `Path=${opts.path ?? "/"}`, `SameSite=${opts.sameSite ?? "Lax"}`];
247
+ if (opts.maxAge !== void 0) parts.push(`Max-Age=${opts.maxAge}`);
248
+ if (opts.domain) parts.push(`Domain=${opts.domain}`);
249
+ if (opts.httpOnly !== false) parts.push("HttpOnly");
250
+ if (opts.secure !== false) parts.push("Secure");
251
+ return parts.join("; ");
252
+ }
253
+ function clearAuthCookie(opts = {}) {
254
+ const name = opts.name ?? "token";
255
+ const parts = [`${name}=`, `Path=${opts.path ?? "/"}`, "Max-Age=0"];
256
+ if (opts.domain) parts.push(`Domain=${opts.domain}`);
257
+ return parts.join("; ");
258
+ }
259
+ function expressJwt(secret, opts = {}) {
260
+ const userKey = opts.userKey ?? "user";
261
+ return async (req, res, next) => {
262
+ try {
263
+ req[userKey] = await authenticate(req, secret, opts);
264
+ next();
265
+ } catch (err) {
266
+ const code = err instanceof JwtError ? err.code : "malformed";
267
+ res.status(401).json({ error: "unauthorized", code });
268
+ }
269
+ };
270
+ }
89
271
 
90
272
  exports.JwtError = JwtError;
273
+ exports.authenticate = authenticate;
274
+ exports.clearAuthCookie = clearAuthCookie;
275
+ exports.createRemoteJWKS = createRemoteJWKS;
91
276
  exports.csrfToken = csrfToken;
92
277
  exports.decode = decode;
278
+ exports.expressJwt = expressJwt;
279
+ exports.extractBearer = extractBearer;
280
+ exports.importJwk = importJwk;
281
+ exports.importPkcs8 = importPkcs8;
282
+ exports.importSpki = importSpki;
283
+ exports.issueTokenPair = issueTokenPair;
93
284
  exports.randomToken = randomToken;
285
+ exports.rotateRefreshToken = rotateRefreshToken;
94
286
  exports.sign = sign;
287
+ exports.toAuthCookie = toAuthCookie;
95
288
  exports.verify = verify;
289
+ exports.verifyRefreshToken = verifyRefreshToken;
96
290
  //# sourceMappingURL=index.cjs.map
97
291
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"names":["toBase64url","hmac","fromBase64url","constantTimeEqual","randomBytes"],"mappings":";;;;;AAcA,IAAM,QAAA,GAA6C;AAAA,EACjD,KAAA,EAAO,SAAA;AAAA,EACP,KAAA,EAAO,SAAA;AAAA,EACP,KAAA,EAAO;AACT,CAAA;AAEO,IAAM,QAAA,GAAN,cAAuB,KAAA,CAAM;AAAA,EAClC,WAAA,CACE,SACO,IAAA,EAQP;AACA,IAAA,KAAA,CAAM,OAAO,CAAA;AATN,IAAA,IAAA,CAAA,IAAA,GAAA,IAAA;AAUP,IAAA,IAAA,CAAK,IAAA,GAAO,UAAA;AAAA,EACd;AACF;AAsBA,IAAM,GAAA,GAAM,IAAI,WAAA,EAAY;AAC5B,IAAM,GAAA,GAAM,IAAI,WAAA,EAAY;AAE5B,SAAS,WAAW,GAAA,EAAsB;AACxC,EAAA,OAAOA,mBAAY,GAAA,CAAI,MAAA,CAAO,KAAK,SAAA,CAAU,GAAG,CAAC,CAAC,CAAA;AACpD;AAEA,SAAS,MAAA,GAAiB;AACxB,EAAA,OAAO,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,GAAA,KAAQ,GAAI,CAAA;AACrC;AAGA,eAAsB,IAAA,CACpB,OAAA,EACA,MAAA,EACA,IAAA,GAAoB,EAAC,EACJ;AACjB,EAAA,MAAM,GAAA,GAAM,KAAK,SAAA,IAAa,OAAA;AAC9B,EAAA,MAAM,MAAM,MAAA,EAAO;AACnB,EAAA,MAAM,IAAA,GAAmB,EAAE,GAAA,EAAK,GAAG,OAAA,EAAQ;AAC3C,EAAA,IAAI,KAAK,SAAA,KAAc,MAAA,EAAW,IAAA,CAAK,GAAA,GAAM,MAAM,IAAA,CAAK,SAAA;AACxD,EAAA,IAAI,IAAA,CAAK,MAAA,EAAQ,IAAA,CAAK,GAAA,GAAM,IAAA,CAAK,MAAA;AACjC,EAAA,IAAI,IAAA,CAAK,QAAA,EAAU,IAAA,CAAK,GAAA,GAAM,IAAA,CAAK,QAAA;AACnC,EAAA,IAAI,IAAA,CAAK,OAAA,EAAS,IAAA,CAAK,GAAA,GAAM,IAAA,CAAK,OAAA;AAElC,EAAA,MAAM,YAAA,GAAe,CAAA,EAAG,UAAA,CAAW,EAAE,GAAA,EAAK,GAAA,EAAK,KAAA,EAAO,CAAC,CAAA,CAAA,EAAI,UAAA,CAAW,IAAI,CAAC,CAAA,CAAA;AAC3E,EAAA,MAAM,MAAM,MAAMC,WAAA,CAAK,QAAQ,YAAA,EAAc,QAAA,CAAS,GAAG,CAAC,CAAA;AAC1D,EAAA,OAAO,CAAA,EAAG,YAAY,CAAA,CAAA,EAAID,kBAAA,CAAY,GAAG,CAAC,CAAA,CAAA;AAC5C;AAWO,SAAS,OAAO,KAAA,EAAyE;AAC9F,EAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,KAAA,CAAM,GAAG,CAAA;AAC7B,EAAA,IAAI,MAAM,MAAA,KAAW,CAAA,QAAS,IAAI,QAAA,CAAS,mBAAmB,WAAW,CAAA;AACzE,EAAA,OAAO;AAAA,IACL,MAAA,EAAQ,IAAA,CAAK,KAAA,CAAM,GAAA,CAAI,MAAA,CAAOE,qBAAc,KAAA,CAAM,CAAC,CAAE,CAAC,CAAC,CAAA;AAAA,IACvD,OAAA,EAAS,IAAA,CAAK,KAAA,CAAM,GAAA,CAAI,MAAA,CAAOA,qBAAc,KAAA,CAAM,CAAC,CAAE,CAAC,CAAC;AAAA,GAC1D;AACF;AAGA,eAAsB,MAAA,CACpB,KAAA,EACA,MAAA,EACA,IAAA,GAAsB,EAAC,EACX;AACZ,EAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,KAAA,CAAM,GAAG,CAAA;AAC7B,EAAA,IAAI,MAAM,MAAA,KAAW,CAAA,QAAS,IAAI,QAAA,CAAS,mBAAmB,WAAW,CAAA;AACzE,EAAA,MAAM,CAAC,CAAA,EAAG,CAAA,EAAG,CAAC,CAAA,GAAI,KAAA;AAElB,EAAA,IAAI,MAAA;AACJ,EAAA,IAAI;AACF,IAAA,MAAA,GAAS,KAAK,KAAA,CAAM,GAAA,CAAI,OAAOA,oBAAA,CAAc,CAAC,CAAC,CAAC,CAAA;AAAA,EAClD,CAAA,CAAA,MAAQ;AACN,IAAA,MAAM,IAAI,QAAA,CAAS,kBAAA,EAAoB,WAAW,CAAA;AAAA,EACpD;AACA,EAAA,MAAM,MAAM,MAAA,CAAO,GAAA;AACnB,EAAA,IAAI,CAAC,GAAA,IAAO,EAAE,GAAA,IAAO,QAAA,CAAA,EAAW,MAAM,IAAI,QAAA,CAAS,CAAA,sBAAA,EAAyB,GAAG,CAAA,CAAA,EAAI,WAAW,CAAA;AAC9F,EAAA,IAAI,KAAK,UAAA,IAAc,CAAC,IAAA,CAAK,UAAA,CAAW,SAAS,GAAG,CAAA;AAClD,IAAA,MAAM,IAAI,QAAA,CAAS,CAAA,UAAA,EAAa,GAAG,gBAAgB,WAAW,CAAA;AAEhE,EAAA,MAAM,QAAA,GAAW,MAAMD,WAAA,CAAK,MAAA,EAAQ,CAAA,EAAG,CAAC,CAAA,CAAA,EAAI,CAAC,CAAA,CAAA,EAAI,QAAA,CAAS,GAAG,CAAC,CAAA;AAC9D,EAAA,IAAI,CAACE,wBAAA,CAAkB,QAAA,EAAUD,oBAAA,CAAc,CAAC,CAAC,CAAA;AAC/C,IAAA,MAAM,IAAI,QAAA,CAAS,+BAAA,EAAiC,WAAW,CAAA;AAEjE,EAAA,IAAI,OAAA;AACJ,EAAA,IAAI;AACF,IAAA,OAAA,GAAU,KAAK,KAAA,CAAM,GAAA,CAAI,OAAOA,oBAAA,CAAc,CAAC,CAAC,CAAC,CAAA;AAAA,EACnD,CAAA,CAAA,MAAQ;AACN,IAAA,MAAM,IAAI,QAAA,CAAS,mBAAA,EAAqB,WAAW,CAAA;AAAA,EACrD;AAEA,EAAA,MAAM,MAAM,MAAA,EAAO;AACnB,EAAA,MAAM,IAAA,GAAO,KAAK,cAAA,IAAkB,CAAA;AACpC,EAAA,IAAI,OAAA,CAAQ,GAAA,KAAQ,MAAA,IAAa,GAAA,GAAM,QAAQ,GAAA,GAAM,IAAA;AACnD,IAAA,MAAM,IAAI,QAAA,CAAS,eAAA,EAAiB,SAAS,CAAA;AAC/C,EAAA,IAAI,OAAA,CAAQ,GAAA,KAAQ,MAAA,IAAa,GAAA,GAAM,OAAO,OAAA,CAAQ,GAAA;AACpD,IAAA,MAAM,IAAI,QAAA,CAAS,sBAAA,EAAwB,YAAY,CAAA;AACzD,EAAA,IAAI,IAAA,CAAK,MAAA,IAAU,OAAA,CAAQ,GAAA,KAAQ,IAAA,CAAK,MAAA;AACtC,IAAA,MAAM,IAAI,QAAA,CAAS,iBAAA,EAAmB,QAAQ,CAAA;AAChD,EAAA,IAAI,KAAK,QAAA,EAAU;AACjB,IAAA,MAAM,GAAA,GAAM,KAAA,CAAM,OAAA,CAAQ,OAAA,CAAQ,GAAG,CAAA,GAAI,OAAA,CAAQ,GAAA,GAAM,OAAA,CAAQ,GAAA,GAAM,CAAC,OAAA,CAAQ,GAAG,IAAI,EAAC;AACtF,IAAA,IAAI,CAAC,GAAA,CAAI,QAAA,CAAS,IAAA,CAAK,QAAQ,GAAG,MAAM,IAAI,QAAA,CAAS,mBAAA,EAAqB,UAAU,CAAA;AAAA,EACtF;AACA,EAAA,OAAO,OAAA;AACT;AAKO,SAAS,WAAA,CAAY,QAAQ,EAAA,EAAY;AAC9C,EAAA,OAAOF,kBAAA,CAAYI,kBAAA,CAAY,KAAK,CAAC,CAAA;AACvC;AAGO,SAAS,SAAA,GAAoB;AAClC,EAAA,OAAO,YAAY,EAAE,CAAA;AACvB","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"]}
1
+ {"version":3,"sources":["../src/index.ts"],"names":["hmac","toBase64url","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 =\n | \"HS256\" | \"HS384\" | \"HS512\"\n | \"RS256\" | \"RS384\" | \"RS512\"\n | \"ES256\" | \"ES384\" | \"ES512\";\n\nconst ALG_HASH: Record<Algorithm, HashAlgorithm> = {\n HS256: \"SHA-256\", HS384: \"SHA-384\", HS512: \"SHA-512\",\n RS256: \"SHA-256\", RS384: \"SHA-384\", RS512: \"SHA-512\",\n ES256: \"SHA-256\", ES384: \"SHA-384\", ES512: \"SHA-512\",\n};\n\n/** A signing/verification key: an HMAC secret, or a Web Crypto key for RS and ES. */\nexport type SigningKey = string | Uint8Array | CryptoKey;\n/** Resolve a verification key from the token header (e.g. by `kid`). */\nexport type KeyResolver = (header: { alg: Algorithm; kid?: string }) => SigningKey | Promise<SigningKey>;\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 | \"reuse\",\n ) {\n super(message);\n this.name = \"JwtError\";\n }\n}\n\nfunction getSubtle(): SubtleCrypto {\n const c = (globalThis as { crypto?: Crypto }).crypto;\n if (!c || !c.subtle) throw new JwtError(\"Web Crypto API unavailable for asymmetric JWT\", \"algorithm\");\n return c.subtle;\n}\n\nfunction b64ToBytes(b64: string): Uint8Array {\n if (typeof atob !== \"undefined\") {\n const bin = atob(b64);\n const out = new Uint8Array(bin.length);\n for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);\n return out;\n }\n return new Uint8Array(Buffer.from(b64, \"base64\"));\n}\n\nfunction subtleSignAlgo(alg: Algorithm): AlgorithmIdentifier | EcdsaParams {\n if (alg.startsWith(\"RS\")) return { name: \"RSASSA-PKCS1-v1_5\" };\n if (alg.startsWith(\"ES\")) return { name: \"ECDSA\", hash: ALG_HASH[alg] };\n throw new JwtError(`not an asymmetric algorithm: ${alg}`, \"algorithm\");\n}\n\nfunction subtleImportAlgo(alg: Algorithm): RsaHashedImportParams | EcKeyImportParams {\n if (alg.startsWith(\"RS\")) return { name: \"RSASSA-PKCS1-v1_5\", hash: ALG_HASH[alg] };\n const curve = alg === \"ES256\" ? \"P-256\" : alg === \"ES384\" ? \"P-384\" : \"P-521\";\n return { name: \"ECDSA\", namedCurve: curve };\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 /** `kid` header — which key signed this (for rotation / JWKS). */\n keyId?: string;\n}\n\nasync function signBytes(alg: Algorithm, key: SigningKey, input: string): Promise<Uint8Array> {\n if (alg.startsWith(\"HS\")) {\n if (typeof key !== \"string\" && !(key instanceof Uint8Array))\n throw new JwtError(\"HMAC algorithms need a string/bytes secret\", \"algorithm\");\n return hmac(key, input, ALG_HASH[alg]);\n }\n if (!(typeof CryptoKey !== \"undefined\" && key instanceof CryptoKey))\n throw new JwtError(`${alg} needs a CryptoKey — use importPkcs8 / importJwk`, \"algorithm\");\n const sig = await getSubtle().sign(subtleSignAlgo(alg), key, enc.encode(input) as unknown as BufferSource);\n return new Uint8Array(sig);\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. Pass an HMAC secret (HS*) or a Web Crypto private key (RS and ES). */\nexport async function sign(\n payload: JwtPayload,\n secret: SigningKey,\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 header: Record<string, unknown> = { alg, typ: \"JWT\" };\n if (opts.keyId) header.kid = opts.keyId;\n const signingInput = `${b64urlJson(header)}.${b64urlJson(body)}`;\n const sig = await signBytes(alg, secret, signingInput);\n return `${signingInput}.${toBase64url(sig)}`;\n}\n\nexport interface VerifyOptions {\n algorithms?: Algorithm[];\n issuer?: string;\n audience?: string | 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/**\n * Verify a JWT's signature and claims. Throws {@link JwtError} on any failure.\n * `key` is an HMAC secret, a Web Crypto public key (RS and ES), or a resolver\n * (e.g. from {@link createRemoteJWKS}) that returns the key for the token header.\n */\nexport async function verify<T extends JwtPayload = JwtPayload>(\n token: string,\n key: SigningKey | KeyResolver,\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; kid?: string };\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 resolved = typeof key === \"function\" ? await key({ alg, kid: header.kid }) : key;\n\n const signingInput = `${h}.${p}`;\n const sig = fromBase64url(s);\n let valid: boolean;\n if (alg.startsWith(\"HS\")) {\n if (typeof resolved !== \"string\" && !(resolved instanceof Uint8Array))\n throw new JwtError(\"HMAC algorithms need a string/bytes secret\", \"algorithm\");\n valid = constantTimeEqual(await hmac(resolved, signingInput, ALG_HASH[alg]), sig);\n } else {\n if (!(typeof CryptoKey !== \"undefined\" && resolved instanceof CryptoKey))\n throw new JwtError(`${alg} needs a CryptoKey — use importSpki / importJwk / createRemoteJWKS`, \"algorithm\");\n valid = await getSubtle().verify(\n subtleSignAlgo(alg),\n resolved,\n sig as unknown as BufferSource,\n enc.encode(signingInput) as unknown as BufferSource,\n );\n }\n if (!valid) 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 want = Array.isArray(opts.audience) ? opts.audience : [opts.audience];\n const have = Array.isArray(payload.aud) ? payload.aud : payload.aud ? [payload.aud] : [];\n if (!want.some((a) => have.includes(a))) throw new JwtError(\"audience mismatch\", \"audience\");\n }\n return payload;\n}\n\n/* ------------------------------ asymmetric keys & JWKS ------------------------------ */\n\nfunction pemToDer(pem: string): Uint8Array {\n const b64 = pem.replace(/-----BEGIN [^-]+-----/, \"\").replace(/-----END [^-]+-----/, \"\").replace(/\\s+/g, \"\");\n return b64ToBytes(b64);\n}\n\n/** Import a PKCS#8 PEM private key for signing (RS and ES). */\nexport function importPkcs8(pem: string, alg: Algorithm): Promise<CryptoKey> {\n return getSubtle().importKey(\"pkcs8\", pemToDer(pem) as unknown as BufferSource, subtleImportAlgo(alg), false, [\"sign\"]);\n}\n\n/** Import an SPKI PEM public key for verification (RS and ES). */\nexport function importSpki(pem: string, alg: Algorithm): Promise<CryptoKey> {\n return getSubtle().importKey(\"spki\", pemToDer(pem) as unknown as BufferSource, subtleImportAlgo(alg), false, [\"verify\"]);\n}\n\n/** Import a JWK (public or private) for verify/sign. */\nexport function importJwk(jwk: JsonWebKey, alg: Algorithm): Promise<CryptoKey> {\n const usage: KeyUsage[] = jwk.d ? [\"sign\"] : [\"verify\"];\n return getSubtle().importKey(\"jwk\", jwk, subtleImportAlgo(alg), false, usage);\n}\n\nexport interface JwksOptions {\n /** Cache the key set this long (ms). Default 600000 (10 min). */\n cacheMaxAgeMs?: number;\n /** Custom fetch (tests / edge). */\n fetch?: typeof fetch;\n}\n\n/**\n * A cached remote JWKS resolver for {@link verify}. Fetches the JSON Web Key\n * Set once, caches it, and returns the right public key by the token's `kid`.\n * @example const jwks = createRemoteJWKS(\"https://issuer/.well-known/jwks.json\");\n * const payload = await verify(token, jwks, { algorithms: [\"RS256\"] });\n */\nexport function createRemoteJWKS(url: string, opts: JwksOptions = {}): KeyResolver {\n const ttl = opts.cacheMaxAgeMs ?? 600000;\n const f = opts.fetch ?? (typeof fetch !== \"undefined\" ? fetch.bind(globalThis) : undefined);\n if (!f) throw new JwtError(\"no global fetch — pass opts.fetch\", \"signature\");\n let cache: { keys: JsonWebKey[]; at: number } | null = null;\n const imported = new Map<string, CryptoKey>();\n\n return async ({ alg, kid }) => {\n if (!cache || Date.now() - cache.at > ttl) {\n const res = await f(url);\n if (!res.ok) throw new JwtError(`JWKS fetch failed: ${res.status}`, \"signature\");\n const data = (await res.json()) as { keys?: JsonWebKey[] };\n cache = { keys: data.keys ?? [], at: Date.now() };\n imported.clear();\n }\n const jwk = kid ? cache.keys.find((k) => (k as { kid?: string }).kid === kid) : cache.keys[0];\n if (!jwk) throw new JwtError(`no JWK found for kid \"${kid}\"`, \"signature\");\n const keyAlg = ((jwk as { alg?: string }).alg as Algorithm) ?? alg;\n const cacheKey = `${keyAlg}:${kid ?? \"\"}`;\n let ck = imported.get(cacheKey);\n if (!ck) {\n ck = await importJwk(jwk, keyAlg);\n imported.set(cacheKey, ck);\n }\n return ck;\n };\n}\n\n/* ------------------------------ refresh-token flow ------------------------------ */\n\nexport interface TokenPair {\n accessToken: string;\n refreshToken: string;\n /** The refresh token's `jti` — persist it to detect reuse on rotation. */\n refreshJti: string;\n}\n\nexport interface IssuePairOptions {\n algorithm?: Algorithm;\n /** Access-token lifetime in seconds. Default 900 (15 min). */\n accessTtl?: number;\n /** Refresh-token lifetime in seconds. Default 2592000 (30 days). */\n refreshTtl?: number;\n issuer?: string;\n audience?: string | string[];\n subject?: string;\n keyId?: string;\n}\n\n/** Issue a short-lived access token + a long-lived refresh token (with a `jti`). */\nexport async function issueTokenPair(\n payload: JwtPayload,\n secret: SigningKey,\n opts: IssuePairOptions = {},\n): Promise<TokenPair> {\n const subject = opts.subject ?? (payload.sub as string | undefined);\n const common = { algorithm: opts.algorithm, issuer: opts.issuer, audience: opts.audience, subject, keyId: opts.keyId };\n const accessToken = await sign(payload, secret, { ...common, expiresIn: opts.accessTtl ?? 900 });\n const refreshJti = randomToken(16);\n const refreshToken = await sign({ typ: \"refresh\", jti: refreshJti }, secret, {\n ...common,\n expiresIn: opts.refreshTtl ?? 2592000,\n });\n return { accessToken, refreshToken, refreshJti };\n}\n\n/** Verify a refresh token (must carry `typ: \"refresh\"`). */\nexport async function verifyRefreshToken<T extends JwtPayload = JwtPayload>(\n token: string,\n key: SigningKey | KeyResolver,\n opts: VerifyOptions = {},\n): Promise<T> {\n const payload = await verify<T>(token, key, opts);\n if (payload.typ !== \"refresh\") throw new JwtError(\"not a refresh token\", \"malformed\");\n return payload;\n}\n\nexport interface RotateOptions extends IssuePairOptions, VerifyOptions {\n /** New access-token payload. Defaults to `{ sub }` from the refresh token. */\n payload?: JwtPayload;\n /** Reuse detection: return true if this refresh `jti` was already used/revoked. */\n isUsed?: (jti: string) => boolean | Promise<boolean>;\n}\n\n/**\n * Rotate a refresh token: verify it, optionally detect reuse, and issue a fresh\n * pair. Returns the new pair plus `usedJti` (persist it as spent).\n */\nexport async function rotateRefreshToken(\n refreshToken: string,\n secret: SigningKey,\n opts: RotateOptions = {},\n): Promise<TokenPair & { usedJti?: string }> {\n const claims = await verifyRefreshToken(refreshToken, secret, opts);\n const jti = claims.jti;\n if (jti && opts.isUsed && (await opts.isUsed(jti)))\n throw new JwtError(\"refresh token reuse detected\", \"reuse\");\n const pair = await issueTokenPair(opts.payload ?? { sub: claims.sub }, secret, {\n ...opts,\n subject: opts.subject ?? (claims.sub as string | undefined),\n });\n return { ...pair, usedJti: jti };\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\n/* ------------------------------ adapters ------------------------------ */\n\n/** Anything we can read a header off: a Fetch Request/Headers, or Node `req.headers`. */\nexport type HeadersLike =\n | Request\n | Headers\n | { headers: Headers | Record<string, string | string[] | undefined> }\n | Record<string, string | string[] | undefined>;\n\nfunction readHeader(src: HeadersLike, name: string): string | undefined {\n const lower = name.toLowerCase();\n const h: unknown = (src as { headers?: unknown }).headers ?? src;\n if (h && typeof (h as Headers).get === \"function\") return (h as Headers).get(name) ?? undefined;\n const rec = h as Record<string, string | string[] | undefined>;\n const v = rec[name] ?? rec[lower];\n return Array.isArray(v) ? v[0] : v ?? undefined;\n}\n\n/** Extract a bearer token from an `Authorization` header (or a named cookie). */\nexport function extractBearer(src: HeadersLike, opts: { cookieName?: string } = {}): string | undefined {\n const auth = readHeader(src, \"authorization\");\n if (auth) {\n const m = /^Bearer\\s+(.+)$/i.exec(auth.trim());\n if (m) return m[1];\n }\n if (opts.cookieName) {\n const cookie = readHeader(src, \"cookie\");\n if (cookie) {\n const found = cookie.split(/;\\s*/).find((c) => c.startsWith(`${opts.cookieName}=`));\n if (found) return decodeURIComponent(found.slice(opts.cookieName.length + 1));\n }\n }\n return undefined;\n}\n\n/** Read the bearer token from a request and verify it. Throws {@link JwtError}. */\nexport async function authenticate<T extends JwtPayload = JwtPayload>(\n src: HeadersLike,\n secret: SigningKey | KeyResolver,\n opts: VerifyOptions & { cookieName?: string } = {},\n): Promise<T> {\n const token = extractBearer(src, { cookieName: opts.cookieName });\n if (!token) throw new JwtError(\"no bearer token\", \"malformed\");\n return verify<T>(token, secret, opts);\n}\n\nexport interface AuthCookieOptions {\n name?: string;\n maxAge?: number; // seconds\n path?: string;\n domain?: string;\n secure?: boolean;\n httpOnly?: boolean;\n sameSite?: \"Strict\" | \"Lax\" | \"None\";\n}\n\n/** Build a hardened `Set-Cookie` value carrying an auth token. */\nexport function toAuthCookie(token: string, opts: AuthCookieOptions = {}): string {\n const name = opts.name ?? \"token\";\n const parts = [`${name}=${encodeURIComponent(token)}`, `Path=${opts.path ?? \"/\"}`, `SameSite=${opts.sameSite ?? \"Lax\"}`];\n if (opts.maxAge !== undefined) parts.push(`Max-Age=${opts.maxAge}`);\n if (opts.domain) parts.push(`Domain=${opts.domain}`);\n if (opts.httpOnly !== false) parts.push(\"HttpOnly\");\n if (opts.secure !== false) parts.push(\"Secure\");\n return parts.join(\"; \");\n}\n\n/** Build a `Set-Cookie` value that clears the auth cookie. */\nexport function clearAuthCookie(opts: Pick<AuthCookieOptions, \"name\" | \"path\" | \"domain\"> = {}): string {\n const name = opts.name ?? \"token\";\n const parts = [`${name}=`, `Path=${opts.path ?? \"/\"}`, \"Max-Age=0\"];\n if (opts.domain) parts.push(`Domain=${opts.domain}`);\n return parts.join(\"; \");\n}\n\n/** Minimal Express-style middleware: verifies the bearer token → `req.user`, else 401. */\nexport function expressJwt<T extends JwtPayload = JwtPayload>(\n secret: SigningKey | KeyResolver,\n opts: VerifyOptions & { cookieName?: string; userKey?: string } = {},\n) {\n const userKey = opts.userKey ?? \"user\";\n return async (\n req: { headers: Record<string, string | string[] | undefined>; [k: string]: unknown },\n res: { status: (n: number) => { json: (b: unknown) => unknown } },\n next: (err?: unknown) => void,\n ): Promise<void> => {\n try {\n req[userKey] = await authenticate<T>(req, secret, opts);\n next();\n } catch (err) {\n const code = err instanceof JwtError ? err.code : \"malformed\";\n res.status(401).json({ error: \"unauthorized\", code });\n }\n };\n}\n"]}
package/dist/index.d.cts CHANGED
@@ -8,10 +8,17 @@
8
8
  *
9
9
  * Zero dependencies (bar @lacspace/crypto) · isomorphic · fully typed.
10
10
  */
11
- type Algorithm = "HS256" | "HS384" | "HS512";
11
+ type Algorithm = "HS256" | "HS384" | "HS512" | "RS256" | "RS384" | "RS512" | "ES256" | "ES384" | "ES512";
12
+ /** A signing/verification key: an HMAC secret, or a Web Crypto key for RS and ES. */
13
+ type SigningKey = string | Uint8Array | CryptoKey;
14
+ /** Resolve a verification key from the token header (e.g. by `kid`). */
15
+ type KeyResolver = (header: {
16
+ alg: Algorithm;
17
+ kid?: string;
18
+ }) => SigningKey | Promise<SigningKey>;
12
19
  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");
20
+ code: "malformed" | "signature" | "expired" | "not_active" | "issuer" | "audience" | "algorithm" | "reuse";
21
+ constructor(message: string, code: "malformed" | "signature" | "expired" | "not_active" | "issuer" | "audience" | "algorithm" | "reuse");
15
22
  }
16
23
  interface JwtPayload {
17
24
  iss?: string;
@@ -30,13 +37,15 @@ interface SignOptions {
30
37
  issuer?: string;
31
38
  audience?: string | string[];
32
39
  subject?: string;
40
+ /** `kid` header — which key signed this (for rotation / JWKS). */
41
+ keyId?: string;
33
42
  }
34
- /** Sign a JWT. */
35
- declare function sign(payload: JwtPayload, secret: string | Uint8Array, opts?: SignOptions): Promise<string>;
43
+ /** Sign a JWT. Pass an HMAC secret (HS*) or a Web Crypto private key (RS and ES). */
44
+ declare function sign(payload: JwtPayload, secret: SigningKey, opts?: SignOptions): Promise<string>;
36
45
  interface VerifyOptions {
37
46
  algorithms?: Algorithm[];
38
47
  issuer?: string;
39
- audience?: string;
48
+ audience?: string | string[];
40
49
  /** Allowed clock skew in seconds. Default 0. */
41
50
  clockTolerance?: number;
42
51
  }
@@ -45,11 +54,105 @@ declare function decode(token: string): {
45
54
  header: Record<string, unknown>;
46
55
  payload: JwtPayload;
47
56
  };
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>;
57
+ /**
58
+ * Verify a JWT's signature and claims. Throws {@link JwtError} on any failure.
59
+ * `key` is an HMAC secret, a Web Crypto public key (RS and ES), or a resolver
60
+ * (e.g. from {@link createRemoteJWKS}) that returns the key for the token header.
61
+ */
62
+ declare function verify<T extends JwtPayload = JwtPayload>(token: string, key: SigningKey | KeyResolver, opts?: VerifyOptions): Promise<T>;
63
+ /** Import a PKCS#8 PEM private key for signing (RS and ES). */
64
+ declare function importPkcs8(pem: string, alg: Algorithm): Promise<CryptoKey>;
65
+ /** Import an SPKI PEM public key for verification (RS and ES). */
66
+ declare function importSpki(pem: string, alg: Algorithm): Promise<CryptoKey>;
67
+ /** Import a JWK (public or private) for verify/sign. */
68
+ declare function importJwk(jwk: JsonWebKey, alg: Algorithm): Promise<CryptoKey>;
69
+ interface JwksOptions {
70
+ /** Cache the key set this long (ms). Default 600000 (10 min). */
71
+ cacheMaxAgeMs?: number;
72
+ /** Custom fetch (tests / edge). */
73
+ fetch?: typeof fetch;
74
+ }
75
+ /**
76
+ * A cached remote JWKS resolver for {@link verify}. Fetches the JSON Web Key
77
+ * Set once, caches it, and returns the right public key by the token's `kid`.
78
+ * @example const jwks = createRemoteJWKS("https://issuer/.well-known/jwks.json");
79
+ * const payload = await verify(token, jwks, { algorithms: ["RS256"] });
80
+ */
81
+ declare function createRemoteJWKS(url: string, opts?: JwksOptions): KeyResolver;
82
+ interface TokenPair {
83
+ accessToken: string;
84
+ refreshToken: string;
85
+ /** The refresh token's `jti` — persist it to detect reuse on rotation. */
86
+ refreshJti: string;
87
+ }
88
+ interface IssuePairOptions {
89
+ algorithm?: Algorithm;
90
+ /** Access-token lifetime in seconds. Default 900 (15 min). */
91
+ accessTtl?: number;
92
+ /** Refresh-token lifetime in seconds. Default 2592000 (30 days). */
93
+ refreshTtl?: number;
94
+ issuer?: string;
95
+ audience?: string | string[];
96
+ subject?: string;
97
+ keyId?: string;
98
+ }
99
+ /** Issue a short-lived access token + a long-lived refresh token (with a `jti`). */
100
+ declare function issueTokenPair(payload: JwtPayload, secret: SigningKey, opts?: IssuePairOptions): Promise<TokenPair>;
101
+ /** Verify a refresh token (must carry `typ: "refresh"`). */
102
+ declare function verifyRefreshToken<T extends JwtPayload = JwtPayload>(token: string, key: SigningKey | KeyResolver, opts?: VerifyOptions): Promise<T>;
103
+ interface RotateOptions extends IssuePairOptions, VerifyOptions {
104
+ /** New access-token payload. Defaults to `{ sub }` from the refresh token. */
105
+ payload?: JwtPayload;
106
+ /** Reuse detection: return true if this refresh `jti` was already used/revoked. */
107
+ isUsed?: (jti: string) => boolean | Promise<boolean>;
108
+ }
109
+ /**
110
+ * Rotate a refresh token: verify it, optionally detect reuse, and issue a fresh
111
+ * pair. Returns the new pair plus `usedJti` (persist it as spent).
112
+ */
113
+ declare function rotateRefreshToken(refreshToken: string, secret: SigningKey, opts?: RotateOptions): Promise<TokenPair & {
114
+ usedJti?: string;
115
+ }>;
50
116
  /** A cryptographically-random URL-safe token (default 32 bytes → 43 chars). */
51
117
  declare function randomToken(bytes?: number): string;
52
118
  /** A CSRF token (alias of a 32-byte random token). */
53
119
  declare function csrfToken(): string;
120
+ /** Anything we can read a header off: a Fetch Request/Headers, or Node `req.headers`. */
121
+ type HeadersLike = Request | Headers | {
122
+ headers: Headers | Record<string, string | string[] | undefined>;
123
+ } | Record<string, string | string[] | undefined>;
124
+ /** Extract a bearer token from an `Authorization` header (or a named cookie). */
125
+ declare function extractBearer(src: HeadersLike, opts?: {
126
+ cookieName?: string;
127
+ }): string | undefined;
128
+ /** Read the bearer token from a request and verify it. Throws {@link JwtError}. */
129
+ declare function authenticate<T extends JwtPayload = JwtPayload>(src: HeadersLike, secret: SigningKey | KeyResolver, opts?: VerifyOptions & {
130
+ cookieName?: string;
131
+ }): Promise<T>;
132
+ interface AuthCookieOptions {
133
+ name?: string;
134
+ maxAge?: number;
135
+ path?: string;
136
+ domain?: string;
137
+ secure?: boolean;
138
+ httpOnly?: boolean;
139
+ sameSite?: "Strict" | "Lax" | "None";
140
+ }
141
+ /** Build a hardened `Set-Cookie` value carrying an auth token. */
142
+ declare function toAuthCookie(token: string, opts?: AuthCookieOptions): string;
143
+ /** Build a `Set-Cookie` value that clears the auth cookie. */
144
+ declare function clearAuthCookie(opts?: Pick<AuthCookieOptions, "name" | "path" | "domain">): string;
145
+ /** Minimal Express-style middleware: verifies the bearer token → `req.user`, else 401. */
146
+ declare function expressJwt<T extends JwtPayload = JwtPayload>(secret: SigningKey | KeyResolver, opts?: VerifyOptions & {
147
+ cookieName?: string;
148
+ userKey?: string;
149
+ }): (req: {
150
+ headers: Record<string, string | string[] | undefined>;
151
+ [k: string]: unknown;
152
+ }, res: {
153
+ status: (n: number) => {
154
+ json: (b: unknown) => unknown;
155
+ };
156
+ }, next: (err?: unknown) => void) => Promise<void>;
54
157
 
55
- export { type Algorithm, JwtError, type JwtPayload, type SignOptions, type VerifyOptions, csrfToken, decode, randomToken, sign, verify };
158
+ export { type Algorithm, type AuthCookieOptions, type HeadersLike, type IssuePairOptions, type JwksOptions, JwtError, type JwtPayload, type KeyResolver, type RotateOptions, type SignOptions, type SigningKey, type TokenPair, type VerifyOptions, authenticate, clearAuthCookie, createRemoteJWKS, csrfToken, decode, expressJwt, extractBearer, importJwk, importPkcs8, importSpki, issueTokenPair, randomToken, rotateRefreshToken, sign, toAuthCookie, verify, verifyRefreshToken };
package/dist/index.d.ts CHANGED
@@ -8,10 +8,17 @@
8
8
  *
9
9
  * Zero dependencies (bar @lacspace/crypto) · isomorphic · fully typed.
10
10
  */
11
- type Algorithm = "HS256" | "HS384" | "HS512";
11
+ type Algorithm = "HS256" | "HS384" | "HS512" | "RS256" | "RS384" | "RS512" | "ES256" | "ES384" | "ES512";
12
+ /** A signing/verification key: an HMAC secret, or a Web Crypto key for RS and ES. */
13
+ type SigningKey = string | Uint8Array | CryptoKey;
14
+ /** Resolve a verification key from the token header (e.g. by `kid`). */
15
+ type KeyResolver = (header: {
16
+ alg: Algorithm;
17
+ kid?: string;
18
+ }) => SigningKey | Promise<SigningKey>;
12
19
  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");
20
+ code: "malformed" | "signature" | "expired" | "not_active" | "issuer" | "audience" | "algorithm" | "reuse";
21
+ constructor(message: string, code: "malformed" | "signature" | "expired" | "not_active" | "issuer" | "audience" | "algorithm" | "reuse");
15
22
  }
16
23
  interface JwtPayload {
17
24
  iss?: string;
@@ -30,13 +37,15 @@ interface SignOptions {
30
37
  issuer?: string;
31
38
  audience?: string | string[];
32
39
  subject?: string;
40
+ /** `kid` header — which key signed this (for rotation / JWKS). */
41
+ keyId?: string;
33
42
  }
34
- /** Sign a JWT. */
35
- declare function sign(payload: JwtPayload, secret: string | Uint8Array, opts?: SignOptions): Promise<string>;
43
+ /** Sign a JWT. Pass an HMAC secret (HS*) or a Web Crypto private key (RS and ES). */
44
+ declare function sign(payload: JwtPayload, secret: SigningKey, opts?: SignOptions): Promise<string>;
36
45
  interface VerifyOptions {
37
46
  algorithms?: Algorithm[];
38
47
  issuer?: string;
39
- audience?: string;
48
+ audience?: string | string[];
40
49
  /** Allowed clock skew in seconds. Default 0. */
41
50
  clockTolerance?: number;
42
51
  }
@@ -45,11 +54,105 @@ declare function decode(token: string): {
45
54
  header: Record<string, unknown>;
46
55
  payload: JwtPayload;
47
56
  };
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>;
57
+ /**
58
+ * Verify a JWT's signature and claims. Throws {@link JwtError} on any failure.
59
+ * `key` is an HMAC secret, a Web Crypto public key (RS and ES), or a resolver
60
+ * (e.g. from {@link createRemoteJWKS}) that returns the key for the token header.
61
+ */
62
+ declare function verify<T extends JwtPayload = JwtPayload>(token: string, key: SigningKey | KeyResolver, opts?: VerifyOptions): Promise<T>;
63
+ /** Import a PKCS#8 PEM private key for signing (RS and ES). */
64
+ declare function importPkcs8(pem: string, alg: Algorithm): Promise<CryptoKey>;
65
+ /** Import an SPKI PEM public key for verification (RS and ES). */
66
+ declare function importSpki(pem: string, alg: Algorithm): Promise<CryptoKey>;
67
+ /** Import a JWK (public or private) for verify/sign. */
68
+ declare function importJwk(jwk: JsonWebKey, alg: Algorithm): Promise<CryptoKey>;
69
+ interface JwksOptions {
70
+ /** Cache the key set this long (ms). Default 600000 (10 min). */
71
+ cacheMaxAgeMs?: number;
72
+ /** Custom fetch (tests / edge). */
73
+ fetch?: typeof fetch;
74
+ }
75
+ /**
76
+ * A cached remote JWKS resolver for {@link verify}. Fetches the JSON Web Key
77
+ * Set once, caches it, and returns the right public key by the token's `kid`.
78
+ * @example const jwks = createRemoteJWKS("https://issuer/.well-known/jwks.json");
79
+ * const payload = await verify(token, jwks, { algorithms: ["RS256"] });
80
+ */
81
+ declare function createRemoteJWKS(url: string, opts?: JwksOptions): KeyResolver;
82
+ interface TokenPair {
83
+ accessToken: string;
84
+ refreshToken: string;
85
+ /** The refresh token's `jti` — persist it to detect reuse on rotation. */
86
+ refreshJti: string;
87
+ }
88
+ interface IssuePairOptions {
89
+ algorithm?: Algorithm;
90
+ /** Access-token lifetime in seconds. Default 900 (15 min). */
91
+ accessTtl?: number;
92
+ /** Refresh-token lifetime in seconds. Default 2592000 (30 days). */
93
+ refreshTtl?: number;
94
+ issuer?: string;
95
+ audience?: string | string[];
96
+ subject?: string;
97
+ keyId?: string;
98
+ }
99
+ /** Issue a short-lived access token + a long-lived refresh token (with a `jti`). */
100
+ declare function issueTokenPair(payload: JwtPayload, secret: SigningKey, opts?: IssuePairOptions): Promise<TokenPair>;
101
+ /** Verify a refresh token (must carry `typ: "refresh"`). */
102
+ declare function verifyRefreshToken<T extends JwtPayload = JwtPayload>(token: string, key: SigningKey | KeyResolver, opts?: VerifyOptions): Promise<T>;
103
+ interface RotateOptions extends IssuePairOptions, VerifyOptions {
104
+ /** New access-token payload. Defaults to `{ sub }` from the refresh token. */
105
+ payload?: JwtPayload;
106
+ /** Reuse detection: return true if this refresh `jti` was already used/revoked. */
107
+ isUsed?: (jti: string) => boolean | Promise<boolean>;
108
+ }
109
+ /**
110
+ * Rotate a refresh token: verify it, optionally detect reuse, and issue a fresh
111
+ * pair. Returns the new pair plus `usedJti` (persist it as spent).
112
+ */
113
+ declare function rotateRefreshToken(refreshToken: string, secret: SigningKey, opts?: RotateOptions): Promise<TokenPair & {
114
+ usedJti?: string;
115
+ }>;
50
116
  /** A cryptographically-random URL-safe token (default 32 bytes → 43 chars). */
51
117
  declare function randomToken(bytes?: number): string;
52
118
  /** A CSRF token (alias of a 32-byte random token). */
53
119
  declare function csrfToken(): string;
120
+ /** Anything we can read a header off: a Fetch Request/Headers, or Node `req.headers`. */
121
+ type HeadersLike = Request | Headers | {
122
+ headers: Headers | Record<string, string | string[] | undefined>;
123
+ } | Record<string, string | string[] | undefined>;
124
+ /** Extract a bearer token from an `Authorization` header (or a named cookie). */
125
+ declare function extractBearer(src: HeadersLike, opts?: {
126
+ cookieName?: string;
127
+ }): string | undefined;
128
+ /** Read the bearer token from a request and verify it. Throws {@link JwtError}. */
129
+ declare function authenticate<T extends JwtPayload = JwtPayload>(src: HeadersLike, secret: SigningKey | KeyResolver, opts?: VerifyOptions & {
130
+ cookieName?: string;
131
+ }): Promise<T>;
132
+ interface AuthCookieOptions {
133
+ name?: string;
134
+ maxAge?: number;
135
+ path?: string;
136
+ domain?: string;
137
+ secure?: boolean;
138
+ httpOnly?: boolean;
139
+ sameSite?: "Strict" | "Lax" | "None";
140
+ }
141
+ /** Build a hardened `Set-Cookie` value carrying an auth token. */
142
+ declare function toAuthCookie(token: string, opts?: AuthCookieOptions): string;
143
+ /** Build a `Set-Cookie` value that clears the auth cookie. */
144
+ declare function clearAuthCookie(opts?: Pick<AuthCookieOptions, "name" | "path" | "domain">): string;
145
+ /** Minimal Express-style middleware: verifies the bearer token → `req.user`, else 401. */
146
+ declare function expressJwt<T extends JwtPayload = JwtPayload>(secret: SigningKey | KeyResolver, opts?: VerifyOptions & {
147
+ cookieName?: string;
148
+ userKey?: string;
149
+ }): (req: {
150
+ headers: Record<string, string | string[] | undefined>;
151
+ [k: string]: unknown;
152
+ }, res: {
153
+ status: (n: number) => {
154
+ json: (b: unknown) => unknown;
155
+ };
156
+ }, next: (err?: unknown) => void) => Promise<void>;
54
157
 
55
- export { type Algorithm, JwtError, type JwtPayload, type SignOptions, type VerifyOptions, csrfToken, decode, randomToken, sign, verify };
158
+ export { type Algorithm, type AuthCookieOptions, type HeadersLike, type IssuePairOptions, type JwksOptions, JwtError, type JwtPayload, type KeyResolver, type RotateOptions, type SignOptions, type SigningKey, type TokenPair, type VerifyOptions, authenticate, clearAuthCookie, createRemoteJWKS, csrfToken, decode, expressJwt, extractBearer, importJwk, importPkcs8, importSpki, issueTokenPair, randomToken, rotateRefreshToken, sign, toAuthCookie, verify, verifyRefreshToken };
package/dist/index.js CHANGED
@@ -1,10 +1,16 @@
1
- import { hmac, toBase64url, fromBase64url, constantTimeEqual, randomBytes } from '@lacspace/crypto';
1
+ import { toBase64url, fromBase64url, constantTimeEqual, hmac, randomBytes } from '@lacspace/crypto';
2
2
 
3
3
  // src/index.ts
4
4
  var ALG_HASH = {
5
5
  HS256: "SHA-256",
6
6
  HS384: "SHA-384",
7
- HS512: "SHA-512"
7
+ HS512: "SHA-512",
8
+ RS256: "SHA-256",
9
+ RS384: "SHA-384",
10
+ RS512: "SHA-512",
11
+ ES256: "SHA-256",
12
+ ES384: "SHA-384",
13
+ ES512: "SHA-512"
8
14
  };
9
15
  var JwtError = class extends Error {
10
16
  constructor(message, code) {
@@ -13,6 +19,41 @@ var JwtError = class extends Error {
13
19
  this.name = "JwtError";
14
20
  }
15
21
  };
22
+ function getSubtle() {
23
+ const c = globalThis.crypto;
24
+ if (!c || !c.subtle) throw new JwtError("Web Crypto API unavailable for asymmetric JWT", "algorithm");
25
+ return c.subtle;
26
+ }
27
+ function b64ToBytes(b64) {
28
+ if (typeof atob !== "undefined") {
29
+ const bin = atob(b64);
30
+ const out = new Uint8Array(bin.length);
31
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
32
+ return out;
33
+ }
34
+ return new Uint8Array(Buffer.from(b64, "base64"));
35
+ }
36
+ function subtleSignAlgo(alg) {
37
+ if (alg.startsWith("RS")) return { name: "RSASSA-PKCS1-v1_5" };
38
+ if (alg.startsWith("ES")) return { name: "ECDSA", hash: ALG_HASH[alg] };
39
+ throw new JwtError(`not an asymmetric algorithm: ${alg}`, "algorithm");
40
+ }
41
+ function subtleImportAlgo(alg) {
42
+ if (alg.startsWith("RS")) return { name: "RSASSA-PKCS1-v1_5", hash: ALG_HASH[alg] };
43
+ const curve = alg === "ES256" ? "P-256" : alg === "ES384" ? "P-384" : "P-521";
44
+ return { name: "ECDSA", namedCurve: curve };
45
+ }
46
+ async function signBytes(alg, key, input) {
47
+ if (alg.startsWith("HS")) {
48
+ if (typeof key !== "string" && !(key instanceof Uint8Array))
49
+ throw new JwtError("HMAC algorithms need a string/bytes secret", "algorithm");
50
+ return hmac(key, input, ALG_HASH[alg]);
51
+ }
52
+ if (!(typeof CryptoKey !== "undefined" && key instanceof CryptoKey))
53
+ throw new JwtError(`${alg} needs a CryptoKey \u2014 use importPkcs8 / importJwk`, "algorithm");
54
+ const sig = await getSubtle().sign(subtleSignAlgo(alg), key, enc.encode(input));
55
+ return new Uint8Array(sig);
56
+ }
16
57
  var enc = new TextEncoder();
17
58
  var dec = new TextDecoder();
18
59
  function b64urlJson(obj) {
@@ -29,8 +70,10 @@ async function sign(payload, secret, opts = {}) {
29
70
  if (opts.issuer) body.iss = opts.issuer;
30
71
  if (opts.audience) body.aud = opts.audience;
31
72
  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]);
73
+ const header = { alg, typ: "JWT" };
74
+ if (opts.keyId) header.kid = opts.keyId;
75
+ const signingInput = `${b64urlJson(header)}.${b64urlJson(body)}`;
76
+ const sig = await signBytes(alg, secret, signingInput);
34
77
  return `${signingInput}.${toBase64url(sig)}`;
35
78
  }
36
79
  function decode(token) {
@@ -41,7 +84,7 @@ function decode(token) {
41
84
  payload: JSON.parse(dec.decode(fromBase64url(parts[1])))
42
85
  };
43
86
  }
44
- async function verify(token, secret, opts = {}) {
87
+ async function verify(token, key, opts = {}) {
45
88
  const parts = token.split(".");
46
89
  if (parts.length !== 3) throw new JwtError("malformed token", "malformed");
47
90
  const [h, p, s] = parts;
@@ -55,9 +98,25 @@ async function verify(token, secret, opts = {}) {
55
98
  if (!alg || !(alg in ALG_HASH)) throw new JwtError(`unsupported algorithm ${alg}`, "algorithm");
56
99
  if (opts.algorithms && !opts.algorithms.includes(alg))
57
100
  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");
101
+ const resolved = typeof key === "function" ? await key({ alg, kid: header.kid }) : key;
102
+ const signingInput = `${h}.${p}`;
103
+ const sig = fromBase64url(s);
104
+ let valid;
105
+ if (alg.startsWith("HS")) {
106
+ if (typeof resolved !== "string" && !(resolved instanceof Uint8Array))
107
+ throw new JwtError("HMAC algorithms need a string/bytes secret", "algorithm");
108
+ valid = constantTimeEqual(await hmac(resolved, signingInput, ALG_HASH[alg]), sig);
109
+ } else {
110
+ if (!(typeof CryptoKey !== "undefined" && resolved instanceof CryptoKey))
111
+ throw new JwtError(`${alg} needs a CryptoKey \u2014 use importSpki / importJwk / createRemoteJWKS`, "algorithm");
112
+ valid = await getSubtle().verify(
113
+ subtleSignAlgo(alg),
114
+ resolved,
115
+ sig,
116
+ enc.encode(signingInput)
117
+ );
118
+ }
119
+ if (!valid) throw new JwtError("signature verification failed", "signature");
61
120
  let payload;
62
121
  try {
63
122
  payload = JSON.parse(dec.decode(fromBase64url(p)));
@@ -73,18 +132,141 @@ async function verify(token, secret, opts = {}) {
73
132
  if (opts.issuer && payload.iss !== opts.issuer)
74
133
  throw new JwtError("issuer mismatch", "issuer");
75
134
  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");
135
+ const want = Array.isArray(opts.audience) ? opts.audience : [opts.audience];
136
+ const have = Array.isArray(payload.aud) ? payload.aud : payload.aud ? [payload.aud] : [];
137
+ if (!want.some((a) => have.includes(a))) throw new JwtError("audience mismatch", "audience");
78
138
  }
79
139
  return payload;
80
140
  }
141
+ function pemToDer(pem) {
142
+ const b64 = pem.replace(/-----BEGIN [^-]+-----/, "").replace(/-----END [^-]+-----/, "").replace(/\s+/g, "");
143
+ return b64ToBytes(b64);
144
+ }
145
+ function importPkcs8(pem, alg) {
146
+ return getSubtle().importKey("pkcs8", pemToDer(pem), subtleImportAlgo(alg), false, ["sign"]);
147
+ }
148
+ function importSpki(pem, alg) {
149
+ return getSubtle().importKey("spki", pemToDer(pem), subtleImportAlgo(alg), false, ["verify"]);
150
+ }
151
+ function importJwk(jwk, alg) {
152
+ const usage = jwk.d ? ["sign"] : ["verify"];
153
+ return getSubtle().importKey("jwk", jwk, subtleImportAlgo(alg), false, usage);
154
+ }
155
+ function createRemoteJWKS(url, opts = {}) {
156
+ const ttl = opts.cacheMaxAgeMs ?? 6e5;
157
+ const f = opts.fetch ?? (typeof fetch !== "undefined" ? fetch.bind(globalThis) : void 0);
158
+ if (!f) throw new JwtError("no global fetch \u2014 pass opts.fetch", "signature");
159
+ let cache = null;
160
+ const imported = /* @__PURE__ */ new Map();
161
+ return async ({ alg, kid }) => {
162
+ if (!cache || Date.now() - cache.at > ttl) {
163
+ const res = await f(url);
164
+ if (!res.ok) throw new JwtError(`JWKS fetch failed: ${res.status}`, "signature");
165
+ const data = await res.json();
166
+ cache = { keys: data.keys ?? [], at: Date.now() };
167
+ imported.clear();
168
+ }
169
+ const jwk = kid ? cache.keys.find((k) => k.kid === kid) : cache.keys[0];
170
+ if (!jwk) throw new JwtError(`no JWK found for kid "${kid}"`, "signature");
171
+ const keyAlg = jwk.alg ?? alg;
172
+ const cacheKey = `${keyAlg}:${kid ?? ""}`;
173
+ let ck = imported.get(cacheKey);
174
+ if (!ck) {
175
+ ck = await importJwk(jwk, keyAlg);
176
+ imported.set(cacheKey, ck);
177
+ }
178
+ return ck;
179
+ };
180
+ }
181
+ async function issueTokenPair(payload, secret, opts = {}) {
182
+ const subject = opts.subject ?? payload.sub;
183
+ const common = { algorithm: opts.algorithm, issuer: opts.issuer, audience: opts.audience, subject, keyId: opts.keyId };
184
+ const accessToken = await sign(payload, secret, { ...common, expiresIn: opts.accessTtl ?? 900 });
185
+ const refreshJti = randomToken(16);
186
+ const refreshToken = await sign({ typ: "refresh", jti: refreshJti }, secret, {
187
+ ...common,
188
+ expiresIn: opts.refreshTtl ?? 2592e3
189
+ });
190
+ return { accessToken, refreshToken, refreshJti };
191
+ }
192
+ async function verifyRefreshToken(token, key, opts = {}) {
193
+ const payload = await verify(token, key, opts);
194
+ if (payload.typ !== "refresh") throw new JwtError("not a refresh token", "malformed");
195
+ return payload;
196
+ }
197
+ async function rotateRefreshToken(refreshToken, secret, opts = {}) {
198
+ const claims = await verifyRefreshToken(refreshToken, secret, opts);
199
+ const jti = claims.jti;
200
+ if (jti && opts.isUsed && await opts.isUsed(jti))
201
+ throw new JwtError("refresh token reuse detected", "reuse");
202
+ const pair = await issueTokenPair(opts.payload ?? { sub: claims.sub }, secret, {
203
+ ...opts,
204
+ subject: opts.subject ?? claims.sub
205
+ });
206
+ return { ...pair, usedJti: jti };
207
+ }
81
208
  function randomToken(bytes = 32) {
82
209
  return toBase64url(randomBytes(bytes));
83
210
  }
84
211
  function csrfToken() {
85
212
  return randomToken(32);
86
213
  }
214
+ function readHeader(src, name) {
215
+ const lower = name.toLowerCase();
216
+ const h = src.headers ?? src;
217
+ if (h && typeof h.get === "function") return h.get(name) ?? void 0;
218
+ const rec = h;
219
+ const v = rec[name] ?? rec[lower];
220
+ return Array.isArray(v) ? v[0] : v ?? void 0;
221
+ }
222
+ function extractBearer(src, opts = {}) {
223
+ const auth = readHeader(src, "authorization");
224
+ if (auth) {
225
+ const m = /^Bearer\s+(.+)$/i.exec(auth.trim());
226
+ if (m) return m[1];
227
+ }
228
+ if (opts.cookieName) {
229
+ const cookie = readHeader(src, "cookie");
230
+ if (cookie) {
231
+ const found = cookie.split(/;\s*/).find((c) => c.startsWith(`${opts.cookieName}=`));
232
+ if (found) return decodeURIComponent(found.slice(opts.cookieName.length + 1));
233
+ }
234
+ }
235
+ return void 0;
236
+ }
237
+ async function authenticate(src, secret, opts = {}) {
238
+ const token = extractBearer(src, { cookieName: opts.cookieName });
239
+ if (!token) throw new JwtError("no bearer token", "malformed");
240
+ return verify(token, secret, opts);
241
+ }
242
+ function toAuthCookie(token, opts = {}) {
243
+ const name = opts.name ?? "token";
244
+ const parts = [`${name}=${encodeURIComponent(token)}`, `Path=${opts.path ?? "/"}`, `SameSite=${opts.sameSite ?? "Lax"}`];
245
+ if (opts.maxAge !== void 0) parts.push(`Max-Age=${opts.maxAge}`);
246
+ if (opts.domain) parts.push(`Domain=${opts.domain}`);
247
+ if (opts.httpOnly !== false) parts.push("HttpOnly");
248
+ if (opts.secure !== false) parts.push("Secure");
249
+ return parts.join("; ");
250
+ }
251
+ function clearAuthCookie(opts = {}) {
252
+ const name = opts.name ?? "token";
253
+ const parts = [`${name}=`, `Path=${opts.path ?? "/"}`, "Max-Age=0"];
254
+ if (opts.domain) parts.push(`Domain=${opts.domain}`);
255
+ return parts.join("; ");
256
+ }
257
+ function expressJwt(secret, opts = {}) {
258
+ const userKey = opts.userKey ?? "user";
259
+ return async (req, res, next) => {
260
+ try {
261
+ req[userKey] = await authenticate(req, secret, opts);
262
+ next();
263
+ } catch (err) {
264
+ const code = err instanceof JwtError ? err.code : "malformed";
265
+ res.status(401).json({ error: "unauthorized", code });
266
+ }
267
+ };
268
+ }
87
269
 
88
- export { JwtError, csrfToken, decode, randomToken, sign, verify };
270
+ export { JwtError, authenticate, clearAuthCookie, createRemoteJWKS, csrfToken, decode, expressJwt, extractBearer, importJwk, importPkcs8, importSpki, issueTokenPair, randomToken, rotateRefreshToken, sign, toAuthCookie, verify, verifyRefreshToken };
89
271
  //# sourceMappingURL=index.js.map
90
272
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +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"]}
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 =\n | \"HS256\" | \"HS384\" | \"HS512\"\n | \"RS256\" | \"RS384\" | \"RS512\"\n | \"ES256\" | \"ES384\" | \"ES512\";\n\nconst ALG_HASH: Record<Algorithm, HashAlgorithm> = {\n HS256: \"SHA-256\", HS384: \"SHA-384\", HS512: \"SHA-512\",\n RS256: \"SHA-256\", RS384: \"SHA-384\", RS512: \"SHA-512\",\n ES256: \"SHA-256\", ES384: \"SHA-384\", ES512: \"SHA-512\",\n};\n\n/** A signing/verification key: an HMAC secret, or a Web Crypto key for RS and ES. */\nexport type SigningKey = string | Uint8Array | CryptoKey;\n/** Resolve a verification key from the token header (e.g. by `kid`). */\nexport type KeyResolver = (header: { alg: Algorithm; kid?: string }) => SigningKey | Promise<SigningKey>;\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 | \"reuse\",\n ) {\n super(message);\n this.name = \"JwtError\";\n }\n}\n\nfunction getSubtle(): SubtleCrypto {\n const c = (globalThis as { crypto?: Crypto }).crypto;\n if (!c || !c.subtle) throw new JwtError(\"Web Crypto API unavailable for asymmetric JWT\", \"algorithm\");\n return c.subtle;\n}\n\nfunction b64ToBytes(b64: string): Uint8Array {\n if (typeof atob !== \"undefined\") {\n const bin = atob(b64);\n const out = new Uint8Array(bin.length);\n for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);\n return out;\n }\n return new Uint8Array(Buffer.from(b64, \"base64\"));\n}\n\nfunction subtleSignAlgo(alg: Algorithm): AlgorithmIdentifier | EcdsaParams {\n if (alg.startsWith(\"RS\")) return { name: \"RSASSA-PKCS1-v1_5\" };\n if (alg.startsWith(\"ES\")) return { name: \"ECDSA\", hash: ALG_HASH[alg] };\n throw new JwtError(`not an asymmetric algorithm: ${alg}`, \"algorithm\");\n}\n\nfunction subtleImportAlgo(alg: Algorithm): RsaHashedImportParams | EcKeyImportParams {\n if (alg.startsWith(\"RS\")) return { name: \"RSASSA-PKCS1-v1_5\", hash: ALG_HASH[alg] };\n const curve = alg === \"ES256\" ? \"P-256\" : alg === \"ES384\" ? \"P-384\" : \"P-521\";\n return { name: \"ECDSA\", namedCurve: curve };\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 /** `kid` header — which key signed this (for rotation / JWKS). */\n keyId?: string;\n}\n\nasync function signBytes(alg: Algorithm, key: SigningKey, input: string): Promise<Uint8Array> {\n if (alg.startsWith(\"HS\")) {\n if (typeof key !== \"string\" && !(key instanceof Uint8Array))\n throw new JwtError(\"HMAC algorithms need a string/bytes secret\", \"algorithm\");\n return hmac(key, input, ALG_HASH[alg]);\n }\n if (!(typeof CryptoKey !== \"undefined\" && key instanceof CryptoKey))\n throw new JwtError(`${alg} needs a CryptoKey — use importPkcs8 / importJwk`, \"algorithm\");\n const sig = await getSubtle().sign(subtleSignAlgo(alg), key, enc.encode(input) as unknown as BufferSource);\n return new Uint8Array(sig);\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. Pass an HMAC secret (HS*) or a Web Crypto private key (RS and ES). */\nexport async function sign(\n payload: JwtPayload,\n secret: SigningKey,\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 header: Record<string, unknown> = { alg, typ: \"JWT\" };\n if (opts.keyId) header.kid = opts.keyId;\n const signingInput = `${b64urlJson(header)}.${b64urlJson(body)}`;\n const sig = await signBytes(alg, secret, signingInput);\n return `${signingInput}.${toBase64url(sig)}`;\n}\n\nexport interface VerifyOptions {\n algorithms?: Algorithm[];\n issuer?: string;\n audience?: string | 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/**\n * Verify a JWT's signature and claims. Throws {@link JwtError} on any failure.\n * `key` is an HMAC secret, a Web Crypto public key (RS and ES), or a resolver\n * (e.g. from {@link createRemoteJWKS}) that returns the key for the token header.\n */\nexport async function verify<T extends JwtPayload = JwtPayload>(\n token: string,\n key: SigningKey | KeyResolver,\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; kid?: string };\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 resolved = typeof key === \"function\" ? await key({ alg, kid: header.kid }) : key;\n\n const signingInput = `${h}.${p}`;\n const sig = fromBase64url(s);\n let valid: boolean;\n if (alg.startsWith(\"HS\")) {\n if (typeof resolved !== \"string\" && !(resolved instanceof Uint8Array))\n throw new JwtError(\"HMAC algorithms need a string/bytes secret\", \"algorithm\");\n valid = constantTimeEqual(await hmac(resolved, signingInput, ALG_HASH[alg]), sig);\n } else {\n if (!(typeof CryptoKey !== \"undefined\" && resolved instanceof CryptoKey))\n throw new JwtError(`${alg} needs a CryptoKey — use importSpki / importJwk / createRemoteJWKS`, \"algorithm\");\n valid = await getSubtle().verify(\n subtleSignAlgo(alg),\n resolved,\n sig as unknown as BufferSource,\n enc.encode(signingInput) as unknown as BufferSource,\n );\n }\n if (!valid) 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 want = Array.isArray(opts.audience) ? opts.audience : [opts.audience];\n const have = Array.isArray(payload.aud) ? payload.aud : payload.aud ? [payload.aud] : [];\n if (!want.some((a) => have.includes(a))) throw new JwtError(\"audience mismatch\", \"audience\");\n }\n return payload;\n}\n\n/* ------------------------------ asymmetric keys & JWKS ------------------------------ */\n\nfunction pemToDer(pem: string): Uint8Array {\n const b64 = pem.replace(/-----BEGIN [^-]+-----/, \"\").replace(/-----END [^-]+-----/, \"\").replace(/\\s+/g, \"\");\n return b64ToBytes(b64);\n}\n\n/** Import a PKCS#8 PEM private key for signing (RS and ES). */\nexport function importPkcs8(pem: string, alg: Algorithm): Promise<CryptoKey> {\n return getSubtle().importKey(\"pkcs8\", pemToDer(pem) as unknown as BufferSource, subtleImportAlgo(alg), false, [\"sign\"]);\n}\n\n/** Import an SPKI PEM public key for verification (RS and ES). */\nexport function importSpki(pem: string, alg: Algorithm): Promise<CryptoKey> {\n return getSubtle().importKey(\"spki\", pemToDer(pem) as unknown as BufferSource, subtleImportAlgo(alg), false, [\"verify\"]);\n}\n\n/** Import a JWK (public or private) for verify/sign. */\nexport function importJwk(jwk: JsonWebKey, alg: Algorithm): Promise<CryptoKey> {\n const usage: KeyUsage[] = jwk.d ? [\"sign\"] : [\"verify\"];\n return getSubtle().importKey(\"jwk\", jwk, subtleImportAlgo(alg), false, usage);\n}\n\nexport interface JwksOptions {\n /** Cache the key set this long (ms). Default 600000 (10 min). */\n cacheMaxAgeMs?: number;\n /** Custom fetch (tests / edge). */\n fetch?: typeof fetch;\n}\n\n/**\n * A cached remote JWKS resolver for {@link verify}. Fetches the JSON Web Key\n * Set once, caches it, and returns the right public key by the token's `kid`.\n * @example const jwks = createRemoteJWKS(\"https://issuer/.well-known/jwks.json\");\n * const payload = await verify(token, jwks, { algorithms: [\"RS256\"] });\n */\nexport function createRemoteJWKS(url: string, opts: JwksOptions = {}): KeyResolver {\n const ttl = opts.cacheMaxAgeMs ?? 600000;\n const f = opts.fetch ?? (typeof fetch !== \"undefined\" ? fetch.bind(globalThis) : undefined);\n if (!f) throw new JwtError(\"no global fetch — pass opts.fetch\", \"signature\");\n let cache: { keys: JsonWebKey[]; at: number } | null = null;\n const imported = new Map<string, CryptoKey>();\n\n return async ({ alg, kid }) => {\n if (!cache || Date.now() - cache.at > ttl) {\n const res = await f(url);\n if (!res.ok) throw new JwtError(`JWKS fetch failed: ${res.status}`, \"signature\");\n const data = (await res.json()) as { keys?: JsonWebKey[] };\n cache = { keys: data.keys ?? [], at: Date.now() };\n imported.clear();\n }\n const jwk = kid ? cache.keys.find((k) => (k as { kid?: string }).kid === kid) : cache.keys[0];\n if (!jwk) throw new JwtError(`no JWK found for kid \"${kid}\"`, \"signature\");\n const keyAlg = ((jwk as { alg?: string }).alg as Algorithm) ?? alg;\n const cacheKey = `${keyAlg}:${kid ?? \"\"}`;\n let ck = imported.get(cacheKey);\n if (!ck) {\n ck = await importJwk(jwk, keyAlg);\n imported.set(cacheKey, ck);\n }\n return ck;\n };\n}\n\n/* ------------------------------ refresh-token flow ------------------------------ */\n\nexport interface TokenPair {\n accessToken: string;\n refreshToken: string;\n /** The refresh token's `jti` — persist it to detect reuse on rotation. */\n refreshJti: string;\n}\n\nexport interface IssuePairOptions {\n algorithm?: Algorithm;\n /** Access-token lifetime in seconds. Default 900 (15 min). */\n accessTtl?: number;\n /** Refresh-token lifetime in seconds. Default 2592000 (30 days). */\n refreshTtl?: number;\n issuer?: string;\n audience?: string | string[];\n subject?: string;\n keyId?: string;\n}\n\n/** Issue a short-lived access token + a long-lived refresh token (with a `jti`). */\nexport async function issueTokenPair(\n payload: JwtPayload,\n secret: SigningKey,\n opts: IssuePairOptions = {},\n): Promise<TokenPair> {\n const subject = opts.subject ?? (payload.sub as string | undefined);\n const common = { algorithm: opts.algorithm, issuer: opts.issuer, audience: opts.audience, subject, keyId: opts.keyId };\n const accessToken = await sign(payload, secret, { ...common, expiresIn: opts.accessTtl ?? 900 });\n const refreshJti = randomToken(16);\n const refreshToken = await sign({ typ: \"refresh\", jti: refreshJti }, secret, {\n ...common,\n expiresIn: opts.refreshTtl ?? 2592000,\n });\n return { accessToken, refreshToken, refreshJti };\n}\n\n/** Verify a refresh token (must carry `typ: \"refresh\"`). */\nexport async function verifyRefreshToken<T extends JwtPayload = JwtPayload>(\n token: string,\n key: SigningKey | KeyResolver,\n opts: VerifyOptions = {},\n): Promise<T> {\n const payload = await verify<T>(token, key, opts);\n if (payload.typ !== \"refresh\") throw new JwtError(\"not a refresh token\", \"malformed\");\n return payload;\n}\n\nexport interface RotateOptions extends IssuePairOptions, VerifyOptions {\n /** New access-token payload. Defaults to `{ sub }` from the refresh token. */\n payload?: JwtPayload;\n /** Reuse detection: return true if this refresh `jti` was already used/revoked. */\n isUsed?: (jti: string) => boolean | Promise<boolean>;\n}\n\n/**\n * Rotate a refresh token: verify it, optionally detect reuse, and issue a fresh\n * pair. Returns the new pair plus `usedJti` (persist it as spent).\n */\nexport async function rotateRefreshToken(\n refreshToken: string,\n secret: SigningKey,\n opts: RotateOptions = {},\n): Promise<TokenPair & { usedJti?: string }> {\n const claims = await verifyRefreshToken(refreshToken, secret, opts);\n const jti = claims.jti;\n if (jti && opts.isUsed && (await opts.isUsed(jti)))\n throw new JwtError(\"refresh token reuse detected\", \"reuse\");\n const pair = await issueTokenPair(opts.payload ?? { sub: claims.sub }, secret, {\n ...opts,\n subject: opts.subject ?? (claims.sub as string | undefined),\n });\n return { ...pair, usedJti: jti };\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\n/* ------------------------------ adapters ------------------------------ */\n\n/** Anything we can read a header off: a Fetch Request/Headers, or Node `req.headers`. */\nexport type HeadersLike =\n | Request\n | Headers\n | { headers: Headers | Record<string, string | string[] | undefined> }\n | Record<string, string | string[] | undefined>;\n\nfunction readHeader(src: HeadersLike, name: string): string | undefined {\n const lower = name.toLowerCase();\n const h: unknown = (src as { headers?: unknown }).headers ?? src;\n if (h && typeof (h as Headers).get === \"function\") return (h as Headers).get(name) ?? undefined;\n const rec = h as Record<string, string | string[] | undefined>;\n const v = rec[name] ?? rec[lower];\n return Array.isArray(v) ? v[0] : v ?? undefined;\n}\n\n/** Extract a bearer token from an `Authorization` header (or a named cookie). */\nexport function extractBearer(src: HeadersLike, opts: { cookieName?: string } = {}): string | undefined {\n const auth = readHeader(src, \"authorization\");\n if (auth) {\n const m = /^Bearer\\s+(.+)$/i.exec(auth.trim());\n if (m) return m[1];\n }\n if (opts.cookieName) {\n const cookie = readHeader(src, \"cookie\");\n if (cookie) {\n const found = cookie.split(/;\\s*/).find((c) => c.startsWith(`${opts.cookieName}=`));\n if (found) return decodeURIComponent(found.slice(opts.cookieName.length + 1));\n }\n }\n return undefined;\n}\n\n/** Read the bearer token from a request and verify it. Throws {@link JwtError}. */\nexport async function authenticate<T extends JwtPayload = JwtPayload>(\n src: HeadersLike,\n secret: SigningKey | KeyResolver,\n opts: VerifyOptions & { cookieName?: string } = {},\n): Promise<T> {\n const token = extractBearer(src, { cookieName: opts.cookieName });\n if (!token) throw new JwtError(\"no bearer token\", \"malformed\");\n return verify<T>(token, secret, opts);\n}\n\nexport interface AuthCookieOptions {\n name?: string;\n maxAge?: number; // seconds\n path?: string;\n domain?: string;\n secure?: boolean;\n httpOnly?: boolean;\n sameSite?: \"Strict\" | \"Lax\" | \"None\";\n}\n\n/** Build a hardened `Set-Cookie` value carrying an auth token. */\nexport function toAuthCookie(token: string, opts: AuthCookieOptions = {}): string {\n const name = opts.name ?? \"token\";\n const parts = [`${name}=${encodeURIComponent(token)}`, `Path=${opts.path ?? \"/\"}`, `SameSite=${opts.sameSite ?? \"Lax\"}`];\n if (opts.maxAge !== undefined) parts.push(`Max-Age=${opts.maxAge}`);\n if (opts.domain) parts.push(`Domain=${opts.domain}`);\n if (opts.httpOnly !== false) parts.push(\"HttpOnly\");\n if (opts.secure !== false) parts.push(\"Secure\");\n return parts.join(\"; \");\n}\n\n/** Build a `Set-Cookie` value that clears the auth cookie. */\nexport function clearAuthCookie(opts: Pick<AuthCookieOptions, \"name\" | \"path\" | \"domain\"> = {}): string {\n const name = opts.name ?? \"token\";\n const parts = [`${name}=`, `Path=${opts.path ?? \"/\"}`, \"Max-Age=0\"];\n if (opts.domain) parts.push(`Domain=${opts.domain}`);\n return parts.join(\"; \");\n}\n\n/** Minimal Express-style middleware: verifies the bearer token → `req.user`, else 401. */\nexport function expressJwt<T extends JwtPayload = JwtPayload>(\n secret: SigningKey | KeyResolver,\n opts: VerifyOptions & { cookieName?: string; userKey?: string } = {},\n) {\n const userKey = opts.userKey ?? \"user\";\n return async (\n req: { headers: Record<string, string | string[] | undefined>; [k: string]: unknown },\n res: { status: (n: number) => { json: (b: unknown) => unknown } },\n next: (err?: unknown) => void,\n ): Promise<void> => {\n try {\n req[userKey] = await authenticate<T>(req, secret, opts);\n next();\n } catch (err) {\n const code = err instanceof JwtError ? err.code : \"malformed\";\n res.status(401).json({ error: \"unauthorized\", code });\n }\n };\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lacspace/jwt",
3
- "version": "1.0.2",
3
+ "version": "1.2.0",
4
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
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",