@lemondouble/lemon-auth 0.5.0 → 0.7.1

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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/client/context.tsx","../../src/client/auto-token-refresh.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","\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { useRouter } from \"next/navigation\";\nimport { useAuth } from \"./context.js\";\nimport { refreshToken } from \"./url.js\";\n\nexport function AutoTokenRefresh({\n children,\n fallback,\n}: {\n children?: React.ReactNode;\n fallback: React.ReactNode;\n}) {\n const { isAuthenticated } = useAuth();\n const router = useRouter();\n const [status, setStatus] = useState<\"loading\" | \"failed\">(\"loading\");\n\n useEffect(() => {\n if (isAuthenticated) return;\n\n let ignore = false;\n\n refreshToken().then((ok) => {\n if (ignore) return;\n if (ok) {\n router.refresh();\n } else {\n setStatus(\"failed\");\n }\n });\n\n return () => {\n ignore = true;\n };\n }, [isAuthenticated, router]);\n\n if (isAuthenticated) return null;\n if (status === \"loading\") return fallback ?? null;\n return children ?? null;\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;;;AC7BA,SAAS,WAAW,gBAAgB;AACpC,SAAS,iBAAiB;;;ACInB,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;;;ADxBO,SAAS,iBAAiB;AAAA,EAC/B;AAAA,EACA;AACF,GAGG;AACD,QAAM,EAAE,gBAAgB,IAAI,QAAQ;AACpC,QAAM,SAAS,UAAU;AACzB,QAAM,CAAC,QAAQ,SAAS,IAAI,SAA+B,SAAS;AAEpE,YAAU,MAAM;AACd,QAAI,gBAAiB;AAErB,QAAI,SAAS;AAEb,iBAAa,EAAE,KAAK,CAAC,OAAO;AAC1B,UAAI,OAAQ;AACZ,UAAI,IAAI;AACN,eAAO,QAAQ;AAAA,MACjB,OAAO;AACL,kBAAU,QAAQ;AAAA,MACpB;AAAA,IACF,CAAC;AAED,WAAO,MAAM;AACX,eAAS;AAAA,IACX;AAAA,EACF,GAAG,CAAC,iBAAiB,MAAM,CAAC;AAE5B,MAAI,gBAAiB,QAAO;AAC5B,MAAI,WAAW,UAAW,QAAO,YAAY;AAC7C,SAAO,YAAY;AACrB;","names":[]}
1
+ {"version":3,"sources":["../../src/client/context.tsx","../../src/client/auto-token-refresh.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","\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { useRouter } from \"next/navigation\";\nimport { useAuth } from \"./context.js\";\nimport { refreshToken } from \"./url.js\";\n\nexport function AutoTokenRefresh({\n children,\n fallback,\n}: {\n children?: React.ReactNode;\n fallback: React.ReactNode;\n}) {\n const { isAuthenticated } = useAuth();\n const router = useRouter();\n const [status, setStatus] = useState<\"loading\" | \"failed\">(\"loading\");\n\n useEffect(() => {\n if (isAuthenticated) return;\n\n let ignore = false;\n\n refreshToken().then((ok) => {\n if (ignore) return;\n if (ok) {\n router.refresh();\n } else {\n setStatus(\"failed\");\n }\n });\n\n return () => {\n ignore = true;\n };\n }, [isAuthenticated, router]);\n\n if (isAuthenticated) return null;\n if (status === \"loading\") return fallback ?? null;\n return children ?? null;\n}\n","import {\n ACCESS_TOKEN_COOKIE,\n DEVICE_ID_COOKIE,\n LOGIN_URL,\n LOGOUT_URL,\n PROFILE_PAGE_URL,\n REFRESH_URL,\n REFRESH_TOKEN_COOKIE,\n} from \"../constants.js\";\nimport { isMockAuthEnabled } from \"../mock.js\";\n\nconst LOGOUT_COOKIE_NAMES = [\n ACCESS_TOKEN_COOKIE,\n REFRESH_TOKEN_COOKIE,\n DEVICE_ID_COOKIE,\n];\n\nexport function loginUrl(redirectUrl: string): string {\n if (isMockAuthEnabled()) return redirectUrl;\n assertValidLoginRedirectUrl(redirectUrl);\n return `${LOGIN_URL}?redirect_url=${encodeURIComponent(redirectUrl)}`;\n}\n\nexport function profileUrl(redirectUrl?: string): string {\n if (isMockAuthEnabled()) return redirectUrl ?? \"/\";\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<boolean> {\n if (isMockAuthEnabled()) {\n expireAuthCookies();\n return true;\n }\n\n try {\n const res = await fetch(LOGOUT_URL, {\n method: \"POST\",\n credentials: \"include\",\n });\n return res.ok;\n } catch {\n return false;\n } finally {\n expireAuthCookies();\n }\n}\n\nexport async function refreshToken(): Promise<boolean> {\n if (isMockAuthEnabled()) return true;\n\n try {\n const res = await fetch(REFRESH_URL, {\n method: \"POST\",\n credentials: \"include\",\n });\n return res.ok;\n } catch {\n return false;\n }\n}\n\nfunction expireAuthCookies(): void {\n if (typeof document === \"undefined\") return;\n\n for (const name of LOGOUT_COOKIE_NAMES) {\n for (const domain of getCookieDomains()) {\n document.cookie = [\n `${name}=`,\n \"Max-Age=0\",\n \"Path=/\",\n \"SameSite=Lax\",\n domain ? `Domain=${domain}` : \"\",\n ]\n .filter(Boolean)\n .join(\"; \");\n }\n }\n}\n\nfunction getCookieDomains(): string[] {\n if (typeof window === \"undefined\") return [\"\"];\n\n const { hostname } = window.location;\n if (!hostname || hostname === \"localhost\" || isIpAddress(hostname)) {\n return [\"\"];\n }\n\n const labels = hostname.split(\".\");\n const domains = [\"\"];\n for (let i = 0; i < labels.length - 1; i += 1) {\n domains.push(`.${labels.slice(i).join(\".\")}`);\n }\n return domains;\n}\n\nfunction isIpAddress(hostname: string): boolean {\n return /^[\\d.]+$/.test(hostname) || hostname.includes(\":\");\n}\n\nfunction assertValidLoginRedirectUrl(redirectUrl: string): void {\n let url: URL;\n try {\n url = new URL(redirectUrl);\n } catch {\n throw new Error(\"loginUrl redirectUrl must be a valid absolute URL\");\n }\n\n if (url.protocol !== \"https:\") {\n throw new Error(\"loginUrl redirectUrl must use https\");\n }\n\n if (!isAllowedLoginRedirectHost(url.hostname)) {\n throw new Error(\n \"loginUrl redirectUrl host must be lemondouble.com or a subdomain of lemondouble.com\"\n );\n }\n}\n\nfunction isAllowedLoginRedirectHost(hostname: string): boolean {\n const normalized = hostname.toLowerCase();\n return (\n normalized === \"lemondouble.com\" ||\n normalized.endsWith(\".lemondouble.com\")\n );\n}\n"],"mappings":"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","names":[]}
@@ -1,14 +1,25 @@
1
1
  import { NextRequest, NextResponse } from 'next/server';
