@lemondouble/lemon-auth 0.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.
@@ -0,0 +1,23 @@
1
+ // src/constants.ts
2
+ var AUTH_SERVER_URL = "https://auth.lemondouble.com";
3
+ var JWKS_URL = `${AUTH_SERVER_URL}/.well-known/jwks.json`;
4
+ var LOGIN_URL = `${AUTH_SERVER_URL}/api/oauth2/google/login`;
5
+ var REFRESH_URL = `${AUTH_SERVER_URL}/api/token/refresh`;
6
+ var LOGOUT_URL = `${AUTH_SERVER_URL}/api/token/logout`;
7
+ var PROFILE_URL = `${AUTH_SERVER_URL}/api/user/me`;
8
+ var PROFILE_PAGE_URL = `${AUTH_SERVER_URL}/profile`;
9
+ var ACCESS_TOKEN_COOKIE = "access_token";
10
+ var REFRESH_TOKEN_COOKIE = "refresh_token";
11
+
12
+ export {
13
+ AUTH_SERVER_URL,
14
+ JWKS_URL,
15
+ LOGIN_URL,
16
+ REFRESH_URL,
17
+ LOGOUT_URL,
18
+ PROFILE_URL,
19
+ PROFILE_PAGE_URL,
20
+ ACCESS_TOKEN_COOKIE,
21
+ REFRESH_TOKEN_COOKIE
22
+ };
23
+ //# sourceMappingURL=chunk-B2EFAFKY.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/constants.ts"],"sourcesContent":["export const AUTH_SERVER_URL = \"https://auth.lemondouble.com\";\nexport const JWKS_URL = `${AUTH_SERVER_URL}/.well-known/jwks.json`;\nexport const LOGIN_URL = `${AUTH_SERVER_URL}/api/oauth2/google/login`;\nexport const REFRESH_URL = `${AUTH_SERVER_URL}/api/token/refresh`;\nexport const LOGOUT_URL = `${AUTH_SERVER_URL}/api/token/logout`;\nexport const PROFILE_URL = `${AUTH_SERVER_URL}/api/user/me`;\nexport const PROFILE_PAGE_URL = `${AUTH_SERVER_URL}/profile`;\n\nexport const ACCESS_TOKEN_COOKIE = \"access_token\";\nexport const REFRESH_TOKEN_COOKIE = \"refresh_token\";\n"],"mappings":";AAAO,IAAM,kBAAkB;AACxB,IAAM,WAAW,GAAG,eAAe;AACnC,IAAM,YAAY,GAAG,eAAe;AACpC,IAAM,cAAc,GAAG,eAAe;AACtC,IAAM,aAAa,GAAG,eAAe;AACrC,IAAM,cAAc,GAAG,eAAe;AACtC,IAAM,mBAAmB,GAAG,eAAe;AAE3C,IAAM,sBAAsB;AAC5B,IAAM,uBAAuB;","names":[]}
@@ -0,0 +1,20 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { L as LemonUser } from '../types-CgrEUXwM.js';
3
+ import 'jose';
4
+
5
+ interface AuthContextValue {
6
+ user: LemonUser | null;
7
+ isAuthenticated: boolean;
8
+ }
9
+ declare function AuthProvider({ user, children, }: {
10
+ user: LemonUser | null;
11
+ children: React.ReactNode;
12
+ }): react_jsx_runtime.JSX.Element;
13
+ declare function useAuth(): AuthContextValue;
14
+
15
+ declare function loginUrl(redirectUrl: string): string;
16
+ declare function profileUrl(redirectUrl?: string): string;
17
+ declare function logout(): Promise<void>;
18
+ declare function refreshToken(): Promise<boolean>;
19
+
20
+ export { AuthProvider, LemonUser, loginUrl, logout, profileUrl, refreshToken, useAuth };
@@ -0,0 +1,56 @@
1
+ import {
2
+ LOGIN_URL,
3
+ LOGOUT_URL,
4
+ PROFILE_PAGE_URL,
5
+ REFRESH_URL
6
+ } from "../chunk-B2EFAFKY.js";
7
+
8
+ // src/client/context.tsx
9
+ import { createContext, useContext } from "react";
10
+ import { jsx } from "react/jsx-runtime";
11
+ var AuthContext = createContext({
12
+ user: null,
13
+ isAuthenticated: false
14
+ });
15
+ function AuthProvider({
16
+ user,
17
+ children
18
+ }) {
19
+ return /* @__PURE__ */ jsx(AuthContext, { value: { user, isAuthenticated: user !== null }, children });
20
+ }
21
+ function useAuth() {
22
+ return useContext(AuthContext);
23
+ }
24
+
25
+ // src/client/url.ts
26
+ function loginUrl(redirectUrl) {
27
+ return `${LOGIN_URL}?redirect_url=${encodeURIComponent(redirectUrl)}`;
28
+ }
29
+ function profileUrl(redirectUrl) {
30
+ if (redirectUrl) {
31
+ return `${PROFILE_PAGE_URL}?redirect_url=${encodeURIComponent(redirectUrl)}`;
32
+ }
33
+ return PROFILE_PAGE_URL;
34
+ }
35
+ async function logout() {
36
+ await fetch(LOGOUT_URL, {
37
+ method: "POST",
38
+ credentials: "include"
39
+ });
40
+ }
41
+ async function refreshToken() {
42
+ const res = await fetch(REFRESH_URL, {
43
+ method: "POST",
44
+ credentials: "include"
45
+ });
46
+ return res.ok;
47
+ }
48
+ export {
49
+ AuthProvider,
50
+ loginUrl,
51
+ logout,
52
+ profileUrl,
53
+ refreshToken,
54
+ useAuth
55
+ };
56
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/client/context.tsx","../../src/client/url.ts"],"sourcesContent":["\"use client\";\n\nimport { createContext, useContext } from \"react\";\nimport type { LemonUser } from \"../types.js\";\n\ninterface AuthContextValue {\n user: LemonUser | null;\n isAuthenticated: boolean;\n}\n\nconst AuthContext = createContext<AuthContextValue>({\n user: null,\n isAuthenticated: false,\n});\n\nexport function AuthProvider({\n user,\n children,\n}: {\n user: LemonUser | null;\n children: React.ReactNode;\n}) {\n return (\n <AuthContext value={{ user, isAuthenticated: user !== null }}>\n {children}\n </AuthContext>\n );\n}\n\nexport function useAuth(): AuthContextValue {\n return useContext(AuthContext);\n}\n","import {\n LOGIN_URL,\n LOGOUT_URL,\n PROFILE_PAGE_URL,\n REFRESH_URL,\n} from \"../constants.js\";\n\nexport function loginUrl(redirectUrl: string): string {\n return `${LOGIN_URL}?redirect_url=${encodeURIComponent(redirectUrl)}`;\n}\n\nexport function profileUrl(redirectUrl?: string): string {\n if (redirectUrl) {\n return `${PROFILE_PAGE_URL}?redirect_url=${encodeURIComponent(redirectUrl)}`;\n }\n return PROFILE_PAGE_URL;\n}\n\nexport async function logout(): Promise<void> {\n await fetch(LOGOUT_URL, {\n method: \"POST\",\n credentials: \"include\",\n });\n}\n\nexport async function refreshToken(): Promise<boolean> {\n const res = await fetch(REFRESH_URL, {\n method: \"POST\",\n credentials: \"include\",\n });\n return res.ok;\n}\n"],"mappings":";;;;;;;;AAEA,SAAS,eAAe,kBAAkB;AAqBtC;AAbJ,IAAM,cAAc,cAAgC;AAAA,EAClD,MAAM;AAAA,EACN,iBAAiB;AACnB,CAAC;AAEM,SAAS,aAAa;AAAA,EAC3B;AAAA,EACA;AACF,GAGG;AACD,SACE,oBAAC,eAAY,OAAO,EAAE,MAAM,iBAAiB,SAAS,KAAK,GACxD,UACH;AAEJ;AAEO,SAAS,UAA4B;AAC1C,SAAO,WAAW,WAAW;AAC/B;;;ACxBO,SAAS,SAAS,aAA6B;AACpD,SAAO,GAAG,SAAS,iBAAiB,mBAAmB,WAAW,CAAC;AACrE;AAEO,SAAS,WAAW,aAA8B;AACvD,MAAI,aAAa;AACf,WAAO,GAAG,gBAAgB,iBAAiB,mBAAmB,WAAW,CAAC;AAAA,EAC5E;AACA,SAAO;AACT;AAEA,eAAsB,SAAwB;AAC5C,QAAM,MAAM,YAAY;AAAA,IACtB,QAAQ;AAAA,IACR,aAAa;AAAA,EACf,CAAC;AACH;AAEA,eAAsB,eAAiC;AACrD,QAAM,MAAM,MAAM,MAAM,aAAa;AAAA,IACnC,QAAQ;AAAA,IACR,aAAa;AAAA,EACf,CAAC;AACD,SAAO,IAAI;AACb;","names":[]}
@@ -0,0 +1,12 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ export { A as AccessTokenClaims, L as LemonUser } from '../types-CgrEUXwM.js';
3
+ import 'jose';
4
+
5
+ interface AuthProxyOptions {
6
+ publicPaths?: string[];
7
+ clientId?: string;
8
+ loginRedirectUrl?: string;
9
+ }
10
+ declare function createAuthProxy(options?: AuthProxyOptions): (request: NextRequest) => Promise<NextResponse>;
11
+
12
+ export { type AuthProxyOptions, createAuthProxy };
@@ -0,0 +1,132 @@
1
+ import {
2
+ ACCESS_TOKEN_COOKIE,
3
+ AUTH_SERVER_URL,
4
+ JWKS_URL,
5
+ LOGIN_URL,
6
+ REFRESH_TOKEN_COOKIE,
7
+ REFRESH_URL
8
+ } from "../chunk-B2EFAFKY.js";
9
+
10
+ // src/proxy/index.ts
11
+ import { NextResponse } from "next/server";
12
+ import { createRemoteJWKSet, jwtVerify } from "jose";
13
+ var REFRESH_BUFFER_SECONDS = 60;
14
+ var jwks = createRemoteJWKSet(new URL(JWKS_URL));
15
+ function createAuthProxy(options = {}) {
16
+ const { publicPaths = [], clientId, loginRedirectUrl } = options;
17
+ return async function proxy(request) {
18
+ const { pathname } = request.nextUrl;
19
+ if (isPublicPath(pathname, publicPaths)) {
20
+ return maybeRefreshAndContinue(request);
21
+ }
22
+ const accessToken = request.cookies.get(ACCESS_TOKEN_COOKIE)?.value;
23
+ const refreshToken = request.cookies.get(REFRESH_TOKEN_COOKIE)?.value;
24
+ if (accessToken && isTokenFresh(accessToken)) {
25
+ const claims = await verifyToken(accessToken);
26
+ if (claims) {
27
+ if (clientId && !claims.approved_clients.includes(clientId)) {
28
+ return redirectToLogin(request, loginRedirectUrl);
29
+ }
30
+ return NextResponse.next();
31
+ }
32
+ }
33
+ if (refreshToken) {
34
+ const result = await tryRefresh(refreshToken);
35
+ if (result) {
36
+ const claims = await verifyToken(result.newAccessToken);
37
+ if (claims) {
38
+ if (clientId && !claims.approved_clients.includes(clientId)) {
39
+ return redirectToLogin(request, loginRedirectUrl);
40
+ }
41
+ const response = NextResponse.next();
42
+ for (const header of result.setCookieHeaders) {
43
+ response.headers.append("Set-Cookie", header);
44
+ }
45
+ return response;
46
+ }
47
+ }
48
+ }
49
+ return redirectToLogin(request, loginRedirectUrl);
50
+ };
51
+ }
52
+ async function maybeRefreshAndContinue(request) {
53
+ const accessToken = request.cookies.get(ACCESS_TOKEN_COOKIE)?.value;
54
+ const refreshToken = request.cookies.get(REFRESH_TOKEN_COOKIE)?.value;
55
+ if (accessToken && isTokenFresh(accessToken)) {
56
+ return NextResponse.next();
57
+ }
58
+ if (refreshToken) {
59
+ const result = await tryRefresh(refreshToken);
60
+ if (result) {
61
+ const response = NextResponse.next();
62
+ for (const header of result.setCookieHeaders) {
63
+ response.headers.append("Set-Cookie", header);
64
+ }
65
+ return response;
66
+ }
67
+ }
68
+ return NextResponse.next();
69
+ }
70
+ function isTokenFresh(token) {
71
+ try {
72
+ const parts = token.split(".");
73
+ if (parts.length !== 3) return false;
74
+ const payload = JSON.parse(
75
+ Buffer.from(parts[1], "base64url").toString()
76
+ );
77
+ return typeof payload.exp === "number" && payload.exp > Math.floor(Date.now() / 1e3) + REFRESH_BUFFER_SECONDS;
78
+ } catch {
79
+ return false;
80
+ }
81
+ }
82
+ async function verifyToken(token) {
83
+ try {
84
+ const { payload } = await jwtVerify(token, jwks, {
85
+ issuer: AUTH_SERVER_URL
86
+ });
87
+ return payload;
88
+ } catch {
89
+ return null;
90
+ }
91
+ }
92
+ async function tryRefresh(refreshToken) {
93
+ try {
94
+ const res = await fetch(REFRESH_URL, {
95
+ method: "POST",
96
+ headers: { Cookie: `refresh_token=${refreshToken}` }
97
+ });
98
+ if (!res.ok) return null;
99
+ const setCookieHeaders = res.headers.getSetCookie();
100
+ const newAccessToken = extractAccessToken(setCookieHeaders);
101
+ if (!newAccessToken) return null;
102
+ return { newAccessToken, setCookieHeaders };
103
+ } catch {
104
+ return null;
105
+ }
106
+ }
107
+ function extractAccessToken(setCookieHeaders) {
108
+ for (const header of setCookieHeaders) {
109
+ const match = header.match(/^access_token=([^;]+)/);
110
+ if (match) return match[1];
111
+ }
112
+ return null;
113
+ }
114
+ function isPublicPath(pathname, publicPaths) {
115
+ return publicPaths.some((p) => {
116
+ if (p.endsWith("*")) {
117
+ return pathname.startsWith(p.slice(0, -1));
118
+ }
119
+ return pathname === p;
120
+ });
121
+ }
122
+ function redirectToLogin(request, loginRedirectUrl) {
123
+ if (loginRedirectUrl) {
124
+ const url = `${LOGIN_URL}?redirect_url=${encodeURIComponent(loginRedirectUrl)}`;
125
+ return NextResponse.redirect(new URL(url, request.url));
126
+ }
127
+ return NextResponse.redirect(new URL("/", request.url));
128
+ }
129
+ export {
130
+ createAuthProxy
131
+ };
132
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/proxy/index.ts"],"sourcesContent":["import { NextRequest, NextResponse } from \"next/server\";\nimport {\n ACCESS_TOKEN_COOKIE,\n REFRESH_TOKEN_COOKIE,\n AUTH_SERVER_URL,\n REFRESH_URL,\n JWKS_URL,\n LOGIN_URL,\n} from \"../constants.js\";\nimport { createRemoteJWKSet, jwtVerify } from \"jose\";\nimport type { AccessTokenClaims } from \"../types.js\";\n\nconst REFRESH_BUFFER_SECONDS = 60;\n\nconst jwks = createRemoteJWKSet(new URL(JWKS_URL));\n\nexport interface AuthProxyOptions {\n publicPaths?: string[];\n clientId?: string;\n loginRedirectUrl?: string;\n}\n\nexport function createAuthProxy(options: AuthProxyOptions = {}) {\n const { publicPaths = [], clientId, loginRedirectUrl } = options;\n\n return async function proxy(request: NextRequest): Promise<NextResponse> {\n const { pathname } = request.nextUrl;\n\n if (isPublicPath(pathname, publicPaths)) {\n return maybeRefreshAndContinue(request);\n }\n\n const accessToken = request.cookies.get(ACCESS_TOKEN_COOKIE)?.value;\n const refreshToken = request.cookies.get(REFRESH_TOKEN_COOKIE)?.value;\n\n if (accessToken && isTokenFresh(accessToken)) {\n const claims = await verifyToken(accessToken);\n if (claims) {\n if (clientId && !claims.approved_clients.includes(clientId)) {\n return redirectToLogin(request, loginRedirectUrl);\n }\n return NextResponse.next();\n }\n }\n\n if (refreshToken) {\n const result = await tryRefresh(refreshToken);\n if (result) {\n const claims = await verifyToken(result.newAccessToken);\n if (claims) {\n if (clientId && !claims.approved_clients.includes(clientId)) {\n return redirectToLogin(request, loginRedirectUrl);\n }\n const response = NextResponse.next();\n for (const header of result.setCookieHeaders) {\n response.headers.append(\"Set-Cookie\", header);\n }\n return response;\n }\n }\n }\n\n return redirectToLogin(request, loginRedirectUrl);\n };\n}\n\nasync function maybeRefreshAndContinue(\n request: NextRequest\n): Promise<NextResponse> {\n const accessToken = request.cookies.get(ACCESS_TOKEN_COOKIE)?.value;\n const refreshToken = request.cookies.get(REFRESH_TOKEN_COOKIE)?.value;\n\n if (accessToken && isTokenFresh(accessToken)) {\n return NextResponse.next();\n }\n\n if (refreshToken) {\n const result = await tryRefresh(refreshToken);\n if (result) {\n const response = NextResponse.next();\n for (const header of result.setCookieHeaders) {\n response.headers.append(\"Set-Cookie\", header);\n }\n return response;\n }\n }\n\n return NextResponse.next();\n}\n\nfunction isTokenFresh(token: string): boolean {\n try {\n const parts = token.split(\".\");\n if (parts.length !== 3) return false;\n const payload = JSON.parse(\n Buffer.from(parts[1], \"base64url\").toString()\n );\n return (\n typeof payload.exp === \"number\" &&\n payload.exp > Math.floor(Date.now() / 1000) + REFRESH_BUFFER_SECONDS\n );\n } catch {\n return false;\n }\n}\n\nasync function verifyToken(\n token: string\n): Promise<AccessTokenClaims | null> {\n try {\n const { payload } = await jwtVerify(token, jwks, {\n issuer: AUTH_SERVER_URL,\n });\n return payload as unknown as AccessTokenClaims;\n } catch {\n return null;\n }\n}\n\ninterface RefreshSuccess {\n newAccessToken: string;\n setCookieHeaders: string[];\n}\n\nasync function tryRefresh(\n refreshToken: string\n): Promise<RefreshSuccess | null> {\n try {\n const res = await fetch(REFRESH_URL, {\n method: \"POST\",\n headers: { Cookie: `refresh_token=${refreshToken}` },\n });\n\n if (!res.ok) return null;\n\n const setCookieHeaders = res.headers.getSetCookie();\n const newAccessToken = extractAccessToken(setCookieHeaders);\n if (!newAccessToken) return null;\n\n return { newAccessToken, setCookieHeaders };\n } catch {\n return null;\n }\n}\n\nfunction extractAccessToken(setCookieHeaders: string[]): string | null {\n for (const header of setCookieHeaders) {\n const match = header.match(/^access_token=([^;]+)/);\n if (match) return match[1];\n }\n return null;\n}\n\nfunction isPublicPath(pathname: string, publicPaths: string[]): boolean {\n return publicPaths.some((p) => {\n if (p.endsWith(\"*\")) {\n return pathname.startsWith(p.slice(0, -1));\n }\n return pathname === p;\n });\n}\n\nfunction redirectToLogin(\n request: NextRequest,\n loginRedirectUrl?: string\n): NextResponse {\n if (loginRedirectUrl) {\n const url = `${LOGIN_URL}?redirect_url=${encodeURIComponent(loginRedirectUrl)}`;\n return NextResponse.redirect(new URL(url, request.url));\n }\n return NextResponse.redirect(new URL(\"/\", request.url));\n}\n\nexport type { AccessTokenClaims } from \"../types.js\";\nexport type { LemonUser } from \"../types.js\";\n"],"mappings":"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","names":[]}
@@ -0,0 +1,28 @@
1
+ import { A as AccessTokenClaims, L as LemonUser } from '../types-CgrEUXwM.js';
2
+ export { U as UserProfile } from '../types-CgrEUXwM.js';
3
+ import 'jose';
4
+
5
+ declare function verifyAccessToken(): Promise<AccessTokenClaims | null>;
6
+ declare function verifyAccessTokenString(token: string): Promise<AccessTokenClaims | null>;
7
+
8
+ declare const getUser: () => Promise<LemonUser | null>;
9
+ declare function requireAuth(redirectTo?: string): Promise<LemonUser>;
10
+ declare function requireClient(clientId: string, redirectTo?: string): Promise<LemonUser>;
11
+
12
+ interface RefreshResult {
13
+ ok: boolean;
14
+ setCookieHeaders: string[];
15
+ }
16
+ declare function refreshTokenFromCookie(refreshTokenCookie: string): Promise<RefreshResult>;
17
+
18
+ declare const AUTH_SERVER_URL = "https://auth.lemondouble.com";
19
+ declare const JWKS_URL = "https://auth.lemondouble.com/.well-known/jwks.json";
20
+ declare const LOGIN_URL = "https://auth.lemondouble.com/api/oauth2/google/login";
21
+ declare const REFRESH_URL = "https://auth.lemondouble.com/api/token/refresh";
22
+ declare const LOGOUT_URL = "https://auth.lemondouble.com/api/token/logout";
23
+ declare const PROFILE_URL = "https://auth.lemondouble.com/api/user/me";
24
+ declare const PROFILE_PAGE_URL = "https://auth.lemondouble.com/profile";
25
+ declare const ACCESS_TOKEN_COOKIE = "access_token";
26
+ declare const REFRESH_TOKEN_COOKIE = "refresh_token";
27
+
28
+ export { ACCESS_TOKEN_COOKIE, AUTH_SERVER_URL, AccessTokenClaims, JWKS_URL, LOGIN_URL, LOGOUT_URL, LemonUser, PROFILE_PAGE_URL, PROFILE_URL, REFRESH_TOKEN_COOKIE, REFRESH_URL, type RefreshResult, getUser, refreshTokenFromCookie, requireAuth, requireClient, verifyAccessToken, verifyAccessTokenString };
@@ -0,0 +1,102 @@
1
+ import {
2
+ ACCESS_TOKEN_COOKIE,
3
+ AUTH_SERVER_URL,
4
+ JWKS_URL,
5
+ LOGIN_URL,
6
+ LOGOUT_URL,
7
+ PROFILE_PAGE_URL,
8
+ PROFILE_URL,
9
+ REFRESH_TOKEN_COOKIE,
10
+ REFRESH_URL
11
+ } from "../chunk-B2EFAFKY.js";
12
+
13
+ // src/server/verify.ts
14
+ import { createRemoteJWKSet, jwtVerify } from "jose";
15
+ import { cookies } from "next/headers";
16
+ var jwks = createRemoteJWKSet(new URL(JWKS_URL));
17
+ async function verifyAccessToken() {
18
+ const cookieStore = await cookies();
19
+ const token = cookieStore.get(ACCESS_TOKEN_COOKIE)?.value;
20
+ if (!token) return null;
21
+ try {
22
+ const { payload } = await jwtVerify(token, jwks, {
23
+ issuer: AUTH_SERVER_URL
24
+ });
25
+ return payload;
26
+ } catch {
27
+ return null;
28
+ }
29
+ }
30
+ async function verifyAccessTokenString(token) {
31
+ try {
32
+ const { payload } = await jwtVerify(token, jwks, {
33
+ issuer: AUTH_SERVER_URL
34
+ });
35
+ return payload;
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+
41
+ // src/server/user.ts
42
+ import { cache } from "react";
43
+ import { redirect } from "next/navigation";
44
+ function claimsToUser(claims) {
45
+ return {
46
+ uid: claims.sub,
47
+ nickname: claims.nickname,
48
+ profileImageUrl: claims.profile_image_url,
49
+ role: claims.role,
50
+ approvedClients: claims.approved_clients
51
+ };
52
+ }
53
+ var getUser = cache(async () => {
54
+ const claims = await verifyAccessToken();
55
+ if (!claims) return null;
56
+ return claimsToUser(claims);
57
+ });
58
+ async function requireAuth(redirectTo = "/") {
59
+ const user = await getUser();
60
+ if (user) return user;
61
+ redirect(redirectTo);
62
+ }
63
+ async function requireClient(clientId, redirectTo = "/") {
64
+ const user = await requireAuth(redirectTo);
65
+ if (!user.approvedClients.includes(clientId)) {
66
+ throw new Error("Client not approved");
67
+ }
68
+ return user;
69
+ }
70
+
71
+ // src/server/refresh.ts
72
+ async function refreshTokenFromCookie(refreshTokenCookie) {
73
+ const res = await fetch(REFRESH_URL, {
74
+ method: "POST",
75
+ headers: {
76
+ Cookie: `refresh_token=${refreshTokenCookie}`
77
+ }
78
+ });
79
+ if (!res.ok) {
80
+ return { ok: false, setCookieHeaders: [] };
81
+ }
82
+ const setCookieHeaders = res.headers.getSetCookie();
83
+ return { ok: true, setCookieHeaders };
84
+ }
85
+ export {
86
+ ACCESS_TOKEN_COOKIE,
87
+ AUTH_SERVER_URL,
88
+ JWKS_URL,
89
+ LOGIN_URL,
90
+ LOGOUT_URL,
91
+ PROFILE_PAGE_URL,
92
+ PROFILE_URL,
93
+ REFRESH_TOKEN_COOKIE,
94
+ REFRESH_URL,
95
+ getUser,
96
+ refreshTokenFromCookie,
97
+ requireAuth,
98
+ requireClient,
99
+ verifyAccessToken,
100
+ verifyAccessTokenString
101
+ };
102
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/server/verify.ts","../../src/server/user.ts","../../src/server/refresh.ts"],"sourcesContent":["import { createRemoteJWKSet, jwtVerify } from \"jose\";\nimport { cookies } from \"next/headers\";\nimport type { AccessTokenClaims } from \"../types.js\";\nimport { JWKS_URL, AUTH_SERVER_URL, ACCESS_TOKEN_COOKIE } from \"../constants.js\";\n\nconst jwks = createRemoteJWKSet(new URL(JWKS_URL));\n\nexport async function verifyAccessToken(): Promise<AccessTokenClaims | null> {\n const cookieStore = await cookies();\n const token = cookieStore.get(ACCESS_TOKEN_COOKIE)?.value;\n if (!token) return null;\n\n try {\n const { payload } = await jwtVerify(token, jwks, {\n issuer: AUTH_SERVER_URL,\n });\n return payload as unknown as AccessTokenClaims;\n } catch {\n return null;\n }\n}\n\nexport async function verifyAccessTokenString(\n token: string\n): Promise<AccessTokenClaims | null> {\n try {\n const { payload } = await jwtVerify(token, jwks, {\n issuer: AUTH_SERVER_URL,\n });\n return payload as unknown as AccessTokenClaims;\n } catch {\n return null;\n }\n}\n","import { cache } from \"react\";\nimport { redirect } from \"next/navigation\";\nimport type { LemonUser } from \"../types.js\";\nimport { verifyAccessToken } from \"./verify.js\";\n\nfunction claimsToUser(\n claims: NonNullable<Awaited<ReturnType<typeof verifyAccessToken>>>\n): LemonUser {\n return {\n uid: claims.sub,\n nickname: claims.nickname,\n profileImageUrl: claims.profile_image_url,\n role: claims.role,\n approvedClients: claims.approved_clients,\n };\n}\n\nexport const getUser = cache(async (): Promise<LemonUser | null> => {\n const claims = await verifyAccessToken();\n if (!claims) return null;\n return claimsToUser(claims);\n});\n\nexport async function requireAuth(redirectTo = \"/\"): Promise<LemonUser> {\n const user = await getUser();\n if (user) return user;\n redirect(redirectTo);\n}\n\nexport async function requireClient(\n clientId: string,\n redirectTo = \"/\"\n): Promise<LemonUser> {\n const user = await requireAuth(redirectTo);\n if (!user.approvedClients.includes(clientId)) {\n throw new Error(\"Client not approved\");\n }\n return user;\n}\n","import { REFRESH_URL } from \"../constants.js\";\n\nexport interface RefreshResult {\n ok: boolean;\n setCookieHeaders: string[];\n}\n\nexport async function refreshTokenFromCookie(\n refreshTokenCookie: string\n): Promise<RefreshResult> {\n const res = await fetch(REFRESH_URL, {\n method: \"POST\",\n headers: {\n Cookie: `refresh_token=${refreshTokenCookie}`,\n },\n });\n\n if (!res.ok) {\n return { ok: false, setCookieHeaders: [] };\n }\n\n const setCookieHeaders = res.headers.getSetCookie();\n return { ok: true, setCookieHeaders };\n}\n"],"mappings":";;;;;;;;;;;;;AAAA,SAAS,oBAAoB,iBAAiB;AAC9C,SAAS,eAAe;AAIxB,IAAM,OAAO,mBAAmB,IAAI,IAAI,QAAQ,CAAC;AAEjD,eAAsB,oBAAuD;AAC3E,QAAM,cAAc,MAAM,QAAQ;AAClC,QAAM,QAAQ,YAAY,IAAI,mBAAmB,GAAG;AACpD,MAAI,CAAC,MAAO,QAAO;AAEnB,MAAI;AACF,UAAM,EAAE,QAAQ,IAAI,MAAM,UAAU,OAAO,MAAM;AAAA,MAC/C,QAAQ;AAAA,IACV,CAAC;AACD,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,eAAsB,wBACpB,OACmC;AACnC,MAAI;AACF,UAAM,EAAE,QAAQ,IAAI,MAAM,UAAU,OAAO,MAAM;AAAA,MAC/C,QAAQ;AAAA,IACV,CAAC;AACD,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;;;ACjCA,SAAS,aAAa;AACtB,SAAS,gBAAgB;AAIzB,SAAS,aACP,QACW;AACX,SAAO;AAAA,IACL,KAAK,OAAO;AAAA,IACZ,UAAU,OAAO;AAAA,IACjB,iBAAiB,OAAO;AAAA,IACxB,MAAM,OAAO;AAAA,IACb,iBAAiB,OAAO;AAAA,EAC1B;AACF;AAEO,IAAM,UAAU,MAAM,YAAuC;AAClE,QAAM,SAAS,MAAM,kBAAkB;AACvC,MAAI,CAAC,OAAQ,QAAO;AACpB,SAAO,aAAa,MAAM;AAC5B,CAAC;AAED,eAAsB,YAAY,aAAa,KAAyB;AACtE,QAAM,OAAO,MAAM,QAAQ;AAC3B,MAAI,KAAM,QAAO;AACjB,WAAS,UAAU;AACrB;AAEA,eAAsB,cACpB,UACA,aAAa,KACO;AACpB,QAAM,OAAO,MAAM,YAAY,UAAU;AACzC,MAAI,CAAC,KAAK,gBAAgB,SAAS,QAAQ,GAAG;AAC5C,UAAM,IAAI,MAAM,qBAAqB;AAAA,EACvC;AACA,SAAO;AACT;;;AC/BA,eAAsB,uBACpB,oBACwB;AACxB,QAAM,MAAM,MAAM,MAAM,aAAa;AAAA,IACnC,QAAQ;AAAA,IACR,SAAS;AAAA,MACP,QAAQ,iBAAiB,kBAAkB;AAAA,IAC7C;AAAA,EACF,CAAC;AAED,MAAI,CAAC,IAAI,IAAI;AACX,WAAO,EAAE,IAAI,OAAO,kBAAkB,CAAC,EAAE;AAAA,EAC3C;AAEA,QAAM,mBAAmB,IAAI,QAAQ,aAAa;AAClD,SAAO,EAAE,IAAI,MAAM,iBAAiB;AACtC;","names":[]}
@@ -0,0 +1,24 @@
1
+ import { JWTPayload } from 'jose';
2
+
3
+ interface AccessTokenClaims extends JWTPayload {
4
+ sub: string;
5
+ nickname: string;
6
+ profile_image_url: string;
7
+ role: "user" | "admin";
8
+ approved_clients: string[];
9
+ }
10
+ interface LemonUser {
11
+ uid: string;
12
+ nickname: string;
13
+ profileImageUrl: string;
14
+ role: "user" | "admin";
15
+ approvedClients: string[];
16
+ }
17
+ interface UserProfile {
18
+ uid: string;
19
+ nickname: string;
20
+ profile_image_url: string;
21
+ role: "user" | "admin";
22
+ }
23
+
24
+ export type { AccessTokenClaims as A, LemonUser as L, UserProfile as U };
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@lemondouble/lemon-auth",
3
+ "version": "0.1.0",
4
+ "description": "Auth library for *.lemondouble.com services",
5
+ "type": "module",
6
+ "exports": {
7
+ "./server": {
8
+ "types": "./dist/server/index.d.ts",
9
+ "import": "./dist/server/index.js"
10
+ },
11
+ "./client": {
12
+ "types": "./dist/client/index.d.ts",
13
+ "import": "./dist/client/index.js"
14
+ },
15
+ "./proxy": {
16
+ "types": "./dist/proxy/index.d.ts",
17
+ "import": "./dist/proxy/index.js"
18
+ }
19
+ },
20
+ "files": [
21
+ "dist"
22
+ ],
23
+ "scripts": {
24
+ "build": "tsup",
25
+ "dev": "tsup --watch",
26
+ "typecheck": "tsc --noEmit",
27
+ "prepublishOnly": "npm run build"
28
+ },
29
+ "peerDependencies": {
30
+ "next": ">=16.0.0",
31
+ "react": ">=19.0.0"
32
+ },
33
+ "dependencies": {
34
+ "jose": "^6.2.2"
35
+ },
36
+ "keywords": [
37
+ "auth",
38
+ "lemondouble",
39
+ "next.js",
40
+ "jwt"
41
+ ],
42
+ "author": "lemondouble",
43
+ "license": "MIT",
44
+ "devDependencies": {
45
+ "@types/node": "^25.6.0",
46
+ "@types/react": "^19.2.14",
47
+ "tsup": "^8.5.1",
48
+ "typescript": "^6.0.3"
49
+ }
50
+ }