peta-auth 0.2.2 → 0.3.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.
@@ -9,13 +9,13 @@ type Password = string | Record<string, string>;
9
9
  * const sealed = await sealData({ userId: 1 }, { password: "my-secret-key" })
10
10
  * ```
11
11
  */
12
- declare const sealData: (data: unknown, {
12
+ declare function sealData(data: unknown, {
13
13
  password,
14
14
  ttl
15
15
  }: {
16
16
  password: Password;
17
17
  ttl?: number;
18
- }) => Promise<string>;
18
+ }): Promise<string>;
19
19
  /**
20
20
  * Unseal data previously sealed with {@link sealData}.
21
21
  *
@@ -24,12 +24,12 @@ declare const sealData: (data: unknown, {
24
24
  * const data = await unsealData<{ userId: number }>(sealed, { password: "my-secret-key" })
25
25
  * ```
26
26
  */
27
- declare const unsealData: <T>(seal: string, {
27
+ declare function unsealData<T>(seal: string, {
28
28
  password,
29
29
  ttl
30
30
  }: {
31
31
  password: Password;
32
32
  ttl?: number;
33
- }) => Promise<T>;
33
+ }): Promise<T>;
34
34
  //#endregion
35
35
  export { sealData as n, unsealData as r, Password as t };
@@ -0,0 +1,54 @@
1
+ import { t as PetaAuthError } from "./errors-DxJ-WUJL.mjs";
2
+ import { defaults, seal, unseal } from "iron-webcrypto";
3
+ //#region src/crypto.ts
4
+ const SEVEN_DAYS = 336 * 3600;
5
+ function normalizePassword(password) {
6
+ return typeof password === "string" ? { 1: password } : password;
7
+ }
8
+ /**
9
+ * Seal arbitrary data with a password (uses iron-webcrypto).
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * const sealed = await sealData({ userId: 1 }, { password: "my-secret-key" })
14
+ * ```
15
+ */
16
+ async function sealData(data, { password, ttl = SEVEN_DAYS }) {
17
+ const map = normalizePassword(password);
18
+ const id = Math.max(...Object.keys(map).map(Number)).toString();
19
+ const secret = map[id];
20
+ if (secret.length < 32) throw new PetaAuthError("PASSWORD_TOO_SHORT", "peta-auth: password must be at least 32 characters");
21
+ return await seal(data, {
22
+ id,
23
+ secret
24
+ }, {
25
+ ...defaults,
26
+ ttl: ttl * 1e3,
27
+ encode: JSON.stringify,
28
+ decode: JSON.parse
29
+ });
30
+ }
31
+ /**
32
+ * Unseal data previously sealed with {@link sealData}.
33
+ *
34
+ * @example
35
+ * ```ts
36
+ * const data = await unsealData<{ userId: number }>(sealed, { password: "my-secret-key" })
37
+ * ```
38
+ */
39
+ async function unsealData(seal, { password, ttl = SEVEN_DAYS }) {
40
+ const map = normalizePassword(password);
41
+ try {
42
+ return await unseal(seal, map, {
43
+ ...defaults,
44
+ ttl: ttl * 1e3,
45
+ encode: JSON.stringify,
46
+ decode: JSON.parse
47
+ });
48
+ } catch (err) {
49
+ if (err instanceof Error && /^(Expired seal|Bad hmac value|Cannot find password|Incorrect number of sealed components)/.test(err.message)) return {};
50
+ throw err;
51
+ }
52
+ }
53
+ //#endregion
54
+ export { sealData as n, unsealData as r, normalizePassword as t };
package/dist/csrf.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { t as IronSession } from "./session-0bF8_7Ui.mjs";
1
+ import { t as IronSession } from "./session-CrAg3ZdE.mjs";
2
2
 
3
3
  //#region src/csrf.d.ts
4
4
  /** Options for CSRF token generation / validation. */
@@ -21,7 +21,7 @@ declare function constantTimeEqual(a: string, b: string): boolean;
21
21
  * // send `token` to the client via form field or header
22
22
  * ```
23
23
  */
24
- declare function generateCsrf(session: IronSession, options?: CSRFOptions): Promise<string>;
24
+ declare function generateCsrf(session: IronSession<Record<string, unknown>>, options?: CSRFOptions): Promise<string>;
25
25
  /**
26
26
  * Validate a CSRF token against the value stored in the session.
27
27
  *
@@ -34,6 +34,6 @@ declare function generateCsrf(session: IronSession, options?: CSRFOptions): Prom
34
34
  * }
35
35
  * ```
