peta-auth 0.2.3 → 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.
- package/dist/{crypto-DR-ETdLZ.d.mts → crypto-BPawHril.d.mts} +4 -4
- package/dist/crypto-Bwut3uFR.mjs +54 -0
- package/dist/csrf.d.mts +3 -3
- package/dist/elysia.d.mts +3 -3
- package/dist/elysia.mjs +10 -7
- package/dist/hono.d.mts +2 -2
- package/dist/hono.mjs +1 -1
- package/dist/index.d.mts +7 -11
- package/dist/index.mjs +3 -3
- package/dist/jwt.d.mts +1 -1
- package/dist/jwt.mjs +40 -12
- package/dist/nuxt.d.mts +4 -4
- package/dist/nuxt.mjs +1 -1
- package/dist/oauth/github.mjs +4 -3
- package/dist/oauth/google.mjs +1 -1
- package/dist/{session-BGCQ1Z1Q.mjs → session-C1USm4OA.mjs} +1 -1
- package/dist/{session-0bF8_7Ui.d.mts → session-CrAg3ZdE.d.mts} +4 -9
- package/dist/{utils-CKT3C1Lq.mjs → utils-CJhYJE0C.mjs} +38 -51
- package/package.json +2 -4
- package/dist/crypto-WcFV83Nz.mjs +0 -84
|
@@ -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
|
|
12
|
+
declare function sealData(data: unknown, {
|
|
13
13
|
password,
|
|
14
14
|
ttl
|
|
15
15
|
}: {
|
|
16
16
|
password: Password;
|
|
17
17
|
ttl?: number;
|
|
18
|
-
})
|
|
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
|
|
27
|
+
declare function unsealData<T>(seal: string, {
|
|
28
28
|
password,
|
|
29
29
|
ttl
|
|
30
30
|
}: {
|
|
31
31
|
password: Password;
|
|
32
32
|
ttl?: number;
|
|
33
|
-
})
|
|
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-
|
|
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-
|
|
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
|
|
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
|
|
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-
|
|
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) =>
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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-
|
|
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
|
|
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-
|
|
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-
|
|
2
|
-
import { i as createSessionFromAdapter, n as SessionAdapter, r as SessionOptions, t as IronSession } from "./session-
|
|
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?:
|
|
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-
|
|
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
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
|
|
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
|
|
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
|
-
|
|
23
|
-
|
|
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
|
|
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
|
|
42
|
-
|
|
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
|
|
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-
|
|
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
|
|
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-
|
|
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
|
/**
|
package/dist/oauth/github.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as defineOAuthHandler } from "../utils-
|
|
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
|
-
|
|
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",
|
|
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 {
|
package/dist/oauth/google.mjs
CHANGED
|
@@ -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-
|
|
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
|
-
|
|
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
|
|
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
|
|
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:
|
|
29
|
+
codeChallenge: Buffer.from(hash).toString("base64url"),
|
|
43
30
|
codeChallengeMethod: "S256",
|
|
44
|
-
setCookie: serialize("peta-auth-pkce", verifier,
|
|
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
|
|
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,
|
|
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 ??
|
|
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
|
-
|
|
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))
|
|
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)
|
|
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,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "peta-auth",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.3.0",
|
|
5
5
|
"description": "Encrypted cookie sessions for Bun — Hono, ElysiaJS & Nuxt adapters",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -54,7 +54,6 @@
|
|
|
54
54
|
"scripts": {
|
|
55
55
|
"build": "tsdown",
|
|
56
56
|
"lint": "biome check --write .",
|
|
57
|
-
"lint:ci": "biome ci .",
|
|
58
57
|
"prepublishOnly": "bun run build",
|
|
59
58
|
"test": "bun test",
|
|
60
59
|
"typecheck": "tsc --noEmit"
|
|
@@ -62,8 +61,7 @@
|
|
|
62
61
|
"dependencies": {
|
|
63
62
|
"@node-rs/argon2": "^2.0.2",
|
|
64
63
|
"cookie": "^1.1.1",
|
|
65
|
-
"iron-webcrypto": "^2.0.0"
|
|
66
|
-
"jose": "^6.2.3"
|
|
64
|
+
"iron-webcrypto": "^2.0.0"
|
|
67
65
|
},
|
|
68
66
|
"peerDependencies": {
|
|
69
67
|
"elysia": ">=1.0.0",
|
package/dist/crypto-WcFV83Nz.mjs
DELETED
|
@@ -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 };
|