2
- import { A as AccessTokenClaims } from '../types-CgrEUXwM.js';
3
- export { L as LemonUser } from '../types-CgrEUXwM.js';
2
+ import { A as AccessTokenClaims } from '../types-B5uRY2Si.js';
3
+ export { a as LemonUser } from '../types-B5uRY2Si.js';
4
4
  import 'jose';
5
5
 
6
+ declare const DEFAULT_AUTH_BYPASS_PATHS: string[];
7
+ declare const DEFAULT_API_PATHS: string[];
8
+ declare const PROXY_AUTH_ERROR: {
9
+ readonly UNAUTHORIZED: "UNAUTHORIZED";
10
+ readonly FORBIDDEN: "FORBIDDEN";
11
+ };
12
+ type ProxyAuthErrorCode = (typeof PROXY_AUTH_ERROR)[keyof typeof PROXY_AUTH_ERROR];
13
+ type LoginRedirectUrl = string | ((request: NextRequest) => string);
6
14
  interface AuthProxyOptions {
7
15
  publicPaths?: string[];
16
+ bypassPaths?: string[];
17
+ apiPaths?: string[];
8
18
  clientId?: string;
9
- loginRedirectUrl?: string;
19
+ loginRedirectUrl?: LoginRedirectUrl;
20
+ unapprovedRedirectUrl?: LoginRedirectUrl;
10
21
  onAuthSuccess?: (claims: AccessTokenClaims, request: NextRequest, response: NextResponse) => Promise<NextResponse> | NextResponse;
11
22
  }
12
23
  declare function createAuthProxy(options?: AuthProxyOptions): (request: NextRequest) => Promise<NextResponse>;
13
24
 
14
- export { AccessTokenClaims, type AuthProxyOptions, createAuthProxy };
25
+ export { AccessTokenClaims, type AuthProxyOptions, DEFAULT_API_PATHS, DEFAULT_AUTH_BYPASS_PATHS, type LoginRedirectUrl, PROXY_AUTH_ERROR, type ProxyAuthErrorCode, createAuthProxy };
@@ -1,73 +1,173 @@
1
+ import {
2
+ isAccessTokenClaims,
3
+ isApprovedClient
4
+ } from "../chunk-6W24RWSJ.js";
1
5
  import {
2
6
  ACCESS_TOKEN_COOKIE,
3
7
  AUTH_SERVER_URL,
8
+ DEVICE_ID_COOKIE,
4
9
  JWKS_URL,
5
10
  LOGIN_URL,
6
11
  REFRESH_TOKEN_COOKIE,
7
- REFRESH_URL
8
- } from "../chunk-3ARMB6QF.js";
12
+ REFRESH_URL,
13
+ getMockAccessTokenClaims
14
+ } from "../chunk-OX5MDVSR.js";
9
15
 
10
16
  // src/proxy/index.ts
11
17
  import { NextResponse } from "next/server";
12
18
  import { createRemoteJWKSet, jwtVerify } from "jose";
13
19
  var REFRESH_BUFFER_SECONDS = 60;
20
+ var DEFAULT_AUTH_BYPASS_PATHS = [
21
+ "/sw.js",
22
+ "/service-worker.js",
23
+ "/manifest.webmanifest",
24
+ "/manifest.json",
25
+ "/favicon.ico",
26
+ "/robots.txt",
27
+ "/sitemap.xml",
28
+ "/browserconfig.xml",
29
+ "/apple-touch-icon.png",
30
+ "/apple-touch-icon-*",
31
+ "/icon-*",
32
+ "/icons/*"
33
+ ];
34
+ var DEFAULT_API_PATHS = ["/api/*"];
35
+ var PROXY_AUTH_ERROR = {
36
+ UNAUTHORIZED: "UNAUTHORIZED",
37
+ FORBIDDEN: "FORBIDDEN"
38
+ };
14
39
  var jwks = createRemoteJWKSet(new URL(JWKS_URL));