36
36
  */
37
- declare function validateCsrf(session: IronSession, token: string, options?: CSRFOptions): boolean;
37
+ declare function validateCsrf(session: IronSession<Record<string, unknown>>, token: string, options?: CSRFOptions): boolean;
38
38
  //#endregion
39
39
  export { CSRFOptions, constantTimeEqual, generateCsrf, validateCsrf };
package/dist/elysia.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { r as SessionOptions, t as IronSession } from "./session-0bF8_7Ui.mjs";
1
+ import { r as SessionOptions, t as IronSession } from "./session-CrAg3ZdE.mjs";
2
2
  import { Elysia } from "elysia";
3
3
 
4
4
  //#region src/elysia.d.ts
@@ -28,13 +28,13 @@ declare function session<T extends Record<string, unknown> = Record<string, unkn
28
28
  response: {};
29
29
  }, {}, {
30
30
  derive: {
31
- readonly session: T & IronSession;
31
+ readonly session: IronSession<T>;
32
32
  };
33
33
  resolve: {};
34
34
  schema: {};
35
35
  standaloneSchema: {};
36
36
  response: import("elysia").ExtractErrorFromHandle<{
37
- readonly session: T & IronSession;
37
+ readonly session: IronSession<T>;
38
38
  }>;
39
39
  }, {
40
40
  derive: {};
package/dist/elysia.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { n as sessionHasData, t as createSessionFromAdapter } from "./session-BGCQ1Z1Q.mjs";
1
+ import { n as sessionHasData, t as createSessionFromAdapter } from "./session-C1USm4OA.mjs";
2
2
  import { parse } from "cookie";
3
3
  import { Elysia } from "elysia";
4
4
  //#region src/elysia.ts
@@ -23,13 +23,16 @@ function session(options) {
23
23
  });
24
24
  }
