peta-auth 0.1.3 → 0.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.
@@ -0,0 +1,35 @@
1
+ //#region src/crypto.d.ts
2
+ /** A password that can be a plain string or a versioned map. */
3
+ type Password = string | Record<string, string>;
4
+ /**
5
+ * Seal arbitrary data with a password (uses iron-webcrypto).
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * const sealed = await sealData({ userId: 1 }, { password: "my-secret-key" })
10
+ * ```
11
+ */
12
+ declare const sealData: (data: unknown, {
13
+ password,
14
+ ttl
15
+ }: {
16
+ password: Password;
17
+ ttl?: number;
18
+ }) => Promise<string>;
19
+ /**
20
+ * Unseal data previously sealed with {@link sealData}.
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * const data = await unsealData<{ userId: number }>(sealed, { password: "my-secret-key" })
25
+ * ```
26
+ */
27
+ declare const unsealData: <T>(seal: string, {
28
+ password,
29
+ ttl
30
+ }: {
31
+ password: Password;
32
+ ttl?: number;
33
+ }) => Promise<T>;
34
+ //#endregion
35
+ export { sealData as n, unsealData as r, Password as t };
@@ -0,0 +1,84 @@
1
+ import { defaults, seal, unseal } from "iron-webcrypto";
2
+ //#region src/crypto.ts
3
+ const SEVEN_DAYS = 336 * 3600;
4
+ const CURRENT_MAJOR_VERSION = 2;
5
+ const VERSION_DELIMITER = "~";
6
+ function normalizePassword(password) {
7
+ return typeof password === "string" ? { 1: password } : password;
8
+ }
9
+ function parseSeal(seal) {
10
+ const index = seal.lastIndexOf(VERSION_DELIMITER);
11
+ if (index === -1) return {
12
+ sealWithoutVersion: seal,
13
+ tokenVersion: null
14
+ };
15
+ return {
16
+ sealWithoutVersion: seal.slice(0, index),
17
+ tokenVersion: parseInt(seal.slice(index + 1), 10) || null
18
+ };
19
+ }
20
+ /**
21
+ * Create a `sealData` function.
22
+ *
23
+ * @internal
24
+ */
25
+ function createSealData() {
26
+ return async function sealData(data, { password, ttl = SEVEN_DAYS }) {
27
+ const map = normalizePassword(password);
28
+ const id = Math.max(...Object.keys(map).map(Number)).toString();
29
+ const secret = map[id];
30
+ return `${await seal(data, {
31
+ id,
32
+ secret
33
+ }, {
34
+ ...defaults,
35
+ ttl: ttl * 1e3,
36
+ encode: JSON.stringify,
37
+ decode: JSON.parse
38
+ })}${VERSION_DELIMITER}${CURRENT_MAJOR_VERSION}`;
39
+ };
40
+ }
41
+ /**
42
+ * Create an `unsealData` function.
43
+ *
44
+ * @internal
45
+ */
46
+ function createUnsealData() {
47
+ return async function unsealData(seal, { password, ttl = SEVEN_DAYS }) {
48
+ const map = normalizePassword(password);
49
+ const { sealWithoutVersion, tokenVersion } = parseSeal(seal);
50
+ try {
51
+ const data = await unseal(sealWithoutVersion, map, {
52
+ ...defaults,
53
+ ttl: ttl * 1e3,
54
+ encode: JSON.stringify,
55
+ decode: JSON.parse
56
+ });
57
+ if (tokenVersion === 2) return data;
58
+ return { ...data?.persistent ? { ...data.persistent } : {} };
59
+ } catch (err) {
60
+ if (err instanceof Error && /^(Expired seal|Bad hmac value|Cannot find password|Incorrect number of sealed components)/.test(err.message)) return {};
61
+ throw err;
62
+ }
63
+ };
64
+ }
65
+ /**
66
+ * Seal arbitrary data with a password (uses iron-webcrypto).
67
+ *
68
+ * @example
69
+ * ```ts
70
+ * const sealed = await sealData({ userId: 1 }, { password: "my-secret-key" })
71
+ * ```
72
+ */
73
+ const sealData = createSealData();
74
+ /**
75
+ * Unseal data previously sealed with {@link sealData}.
76
+ *
77
+ * @example
78
+ * ```ts
79
+ * const data = await unsealData<{ userId: number }>(sealed, { password: "my-secret-key" })
80
+ * ```
81
+ */
82
+ const unsealData = createUnsealData();
83
+ //#endregion
84
+ export { sealData as n, unsealData as r, normalizePassword as t };
package/dist/csrf.d.mts CHANGED
@@ -1,10 +1,39 @@
1
- import { t as IronSession } from "./session-z20gaFVT.mjs";
1
+ import { t as IronSession } from "./session-0bF8_7Ui.mjs";
2
2
 