15
40
  function createAuthProxy(options = {}) {
16
- const { publicPaths = [], clientId, loginRedirectUrl, onAuthSuccess } = options;
41
+ const {
42
+ publicPaths = [],
43
+ bypassPaths = [],
44
+ apiPaths = DEFAULT_API_PATHS,
45
+ clientId,
46
+ loginRedirectUrl,
47
+ unapprovedRedirectUrl,
48
+ onAuthSuccess
49
+ } = options;
50
+ const effectiveBypassPaths = [
51
+ ...DEFAULT_AUTH_BYPASS_PATHS,
52
+ ...bypassPaths
53
+ ];
17
54
  return async function proxy(request) {
18
55
  const { pathname } = request.nextUrl;
56
+ if (isPublicPath(pathname, effectiveBypassPaths)) {
57
+ return NextResponse.next();
58
+ }
19
59
  if (isPublicPath(pathname, publicPaths)) {
20
60
  return maybeRefreshAndContinue(request);
21
61
  }
62
+ const isApiRequest = isPublicPath(pathname, apiPaths);
63
+ const mockClaims = getMockAccessTokenClaims();
64
+ if (mockClaims) {
65
+ return handleVerifiedClaims(
66
+ mockClaims,
67
+ request,
68
+ isApiRequest,
69
+ clientId,
70
+ unapprovedRedirectUrl,
71
+ onAuthSuccess,
72
+ NextResponse.next()
73
+ );
74
+ }
22
75
  const accessToken = request.cookies.get(ACCESS_TOKEN_COOKIE)?.value;
23
76
  const refreshToken = request.cookies.get(REFRESH_TOKEN_COOKIE)?.value;
77
+ const deviceId = request.cookies.get(DEVICE_ID_COOKIE)?.value;
24
78
  if (accessToken && isTokenFresh(accessToken)) {
25
79
  const claims = await verifyToken(accessToken);
26
80
  if (claims) {
27
- if (clientId && !claims.approved_clients.includes(clientId)) {
28
- return redirectToLogin(request, loginRedirectUrl);
29
- }
30
- const response = NextResponse.next();
31
- return onAuthSuccess ? await onAuthSuccess(claims, request, response) : response;
81
+ return handleVerifiedClaims(
82
+ claims,
83
+ request,
84
+ isApiRequest,
85
+ clientId,
86
+ unapprovedRedirectUrl,
87
+ onAuthSuccess,
88
+ NextResponse.next()
89
+ );
32
90
  }
33
91
  }
34
92
  if (refreshToken) {
35
- const result = await tryRefresh(refreshToken);
93
+ const result = await tryRefresh(refreshToken, deviceId);
36
94
  if (result) {
37
95
  const claims = await verifyToken(result.newAccessToken);
38
96
  if (claims) {
39
- if (clientId && !claims.approved_clients.includes(clientId)) {
40
- return redirectToLogin(request, loginRedirectUrl);
41
- }
42
- const response = NextResponse.next();
43
- for (const header of result.setCookieHeaders) {
44
- response.headers.append("Set-Cookie", header);
45
- }
46
- return onAuthSuccess ? await onAuthSuccess(claims, request, response) : response;
97
+ const response = nextWithRefreshedCookies(
98
+ request,
99
+ result.setCookieHeaders
100
+ );
101
+ return handleVerifiedClaims(
102
+ claims,
103
+ request,
104
+ isApiRequest,
105
+ clientId,
106
+ unapprovedRedirectUrl,
107
+ onAuthSuccess,
108
+ response
109
+ );
47
110
  }
48
111
  }
49
112
  }
50
- return redirectToLogin(request, loginRedirectUrl);
113
+ return isApiRequest ? unauthorizedJson() : redirectToLogin(request, loginRedirectUrl);
51
114
  };
52
115
  }
116
+ async function handleVerifiedClaims(claims, request, isApiRequest, clientId, unapprovedRedirectUrl, onAuthSuccess, response) {
117
+ if (!isApprovedClient(claims.approved_clients, clientId)) {
118
+ return isApiRequest ? forbiddenJson() : redirectToUnapproved(request, unapprovedRedirectUrl);
119
+ }
120
+ return onAuthSuccess ? await onAuthSuccess(claims, request, response) : response;
121
+ }
122
+ function unauthorizedJson() {
123
+ return NextResponse.json(
124
+ { code: PROXY_AUTH_ERROR.UNAUTHORIZED },
125
+ { status: 401 }
126
+ );
127
+ }
128
+ function forbiddenJson() {
129
+ return NextResponse.json(
130
+ { code: PROXY_AUTH_ERROR.FORBIDDEN },
131
+ { status: 403 }
132
+ );
133
+ }
53
134
  async function maybeRefreshAndContinue(request) {
54
135
  const accessToken = request.cookies.get(ACCESS_TOKEN_COOKIE)?.value;
55
136
  const refreshToken = request.cookies.get(REFRESH_TOKEN_COOKIE)?.value;
137
+ const deviceId = request.cookies.get(DEVICE_ID_COOKIE)?.value;
56
138
  if (accessToken && isTokenFresh(accessToken)) {
57
139
  return NextResponse.next();
58
140
  }
59
141
  if (refreshToken) {
60
- const result = await tryRefresh(refreshToken);
142
+ const result = await tryRefresh(refreshToken, deviceId);
61
143
  if (result) {
62
- const response = NextResponse.next();
63
- for (const header of result.setCookieHeaders) {
64
- response.headers.append("Set-Cookie", header);
65
- }
66
- return response;
144
+ return nextWithRefreshedCookies(request, result.setCookieHeaders);
67
145
  }
68
146
  }
69
147
  return NextResponse.next();
70
148
  }