25
25
  function requireSession(key) {
26
- return (app) => app.onBeforeHandle((context) => {
27
- const session = context.session;
28
- if (!sessionHasData(session, key)) return new Response(JSON.stringify({ error: "unauthorized" }), {
29
- status: 401,
30
- headers: { "Content-Type": "application/json" }
26
+ return (app) => {
27
+ app.onBeforeHandle((context) => {
28
+ const session = context.session;
29
+ if (!sessionHasData(session, key)) return new Response(JSON.stringify({ error: "unauthorized" }), {
30
+ status: 401,
31
+ headers: { "Content-Type": "application/json" }
32
+ });
31
33
  });
32
- });
34
+ return app;
35
+ };
33
36
  }
34
37
  //#endregion
35
38
  export { requireSession, session };
package/dist/hono.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { r as SessionOptions, t as IronSession } from "./session-0bF8_7Ui.mjs";
1
+ import { r as SessionOptions, t as IronSession } from "./session-CrAg3ZdE.mjs";
2
2
  import { MiddlewareHandler } from "hono";
3
3
 
4
4
  //#region src/hono.d.ts
@@ -14,7 +14,7 @@ import { MiddlewareHandler } from "hono";
14
14
  */
15
15
  declare function session<T extends Record<string, unknown> = Record<string, unknown>>(options: SessionOptions): MiddlewareHandler<{
16
16
  Variables: {
17
- session: T & IronSession;
17
+ session: IronSession<T>;
18
18
  };
19
19
  }>;
20
20
  /**
package/dist/hono.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { n as sessionHasData, t as createSessionFromAdapter } from "./session-BGCQ1Z1Q.mjs";
1
+ import { n as sessionHasData, t as createSessionFromAdapter } from "./session-C1USm4OA.mjs";
2
2
  import { parse } from "cookie";
3
3
  import { createMiddleware } from "hono/factory";
4
4
  //#region src/hono.ts
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
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";
1
+ import { n as sealData, r as unsealData, t as Password } from "./crypto-BPawHril.mjs";
2
+ import { i as createSessionFromAdapter, n as SessionAdapter, r as SessionOptions, t as IronSession } from "./session-CrAg3ZdE.mjs";
3
3
  import { CSRFOptions, generateCsrf, validateCsrf } from "./csrf.mjs";
4
4
  import { JWTOptions, signJWT, verifyJWT } from "./jwt.mjs";
5
5
 
@@ -16,14 +16,6 @@ declare class PetaAuthError extends Error {
16
16
  }
17
17
  //#endregion
18
18
  //#region src/password.d.ts
19
- interface HashOptions {
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;
26
- }
27
19
  /**
28
20
  * Hash a password with argon2id.
29
21
  *
@@ -32,7 +24,11 @@ interface HashOptions {
32
24
  * const hash = await hashPassword("my-password")
33
25
  * ```
34
26
  */
35
- declare function hashPassword(password: string, options?: HashOptions): Promise<string>;
27
+ declare function hashPassword(password: string, options?: {
28
+ memoryCost?: number;
29
+ timeCost?: number;
30
+ parallelism?: number;
31
+ }): Promise<string>;
36
32
  /**
37
33
  * Verify a password against an argon2id hash.
38
34
  *
package/dist/index.mjs CHANGED
@@ -1,8 +1,8 @@
1
- import { n as sealData, r as unsealData } from "./crypto-WcFV83Nz.mjs";
2
- import { generateCsrf, validateCsrf } from "./csrf.mjs";
3
1
  import { t as PetaAuthError } from "./errors-DxJ-WUJL.mjs";
2
+ import { n as sealData, r as unsealData } from "./crypto-Bwut3uFR.mjs";
3
+ import { generateCsrf, validateCsrf } from "./csrf.mjs";
4
4
  import { signJWT, verifyJWT } from "./jwt.mjs";
5
- import { t as createSessionFromAdapter } from "./session-BGCQ1Z1Q.mjs";
5
+ import { t as createSessionFromAdapter } from "./session-C1USm4OA.mjs";
6
6
  import { hash, verify } from "@node-rs/argon2";
7
7
  //#region src/password.ts
8
8
  const ARGON2_MEMORY_COST = 19456;
package/dist/jwt.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { t as Password } from "./crypto-DR-ETdLZ.mjs";
1
+ import { t as Password } from "./crypto-BPawHril.mjs";
2
2
 
3
3
  //#region src/jwt.d.ts
4
4
  /** Options for JWT sign / verify operations. */
package/dist/jwt.mjs CHANGED
@@ -1,10 +1,6 @@
1
- import { t as normalizePassword } from "./crypto-WcFV83Nz.mjs";
2
1
  import { t as PetaAuthError } from "./errors-DxJ-WUJL.mjs";
3
- import * as jose from "jose";
2
+ import { t as normalizePassword } from "./crypto-Bwut3uFR.mjs";
4
3
  //#region src/jwt.ts
5
- function toKey(secret) {
6
- return new TextEncoder().encode(secret);
7
- }
8
4
  /**
9
5
  * Sign a JWT payload.
10
6
  *
@@ -17,10 +13,24 @@ async function signJWT(payload, options) {
17
13
  const map = normalizePassword(options.password);
18
14
  const secret = map[Math.max(...Object.keys(map).map(Number)).toString()];
19
15
  if (!secret || secret.length < 32) throw new PetaAuthError("JWT_PASSWORD_TOO_SHORT", "peta-auth/jwt: password must be at least 32 characters");
20
- const jwt = new jose.SignJWT(payload).setProtectedHeader({ alg: "HS256" }).setIssuedAt();
16
+ const header = {
17
+ alg: "HS256",
18
+ typ: "JWT"
19
+ };
20
+ const now = Math.floor(Date.now() / 1e3);
21
21
  const ttl = options.expiresIn ?? 86400;
22
- jwt.setExpirationTime(Math.floor(Date.now() / 1e3) + ttl);
23
- return jwt.sign(toKey(secret));
22
+ const claims = {
23
+ ...payload,
24
+ iat: now,
25
+ exp: now + ttl
26
+ };
27
+ const toSign = `${Buffer.from(JSON.stringify(header)).toString("base64url")}.${Buffer.from(JSON.stringify(claims)).toString("base64url")}`;
28
+ const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret), {
29
+ name: "HMAC",
30
+ hash: "SHA-256"
31
+ }, false, ["sign"]);
32
+ const sig = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(toSign));
33
+ return `${toSign}.${Buffer.from(sig).toString("base64url")}`;
24
34
  }
25
35
  /**
26
36
  * Verify and decode a JWT.
@@ -33,16 +43,34 @@ async function signJWT(payload, options) {
33
43
  * ```
34
44
  */
35
45
  async function verifyJWT(token, options) {
36
- let result = null;
46
+ let headerB64, payloadB64, sigB64;
47
+ try {
48
+ const parts = token.split(".");
49
+ if (parts.length !== 3) return null;
50
+ [headerB64, payloadB64, sigB64] = parts;
51
+ if (JSON.parse(Buffer.from(headerB64, "base64url").toString()).alg !== "HS256") return null;
52
+ } catch {
53
+ return null;
54
+ }
37
55
  const passwords = normalizePassword(options.password);
56
+ const toSign = `${headerB64}.${payloadB64}`;
57
+ const sig = Buffer.from(sigB64, "base64url");
58
+ const data = new TextEncoder().encode(toSign);
38
59
  for (const secret of Object.values(passwords)) {
39
60
  if (!secret) continue;
40
61
  try {
41
- const { payload } = await jose.jwtVerify(token, toKey(secret));
42
- if (!result) result = payload;
62
+ const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret), {
63
+ name: "HMAC",
64
+ hash: "SHA-256"
65
+ }, false, ["verify"]);
66
+ if (!await crypto.subtle.verify("HMAC", key, sig, data)) continue;
67
+ const payload = JSON.parse(Buffer.from(payloadB64, "base64url").toString());
68
+ const exp = payload?.exp;
69
+ if (exp !== void 0 && exp < Math.floor(Date.now() / 1e3)) continue;
70
+ return payload;
43
71
  } catch {}
44
72
  }
45
- return result;
73
+ return null;
46
74
  }