3
3
  //#region src/csrf.d.ts
4
+ /** Options for CSRF token generation / validation. */
4
5
  interface CSRFOptions {
6
+ /** Key used to store the token in the session (default `"_csrfToken"`). */
5
7
  key?: string;
6
8
  }
9
+ /**
10
+ * Constant-time string comparison to prevent timing side-channel attacks.
11
+ * Always iterates over the full length of the input, regardless of where
12
+ * the first difference occurs.
13
+ */
14
+ declare function constantTimeEqual(a: string, b: string): boolean;
15
+ /**
16
+ * Generate a CSRF token and store it in the session.
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * const token = await generateCsrf(session)
21
+ * // send `token` to the client via form field or header
22
+ * ```
23
+ */
7
24
  declare function generateCsrf(session: IronSession, options?: CSRFOptions): Promise<string>;
25
+ /**
26
+ * Validate a CSRF token against the value stored in the session.
27
+ *
28
+ * Uses constant-time comparison to prevent timing side-channel attacks.
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * if (!validateCsrf(session, submittedToken)) {
33
+ * // reject request
34
+ * }
35
+ * ```
36
+ */
8
37
  declare function validateCsrf(session: IronSession, token: string, options?: CSRFOptions): boolean;
9
38
  //#endregion
10
- export { CSRFOptions, generateCsrf, validateCsrf };
39
+ export { CSRFOptions, constantTimeEqual, generateCsrf, validateCsrf };
package/dist/csrf.mjs CHANGED
@@ -1,13 +1,45 @@
1
1
  //#region src/csrf.ts
2
+ /**
3
+ * Constant-time string comparison to prevent timing side-channel attacks.
4
+ * Always iterates over the full length of the input, regardless of where
5
+ * the first difference occurs.
6
+ */
7
+ function constantTimeEqual(a, b) {
8
+ if (a.length !== b.length) return false;
9
+ let result = 0;
10
+ for (let i = 0; i < a.length; i++) result |= a.charCodeAt(i) ^ b.charCodeAt(i);
11
+ return result === 0;
12
+ }
13
+ /**
14
+ * Generate a CSRF token and store it in the session.
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * const token = await generateCsrf(session)
19
+ * // send `token` to the client via form field or header
20
+ * ```
21
+ */
2
22
  async function generateCsrf(session, options) {
3
23
  const key = options?.key ?? "_csrfToken";
4
24
  const token = crypto.randomUUID();
5
25
  session[key] = token;
6
26
  return token;
7
27
  }
28
+ /**
29
+ * Validate a CSRF token against the value stored in the session.
30
+ *
31
+ * Uses constant-time comparison to prevent timing side-channel attacks.
32
+ *
33
+ * @example
34
+ * ```ts
35
+ * if (!validateCsrf(session, submittedToken)) {
36
+ * // reject request
37
+ * }
38
+ * ```
39
+ */
8
40
  function validateCsrf(session, token, options) {
9
41
  const stored = session[options?.key ?? "_csrfToken"];
10
- return typeof stored === "string" && stored === token;
42
+ return typeof stored === "string" && constantTimeEqual(stored, token);
11
43
  }
12
44
  //#endregion
13
- export { generateCsrf, validateCsrf };
45
+ export { constantTimeEqual, generateCsrf, validateCsrf };
package/dist/elysia.d.mts CHANGED
@@ -1,7 +1,16 @@
1
- import { r as SessionOptions, t as IronSession } from "./session-z20gaFVT.mjs";
1
+ import { r as SessionOptions, t as IronSession } from "./session-0bF8_7Ui.mjs";
2
2
  import { Elysia } from "elysia";