149
+ function nextWithRefreshedCookies(request, setCookieHeaders) {
150
+ const merged = /* @__PURE__ */ new Map();
151
+ for (const { name, value } of request.cookies.getAll()) {
152
+ merged.set(name, value);
153
+ }
154
+ for (const header of setCookieHeaders) {
155
+ const pair = header.split(";")[0] ?? "";
156
+ const separator = pair.indexOf("=");
157
+ if (separator <= 0) continue;
158
+ merged.set(pair.slice(0, separator).trim(), pair.slice(separator + 1).trim());
159
+ }
160
+ const requestHeaders = new Headers(request.headers);
161
+ requestHeaders.set(
162
+ "cookie",
163
+ Array.from(merged, ([name, value]) => `${name}=${value}`).join("; ")
164
+ );
165
+ const response = NextResponse.next({ request: { headers: requestHeaders } });
166
+ for (const header of setCookieHeaders) {
167
+ response.headers.append("Set-Cookie", header);
168
+ }
169
+ return response;
170
+ }
71
171
  function isTokenFresh(token) {
72
172
  try {
73
173
  const parts = token.split(".");
@@ -85,16 +185,16 @@ async function verifyToken(token) {
85
185
  const { payload } = await jwtVerify(token, jwks, {
86
186
  issuer: AUTH_SERVER_URL
87
187
  });
88
- return payload;
188
+ return isAccessTokenClaims(payload) ? payload : null;
89
189
  } catch {
90
190
  return null;
91
191
  }
92
192
  }
93
- async function tryRefresh(refreshToken) {
193
+ async function tryRefresh(refreshToken, deviceId) {
94
194
  try {
95
195
  const res = await fetch(REFRESH_URL, {
96
196
  method: "POST",
97
- headers: { Cookie: `lemon_refresh_token=${refreshToken}` }
197
+ headers: { Cookie: buildRefreshCookieHeader(refreshToken, deviceId) }
98
198
  });
99
199
  if (!res.ok) return null;
100
200
  const setCookieHeaders = res.headers.getSetCookie();
@@ -105,9 +205,16 @@ async function tryRefresh(refreshToken) {
105
205
  return null;
106
206
  }
107
207
  }
208
+ function buildRefreshCookieHeader(refreshToken, deviceId) {
209
+ const cookies = [`${REFRESH_TOKEN_COOKIE}=${refreshToken}`];
210
+ if (deviceId) {
211
+ cookies.push(`${DEVICE_ID_COOKIE}=${deviceId}`);
212
+ }
213
+ return cookies.join("; ");
214
+ }
108
215
  function extractAccessToken(setCookieHeaders) {
109
216
  for (const header of setCookieHeaders) {
110
- const match = header.match(/^lemon_access_token=([^;]+)/);
217
+ const match = header.match(new RegExp(`^${ACCESS_TOKEN_COOKIE}=([^;]+)`));
111
218
  if (match) return match[1];
112
219
  }
113
220
  return null;
@@ -121,13 +228,33 @@ function isPublicPath(pathname, publicPaths) {
121
228
  });
122
229
  }
123
230
  function redirectToLogin(request, loginRedirectUrl) {
124
- if (loginRedirectUrl) {
125
- const url = `${LOGIN_URL}?redirect_url=${encodeURIComponent(loginRedirectUrl)}`;
231
+ const redirectUrl = resolveLoginRedirectUrl(request, loginRedirectUrl);
232
+ if (redirectUrl) {
233
+ const url = `${LOGIN_URL}?redirect_url=${encodeURIComponent(redirectUrl)}`;
126
234
  return NextResponse.redirect(new URL(url, request.url));
127
235
  }
128
236
  return NextResponse.redirect(new URL("/", request.url));
129
237
  }
238
+ function redirectToUnapproved(request, unapprovedRedirectUrl) {
239
+ const redirectUrl = resolveLoginRedirectUrl(request, unapprovedRedirectUrl);
240
+ if (redirectUrl) {
241
+ return NextResponse.redirect(new URL(redirectUrl, request.url));
242
+ }
243
+ const errorUrl = new URL(`${AUTH_SERVER_URL}/error`);
244
+ errorUrl.searchParams.set("code", PROXY_AUTH_ERROR.FORBIDDEN);
245
+ errorUrl.searchParams.set("from", request.url);
246
+ return NextResponse.redirect(errorUrl);
247
+ }
248
+ function resolveLoginRedirectUrl(request, loginRedirectUrl) {
249
+ if (typeof loginRedirectUrl === "function") {
250
+ return loginRedirectUrl(request);
251
+ }
252
+ return loginRedirectUrl;
253
+ }
130
254
  export {
255
+ DEFAULT_API_PATHS,
256
+ DEFAULT_AUTH_BYPASS_PATHS,
257
+ PROXY_AUTH_ERROR,
131
258
  createAuthProxy
132
259
  };
133
260
  //# sourceMappingURL=index.js.map
@@ -1 +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 onAuthSuccess?: (\n claims: AccessTokenClaims,\n request: NextRequest,\n response: NextResponse\n ) => Promise<NextResponse> | NextResponse;\n}\n\nexport function createAuthProxy(options: AuthProxyOptions = {}) {\n const { publicPaths = [], clientId, loginRedirectUrl, onAuthSuccess } =\n 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 const response = NextResponse.next();\n return onAuthSuccess\n ? await onAuthSuccess(claims, request, response)\n : response;\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 onAuthSuccess\n ? await onAuthSuccess(claims, request, response)\n : 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: `lemon_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(/^lemon_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":[]}
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 DEVICE_ID_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\";\nimport { isAccessTokenClaims } from \"../claims.js\";\nimport { isApprovedClient } from \"../approval.js\";\nimport { getMockAccessTokenClaims } from \"../mock.js\";\n\nconst REFRESH_BUFFER_SECONDS = 60;\n\nexport const DEFAULT_AUTH_BYPASS_PATHS = [\n \"/sw.js\",\n \"/service-worker.js\",\n \"/manifest.webmanifest\",\n \"/manifest.json\",\n \"/favicon.ico\",\n \"/robots.txt\",\n \"/sitemap.xml\",\n \"/browserconfig.xml\",\n \"/apple-touch-icon.png\",\n \"/apple-touch-icon-*\",\n \"/icon-*\",\n \"/icons/*\",\n];\n\nexport const DEFAULT_API_PATHS = [\"/api/*\"];\n\nexport const PROXY_AUTH_ERROR = {\n UNAUTHORIZED: \"UNAUTHORIZED\",\n FORBIDDEN: \"FORBIDDEN\",\n} as const;\nexport type ProxyAuthErrorCode =\n (typeof PROXY_AUTH_ERROR)[keyof typeof PROXY_AUTH_ERROR];\n\nconst jwks = createRemoteJWKSet(new URL(JWKS_URL));\n\nexport type LoginRedirectUrl = string | ((request: NextRequest) => string);\n\nexport interface AuthProxyOptions {\n publicPaths?: string[];\n bypassPaths?: string[];\n apiPaths?: string[];\n clientId?: string;\n loginRedirectUrl?: LoginRedirectUrl;\n unapprovedRedirectUrl?: LoginRedirectUrl;\n onAuthSuccess?: (\n claims: AccessTokenClaims,\n request: NextRequest,\n response: NextResponse\n ) => Promise<NextResponse> | NextResponse;\n}\n\nexport function createAuthProxy(options: AuthProxyOptions = {}) {\n const {\n publicPaths = [],\n bypassPaths = [],\n apiPaths = DEFAULT_API_PATHS,\n clientId,\n loginRedirectUrl,\n unapprovedRedirectUrl,\n onAuthSuccess,\n } = options;\n const effectiveBypassPaths = [\n ...DEFAULT_AUTH_BYPASS_PATHS,\n ...bypassPaths,\n ];\n\n return async function proxy(request: NextRequest): Promise<NextResponse> {\n const { pathname } = request.nextUrl;\n\n if (isPublicPath(pathname, effectiveBypassPaths)) {\n return NextResponse.next();\n }\n\n if (isPublicPath(pathname, publicPaths)) {\n return maybeRefreshAndContinue(request);\n }\n\n const isApiRequest = isPublicPath(pathname, apiPaths);\n const mockClaims = getMockAccessTokenClaims();\n if (mockClaims) {\n return handleVerifiedClaims(\n mockClaims,\n request,\n isApiRequest,\n clientId,\n unapprovedRedirectUrl,\n onAuthSuccess,\n NextResponse.next()\n );\n }\n\n const accessToken = request.cookies.get(ACCESS_TOKEN_COOKIE)?.value;\n const refreshToken = request.cookies.get(REFRESH_TOKEN_COOKIE)?.value;\n const deviceId = request.cookies.get(DEVICE_ID_COOKIE)?.value;\n\n if (accessToken && isTokenFresh(accessToken)) {\n const claims = await verifyToken(accessToken);\n if (claims) {\n return handleVerifiedClaims(\n claims,\n request,\n isApiRequest,\n clientId,\n unapprovedRedirectUrl,\n onAuthSuccess,\n NextResponse.next()\n );\n }\n }\n\n if (refreshToken) {\n const result = await tryRefresh(refreshToken, deviceId);\n if (result) {\n const claims = await verifyToken(result.newAccessToken);\n if (claims) {\n const response = nextWithRefreshedCookies(\n request,\n result.setCookieHeaders\n );\n return handleVerifiedClaims(\n claims,\n request,\n isApiRequest,\n clientId,\n unapprovedRedirectUrl,\n onAuthSuccess,\n response\n );\n }\n }\n }\n\n return isApiRequest\n ? unauthorizedJson()\n : redirectToLogin(request, loginRedirectUrl);\n };\n}\n\nasync function handleVerifiedClaims(\n claims: AccessTokenClaims,\n request: NextRequest,\n isApiRequest: boolean,\n clientId: string | undefined,\n unapprovedRedirectUrl: LoginRedirectUrl | undefined,\n onAuthSuccess: AuthProxyOptions[\"onAuthSuccess\"],\n response: NextResponse\n): Promise<NextResponse> {\n if (!isApprovedClient(claims.approved_clients, clientId)) {\n return isApiRequest\n ? forbiddenJson()\n : redirectToUnapproved(request, unapprovedRedirectUrl);\n }\n\n return onAuthSuccess\n ? await onAuthSuccess(claims, request, response)\n : response;\n}\n\nfunction unauthorizedJson(): NextResponse {\n return NextResponse.json(\n { code: PROXY_AUTH_ERROR.UNAUTHORIZED },\n { status: 401 }\n );\n}\n\nfunction forbiddenJson(): NextResponse {\n return NextResponse.json(\n { code: PROXY_AUTH_ERROR.FORBIDDEN },\n { status: 403 }\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 const deviceId = request.cookies.get(DEVICE_ID_COOKIE)?.value;\n\n if (accessToken && isTokenFresh(accessToken)) {\n return NextResponse.next();\n }\n\n if (refreshToken) {\n const result = await tryRefresh(refreshToken, deviceId);\n if (result) {\n return nextWithRefreshedCookies(request, result.setCookieHeaders);\n }\n }\n\n return NextResponse.next();\n}\n\n/**\n * 갱신된 토큰을 이 요청의 cookie 헤더에도 반영한 응답을 만든다.\n *\n * Set-Cookie 는 브라우저에만 적용된다. 그것만 붙이면 지금 이 요청을 처리하는\n * 서버 컴포넌트는 여전히 만료된 access token 을 읽고, 갱신에 성공했는데도\n * 그 요청은 로그아웃으로 판정된다. access token 이 만료된 뒤 첫 요청마다\n * 발생하며, PWA 콜드 스타트처럼 항상 만료 상태에서 시작하는 경우 매번 겪는다.\n *\n * Next.js 는 미들웨어가 넘긴 요청 헤더를 다운스트림에 그대로 전달하므로\n * cookie 헤더를 새 토큰으로 바꿔 끼워 같은 요청에서 인증이 보이게 한다.\n * (response.headers.append 는 NextResponse 의 쿠키 프록시를 우회하기 때문에\n * Next 의 x-middleware-set-cookie 병합 경로도 타지 않는다)\n */\nfunction nextWithRefreshedCookies(\n request: NextRequest,\n setCookieHeaders: string[]\n): NextResponse {\n const merged = new Map<string, string>();\n for (const { name, value } of request.cookies.getAll()) {\n merged.set(name, value);\n }\n\n // auth 서버는 도메인마다 같은 쿠키를 한 번씩 보내므로 이름 기준으로 덮어쓴다.\n for (const header of setCookieHeaders) {\n const pair = header.split(\";\")[0] ?? \"\";\n const separator = pair.indexOf(\"=\");\n if (separator <= 0) continue;\n merged.set(pair.slice(0, separator).trim(), pair.slice(separator + 1).trim());\n }\n\n const requestHeaders = new Headers(request.headers);\n requestHeaders.set(\n \"cookie\",\n Array.from(merged, ([name, value]) => `${name}=${value}`).join(\"; \")\n );\n\n const response = NextResponse.next({ request: { headers: requestHeaders } });\n for (const header of setCookieHeaders) {\n response.headers.append(\"Set-Cookie\", header);\n }\n return response;\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 isAccessTokenClaims(payload) ? payload : null;\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 deviceId?: string\n): Promise<RefreshSuccess | null> {\n try {\n const res = await fetch(REFRESH_URL, {\n method: \"POST\",\n headers: { Cookie: buildRefreshCookieHeader(refreshToken, deviceId) },\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 buildRefreshCookieHeader(\n refreshToken: string,\n deviceId?: string\n): string {\n const cookies = [`${REFRESH_TOKEN_COOKIE}=${refreshToken}`];\n if (deviceId) {\n cookies.push(`${DEVICE_ID_COOKIE}=${deviceId}`);\n }\n return cookies.join(\"; \");\n}\n\nfunction extractAccessToken(setCookieHeaders: string[]): string | null {\n for (const header of setCookieHeaders) {\n const match = header.match(new RegExp(`^${ACCESS_TOKEN_COOKIE}=([^;]+)`));\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?: LoginRedirectUrl\n): NextResponse {\n const redirectUrl = resolveLoginRedirectUrl(request, loginRedirectUrl);\n if (redirectUrl) {\n const url = `${LOGIN_URL}?redirect_url=${encodeURIComponent(redirectUrl)}`;\n return NextResponse.redirect(new URL(url, request.url));\n }\n return NextResponse.redirect(new URL(\"/\", request.url));\n}\n\nfunction redirectToUnapproved(\n request: NextRequest,\n unapprovedRedirectUrl?: LoginRedirectUrl\n): NextResponse {\n const redirectUrl = resolveLoginRedirectUrl(request, unapprovedRedirectUrl);\n if (redirectUrl) {\n return NextResponse.redirect(new URL(redirectUrl, request.url));\n }\n const errorUrl = new URL(`${AUTH_SERVER_URL}/error`);\n errorUrl.searchParams.set(\"code\", PROXY_AUTH_ERROR.FORBIDDEN);\n errorUrl.searchParams.set(\"from\", request.url);\n return NextResponse.redirect(errorUrl);\n}\n\nfunction resolveLoginRedirectUrl(\n request: NextRequest,\n loginRedirectUrl?: LoginRedirectUrl\n): string | undefined {\n if (typeof loginRedirectUrl === \"function\") {\n return loginRedirectUrl(request);\n }\n return loginRedirectUrl;\n}\n\nexport type { AccessTokenClaims } from \"../types.js\";\nexport type { LemonUser } from \"../types.js\";\n"],"mappings":"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","names":[]}
@@ -1,19 +1,27 @@
1
- import { A as AccessTokenClaims, L as LemonUser } from '../types-CgrEUXwM.js';
2
- export { U as UserProfile } from '../types-CgrEUXwM.js';
1
+ import { A as AccessTokenClaims, L as LemonSession, a as LemonUser } from '../types-B5uRY2Si.js';
2
+ export { U as UserProfile } from '../types-B5uRY2Si.js';
3
3
  import 'jose';
4
4
 
5
5
  declare function verifyAccessToken(): Promise<AccessTokenClaims | null>;
6
6
  declare function verifyAccessTokenString(token: string): Promise<AccessTokenClaims | null>;
7
7
 
8
8
  declare const getUser: () => Promise<LemonUser | null>;
9
+ interface GetSessionOptions {
10
+ clientId?: string;
11
+ }
12
+ declare function getSession(options?: GetSessionOptions): Promise<LemonSession>;
9
13
  declare function requireAuth(redirectTo?: string): Promise<LemonUser>;
10
- declare function requireClient(clientId: string, redirectTo?: string): Promise<LemonUser>;
14
+ interface RequireClientOptions {
15
+ loginRedirectTo?: string;
16
+ unapprovedRedirectTo?: string;
17
+ }
18
+ declare function requireClient(clientId: string, optionsOrRedirectTo?: string | RequireClientOptions): Promise<LemonUser>;
11
19
 
12
20
  interface RefreshResult {
13
21
  ok: boolean;
14
22
  setCookieHeaders: string[];
15
23
  }
16
- declare function refreshTokenFromCookie(refreshTokenCookie: string): Promise<RefreshResult>;
24
+ declare function refreshTokenFromCookie(refreshTokenCookie: string, deviceIdCookie?: string): Promise<RefreshResult>;
17
25
 
18
26
  declare const AUTH_SERVER_URL = "https://auth.lemondouble.com";
19
27
  declare const JWKS_URL = "https://auth.lemondouble.com/.well-known/jwks.json";
@@ -24,5 +32,6 @@ declare const PROFILE_URL = "https://auth.lemondouble.com/api/user/me";
24
32
  declare const PROFILE_PAGE_URL = "https://auth.lemondouble.com/profile";
25
33
  declare const ACCESS_TOKEN_COOKIE = "lemon_access_token";
26
34
  declare const REFRESH_TOKEN_COOKIE = "lemon_refresh_token";
35
+ declare const DEVICE_ID_COOKIE = "device_id";
27
36
 
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 };
37
+ export { ACCESS_TOKEN_COOKIE, AUTH_SERVER_URL, AccessTokenClaims, DEVICE_ID_COOKIE, type GetSessionOptions, JWKS_URL, LOGIN_URL, LOGOUT_URL, LemonSession, LemonUser, PROFILE_PAGE_URL, PROFILE_URL, REFRESH_TOKEN_COOKIE, REFRESH_URL, type RefreshResult, type RequireClientOptions, getSession, getUser, refreshTokenFromCookie, requireAuth, requireClient, verifyAccessToken, verifyAccessTokenString };
@@ -1,20 +1,29 @@
1
+ import {
2
+ isAccessTokenClaims,
3
+ isApprovedClient
4
+ } from "../chunk-6W24RWSJ.js";
1
5
  import {
2
6
  ACCESS_TOKEN_COOKIE,
3
7
  AUTH_SERVER_URL,
8
+ DEVICE_ID_COOKIE,
4
9
  JWKS_URL,
5
10
  LOGIN_URL,
6
11
  LOGOUT_URL,
7
12
  PROFILE_PAGE_URL,
8
13
  PROFILE_URL,
9
14
  REFRESH_TOKEN_COOKIE,
10
- REFRESH_URL
11
- } from "../chunk-3ARMB6QF.js";
15
+ REFRESH_URL,
16
+ getMockAccessTokenClaims,
17
+ isMockAuthEnabled
18
+ } from "../chunk-OX5MDVSR.js";
12
19
 
13
20
  // src/server/verify.ts
14
21
  import { createRemoteJWKSet, jwtVerify } from "jose";
15
22
  import { cookies } from "next/headers";
16
23
  var jwks = createRemoteJWKSet(new URL(JWKS_URL));
17
24
  async function verifyAccessToken() {
25
+ const mockClaims = getMockAccessTokenClaims();
26
+ if (mockClaims) return mockClaims;
18
27
  const cookieStore = await cookies();
19
28
  const token = cookieStore.get(ACCESS_TOKEN_COOKIE)?.value;
20
29
  if (!token) return null;
@@ -22,17 +31,19 @@ async function verifyAccessToken() {
22
31
  const { payload } = await jwtVerify(token, jwks, {
23
32
  issuer: AUTH_SERVER_URL
24
33
  });
25
- return payload;
34
+ return isAccessTokenClaims(payload) ? payload : null;
26
35
  } catch {
27
36
  return null;
28
37
  }
29
38
  }
30
39
  async function verifyAccessTokenString(token) {
40
+ const mockClaims = getMockAccessTokenClaims();
41
+ if (mockClaims) return mockClaims;
31
42
  try {
32
43
  const { payload } = await jwtVerify(token, jwks, {
33
44
  issuer: AUTH_SERVER_URL
34
45
  });
35
- return payload;
46
+ return isAccessTokenClaims(payload) ? payload : null;
36
47
  } catch {
37
48
  return null;
38
49
  }
@@ -55,25 +66,47 @@ var getUser = cache(async () => {
55
66
  if (!claims) return null;
56
67
  return claimsToUser(claims);
57
68
  });
69
+ var getSessionByClientId = cache(
70
+ async (clientId) => {
71
+ const user = await getUser();
72
+ if (!user) return { type: "none" };
73
+ if (!isApprovedClient(user.approvedClients, clientId)) {
74
+ return { type: "unapproved", user };
75
+ }
76
+ return { type: "authenticated", user };
77
+ }
78
+ );
79
+ async function getSession(options = {}) {
80
+ return getSessionByClientId(options.clientId);
81
+ }
58
82
  async function requireAuth(redirectTo = "/") {
59
83
  const user = await getUser();
60
84
  if (user) return user;
61
85
  redirect(redirectTo);
62
86
  }
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");
87
+ async function requireClient(clientId, optionsOrRedirectTo = "/") {
88
+ const options = typeof optionsOrRedirectTo === "string" ? { loginRedirectTo: optionsOrRedirectTo } : optionsOrRedirectTo;
89
+ const session = await getSession({ clientId });
90
+ if (session.type === "none") {
91
+ redirect(options.loginRedirectTo ?? "/");
67
92
  }
68
- return user;
93
+ if (session.type === "unapproved") {
94
+ redirect(
95
+ options.unapprovedRedirectTo ?? `${AUTH_SERVER_URL}/error?code=FORBIDDEN`
96
+ );
97
+ }
98
+ return session.user;
69
99
  }
70
100
 
71
101
  // src/server/refresh.ts
72
- async function refreshTokenFromCookie(refreshTokenCookie) {
102
+ async function refreshTokenFromCookie(refreshTokenCookie, deviceIdCookie) {
103
+ if (isMockAuthEnabled()) {
104
+ return { ok: true, setCookieHeaders: [] };
105
+ }
73
106
  const res = await fetch(REFRESH_URL, {
74
107
  method: "POST",
75
108
  headers: {
76
- Cookie: `lemon_refresh_token=${refreshTokenCookie}`
109
+ Cookie: buildRefreshCookieHeader(refreshTokenCookie, deviceIdCookie)
77
110
  }
78
111
  });
79
112
  if (!res.ok) {
@@ -82,9 +115,17 @@ async function refreshTokenFromCookie(refreshTokenCookie) {
82
115
  const setCookieHeaders = res.headers.getSetCookie();
83
116
  return { ok: true, setCookieHeaders };
84
117
  }
118
+ function buildRefreshCookieHeader(refreshTokenCookie, deviceIdCookie) {
119
+ const cookies2 = [`${REFRESH_TOKEN_COOKIE}=${refreshTokenCookie}`];
120
+ if (deviceIdCookie) {
121
+ cookies2.push(`${DEVICE_ID_COOKIE}=${deviceIdCookie}`);
122
+ }
123
+ return cookies2.join("; ");
124
+ }
85
125
  export {
86
126
  ACCESS_TOKEN_COOKIE,
87
127
  AUTH_SERVER_URL,
128
+ DEVICE_ID_COOKIE,
88
129
  JWKS_URL,
89
130
  LOGIN_URL,
90
131
  LOGOUT_URL,
@@ -92,6 +133,7 @@ export {
92
133
  PROFILE_URL,
93
134
  REFRESH_TOKEN_COOKIE,
94
135
  REFRESH_URL,
136
+ getSession,
95
137
  getUser,
96
138
  refreshTokenFromCookie,
97
139
  requireAuth,
@@ -1 +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: `lemon_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,uBAAuB,kBAAkB;AAAA,IACnD;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":[]}
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\";\nimport { isAccessTokenClaims } from \"../claims.js\";\nimport { getMockAccessTokenClaims } from \"../mock.js\";\n\nconst jwks = createRemoteJWKSet(new URL(JWKS_URL));\n\nexport async function verifyAccessToken(): Promise<AccessTokenClaims | null> {\n const mockClaims = getMockAccessTokenClaims();\n if (mockClaims) return mockClaims;\n\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 isAccessTokenClaims(payload) ? payload : null;\n } catch {\n return null;\n }\n}\n\nexport async function verifyAccessTokenString(\n token: string\n): Promise<AccessTokenClaims | null> {\n const mockClaims = getMockAccessTokenClaims();\n if (mockClaims) return mockClaims;\n\n try {\n const { payload } = await jwtVerify(token, jwks, {\n issuer: AUTH_SERVER_URL,\n });\n return isAccessTokenClaims(payload) ? payload : null;\n } catch {\n return null;\n }\n}\n","import { cache } from \"react\";\nimport { redirect } from \"next/navigation\";\nimport type { LemonSession, LemonUser } from \"../types.js\";\nimport { AUTH_SERVER_URL } from \"../constants.js\";\nimport { isApprovedClient } from \"../approval.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 interface GetSessionOptions {\n clientId?: string;\n}\n\nconst getSessionByClientId = cache(\n async (clientId?: string): Promise<LemonSession> => {\n const user = await getUser();\n if (!user) return { type: \"none\" };\n if (!isApprovedClient(user.approvedClients, clientId)) {\n return { type: \"unapproved\", user };\n }\n return { type: \"authenticated\", user };\n }\n);\n\nexport async function getSession(\n options: GetSessionOptions = {}\n): Promise<LemonSession> {\n return getSessionByClientId(options.clientId);\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 interface RequireClientOptions {\n loginRedirectTo?: string;\n unapprovedRedirectTo?: string;\n}\n\nexport async function requireClient(\n clientId: string,\n optionsOrRedirectTo: string | RequireClientOptions = \"/\"\n): Promise<LemonUser> {\n const options =\n typeof optionsOrRedirectTo === \"string\"\n ? { loginRedirectTo: optionsOrRedirectTo }\n : optionsOrRedirectTo;\n const session = await getSession({ clientId });\n if (session.type === \"none\") {\n redirect(options.loginRedirectTo ?? \"/\");\n }\n if (session.type === \"unapproved\") {\n redirect(\n options.unapprovedRedirectTo ??\n `${AUTH_SERVER_URL}/error?code=FORBIDDEN`\n );\n }\n return session.user;\n}\n","import {\n DEVICE_ID_COOKIE,\n REFRESH_TOKEN_COOKIE,\n REFRESH_URL,\n} from \"../constants.js\";\nimport { isMockAuthEnabled } from \"../mock.js\";\n\nexport interface RefreshResult {\n ok: boolean;\n setCookieHeaders: string[];\n}\n\nexport async function refreshTokenFromCookie(\n refreshTokenCookie: string,\n deviceIdCookie?: string\n): Promise<RefreshResult> {\n if (isMockAuthEnabled()) {\n return { ok: true, setCookieHeaders: [] };\n }\n\n const res = await fetch(REFRESH_URL, {\n method: \"POST\",\n headers: {\n Cookie: buildRefreshCookieHeader(refreshTokenCookie, deviceIdCookie),\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\nfunction buildRefreshCookieHeader(\n refreshTokenCookie: string,\n deviceIdCookie?: string\n): string {\n const cookies = [`${REFRESH_TOKEN_COOKIE}=${refreshTokenCookie}`];\n if (deviceIdCookie) {\n cookies.push(`${DEVICE_ID_COOKIE}=${deviceIdCookie}`);\n }\n return cookies.join(\"; \");\n}\n"],"mappings":"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","names":["cookies"]}
@@ -1,6 +1,7 @@
1
1
  import { JWTPayload } from 'jose';
2
2
 
3
3
  interface AccessTokenClaims extends JWTPayload {
4
+ token_type: "access";
4
5
  sub: string;
5
6
  nickname: string;
6
7
  profile_image_url: string;
@@ -14,6 +15,15 @@ interface LemonUser {
14
15
  role: "user" | "admin";
15
16
  approvedClients: string[];
16
17
  }
18
+ type LemonSession = {
19
+ type: "none";
20
+ } | {
21
+ type: "unapproved";
22
+ user: LemonUser;
23
+ } | {
24
+ type: "authenticated";
25
+ user: LemonUser;
26
+ };
17
27
  interface UserProfile {
18
28
  uid: string;
19
29
  nickname: string;
@@ -21,4 +31,4 @@ interface UserProfile {
21
31
  role: "user" | "admin";
22
32
  }
23
33
 
24
- export type { AccessTokenClaims as A, LemonUser as L, UserProfile as U };
34
+ export type { AccessTokenClaims as A, LemonSession as L, UserProfile as U, LemonUser as a };