47
75
  //#endregion
48
76
  export { signJWT, verifyJWT };
package/dist/nuxt.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { r as SessionOptions, t as IronSession } from "./session-0bF8_7Ui.mjs";
1
+ import { r as SessionOptions, t as IronSession } from "./session-CrAg3ZdE.mjs";
2
2
  import { H3Event } from "h3";
3
3
 
4
4
  //#region src/nuxt.d.ts
@@ -13,7 +13,7 @@ import { H3Event } from "h3";
13
13
  * await session.save()
14
14
  * ```
15
15
  */
16
- declare function useSession<T extends Record<string, unknown> = Record<string, unknown>>(event: H3Event, options: SessionOptions): Promise<T & IronSession>;
16
+ declare function useSession<T extends Record<string, unknown> = Record<string, unknown>>(event: H3Event, options: SessionOptions): Promise<IronSession<T>>;
17
17
  /**
18
18
  * Guard that requires session data.
19
19
  *
@@ -26,7 +26,7 @@ declare function useSession<T extends Record<string, unknown> = Record<string, u
26
26
  * requireSession(event, session, "role") // require specific key
27
27
  * ```
28
28
  */
29
- declare function requireSession(event: H3Event, session: IronSession): void;
30
- declare function requireSession<K extends string>(event: H3Event, session: IronSession, key: K): void;
29
+ declare function requireSession(event: H3Event, session: IronSession<Record<string, unknown>>): void;
30
+ declare function requireSession<K extends string>(event: H3Event, session: IronSession<Record<string, unknown>>, key: K): void;
31
31
  //#endregion
32
32
  export { requireSession, useSession };
package/dist/nuxt.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { t as PetaAuthError } from "./errors-DxJ-WUJL.mjs";
2
- import { n as sessionHasData, t as createSessionFromAdapter } from "./session-BGCQ1Z1Q.mjs";
2
+ import { n as sessionHasData, t as createSessionFromAdapter } from "./session-C1USm4OA.mjs";
3
3
  import { appendHeader, createError, getCookie } from "h3";
4
4
  //#region src/nuxt.ts