3
3
 
4
4
  //#region src/elysia.d.ts
5
+ /**
6
+ * Elysia plugin that provides a session via the `session` store property.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * app.use(session({ password: "...", cookieName: "my-session" }))
11
+ * app.get("/me", ({ session }) => session)
12
+ * ```
13
+ */
5
14
  declare function session<T extends Record<string, unknown> = Record<string, unknown>>(options: SessionOptions): Elysia<"", {
6
15
  decorator: {};
7
16
  store: {};
@@ -34,6 +43,18 @@ declare function session<T extends Record<string, unknown> = Record<string, unkn
34
43
  standaloneSchema: {};
35
44
  response: {};
36
45
  }>;
46
+ /**
47
+ * Elysia guard (onBeforeHandle) that requires session data.
48
+ *
49
+ * Returns 401 when the session is empty.
50
+ *
51
+ * @example
52
+ * ```ts
53
+ * app.guard({ beforeHandle: requireSession() }, (app) =>
54
+ * app.get("/admin", () => "ok")
55
+ * )
56
+ * ```
57
+ */
37
58
  declare function requireSession(): (app: Elysia) => Elysia;
38
59
  declare function requireSession<K extends string>(key: K): (app: Elysia) => Elysia;
39
60
  //#endregion
package/dist/elysia.mjs CHANGED
@@ -1,14 +1,23 @@
1
- import { t as createSessionFromAdapter } from "./session-DSwf3XPH.mjs";
1
+ import { n as sessionHasData, t as createSessionFromAdapter } from "./session-BGCQ1Z1Q.mjs";
2
2
  import { parse } from "cookie";
3
3
  import { Elysia } from "elysia";
4
4
  //#region src/elysia.ts
