@lemondouble/lemon-auth 0.10.0 → 0.11.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.
@@ -31,23 +31,18 @@ async function logout() {
31
31
  return false;
32
32
  }
33
33
  }
34
- async function refreshSession() {
35
- if (isMockAuthEnabled()) return "ok";
34
+ async function refreshToken() {
35
+ if (isMockAuthEnabled()) return true;
36
36
  try {
37
37
  const res = await fetch(REFRESH_URL, {
38
38
  method: "POST",
39
39
  credentials: "include"
40
40
  });
41
- if (res.ok) return "ok";
42
- if (res.status === 401 || res.status === 403) return "unauthorized";
43
- return "unavailable";
41
+ return res.ok;
44
42
  } catch {
45
- return "unavailable";
43
+ return false;
46
44
  }
47
45
  }
48
- async function refreshToken() {
49
- return await refreshSession() === "ok";
50
- }
51
46
  function assertValidLoginRedirectUrl(redirectUrl) {
52
47
  let url;
53
48
  try {
@@ -73,7 +68,6 @@ export {
73
68
  loginUrl,
74
69
  profileUrl,
75
70
  logout,
76
- refreshSession,
77
71
  refreshToken
78
72
  };
79
- //# sourceMappingURL=chunk-ICPDIWYK.js.map
73
+ //# sourceMappingURL=chunk-TKMZEMW7.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/client/url.ts"],"sourcesContent":["import {\n LOGIN_URL,\n LOGOUT_URL,\n PROFILE_PAGE_URL,\n REFRESH_URL,\n} from \"../constants.js\";\nimport { isMockAuthEnabled } from \"../mock.js\";\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\n/** 쿠키는 httpOnly 라 삭제는 응답의 Set-Cookie 가 한다. */\nexport async function logout(): Promise<boolean> {\n if (isMockAuthEnabled()) return true;\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 }\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 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":";;;;;;;;;AAQO,SAAS,SAAS,aAA6B;AACpD,MAAI,kBAAkB,EAAG,QAAO;AAChC,8BAA4B,WAAW;AACvC,SAAO,GAAG,SAAS,iBAAiB,mBAAmB,WAAW,CAAC;AACrE;AAEO,SAAS,WAAW,aAA8B;AACvD,MAAI,kBAAkB,EAAG,QAAO,eAAe;AAC/C,MAAI,aAAa;AACf,WAAO,GAAG,gBAAgB,iBAAiB,mBAAmB,WAAW,CAAC;AAAA,EAC5E;AACA,SAAO;AACT;AAGA,eAAsB,SAA2B;AAC/C,MAAI,kBAAkB,EAAG,QAAO;AAEhC,MAAI;AACF,UAAM,MAAM,MAAM,MAAM,YAAY;AAAA,MAClC,QAAQ;AAAA,MACR,aAAa;AAAA,IACf,CAAC;AACD,WAAO,IAAI;AAAA,EACb,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,eAAsB,eAAiC;AACrD,MAAI,kBAAkB,EAAG,QAAO;AAEhC,MAAI;AACF,UAAM,MAAM,MAAM,MAAM,aAAa;AAAA,MACnC,QAAQ;AAAA,MACR,aAAa;AAAA,IACf,CAAC;AACD,WAAO,IAAI;AAAA,EACb,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,4BAA4B,aAA2B;AAC9D,MAAI;AACJ,MAAI;AACF,UAAM,IAAI,IAAI,WAAW;AAAA,EAC3B,QAAQ;AACN,UAAM,IAAI,MAAM,mDAAmD;AAAA,EACrE;AAEA,MAAI,IAAI,aAAa,UAAU;AAC7B,UAAM,IAAI,MAAM,qCAAqC;AAAA,EACvD;AAEA,MAAI,CAAC,2BAA2B,IAAI,QAAQ,GAAG;AAC7C,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACF;AAEA,SAAS,2BAA2B,UAA2B;AAC7D,QAAM,aAAa,SAAS,YAAY;AACxC,SACE,eAAe,qBACf,WAAW,SAAS,kBAAkB;AAE1C;","names":[]}
@@ -1,7 +1,6 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { a as LemonUser } from '../url-qhLR3uNu.js';
3
- export { l as loginUrl, b as logout, p as profileUrl, r as refreshToken } from '../url-qhLR3uNu.js';
4
- import * as react from 'react';
2
+ import { a as LemonUser } from '../url-O6F1o0px.js';
3
+ export { l as loginUrl, b as logout, p as profileUrl, r as refreshToken } from '../url-O6F1o0px.js';
5
4
 
6
5
  interface AuthContextValue {
7
6
  user: LemonUser | null;
@@ -13,27 +12,4 @@ declare function AuthProvider({ user, children, }: {
13
12
  }): react_jsx_runtime.JSX.Element;
14
13
  declare function useAuth(): AuthContextValue;
15
14
 
16
- /**
17
- * 토큰을 갱신하는 유일한 주체. 프록시가 만료된 세션을 이리로 보낸다.
18
- *
19
- * 갱신을 서버(미들웨어)가 아니라 브라우저가 하는 이유는 두 가지다.
20
- *
21
- * 1. 미들웨어는 요청마다 별도 인스턴스로 떠서 서로를 모른다. 프리페치가
22
- * 깔리면 같은 refresh token 으로 갱신이 동시에 나가고, 회전 경쟁이 난다.
23
- * 브라우저의 복구 경로는 한 번에 하나만 뜬다.
24
- * 2. 브라우저 요청에는 진짜 UA 와 WAF 통과 쿠키가 실린다. 서버간 fetch 는
25
- * UA 가 런타임 기본값이라 봇 방어에 걸릴 수 있다.
26
- *
27
- * 끝나면 router 가 아니라 window.location 으로 next 에 간다. 소프트 네비는
28
- * 루트 레이아웃(과 AuthProvider)을 다시 렌더하지 않으므로, 옛 세션 스냅샷이
29
- * 남아 화면이 어긋나거나 라우터 캐시에 굳은 리다이렉트가 되살아난다.
30
- *
31
- * 실패해도 next 로 간다. 죽은 토큰이면 서버가 401 응답에서 세션 쿠키를
32
- * 지워 주므로(auth-server v2026.08.03.2+), 이후 판단 — 로그인으로 보낼지,
33
- * 익명으로 보여줄지 — 는 프록시와 DAL 이 알아서 한다.
34
- */
35
- declare function SessionRestore({ fallback }: {
36
- fallback: React.ReactNode;
37
- }): string | number | bigint | boolean | Iterable<react.ReactNode> | Promise<string | number | bigint | boolean | react.ReactPortal | react.ReactElement<unknown, string | react.JSXElementConstructor<any>> | Iterable<react.ReactNode> | null | undefined> | react_jsx_runtime.JSX.Element | null | undefined;
38
-
39
- export { AuthProvider, LemonUser, SessionRestore, useAuth };
15
+ export { AuthProvider, LemonUser, useAuth };
@@ -3,9 +3,8 @@ import {
3
3
  loginUrl,
4
4
  logout,
5
5
  profileUrl,
6
- refreshSession,
7
6
  refreshToken
8
- } from "../chunk-ICPDIWYK.js";
7
+ } from "../chunk-TKMZEMW7.js";
9
8
  import "../chunk-JIXVTKLG.js";
10
9
 
11
10
  // src/client/context.tsx
@@ -24,59 +23,8 @@ function AuthProvider({
24
23
  function useAuth() {
25
24
  return useContext(AuthContext);
26
25
  }
27
-
28
- // src/client/session-restore.tsx
29
- import { useEffect, useState } from "react";
30
- import { useSearchParams } from "next/navigation";
31
- import { jsx as jsx2, jsxs } from "react/jsx-runtime";
32
- function SessionRestore({ fallback }) {
33
- const searchParams = useSearchParams();
34
- const [unavailable, setUnavailable] = useState(false);
35
- useEffect(() => {
36
- let ignore = false;
37
- const next = safeNextPath(searchParams.get("next"));
38
- refreshSession().then((result) => {
39
- if (ignore) return;
40
- if (result === "unavailable") {
41
- setUnavailable(true);
42
- return;
43
- }
44
- window.location.replace(next);
45
- });
46
- return () => {
47
- ignore = true;
48
- };
49
- }, [searchParams]);
50
- if (unavailable) {
51
- return /* @__PURE__ */ jsxs(
52
- "div",
53
- {
54
- style: {
55
- minHeight: "100dvh",
56
- display: "flex",
57
- flexDirection: "column",
58
- alignItems: "center",
59
- justifyContent: "center",
60
- gap: 12
61
- },
62
- children: [
63
- /* @__PURE__ */ jsx2("p", { children: "\uC778\uC99D \uC11C\uBC84\uC5D0 \uC5F0\uACB0\uD558\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4." }),
64
- /* @__PURE__ */ jsx2("button", { type: "button", onClick: () => window.location.reload(), children: "\uB2E4\uC2DC \uC2DC\uB3C4" })
65
- ]
66
- }
67
- );
68
- }
69
- return fallback;
70
- }
71
- function safeNextPath(next) {
72
- if (!next) return "/";
73
- if (!next.startsWith("/")) return "/";
74
- if (next.startsWith("//")) return "/";
75
- return next;
76
- }
77
26
  export {
78
27
  AuthProvider,
79
- SessionRestore,
80
28
  loginUrl,
81
29
  logout,
82
30
  profileUrl,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/client/context.tsx","../../src/client/session-restore.tsx"],"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 { useSearchParams } from \"next/navigation\";\nimport { refreshSession } from \"./url.js\";\n\n/**\n * 토큰을 갱신하는 유일한 주체. 프록시가 만료된 세션을 이리로 보낸다.\n *\n * 갱신을 서버(미들웨어)가 아니라 브라우저가 하는 이유는 두 가지다.\n *\n * 1. 미들웨어는 요청마다 별도 인스턴스로 떠서 서로를 모른다. 프리페치가\n * 깔리면 같은 refresh token 으로 갱신이 동시에 나가고, 회전 경쟁이 난다.\n * 브라우저의 복구 경로는 한 번에 하나만 뜬다.\n * 2. 브라우저 요청에는 진짜 UA 와 WAF 통과 쿠키가 실린다. 서버간 fetch 는\n * UA 가 런타임 기본값이라 봇 방어에 걸릴 수 있다.\n *\n * 끝나면 router 가 아니라 window.location 으로 next 에 간다. 소프트 네비는\n * 루트 레이아웃(과 AuthProvider)을 다시 렌더하지 않으므로, 옛 세션 스냅샷이\n * 남아 화면이 어긋나거나 라우터 캐시에 굳은 리다이렉트가 되살아난다.\n *\n * 실패해도 next 로 간다. 죽은 토큰이면 서버가 401 응답에서 세션 쿠키를\n * 지워 주므로(auth-server v2026.08.03.2+), 이후 판단 — 로그인으로 보낼지,\n * 익명으로 보여줄지 — 는 프록시와 DAL 이 알아서 한다.\n */\nexport function SessionRestore({ fallback }: { fallback: React.ReactNode }) {\n const searchParams = useSearchParams();\n const [unavailable, setUnavailable] = useState(false);\n\n useEffect(() => {\n let ignore = false;\n const next = safeNextPath(searchParams.get(\"next\"));\n\n refreshSession().then((result) => {\n if (ignore) return;\n\n if (result === \"unavailable\") {\n // 서버 · 네트워크 사정이다. 토큰은 살아 있을 수 있으므로\n // 로그인으로 보내지 않고 재시도만 권한다.\n setUnavailable(true);\n return;\n }\n\n window.location.replace(next);\n });\n\n return () => {\n ignore = true;\n };\n }, [searchParams]);\n\n if (unavailable) {\n return (\n <div\n style={{\n minHeight: \"100dvh\",\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n justifyContent: \"center\",\n gap: 12,\n }}\n >\n <p>인증 서버에 연결하지 못했습니다.</p>\n <button type=\"button\" onClick={() => window.location.reload()}>\n 다시 시도\n </button>\n </div>\n );\n }\n\n return fallback;\n}\n\n/**\n * next 파라미터는 사용자가 조작할 수 있으므로 열린 리다이렉트가 되지 않게\n * 같은 출처의 경로만 허용한다. `//evil.com` 은 브라우저가 프로토콜 상대\n * URL 로 읽으므로 반드시 걸러야 한다.\n */\nfunction safeNextPath(next: string | null): string {\n if (!next) return \"/\";\n if (!next.startsWith(\"/\")) return \"/\";\n if (next.startsWith(\"//\")) return \"/\";\n return next;\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,uBAAuB;AAkD1B,SAUE,OAAAA,MAVF;AA5BC,SAAS,eAAe,EAAE,SAAS,GAAkC;AAC1E,QAAM,eAAe,gBAAgB;AACrC,QAAM,CAAC,aAAa,cAAc,IAAI,SAAS,KAAK;AAEpD,YAAU,MAAM;AACd,QAAI,SAAS;AACb,UAAM,OAAO,aAAa,aAAa,IAAI,MAAM,CAAC;AAElD,mBAAe,EAAE,KAAK,CAAC,WAAW;AAChC,UAAI,OAAQ;AAEZ,UAAI,WAAW,eAAe;AAG5B,uBAAe,IAAI;AACnB;AAAA,MACF;AAEA,aAAO,SAAS,QAAQ,IAAI;AAAA,IAC9B,CAAC;AAED,WAAO,MAAM;AACX,eAAS;AAAA,IACX;AAAA,EACF,GAAG,CAAC,YAAY,CAAC;AAEjB,MAAI,aAAa;AACf,WACE;AAAA,MAAC;AAAA;AAAA,QACC,OAAO;AAAA,UACL,WAAW;AAAA,UACX,SAAS;AAAA,UACT,eAAe;AAAA,UACf,YAAY;AAAA,UACZ,gBAAgB;AAAA,UAChB,KAAK;AAAA,QACP;AAAA,QAEA;AAAA,0BAAAA,KAAC,OAAE,sGAAkB;AAAA,UACrB,gBAAAA,KAAC,YAAO,MAAK,UAAS,SAAS,MAAM,OAAO,SAAS,OAAO,GAAG,uCAE/D;AAAA;AAAA;AAAA,IACF;AAAA,EAEJ;AAEA,SAAO;AACT;AAOA,SAAS,aAAa,MAA6B;AACjD,MAAI,CAAC,KAAM,QAAO;AAClB,MAAI,CAAC,KAAK,WAAW,GAAG,EAAG,QAAO;AAClC,MAAI,KAAK,WAAW,IAAI,EAAG,QAAO;AAClC,SAAO;AACT;","names":["jsx"]}
1
+ {"version":3,"sources":["../../src/client/context.tsx"],"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"],"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;","names":[]}