5
5
  /**
@@ -1,4 +1,4 @@
1
- import { t as defineOAuthHandler } from "../utils-CKT3C1Lq.mjs";
1
+ import { t as defineOAuthHandler } from "../utils-CJhYJE0C.mjs";
2
2
  //#region src/oauth/github.ts
3
3
  const githubProvider = {
4
4
  name: "github",
@@ -18,11 +18,12 @@ const githubProvider = {
18
18
  },
19
19
  buildAuthUrl(config, redirectURL, state, _pkce) {
20
20
  const c = config;
21
- if (c.emailRequired && !c.scope.includes("user:email")) c.scope.push("user:email");
21
+ let scope = c.scope;
22
+ if (c.emailRequired && !scope.includes("user:email")) scope = [...c.scope, "user:email"];
22
23
  const authUrl = new URL(c.authorizationURL);
23
24
  authUrl.searchParams.set("client_id", c.clientId);
24
25
  authUrl.searchParams.set("redirect_uri", redirectURL);
25
- authUrl.searchParams.set("scope", c.scope.join(" "));
26
+ authUrl.searchParams.set("scope", scope.join(" "));
26
27
  authUrl.searchParams.set("state", state.state ?? "");
27
28
  for (const [key, value] of Object.entries(c.authorizationParams)) authUrl.searchParams.set(key, value);
28
29
  return {
@@ -1,4 +1,4 @@
1
- import { t as defineOAuthHandler } from "../utils-CKT3C1Lq.mjs";
1
+ import { t as defineOAuthHandler } from "../utils-CJhYJE0C.mjs";
2
2
  //#region src/oauth/google.ts
3
3
  const googleProvider = {
4
4
  name: "google",
@@ -1,5 +1,5 @@
1
- import { n as sealData, r as unsealData, t as normalizePassword } from "./crypto-WcFV83Nz.mjs";
2
1
  import { t as PetaAuthError } from "./errors-DxJ-WUJL.mjs";
2
+ import { n as sealData, r as unsealData, t as normalizePassword } from "./crypto-Bwut3uFR.mjs";
3
3
  import { serialize } from "cookie";
4
4
  //#region src/session.ts
5
5
  const TIMESTAMP_SKEW_SECONDS = 60;
@@ -1,4 +1,4 @@
1
- import { t as Password } from "./crypto-DR-ETdLZ.mjs";
1
+ import { t as Password } from "./crypto-BPawHril.mjs";
2
2
  import { SerializeOptions } from "cookie";
3
3
 
4
4
  //#region src/session.d.ts
@@ -13,17 +13,12 @@ interface SessionOptions {
13
13
  /** Extra cookie serialization options. */
14
14
  cookieOptions?: Omit<SerializeOptions, "encode">;
15
15
  }