5
+ /**
6
+ * Elysia plugin that provides a session via the `session` store property.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * app.use(session({ password: "...", cookieName: "my-session" }))
11
+ * app.get("/me", ({ session }) => session)
12
+ * ```
13
+ */
5
14
  function session(options) {
6
- return new Elysia({ name: "peta-auth" }).derive({ as: "scoped" }, async ({ headers: reqHeaders, set }) => {
7
- const cookieStr = reqHeaders instanceof Headers ? reqHeaders.get("cookie") ?? "" : reqHeaders.cookie ?? "";
15
+ return new Elysia({ name: "peta-auth" }).derive({ as: "scoped" }, async ({ headers, set }) => {
16
+ const cookieString = headers instanceof Headers ? headers.get("cookie") ?? "" : headers.cookie ?? "";
8
17
  return { session: await createSessionFromAdapter({
9
- getCookie: (name) => parse(cookieStr)[name],
10
- setCookie: (v) => {
11
- set.headers["Set-Cookie"] = v;
18
+ getCookie: (name) => parse(cookieString)[name],
19
+ setCookie: (value) => {
20
+ set.headers["Set-Cookie"] = value;
12
21
  }
13
22
  }, options) };
14
23
  });
@@ -16,7 +25,7 @@ function session(options) {
16
25
  function requireSession(key) {
17
26
  return (app) => app.onBeforeHandle((context) => {
18
27
  const session = context.session;
19
- if (!(key ? !!session[key] : Object.keys(session).some((k) => k !== "save" && k !== "destroy" && k !== "updateConfig"))) return new Response(JSON.stringify({ error: "unauthorized" }), {
28
+ if (!sessionHasData(session, key)) return new Response(JSON.stringify({ error: "unauthorized" }), {
20
29
  status: 401,
21
30
  headers: { "Content-Type": "application/json" }
22
31
  });
@@ -0,0 +1,17 @@
1
+ //#region src/errors.ts
2
+ /**
3
+ * Typed error for peta-auth.
4
+ *
5
+ * Carries a machine-readable `code` and a human-readable `message`.
6
+ * Thrown instead of raw `new Error(...)` throughout the library.
7
+ */
8
+ var PetaAuthError = class extends Error {
9
+ code;
10
+ constructor(code, message) {
11
+ super(message);
12
+ this.name = "PetaAuthError";
13
+ this.code = code;
14
+ }
15
+ };
16
+ //#endregion
17
+ export { PetaAuthError as t };
package/dist/hono.d.mts CHANGED
@@ -1,12 +1,33 @@
1
- import { r as SessionOptions, t as IronSession } from "./session-z20gaFVT.mjs";
1
+ import { r as SessionOptions, t as IronSession } from "./session-0bF8_7Ui.mjs";
2
2
  import { MiddlewareHandler } from "hono";
3
3
 
4
4
  //#region src/hono.d.ts
5
+ /**
6
+ * Hono middleware that creates a session and makes it available
7
+ * via `c.var.session`.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * app.use("*", session({ password: "...", cookieName: "my-session" }))
12
+ * app.get("/me", (c) => c.json(c.var.session))
13
+ * ```
14
+ */
5
15
  declare function session<T extends Record<string, unknown> = Record<string, unknown>>(options: SessionOptions): MiddlewareHandler<{
6
16
  Variables: {
7
17
  session: T & IronSession;
8
18
  };
9
19
  }>;
20
+ /**
21
+ * Hono middleware that guards a route by requiring session data.
22
+ *
23
+ * Returns 401 when the session is empty.
24
+ *
25
+ * @example
26
+ * ```ts
27
+ * app.use("/admin", requireSession())
28
+ * app.use("/admin", requireSession("role"))
29
+ * ```
30
+ */
10
31
  declare function requireSession(): MiddlewareHandler;
11
32
  declare function requireSession<K extends string>(key: K): MiddlewareHandler;
12
33
  //#endregion
package/dist/hono.mjs CHANGED
@@ -1,12 +1,22 @@
1
- import { t as createSessionFromAdapter } from "./session-DSwf3XPH.mjs";
1
+ import { n as sessionHasData, t as createSessionFromAdapter } from "./session-BGCQ1Z1Q.mjs";
2
2
  import { parse } from "cookie";
3
3
  import { createMiddleware } from "hono/factory";
4
4
  //#region src/hono.ts
5
+ /**
6
+ * Hono middleware that creates a session and makes it available
7
+ * via `c.var.session`.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * app.use("*", session({ password: "...", cookieName: "my-session" }))
12
+ * app.get("/me", (c) => c.json(c.var.session))
13
+ * ```
14
+ */
5
15
  function session(options) {
6
16
  return createMiddleware(async (c, next) => {
7
17
  c.set("session", await createSessionFromAdapter({
8
18
  getCookie: (name) => parse(c.req.header("cookie") ?? "")[name],
9
- setCookie: (v) => c.res.headers.append("Set-Cookie", v)
19
+ setCookie: (value) => c.res.headers.append("Set-Cookie", value)
10
20
  }, options));
11
21
  await next();
12
22
  });
@@ -14,7 +24,7 @@ function session(options) {
14
24
  function requireSession(key) {
15
25
  return createMiddleware(async (c, next) => {
16
26
  const s = c.var.session;
17
- if (!(key ? !!s[key] : Object.keys(s).some((k) => k !== "save" && k !== "destroy" && k !== "updateConfig"))) return c.json({ error: "unauthorized" }, 401);
27
+ if (!sessionHasData(s, key)) return c.json({ error: "unauthorized" }, 401);
18
28
  await next();
19
29
  });
20
30
  }
package/dist/index.d.mts CHANGED
@@ -1,27 +1,81 @@
1
- import { n as sealData, r as unsealData, t as Password } from "./crypto-Ln_Mj_zp.mjs";
2
- import { i as createSessionFromAdapter, n as SessionAdapter, r as SessionOptions, t as IronSession } from "./session-z20gaFVT.mjs";
1
+ import { n as sealData, r as unsealData, t as Password } from "./crypto-DR-ETdLZ.mjs";
2
+ import { i as createSessionFromAdapter, n as SessionAdapter, r as SessionOptions, t as IronSession } from "./session-0bF8_7Ui.mjs";
3
3
  import { CSRFOptions, generateCsrf, validateCsrf } from "./csrf.mjs";
4
4
  import { JWTOptions, signJWT, verifyJWT } from "./jwt.mjs";
5
5
 
6
+ //#region src/errors.d.ts
7
+ /**
8
+ * Typed error for peta-auth.
9
+ *
10
+ * Carries a machine-readable `code` and a human-readable `message`.
11
+ * Thrown instead of raw `new Error(...)` throughout the library.
12
+ */
13
+ declare class PetaAuthError extends Error {
14
+ readonly code: string;
15
+ constructor(code: string, message: string);
16
+ }
17
+ //#endregion
6
18
  //#region src/password.d.ts
7
19
  interface HashOptions {
8
- cost?: number;
20
+ /** Memory cost in KiB (default: 19456 = 19 MiB). */
21
+ memoryCost?: number;
22
+ /** Time cost (iterations) (default: 2). */
23
+ timeCost?: number;
24
+ /** Parallelism (default: 1). */
25
+ parallelism?: number;
9
26
  }
27
+ /**
28
+ * Hash a password with argon2id.
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * const hash = await hashPassword("my-password")
33
+ * ```
34
+ */
10
35
  declare function hashPassword(password: string, options?: HashOptions): Promise<string>;
36
+ /**
37
+ * Verify a password against an argon2id hash.
38
+ *
39
+ * @example
40
+ * ```ts
41
+ * const ok = await verifyPassword(hash, "my-password")
42
+ * ```
43
+ */
11
44
  declare function verifyPassword(hash: string, password: string): Promise<boolean>;
12
45
  //#endregion
13
46
  //#region src/reset-password.d.ts
47
+ /** Options for password reset token generation. */
14
48
  interface PasswordResetOptions {
49
+ /** Password(s) used to sign the reset token. */
15
50
  password: Password;
16
- exp?: number;
51
+ /** Token lifetime in seconds (default 1 hour). */
52
+ expiresIn?: number;
17
53
  }
54
+ /**
55
+ * Create a password-reset token for a user.
56
+ *
57
+ * @example
58
+ * ```ts
59
+ * const token = await createPasswordResetToken(userId, { password: "..." })
60
+ * ```
61
+ */
18
62
  declare function createPasswordResetToken(userId: string, options: PasswordResetOptions): Promise<string>;
63
+ /**
64
+ * Verify a password-reset token.
65
+ *
66
+ * Returns the user ID when the token is valid, or `null` if expired/invalid.
67
+ */
19
68
  declare function verifyPasswordResetToken(token: string, password: Password): Promise<{
20
69
  userId: string;
21
70
  } | null>;
71
+ /**
72
+ * Verify a password-reset token and apply the new password.
73
+ *
74
+ * Returns `{ userId, hash }` on success, or `null` if the token is invalid.
75
+ */
22
76
  declare function resetPassword(token: string, newPassword: string, password: Password): Promise<{
23
77
  userId: string;
24
78
  hash: string;
25
79
  } | null>;
26
80
  //#endregion
27
- export { type CSRFOptions, type IronSession, type JWTOptions, type Password, type PasswordResetOptions, type SessionAdapter, type SessionOptions, createPasswordResetToken, createSessionFromAdapter, generateCsrf, hashPassword, resetPassword, sealData, signJWT, unsealData, validateCsrf, verifyJWT, verifyPassword, verifyPasswordResetToken };
81
+ export { type CSRFOptions, type IronSession, type JWTOptions, type Password, type PasswordResetOptions, PetaAuthError, type SessionAdapter, type SessionOptions, createPasswordResetToken, createSessionFromAdapter, generateCsrf, hashPassword, resetPassword, sealData, signJWT, unsealData, validateCsrf, verifyJWT, verifyPassword, verifyPasswordResetToken };
package/dist/index.mjs CHANGED
@@ -1,31 +1,79 @@
1
- import { n as sealData, r as unsealData, t as createSessionFromAdapter } from "./session-DSwf3XPH.mjs";
1
+ import { n as sealData, r as unsealData } from "./crypto-WcFV83Nz.mjs";
2
2
  import { generateCsrf, validateCsrf } from "./csrf.mjs";
3
+ import { t as PetaAuthError } from "./errors-DxJ-WUJL.mjs";
3
4
  import { signJWT, verifyJWT } from "./jwt.mjs";
4
- import { compareSync, genSaltSync, hashSync } from "bcryptjs";
5
+ import { t as createSessionFromAdapter } from "./session-BGCQ1Z1Q.mjs";
6
+ import { hash, verify } from "@node-rs/argon2";
5
7
  //#region src/password.ts
8
+ const ARGON2_MEMORY_COST = 19456;
9
+ const ARGON2_TIME_COST = 2;
10
+ const ARGON2_PARALLELISM = 1;
11
+ /**
12
+ * Hash a password with argon2id.
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * const hash = await hashPassword("my-password")
17
+ * ```
18
+ */
6
19
  async function hashPassword(password, options = {}) {
7
- return hashSync(password, genSaltSync(options.cost ?? 10));
20
+ return hash(password, {
21
+ algorithm: 2,
22
+ memoryCost: options.memoryCost ?? ARGON2_MEMORY_COST,
23
+ timeCost: options.timeCost ?? ARGON2_TIME_COST,
24
+ parallelism: options.parallelism ?? ARGON2_PARALLELISM
25
+ });
8
26
  }
27
+ /**
28
+ * Verify a password against an argon2id hash.
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * const ok = await verifyPassword(hash, "my-password")
33
+ * ```
34
+ */
9
35
  async function verifyPassword(hash, password) {
10
- return compareSync(password, hash);
36
+ try {
37
+ return await verify(hash, password);
38
+ } catch {
39
+ return false;
40
+ }
11
41
  }
12
42
  //#endregion
13
43
  //#region src/reset-password.ts
14
- const DEFAULT_EXPIRY = 3600;
44
+ const DEFAULT_EXPIRES_IN = 3600;
45
+ /**
46
+ * Create a password-reset token for a user.
47
+ *
48
+ * @example
49
+ * ```ts
50
+ * const token = await createPasswordResetToken(userId, { password: "..." })
51
+ * ```
52
+ */
15
53
  async function createPasswordResetToken(userId, options) {
16
54
  return signJWT({
17
55
  userId,
18
56
  purpose: "password-reset"
19
57
  }, {
20
58
  password: options.password,
21
- exp: options.exp ?? DEFAULT_EXPIRY
59
+ expiresIn: options.expiresIn ?? DEFAULT_EXPIRES_IN
22
60
  });
23
61
  }
62
+ /**
63
+ * Verify a password-reset token.
64
+ *
65
+ * Returns the user ID when the token is valid, or `null` if expired/invalid.
66
+ */
24
67
  async function verifyPasswordResetToken(token, password) {
25
68
  const payload = await verifyJWT(token, { password });
26
- if (!payload || payload.purpose !== "password-reset") return null;
69
+ if (payload?.purpose !== "password-reset") return null;
27
70
  return { userId: payload.userId };
28
71
  }
72
+ /**
73
+ * Verify a password-reset token and apply the new password.
74
+ *
75
+ * Returns `{ userId, hash }` on success, or `null` if the token is invalid.
76
+ */
29
77
  async function resetPassword(token, newPassword, password) {
30
78
  const payload = await verifyPasswordResetToken(token, password);
31
79
  if (!payload) return null;
@@ -35,4 +83,4 @@ async function resetPassword(token, newPassword, password) {
35
83
  };
36
84
  }
37
85
  //#endregion
38
- export { createPasswordResetToken, createSessionFromAdapter, generateCsrf, hashPassword, resetPassword, sealData, signJWT, unsealData, validateCsrf, verifyJWT, verifyPassword, verifyPasswordResetToken };
86
+ export { PetaAuthError, createPasswordResetToken, createSessionFromAdapter, generateCsrf, hashPassword, resetPassword, sealData, signJWT, unsealData, validateCsrf, verifyJWT, verifyPassword, verifyPasswordResetToken };
package/dist/jwt.d.mts CHANGED
@@ -1,11 +1,32 @@
1
- import { t as Password } from "./crypto-Ln_Mj_zp.mjs";
1
+ import { t as Password } from "./crypto-DR-ETdLZ.mjs";
2
2
 
3
3
  //#region src/jwt.d.ts
4
+ /** Options for JWT sign / verify operations. */
4
5
  interface JWTOptions {
6
+ /** Password used to sign the JWT. */
5
7
  password: Password;
6
- exp?: number;
8
+ /** Time-to-live in seconds from now. */
9
+ expiresIn?: number;
7
10
  }
11
+ /**
12
+ * Sign a JWT payload.
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * const token = await signJWT({ userId: "abc" }, { password: "my-32-char-secret...", expiresIn: 3600 })
17
+ * ```
18
+ */
8
19
  declare function signJWT(payload: Record<string, unknown>, options: JWTOptions): Promise<string>;
20
+ /**
21
+ * Verify and decode a JWT.
22
+ *
23
+ * Returns `null` when the token is invalid or expired.
24
+ *
25
+ * @example
26
+ * ```ts
27
+ * const payload = await verifyJWT<{ userId: string }>(token, { password: "my-32-char-secret..." })
28
+ * ```
29
+ */
9
30
  declare function verifyJWT<T = Record<string, unknown>>(token: string, options: JWTOptions): Promise<T | null>;
10
31
  //#endregion
11
32
  export { JWTOptions, signJWT, verifyJWT };
package/dist/jwt.mjs CHANGED
@@ -1,28 +1,48 @@
1
+ import { t as normalizePassword } from "./crypto-WcFV83Nz.mjs";
2
+ import { t as PetaAuthError } from "./errors-DxJ-WUJL.mjs";
1
3
  import * as jose from "jose";
2
4
  //#region src/jwt.ts
3
- function toPasswordMap(password) {
4
- return typeof password === "string" ? { 1: password } : password;
5
- }
6
5
  function toKey(secret) {
7
6
  return new TextEncoder().encode(secret);
8
7
  }
8
+ /**
9
+ * Sign a JWT payload.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * const token = await signJWT({ userId: "abc" }, { password: "my-32-char-secret...", expiresIn: 3600 })
14
+ * ```
15
+ */
9
16
  async function signJWT(payload, options) {
10
- const map = toPasswordMap(options.password);
17
+ const map = normalizePassword(options.password);
11
18
  const secret = map[Math.max(...Object.keys(map).map(Number)).toString()];
12
- if (!secret || secret.length < 32) throw new Error("peta-auth/jwt: password must be at least 32 characters");
19
+ if (!secret || secret.length < 32) throw new PetaAuthError("JWT_PASSWORD_TOO_SHORT", "peta-auth/jwt: password must be at least 32 characters");
13
20
  const jwt = new jose.SignJWT(payload).setProtectedHeader({ alg: "HS256" }).setIssuedAt();
14
- if (options.exp !== void 0) jwt.setExpirationTime(Math.floor(Date.now() / 1e3) + options.exp);
21
+ const ttl = options.expiresIn ?? 86400;
22
+ jwt.setExpirationTime(Math.floor(Date.now() / 1e3) + ttl);
15
23
  return jwt.sign(toKey(secret));
16
24
  }
25
+ /**
26
+ * Verify and decode a JWT.
27
+ *
28
+ * Returns `null` when the token is invalid or expired.
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * const payload = await verifyJWT<{ userId: string }>(token, { password: "my-32-char-secret..." })
33
+ * ```
34
+ */
17
35
  async function verifyJWT(token, options) {
18
- for (const secret of Object.values(toPasswordMap(options.password))) {
36
+ let result = null;
37
+ const passwords = normalizePassword(options.password);
38
+ for (const secret of Object.values(passwords)) {
19
39
  if (!secret) continue;
20
40
  try {
21
41
  const { payload } = await jose.jwtVerify(token, toKey(secret));
22
- return payload;
42
+ if (!result) result = payload;
23
43
  } catch {}
24
44
  }
25
- return null;
45
+ return result;
26
46
  }
27
47
  //#endregion
28
48
  export { signJWT, verifyJWT };