@lacspace/jwt 1.0.1 → 1.1.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 +23 -0
- package/dist/index.cjs +60 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +38 -1
- package/dist/index.d.ts +38 -1
- package/dist/index.js +56 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -77,4 +77,27 @@ 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
|
+
## Licensing
|
|
98
|
+
|
|
99
|
+
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.
|
|
100
|
+
|
|
101
|
+
Not every Lacspace package is free. We also offer **Commercial** (paid), **Client-specific**, and **Private** (proprietary) packages under separate terms. See the full **[Lacspace Licence Centre](https://lacspace.com/licenses)**.
|
|
102
|
+
|
|
80
103
|
<div align="center"><sub>Built with care by <a href="https://lacspace.com">Lacspace</a> · Lacspace Free Licence · <a href="https://github.com/lacspace/npm-packages">source</a></sub></div>
|
package/dist/index.cjs
CHANGED
|
@@ -86,12 +86,72 @@ function randomToken(bytes = 32) {
|
|
|
86
86
|
function csrfToken() {
|
|
87
87
|
return randomToken(32);
|
|
88
88
|
}
|
|
89
|
+
function readHeader(src, name) {
|
|
90
|
+
const lower = name.toLowerCase();
|
|
91
|
+
const h = src.headers ?? src;
|
|
92
|
+
if (h && typeof h.get === "function") return h.get(name) ?? void 0;
|
|
93
|
+
const rec = h;
|
|
94
|
+
const v = rec[name] ?? rec[lower];
|
|
95
|
+
return Array.isArray(v) ? v[0] : v ?? void 0;
|
|
96
|
+
}
|
|
97
|
+
function extractBearer(src, opts = {}) {
|
|
98
|
+
const auth = readHeader(src, "authorization");
|
|
99
|
+
if (auth) {
|
|
100
|
+
const m = /^Bearer\s+(.+)$/i.exec(auth.trim());
|
|
101
|
+
if (m) return m[1];
|
|
102
|
+
}
|
|
103
|
+
if (opts.cookieName) {
|
|
104
|
+
const cookie = readHeader(src, "cookie");
|
|
105
|
+
if (cookie) {
|
|
106
|
+
const found = cookie.split(/;\s*/).find((c) => c.startsWith(`${opts.cookieName}=`));
|
|
107
|
+
if (found) return decodeURIComponent(found.slice(opts.cookieName.length + 1));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return void 0;
|
|
111
|
+
}
|
|
112
|
+
async function authenticate(src, secret, opts = {}) {
|
|
113
|
+
const token = extractBearer(src, { cookieName: opts.cookieName });
|
|
114
|
+
if (!token) throw new JwtError("no bearer token", "malformed");
|
|
115
|
+
return verify(token, secret, opts);
|
|
116
|
+
}
|
|
117
|
+
function toAuthCookie(token, opts = {}) {
|
|
118
|
+
const name = opts.name ?? "token";
|
|
119
|
+
const parts = [`${name}=${encodeURIComponent(token)}`, `Path=${opts.path ?? "/"}`, `SameSite=${opts.sameSite ?? "Lax"}`];
|
|
120
|
+
if (opts.maxAge !== void 0) parts.push(`Max-Age=${opts.maxAge}`);
|
|
121
|
+
if (opts.domain) parts.push(`Domain=${opts.domain}`);
|
|
122
|
+
if (opts.httpOnly !== false) parts.push("HttpOnly");
|
|
123
|
+
if (opts.secure !== false) parts.push("Secure");
|
|
124
|
+
return parts.join("; ");
|
|
125
|
+
}
|
|
126
|
+
function clearAuthCookie(opts = {}) {
|
|
127
|
+
const name = opts.name ?? "token";
|
|
128
|
+
const parts = [`${name}=`, `Path=${opts.path ?? "/"}`, "Max-Age=0"];
|
|
129
|
+
if (opts.domain) parts.push(`Domain=${opts.domain}`);
|
|
130
|
+
return parts.join("; ");
|
|
131
|
+
}
|
|
132
|
+
function expressJwt(secret, opts = {}) {
|
|
133
|
+
const userKey = opts.userKey ?? "user";
|
|
134
|
+
return async (req, res, next) => {
|
|
135
|
+
try {
|
|
136
|
+
req[userKey] = await authenticate(req, secret, opts);
|
|
137
|
+
next();
|
|
138
|
+
} catch (err) {
|
|
139
|
+
const code = err instanceof JwtError ? err.code : "malformed";
|
|
140
|
+
res.status(401).json({ error: "unauthorized", code });
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
}
|
|
89
144
|
|
|
90
145
|
exports.JwtError = JwtError;
|
|
146
|
+
exports.authenticate = authenticate;
|
|
147
|
+
exports.clearAuthCookie = clearAuthCookie;
|
|
91
148
|
exports.csrfToken = csrfToken;
|
|
92
149
|
exports.decode = decode;
|
|
150
|
+
exports.expressJwt = expressJwt;
|
|
151
|
+
exports.extractBearer = extractBearer;
|
|
93
152
|
exports.randomToken = randomToken;
|
|
94
153
|
exports.sign = sign;
|
|
154
|
+
exports.toAuthCookie = toAuthCookie;
|
|
95
155
|
exports.verify = verify;
|
|
96
156
|
//# sourceMappingURL=index.cjs.map
|
|
97
157
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"names":["toBase64url","hmac","fromBase64url","constantTimeEqual","randomBytes"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * @lacspace/jwt\n * JSON Web Tokens (HMAC) + secure random tokens — done safely.\n *\n * Sign and verify HS256/HS384/HS512 JWTs over Web Crypto (isomorphic), with\n * strict expiry / not-before / issuer / audience checks and constant-time\n * signature comparison. Plus opaque secure tokens and CSRF tokens.\n *\n * Zero dependencies (bar @lacspace/crypto) · isomorphic · fully typed.\n */\n\nimport { hmac, toBase64url, fromBase64url, constantTimeEqual, randomBytes, type HashAlgorithm } from \"@lacspace/crypto\";\n\nexport type Algorithm = \"HS256\" | \"HS384\" | \"HS512\";\nconst ALG_HASH: Record<Algorithm, HashAlgorithm> = {\n HS256: \"SHA-256\",\n HS384: \"SHA-384\",\n HS512: \"SHA-512\",\n};\n\nexport class JwtError extends Error {\n constructor(\n message: string,\n public code:\n | \"malformed\"\n | \"signature\"\n | \"expired\"\n | \"not_active\"\n | \"issuer\"\n | \"audience\"\n | \"algorithm\",\n ) {\n super(message);\n this.name = \"JwtError\";\n }\n}\n\nexport interface JwtPayload {\n iss?: string;\n sub?: string;\n aud?: string | string[];\n exp?: number;\n nbf?: number;\n iat?: number;\n jti?: string;\n [key: string]: unknown;\n}\n\nexport interface SignOptions {\n algorithm?: Algorithm;\n /** Seconds from now until expiry, e.g. 3600. */\n expiresIn?: number;\n issuer?: string;\n audience?: string | string[];\n subject?: string;\n}\n\nconst enc = new TextEncoder();\nconst dec = new TextDecoder();\n\nfunction b64urlJson(obj: unknown): string {\n return toBase64url(enc.encode(JSON.stringify(obj)));\n}\n\nfunction nowSec(): number {\n return Math.floor(Date.now() / 1000);\n}\n\n/** Sign a JWT. */\nexport async function sign(\n payload: JwtPayload,\n secret: string | Uint8Array,\n opts: SignOptions = {},\n): Promise<string> {\n const alg = opts.algorithm ?? \"HS256\";\n const iat = nowSec();\n const body: JwtPayload = { iat, ...payload };\n if (opts.expiresIn !== undefined) body.exp = iat + opts.expiresIn;\n if (opts.issuer) body.iss = opts.issuer;\n if (opts.audience) body.aud = opts.audience;\n if (opts.subject) body.sub = opts.subject;\n\n const signingInput = `${b64urlJson({ alg, typ: \"JWT\" })}.${b64urlJson(body)}`;\n const sig = await hmac(secret, signingInput, ALG_HASH[alg]);\n return `${signingInput}.${toBase64url(sig)}`;\n}\n\nexport interface VerifyOptions {\n algorithms?: Algorithm[];\n issuer?: string;\n audience?: string;\n /** Allowed clock skew in seconds. Default 0. */\n clockTolerance?: number;\n}\n\n/** Decode a JWT without verifying (never trust the result for auth). */\nexport function decode(token: string): { header: Record<string, unknown>; payload: JwtPayload } {\n const parts = token.split(\".\");\n if (parts.length !== 3) throw new JwtError(\"malformed token\", \"malformed\");\n return {\n header: JSON.parse(dec.decode(fromBase64url(parts[0]!))),\n payload: JSON.parse(dec.decode(fromBase64url(parts[1]!))),\n };\n}\n\n/** Verify a JWT's signature and claims. Throws {@link JwtError} on any failure. */\nexport async function verify<T extends JwtPayload = JwtPayload>(\n token: string,\n secret: string | Uint8Array,\n opts: VerifyOptions = {},\n): Promise<T> {\n const parts = token.split(\".\");\n if (parts.length !== 3) throw new JwtError(\"malformed token\", \"malformed\");\n const [h, p, s] = parts as [string, string, string];\n\n let header: { alg?: Algorithm };\n try {\n header = JSON.parse(dec.decode(fromBase64url(h)));\n } catch {\n throw new JwtError(\"malformed header\", \"malformed\");\n }\n const alg = header.alg;\n if (!alg || !(alg in ALG_HASH)) throw new JwtError(`unsupported algorithm ${alg}`, \"algorithm\");\n if (opts.algorithms && !opts.algorithms.includes(alg))\n throw new JwtError(`algorithm ${alg} not allowed`, \"algorithm\");\n\n const expected = await hmac(secret, `${h}.${p}`, ALG_HASH[alg]);\n if (!constantTimeEqual(expected, fromBase64url(s)))\n throw new JwtError(\"signature verification failed\", \"signature\");\n\n let payload: T;\n try {\n payload = JSON.parse(dec.decode(fromBase64url(p)));\n } catch {\n throw new JwtError(\"malformed payload\", \"malformed\");\n }\n\n const now = nowSec();\n const skew = opts.clockTolerance ?? 0;\n if (payload.exp !== undefined && now > payload.exp + skew)\n throw new JwtError(\"token expired\", \"expired\");\n if (payload.nbf !== undefined && now + skew < payload.nbf)\n throw new JwtError(\"token not yet active\", \"not_active\");\n if (opts.issuer && payload.iss !== opts.issuer)\n throw new JwtError(\"issuer mismatch\", \"issuer\");\n if (opts.audience) {\n const aud = Array.isArray(payload.aud) ? payload.aud : payload.aud ? [payload.aud] : [];\n if (!aud.includes(opts.audience)) throw new JwtError(\"audience mismatch\", \"audience\");\n }\n return payload;\n}\n\n/* ------------------------------ opaque tokens ------------------------------ */\n\n/** A cryptographically-random URL-safe token (default 32 bytes → 43 chars). */\nexport function randomToken(bytes = 32): string {\n return toBase64url(randomBytes(bytes));\n}\n\n/** A CSRF token (alias of a 32-byte random token). */\nexport function csrfToken(): string {\n return randomToken(32);\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":["toBase64url","hmac","fromBase64url","constantTimeEqual","randomBytes"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * @lacspace/jwt\n * JSON Web Tokens (HMAC) + secure random tokens — done safely.\n *\n * Sign and verify HS256/HS384/HS512 JWTs over Web Crypto (isomorphic), with\n * strict expiry / not-before / issuer / audience checks and constant-time\n * signature comparison. Plus opaque secure tokens and CSRF tokens.\n *\n * Zero dependencies (bar @lacspace/crypto) · isomorphic · fully typed.\n */\n\nimport { hmac, toBase64url, fromBase64url, constantTimeEqual, randomBytes, type HashAlgorithm } from \"@lacspace/crypto\";\n\nexport type Algorithm = \"HS256\" | \"HS384\" | \"HS512\";\nconst ALG_HASH: Record<Algorithm, HashAlgorithm> = {\n HS256: \"SHA-256\",\n HS384: \"SHA-384\",\n HS512: \"SHA-512\",\n};\n\nexport class JwtError extends Error {\n constructor(\n message: string,\n public code:\n | \"malformed\"\n | \"signature\"\n | \"expired\"\n | \"not_active\"\n | \"issuer\"\n | \"audience\"\n | \"algorithm\",\n ) {\n super(message);\n this.name = \"JwtError\";\n }\n}\n\nexport interface JwtPayload {\n iss?: string;\n sub?: string;\n aud?: string | string[];\n exp?: number;\n nbf?: number;\n iat?: number;\n jti?: string;\n [key: string]: unknown;\n}\n\nexport interface SignOptions {\n algorithm?: Algorithm;\n /** Seconds from now until expiry, e.g. 3600. */\n expiresIn?: number;\n issuer?: string;\n audience?: string | string[];\n subject?: string;\n}\n\nconst enc = new TextEncoder();\nconst dec = new TextDecoder();\n\nfunction b64urlJson(obj: unknown): string {\n return toBase64url(enc.encode(JSON.stringify(obj)));\n}\n\nfunction nowSec(): number {\n return Math.floor(Date.now() / 1000);\n}\n\n/** Sign a JWT. */\nexport async function sign(\n payload: JwtPayload,\n secret: string | Uint8Array,\n opts: SignOptions = {},\n): Promise<string> {\n const alg = opts.algorithm ?? \"HS256\";\n const iat = nowSec();\n const body: JwtPayload = { iat, ...payload };\n if (opts.expiresIn !== undefined) body.exp = iat + opts.expiresIn;\n if (opts.issuer) body.iss = opts.issuer;\n if (opts.audience) body.aud = opts.audience;\n if (opts.subject) body.sub = opts.subject;\n\n const signingInput = `${b64urlJson({ alg, typ: \"JWT\" })}.${b64urlJson(body)}`;\n const sig = await hmac(secret, signingInput, ALG_HASH[alg]);\n return `${signingInput}.${toBase64url(sig)}`;\n}\n\nexport interface VerifyOptions {\n algorithms?: Algorithm[];\n issuer?: string;\n audience?: string;\n /** Allowed clock skew in seconds. Default 0. */\n clockTolerance?: number;\n}\n\n/** Decode a JWT without verifying (never trust the result for auth). */\nexport function decode(token: string): { header: Record<string, unknown>; payload: JwtPayload } {\n const parts = token.split(\".\");\n if (parts.length !== 3) throw new JwtError(\"malformed token\", \"malformed\");\n return {\n header: JSON.parse(dec.decode(fromBase64url(parts[0]!))),\n payload: JSON.parse(dec.decode(fromBase64url(parts[1]!))),\n };\n}\n\n/** Verify a JWT's signature and claims. Throws {@link JwtError} on any failure. */\nexport async function verify<T extends JwtPayload = JwtPayload>(\n token: string,\n secret: string | Uint8Array,\n opts: VerifyOptions = {},\n): Promise<T> {\n const parts = token.split(\".\");\n if (parts.length !== 3) throw new JwtError(\"malformed token\", \"malformed\");\n const [h, p, s] = parts as [string, string, string];\n\n let header: { alg?: Algorithm };\n try {\n header = JSON.parse(dec.decode(fromBase64url(h)));\n } catch {\n throw new JwtError(\"malformed header\", \"malformed\");\n }\n const alg = header.alg;\n if (!alg || !(alg in ALG_HASH)) throw new JwtError(`unsupported algorithm ${alg}`, \"algorithm\");\n if (opts.algorithms && !opts.algorithms.includes(alg))\n throw new JwtError(`algorithm ${alg} not allowed`, \"algorithm\");\n\n const expected = await hmac(secret, `${h}.${p}`, ALG_HASH[alg]);\n if (!constantTimeEqual(expected, fromBase64url(s)))\n throw new JwtError(\"signature verification failed\", \"signature\");\n\n let payload: T;\n try {\n payload = JSON.parse(dec.decode(fromBase64url(p)));\n } catch {\n throw new JwtError(\"malformed payload\", \"malformed\");\n }\n\n const now = nowSec();\n const skew = opts.clockTolerance ?? 0;\n if (payload.exp !== undefined && now > payload.exp + skew)\n throw new JwtError(\"token expired\", \"expired\");\n if (payload.nbf !== undefined && now + skew < payload.nbf)\n throw new JwtError(\"token not yet active\", \"not_active\");\n if (opts.issuer && payload.iss !== opts.issuer)\n throw new JwtError(\"issuer mismatch\", \"issuer\");\n if (opts.audience) {\n const aud = Array.isArray(payload.aud) ? payload.aud : payload.aud ? [payload.aud] : [];\n if (!aud.includes(opts.audience)) throw new JwtError(\"audience mismatch\", \"audience\");\n }\n return payload;\n}\n\n/* ------------------------------ opaque tokens ------------------------------ */\n\n/** A cryptographically-random URL-safe token (default 32 bytes → 43 chars). */\nexport function randomToken(bytes = 32): string {\n return toBase64url(randomBytes(bytes));\n}\n\n/** A CSRF token (alias of a 32-byte random token). */\nexport function csrfToken(): string {\n return randomToken(32);\n}\n\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: string | Uint8Array,\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: string | Uint8Array,\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
|
@@ -51,5 +51,42 @@ declare function verify<T extends JwtPayload = JwtPayload>(token: string, secret
|
|
|
51
51
|
declare function randomToken(bytes?: number): string;
|
|
52
52
|
/** A CSRF token (alias of a 32-byte random token). */
|
|
53
53
|
declare function csrfToken(): string;
|
|
54
|
+
/** Anything we can read a header off: a Fetch Request/Headers, or Node `req.headers`. */
|
|
55
|
+
type HeadersLike = Request | Headers | {
|
|
56
|
+
headers: Headers | Record<string, string | string[] | undefined>;
|
|
57
|
+
} | Record<string, string | string[] | undefined>;
|
|
58
|
+
/** Extract a bearer token from an `Authorization` header (or a named cookie). */
|
|
59
|
+
declare function extractBearer(src: HeadersLike, opts?: {
|
|
60
|
+
cookieName?: string;
|
|
61
|
+
}): string | undefined;
|
|
62
|
+
/** Read the bearer token from a request and verify it. Throws {@link JwtError}. */
|
|
63
|
+
declare function authenticate<T extends JwtPayload = JwtPayload>(src: HeadersLike, secret: string | Uint8Array, opts?: VerifyOptions & {
|
|
64
|
+
cookieName?: string;
|
|
65
|
+
}): Promise<T>;
|
|
66
|
+
interface AuthCookieOptions {
|
|
67
|
+
name?: string;
|
|
68
|
+
maxAge?: number;
|
|
69
|
+
path?: string;
|
|
70
|
+
domain?: string;
|
|
71
|
+
secure?: boolean;
|
|
72
|
+
httpOnly?: boolean;
|
|
73
|
+
sameSite?: "Strict" | "Lax" | "None";
|
|
74
|
+
}
|
|
75
|
+
/** Build a hardened `Set-Cookie` value carrying an auth token. */
|
|
76
|
+
declare function toAuthCookie(token: string, opts?: AuthCookieOptions): string;
|
|
77
|
+
/** Build a `Set-Cookie` value that clears the auth cookie. */
|
|
78
|
+
declare function clearAuthCookie(opts?: Pick<AuthCookieOptions, "name" | "path" | "domain">): string;
|
|
79
|
+
/** Minimal Express-style middleware: verifies the bearer token → `req.user`, else 401. */
|
|
80
|
+
declare function expressJwt<T extends JwtPayload = JwtPayload>(secret: string | Uint8Array, opts?: VerifyOptions & {
|
|
81
|
+
cookieName?: string;
|
|
82
|
+
userKey?: string;
|
|
83
|
+
}): (req: {
|
|
84
|
+
headers: Record<string, string | string[] | undefined>;
|
|
85
|
+
[k: string]: unknown;
|
|
86
|
+
}, res: {
|
|
87
|
+
status: (n: number) => {
|
|
88
|
+
json: (b: unknown) => unknown;
|
|
89
|
+
};
|
|
90
|
+
}, next: (err?: unknown) => void) => Promise<void>;
|
|
54
91
|
|
|
55
|
-
export { type Algorithm, JwtError, type JwtPayload, type SignOptions, type VerifyOptions, csrfToken, decode, randomToken, sign, verify };
|
|
92
|
+
export { type Algorithm, type AuthCookieOptions, type HeadersLike, JwtError, type JwtPayload, type SignOptions, type VerifyOptions, authenticate, clearAuthCookie, csrfToken, decode, expressJwt, extractBearer, randomToken, sign, toAuthCookie, verify };
|
package/dist/index.d.ts
CHANGED
|
@@ -51,5 +51,42 @@ declare function verify<T extends JwtPayload = JwtPayload>(token: string, secret
|
|
|
51
51
|
declare function randomToken(bytes?: number): string;
|
|
52
52
|
/** A CSRF token (alias of a 32-byte random token). */
|
|
53
53
|
declare function csrfToken(): string;
|
|
54
|
+
/** Anything we can read a header off: a Fetch Request/Headers, or Node `req.headers`. */
|
|
55
|
+
type HeadersLike = Request | Headers | {
|
|
56
|
+
headers: Headers | Record<string, string | string[] | undefined>;
|
|
57
|
+
} | Record<string, string | string[] | undefined>;
|
|
58
|
+
/** Extract a bearer token from an `Authorization` header (or a named cookie). */
|
|
59
|
+
declare function extractBearer(src: HeadersLike, opts?: {
|
|
60
|
+
cookieName?: string;
|
|
61
|
+
}): string | undefined;
|
|
62
|
+
/** Read the bearer token from a request and verify it. Throws {@link JwtError}. */
|
|
63
|
+
declare function authenticate<T extends JwtPayload = JwtPayload>(src: HeadersLike, secret: string | Uint8Array, opts?: VerifyOptions & {
|
|
64
|
+
cookieName?: string;
|
|
65
|
+
}): Promise<T>;
|
|
66
|
+
interface AuthCookieOptions {
|
|
67
|
+
name?: string;
|
|
68
|
+
maxAge?: number;
|
|
69
|
+
path?: string;
|
|
70
|
+
domain?: string;
|
|
71
|
+
secure?: boolean;
|
|
72
|
+
httpOnly?: boolean;
|
|
73
|
+
sameSite?: "Strict" | "Lax" | "None";
|
|
74
|
+
}
|
|
75
|
+
/** Build a hardened `Set-Cookie` value carrying an auth token. */
|
|
76
|
+
declare function toAuthCookie(token: string, opts?: AuthCookieOptions): string;
|
|
77
|
+
/** Build a `Set-Cookie` value that clears the auth cookie. */
|
|
78
|
+
declare function clearAuthCookie(opts?: Pick<AuthCookieOptions, "name" | "path" | "domain">): string;
|
|
79
|
+
/** Minimal Express-style middleware: verifies the bearer token → `req.user`, else 401. */
|
|
80
|
+
declare function expressJwt<T extends JwtPayload = JwtPayload>(secret: string | Uint8Array, opts?: VerifyOptions & {
|
|
81
|
+
cookieName?: string;
|
|
82
|
+
userKey?: string;
|
|
83
|
+
}): (req: {
|
|
84
|
+
headers: Record<string, string | string[] | undefined>;
|
|
85
|
+
[k: string]: unknown;
|
|
86
|
+
}, res: {
|
|
87
|
+
status: (n: number) => {
|
|
88
|
+
json: (b: unknown) => unknown;
|
|
89
|
+
};
|
|
90
|
+
}, next: (err?: unknown) => void) => Promise<void>;
|
|
54
91
|
|
|
55
|
-
export { type Algorithm, JwtError, type JwtPayload, type SignOptions, type VerifyOptions, csrfToken, decode, randomToken, sign, verify };
|
|
92
|
+
export { type Algorithm, type AuthCookieOptions, type HeadersLike, JwtError, type JwtPayload, type SignOptions, type VerifyOptions, authenticate, clearAuthCookie, csrfToken, decode, expressJwt, extractBearer, randomToken, sign, toAuthCookie, verify };
|
package/dist/index.js
CHANGED
|
@@ -84,7 +84,62 @@ function randomToken(bytes = 32) {
|
|
|
84
84
|
function csrfToken() {
|
|
85
85
|
return randomToken(32);
|
|
86
86
|
}
|
|
87
|
+
function readHeader(src, name) {
|
|
88
|
+
const lower = name.toLowerCase();
|
|
89
|
+
const h = src.headers ?? src;
|
|
90
|
+
if (h && typeof h.get === "function") return h.get(name) ?? void 0;
|
|
91
|
+
const rec = h;
|
|
92
|
+
const v = rec[name] ?? rec[lower];
|
|
93
|
+
return Array.isArray(v) ? v[0] : v ?? void 0;
|
|
94
|
+
}
|
|
95
|
+
function extractBearer(src, opts = {}) {
|
|
96
|
+
const auth = readHeader(src, "authorization");
|
|
97
|
+
if (auth) {
|
|
98
|
+
const m = /^Bearer\s+(.+)$/i.exec(auth.trim());
|
|
99
|
+
if (m) return m[1];
|
|
100
|
+
}
|
|
101
|
+
if (opts.cookieName) {
|
|
102
|
+
const cookie = readHeader(src, "cookie");
|
|
103
|
+
if (cookie) {
|
|
104
|
+
const found = cookie.split(/;\s*/).find((c) => c.startsWith(`${opts.cookieName}=`));
|
|
105
|
+
if (found) return decodeURIComponent(found.slice(opts.cookieName.length + 1));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return void 0;
|
|
109
|
+
}
|
|
110
|
+
async function authenticate(src, secret, opts = {}) {
|
|
111
|
+
const token = extractBearer(src, { cookieName: opts.cookieName });
|
|
112
|
+
if (!token) throw new JwtError("no bearer token", "malformed");
|
|
113
|
+
return verify(token, secret, opts);
|
|
114
|
+
}
|
|
115
|
+
function toAuthCookie(token, opts = {}) {
|
|
116
|
+
const name = opts.name ?? "token";
|
|
117
|
+
const parts = [`${name}=${encodeURIComponent(token)}`, `Path=${opts.path ?? "/"}`, `SameSite=${opts.sameSite ?? "Lax"}`];
|
|
118
|
+
if (opts.maxAge !== void 0) parts.push(`Max-Age=${opts.maxAge}`);
|
|
119
|
+
if (opts.domain) parts.push(`Domain=${opts.domain}`);
|
|
120
|
+
if (opts.httpOnly !== false) parts.push("HttpOnly");
|
|
121
|
+
if (opts.secure !== false) parts.push("Secure");
|
|
122
|
+
return parts.join("; ");
|
|
123
|
+
}
|
|
124
|
+
function clearAuthCookie(opts = {}) {
|
|
125
|
+
const name = opts.name ?? "token";
|
|
126
|
+
const parts = [`${name}=`, `Path=${opts.path ?? "/"}`, "Max-Age=0"];
|
|
127
|
+
if (opts.domain) parts.push(`Domain=${opts.domain}`);
|
|
128
|
+
return parts.join("; ");
|
|
129
|
+
}
|
|
130
|
+
function expressJwt(secret, opts = {}) {
|
|
131
|
+
const userKey = opts.userKey ?? "user";
|
|
132
|
+
return async (req, res, next) => {
|
|
133
|
+
try {
|
|
134
|
+
req[userKey] = await authenticate(req, secret, opts);
|
|
135
|
+
next();
|
|
136
|
+
} catch (err) {
|
|
137
|
+
const code = err instanceof JwtError ? err.code : "malformed";
|
|
138
|
+
res.status(401).json({ error: "unauthorized", code });
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
}
|
|
87
142
|
|
|
88
|
-
export { JwtError, csrfToken, decode, randomToken, sign, verify };
|
|
143
|
+
export { JwtError, authenticate, clearAuthCookie, csrfToken, decode, expressJwt, extractBearer, randomToken, sign, toAuthCookie, verify };
|
|
89
144
|
//# sourceMappingURL=index.js.map
|
|
90
145
|
//# 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 = \"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\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: string | Uint8Array,\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: string | Uint8Array,\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
|
|
3
|
+
"version": "1.1.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",
|