16
- /** A session instance returned by {@link createSessionFromAdapter}. */
17
- interface IronSession {
18
- /** Persist the session to the response cookie. */
16
+ interface SessionMethods {
19
17
  save(): Promise<void>;
20
- /** Clear the session cookie. */
21
18
  destroy(): void;
22
- /** Update session config at runtime. */
23
19
  updateConfig(options: SessionOptions): void;
24
- /** Arbitrary session data keys. */
25
- [key: string]: unknown;
26
20
  }
21
+ type IronSession<T extends Record<string, unknown> = Record<string, unknown>> = T & SessionMethods;
27
22
  /** An adapter between the framework and the session cookie store. */
28
23
  interface SessionAdapter {
29
24
  /** Read a cookie by name from the incoming request. */
@@ -48,6 +43,6 @@ interface SessionAdapter {
48
43
  * await session.save()
49
44
  * ```
50
45
  */
51
- declare function createSessionFromAdapter<T extends Record<string, unknown> = Record<string, unknown>>(adapter: SessionAdapter, options: SessionOptions): Promise<T & IronSession>;
46
+ declare function createSessionFromAdapter<T extends Record<string, unknown> = Record<string, unknown>>(adapter: SessionAdapter, options: SessionOptions): Promise<IronSession<T>>;
52
47
  //#endregion
53
48
  export { createSessionFromAdapter as i, SessionAdapter as n, SessionOptions as r, IronSession as t };
@@ -1,24 +1,9 @@
1
- import { constantTimeEqual } from "./csrf.mjs";
2
1
  import { t as PetaAuthError } from "./errors-DxJ-WUJL.mjs";
2
+ import { constantTimeEqual } from "./csrf.mjs";
3
3
  import { parse, serialize } from "cookie";
4
4
  //#region src/oauth/utils.ts
5
5
  const IS_DEVELOPMENT = process.env.NODE_ENV === "development";
6
6
  const OAUTH_COOKIE_MAX_AGE = 600;
7
- function encodeBase64Url(input) {
8
- return btoa(String.fromCharCode(...input)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
9
- }
10
- function getRandomBytes(size = 32) {
11
- return crypto.getRandomValues(new Uint8Array(size));
12
- }
13
- function oauthCookieOptions(maxAge) {
14
- return {
15
- path: "/",
16
- httpOnly: true,
17
- sameSite: "lax",
18
- secure: !IS_DEVELOPMENT,
19
- maxAge
20
- };
21
- }
22
7
  /**
23
8
  * Extract the OAuth redirect URL from a request.
24
9
  */
@@ -36,12 +21,20 @@ async function handlePKCE(request) {
36
21
  const code = new URL(request.url).searchParams.get("code");
37
22
  const cookies = parse(request.headers.get("cookie") ?? "");
38
23
  if (code) return { codeVerifier: cookies["peta-auth-pkce"] };
39
- const verifier = encodeBase64Url(getRandomBytes(32));
24
+ const verifierBytes = crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(32));
25
+ const verifier = Buffer.from(verifierBytes).toString("base64url");
40
26
  const encoder = new TextEncoder();
27
+ const hash = new Uint8Array(await crypto.subtle.digest("SHA-256", encoder.encode(verifier)));
41
28
  return {
42
- codeChallenge: encodeBase64Url(new Uint8Array(await crypto.subtle.digest("SHA-256", encoder.encode(verifier)))),
29
+ codeChallenge: Buffer.from(hash).toString("base64url"),
43
30
  codeChallengeMethod: "S256",
44
- setCookie: serialize("peta-auth-pkce", verifier, oauthCookieOptions(OAUTH_COOKIE_MAX_AGE))
31
+ setCookie: serialize("peta-auth-pkce", verifier, {
32
+ path: "/",
33
+ httpOnly: true,
34
+ sameSite: "strict",
35
+ secure: !IS_DEVELOPMENT,
36
+ maxAge: OAUTH_COOKIE_MAX_AGE
37
+ })
45
38
  };
46
39
  }
47
40
  /**
@@ -54,10 +47,17 @@ function handleState(request) {
54
47
  state: queryState,
55
48
  expectedState: cookies["peta-auth-state"]
56
49
  };
57
- const state = encodeBase64Url(getRandomBytes(8));
50
+ const stateBytes = crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(8));
51
+ const state = Buffer.from(stateBytes).toString("base64url");
58
52
  return {
59
53
  state,
60
- setCookie: serialize("peta-auth-state", state, oauthCookieOptions(OAUTH_COOKIE_MAX_AGE))
54
+ setCookie: serialize("peta-auth-state", state, {
55
+ path: "/",
56
+ httpOnly: true,
57
+ sameSite: "strict",
58
+ secure: !IS_DEVELOPMENT,
59
+ maxAge: OAUTH_COOKIE_MAX_AGE
60
+ })
61
61
  };
62
62
  }
63
63
  /**
@@ -68,7 +68,7 @@ async function requestAccessToken(url, options) {
68
68
  "Content-Type": "application/x-www-form-urlencoded",
69
69
  ...options.headers
70
70
  };
71
- const bodyParams = options.body ?? options.params ?? {};
71
+ const bodyParams = options.body ?? {};
72
72
  const body = new URLSearchParams();
73
73
  for (const [key, value] of Object.entries(bodyParams)) if (value !== void 0) body.append(key, value);
74
74
  const response = await fetch(url, {
@@ -103,24 +103,6 @@ function jsonError(error, status) {
103
103
  });
104
104
  }
105
105
  /**
106
- * Handle missing OAuth configuration.
107
- */
108
- function handleMissingConfiguration(provider, missingKeys, onError) {
109
- const envVars = missingKeys.map((key) => `PETA_OAUTH_${provider.toUpperCase()}_${key.replace(/([A-Z])/g, "_$1").toUpperCase()}`);
110
- const error = /* @__PURE__ */ new Error(`Missing ${envVars.join(" or ")} env ${missingKeys.length > 1 ? "variables" : "variable"}.`);
111
- if (onError) return onError(error);
112
- return jsonError(error, 500);
113
- }
114
- /**
115
- * Handle OAuth access token errors.
116
- */
117
- function handleAccessTokenError(provider, errorData, onError) {
118
- const message = `${provider} login failed: ${errorData.error_description || errorData.error || "Unknown error"}`;
119
- const error = new Error(message);
120
- if (onError) return onError(error);
121
- return jsonError(error, 401);
122
- }
123
- /**
124
106
  * Define an OAuth event handler using a provider-specific config.
125
107
  *
126
108
  * Handles the shared OAuth flow (redirect, callback, token exchange, user fetch)
@@ -143,7 +125,10 @@ function defineOAuthHandler(provider, options) {
143
125
  const missing = [];
144
126
  if (!config.clientId) missing.push("clientId");
145
127
  if (!config.clientSecret) missing.push("clientSecret");
146
- return handleMissingConfiguration(provider.name, missing, onError);
128
+ const envVars = missing.map((key) => `PETA_OAUTH_${provider.name.toUpperCase()}_${key.replace(/([A-Z])/g, "_$1").toUpperCase()}`);
129
+ const err = /* @__PURE__ */ new Error(`Missing ${envVars.join(" or ")} env ${missing.length > 1 ? "variables" : "variable"}.`);
130
+ if (onError) return onError(err);
131
+ return jsonError(err, 500);
147
132
  }
148
133
  const redirectURL = config.redirectURL || getOAuthRedirectURL(request);
149
134
  const state = handleState(request);
@@ -152,9 +137,19 @@ function defineOAuthHandler(provider, options) {
152
137
  const { url: authUrl, cookies } = provider.buildAuthUrl(config, redirectURL, state, pkce);
153
138
  return redirect(authUrl, cookies);
154
139
  }
155
- if (!queryState || !state.expectedState || !constantTimeEqual(queryState, state.expectedState)) return handleInvalidState(provider.name, onError);
140
+ if (!queryState || !state.expectedState || !constantTimeEqual(queryState, state.expectedState)) {
141
+ const err = /* @__PURE__ */ new Error(`${provider.name} login failed: state mismatch`);
142
+ if (onError) return onError(err);
143
+ return jsonError(err, 500);
144
+ }
156
145
  const tokens = await requestAccessToken(config.tokenURL, { body: provider.requestTokenBody(config, redirectURL, queryCode, pkce) });
157
- if (tokens.error) return handleAccessTokenError(provider.name, tokens, onError);
146
+ if (tokens.error) {
147
+ const errorData = tokens;
148
+ const message = `${provider.name} login failed: ${errorData.error_description || errorData.error || "Unknown error"}`;
149
+ const err = new Error(message);
150
+ if (onError) return onError(err);
151
+ return jsonError(err, 401);
152
+ }
158
153
  return onSuccess({
159
154
  user: await provider.fetchUser(config, tokens, request),
160
155
  tokens,
@@ -162,13 +157,5 @@ function defineOAuthHandler(provider, options) {
162
157
  });
163
158
  };
164
159
  }
165
- /**
166
- * Handle OAuth state mismatch.
167
- */
168
- function handleInvalidState(provider, onError) {
169
- const error = /* @__PURE__ */ new Error(`${provider} login failed: state mismatch`);
170
- if (onError) return onError(error);
171
- return jsonError(error, 500);
172
- }
173
160
  //#endregion
174
161
  export { defineOAuthHandler as t };
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "peta-auth",
3
- "version": "0.2.2",
3
+ "private": false,
4
+ "version": "0.3.0",
4
5
  "description": "Encrypted cookie sessions for Bun — Hono, ElysiaJS & Nuxt adapters",
5
6
  "license": "MIT",
6
7
  "repository": {
@@ -8,6 +9,7 @@
8
9
  "url": "git+https://github.com/zfadhli/peta-stack.git"
9
10
  },
10
11
  "type": "module",
12
+ "main": "./dist/index.mjs",
11
13
  "module": "./dist/index.mjs",
12
14
  "types": "./dist/index.d.mts",
13
15
  "exports": {
@@ -45,21 +47,21 @@
45
47
  }
46
48
  },
47
49
  "files": [
48
- "dist"
50
+ "dist",
51
+ "README.md",
52
+ "LICENSE"
49
53
  ],
50
54
  "scripts": {
51
55
  "build": "tsdown",
52
56
  "lint": "biome check --write .",
53
- "lint:ci": "biome ci .",
54
- "prepublish": "bun run build",
57
+ "prepublishOnly": "bun run build",
55
58
  "test": "bun test",
56
59
  "typecheck": "tsc --noEmit"
57
60
  },
58
61
  "dependencies": {
59
62
  "@node-rs/argon2": "^2.0.2",
60
63
  "cookie": "^1.1.1",
61
- "iron-webcrypto": "^2.0.0",
62
- "jose": "^6.2.3"
64
+ "iron-webcrypto": "^2.0.0"
63
65
  },
64
66
  "peerDependencies": {
65
67
  "elysia": ">=1.0.0",
@@ -1,84 +0,0 @@
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 };