@aiquants/auth-react-router 0.7.0 → 0.8.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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/client.ts","../src/client/GoogleForm.tsx"],"sourcesContent":["/**\n * @aiquants/auth-react-router/client — client-side auth components barrel.\n * クライアントサイド認証コンポーネントのバレル。\n */\nexport type { GoogleFormProps } from \"./client/GoogleForm\"\nexport { GoogleForm } from \"./client/GoogleForm\"\n","/**\n * @module GoogleForm\n * @description Google sign-in form button (tailwind-variants styled).\n * Google サインインフォームボタン (tailwind-variants スタイル)。\n *\n * 原本 (quants-react-router components/elements/GoogleForm.tsx) を挙動保存で移植。\n * ラベル・action 値は props 化し、既定値は原本と同一 (\"Sign In with Google\")。\n */\nimport { FaGoogle } from \"react-icons/fa\"\nimport { Form } from \"react-router\"\nimport { tv } from \"tailwind-variants\"\n\nconst googleFormStyles = tv({\n slots: {\n googleForm: \"my-4\",\n btn: \"w-full flex items-center justify-center gap-3 rounded-2xl bg-gradient-to-r from-blue-500 to-indigo-500 px-6 py-3 text-white font-bold shadow-lg transition-all duration-300 hover:from-blue-600 hover:to-indigo-600 hover:scale-105 focus:outline-none focus:ring-2 focus:ring-blue-300\",\n icon: \"\",\n btnText: \"text-base tracking-wide\",\n },\n})\n\n/** GoogleForm のプロパティ。 */\nexport type GoogleFormProps = {\n /** フォーム action の値 (loginAction の switch と一致させる)。既定 \"Sign In with Google\"。 */\n actionValue?: string\n /** ボタンの表示ラベル。既定 \"Sign in with Google\"。 */\n label?: string\n}\n\n/**\n * Renders the Google sign-in form (POST with `_action`).\n * Google サインインフォーム (`_action` 付き POST) を描画する。\n */\nexport const GoogleForm = ({ actionValue = \"Sign In with Google\", label = \"Sign in with Google\" }: GoogleFormProps = {}) => {\n const { googleForm, btn, icon: _icon, btnText } = googleFormStyles()\n return (\n <Form method=\"POST\" className={googleForm()}>\n <button type=\"submit\" name=\"_action\" value={actionValue} className={btn()}>\n <FaGoogle size={24} className=\"text-white drop-shadow\" />\n <span className={btnText()}>{label}</span>\n </button>\n </Form>\n )\n}\n"],"mappings":"yaAAA,IAAAA,EAAA,GAAAC,EAAAD,EAAA,gBAAAE,IAAA,eAAAC,EAAAH,GCQA,IAAAI,EAAyB,0BACzBC,EAAqB,wBACrBC,EAAmB,6BA2BPC,EAAA,6BAzBNC,KAAmB,MAAG,CACxB,MAAO,CACH,WAAY,OACZ,IAAK,0RACL,KAAM,GACN,QAAS,yBACb,CACJ,CAAC,EAcYC,EAAa,CAAC,CAAE,YAAAC,EAAc,sBAAuB,MAAAC,EAAQ,qBAAsB,EAAqB,CAAC,IAAM,CACxH,GAAM,CAAE,WAAAC,EAAY,IAAAC,EAAK,KAAMC,EAAO,QAAAC,CAAQ,EAAIP,EAAiB,EACnE,SACI,OAAC,QAAK,OAAO,OAAO,UAAWI,EAAW,EACtC,oBAAC,UAAO,KAAK,SAAS,KAAK,UAAU,MAAOF,EAAa,UAAWG,EAAI,EACpE,oBAAC,YAAS,KAAM,GAAI,UAAU,yBAAyB,KACvD,OAAC,QAAK,UAAWE,EAAQ,EAAI,SAAAJ,EAAM,GACvC,EACJ,CAER","names":["client_exports","__export","GoogleForm","__toCommonJS","import_fa","import_react_router","import_tailwind_variants","import_jsx_runtime","googleFormStyles","GoogleForm","actionValue","label","googleForm","btn","_icon","btnText"]}
1
+ {"version":3,"sources":["../src/client.ts","../src/client/GoogleForm.tsx"],"sourcesContent":["/**\n * @aiquants/auth-react-router/client — client-side auth components barrel.\n * クライアントサイド認証コンポーネントのバレル。\n */\nexport type { GoogleFormProps } from \"./client/GoogleForm\"\nexport { GoogleForm } from \"./client/GoogleForm\"\n","/**\n * @module GoogleForm\n * @description Google sign-in form button (tailwind-variants styled).\n * Google サインインフォームボタン (tailwind-variants スタイル)。\n *\n * 原本 (quants-react-router components/elements/GoogleForm.tsx) を挙動保存で移植。\n * ラベル・action 値は props 化し、既定値は原本と同一 (\"Sign In with Google\")。\n */\n// react-icons はコレクション単位の exports キー (\"./fa\" など) だけを公開しており、ワイルドカードを持たない。\n// `react-icons/fa/index.js` は実ファイルが存在しても exports マップに無いため、TypeScript (bundler/nodenext)・\n// Node ESM・esbuild/Vite の全てが解決を拒否する。この指定子に拡張子を足してはならない (#2044 の再発防止)。\nimport { FaGoogle } from \"react-icons/fa\"\nimport { Form } from \"react-router\"\nimport { tv } from \"tailwind-variants\"\n\nconst googleFormStyles = tv({\n slots: {\n googleForm: \"my-4\",\n btn: \"w-full flex items-center justify-center gap-3 rounded-2xl bg-gradient-to-r from-blue-500 to-indigo-500 px-6 py-3 text-white font-bold shadow-lg transition-all duration-300 hover:from-blue-600 hover:to-indigo-600 hover:scale-105 focus:outline-none focus:ring-2 focus:ring-blue-300\",\n icon: \"\",\n btnText: \"text-base tracking-wide\",\n },\n})\n\n/** GoogleForm のプロパティ。 */\nexport type GoogleFormProps = {\n /** フォーム action の値 (loginAction の switch と一致させる)。既定 \"Sign In with Google\"。 */\n actionValue?: string\n /** ボタンの表示ラベル。既定 \"Sign in with Google\"。 */\n label?: string\n}\n\n/**\n * Renders the Google sign-in form (POST with `_action`).\n * Google サインインフォーム (`_action` 付き POST) を描画する。\n */\nexport const GoogleForm = ({ actionValue = \"Sign In with Google\", label = \"Sign in with Google\" }: GoogleFormProps = {}) => {\n const { googleForm, btn, icon: _icon, btnText } = googleFormStyles()\n return (\n <Form method=\"POST\" className={googleForm()}>\n <button type=\"submit\" name=\"_action\" value={actionValue} className={btn()}>\n <FaGoogle size={24} className=\"text-white drop-shadow\" />\n <span className={btnText()}>{label}</span>\n </button>\n </Form>\n )\n}\n"],"mappings":"yaAAA,IAAAA,EAAA,GAAAC,EAAAD,EAAA,gBAAAE,IAAA,eAAAC,EAAAH,GCWA,IAAAI,EAAyB,0BACzBC,EAAqB,wBACrBC,EAAmB,6BA2BPC,EAAA,6BAzBNC,KAAmB,MAAG,CACxB,MAAO,CACH,WAAY,OACZ,IAAK,0RACL,KAAM,GACN,QAAS,yBACb,CACJ,CAAC,EAcYC,EAAa,CAAC,CAAE,YAAAC,EAAc,sBAAuB,MAAAC,EAAQ,qBAAsB,EAAqB,CAAC,IAAM,CACxH,GAAM,CAAE,WAAAC,EAAY,IAAAC,EAAK,KAAMC,EAAO,QAAAC,CAAQ,EAAIP,EAAiB,EACnE,SACI,OAAC,QAAK,OAAO,OAAO,UAAWI,EAAW,EACtC,oBAAC,UAAO,KAAK,SAAS,KAAK,UAAU,MAAOF,EAAa,UAAWG,EAAI,EACpE,oBAAC,YAAS,KAAM,GAAI,UAAU,yBAAyB,KACvD,OAAC,QAAK,UAAWE,EAAQ,EAAI,SAAAJ,EAAM,GACvC,EACJ,CAER","names":["client_exports","__export","GoogleForm","__toCommonJS","import_fa","import_react_router","import_tailwind_variants","import_jsx_runtime","googleFormStyles","GoogleForm","actionValue","label","googleForm","btn","_icon","btnText"]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/client/GoogleForm.tsx"],"sourcesContent":["/**\n * @module GoogleForm\n * @description Google sign-in form button (tailwind-variants styled).\n * Google サインインフォームボタン (tailwind-variants スタイル)。\n *\n * 原本 (quants-react-router components/elements/GoogleForm.tsx) を挙動保存で移植。\n * ラベル・action 値は props 化し、既定値は原本と同一 (\"Sign In with Google\")。\n */\nimport { FaGoogle } from \"react-icons/fa\"\nimport { Form } from \"react-router\"\nimport { tv } from \"tailwind-variants\"\n\nconst googleFormStyles = tv({\n slots: {\n googleForm: \"my-4\",\n btn: \"w-full flex items-center justify-center gap-3 rounded-2xl bg-gradient-to-r from-blue-500 to-indigo-500 px-6 py-3 text-white font-bold shadow-lg transition-all duration-300 hover:from-blue-600 hover:to-indigo-600 hover:scale-105 focus:outline-none focus:ring-2 focus:ring-blue-300\",\n icon: \"\",\n btnText: \"text-base tracking-wide\",\n },\n})\n\n/** GoogleForm のプロパティ。 */\nexport type GoogleFormProps = {\n /** フォーム action の値 (loginAction の switch と一致させる)。既定 \"Sign In with Google\"。 */\n actionValue?: string\n /** ボタンの表示ラベル。既定 \"Sign in with Google\"。 */\n label?: string\n}\n\n/**\n * Renders the Google sign-in form (POST with `_action`).\n * Google サインインフォーム (`_action` 付き POST) を描画する。\n */\nexport const GoogleForm = ({ actionValue = \"Sign In with Google\", label = \"Sign in with Google\" }: GoogleFormProps = {}) => {\n const { googleForm, btn, icon: _icon, btnText } = googleFormStyles()\n return (\n <Form method=\"POST\" className={googleForm()}>\n <button type=\"submit\" name=\"_action\" value={actionValue} className={btn()}>\n <FaGoogle size={24} className=\"text-white drop-shadow\" />\n <span className={btnText()}>{label}</span>\n </button>\n </Form>\n )\n}\n"],"mappings":"AAQA,OAAS,YAAAA,MAAgB,iBACzB,OAAS,QAAAC,MAAY,eACrB,OAAS,MAAAC,MAAU,oBA2BP,OACI,OAAAC,EADJ,QAAAC,MAAA,oBAzBZ,IAAMC,EAAmBH,EAAG,CACxB,MAAO,CACH,WAAY,OACZ,IAAK,0RACL,KAAM,GACN,QAAS,yBACb,CACJ,CAAC,EAcYI,EAAa,CAAC,CAAE,YAAAC,EAAc,sBAAuB,MAAAC,EAAQ,qBAAsB,EAAqB,CAAC,IAAM,CACxH,GAAM,CAAE,WAAAC,EAAY,IAAAC,EAAK,KAAMC,EAAO,QAAAC,CAAQ,EAAIP,EAAiB,EACnE,OACIF,EAACF,EAAA,CAAK,OAAO,OAAO,UAAWQ,EAAW,EACtC,SAAAL,EAAC,UAAO,KAAK,SAAS,KAAK,UAAU,MAAOG,EAAa,UAAWG,EAAI,EACpE,UAAAP,EAACH,EAAA,CAAS,KAAM,GAAI,UAAU,yBAAyB,EACvDG,EAAC,QAAK,UAAWS,EAAQ,EAAI,SAAAJ,EAAM,GACvC,EACJ,CAER","names":["FaGoogle","Form","tv","jsx","jsxs","googleFormStyles","GoogleForm","actionValue","label","googleForm","btn","_icon","btnText"]}
1
+ {"version":3,"sources":["../src/client/GoogleForm.tsx"],"sourcesContent":["/**\n * @module GoogleForm\n * @description Google sign-in form button (tailwind-variants styled).\n * Google サインインフォームボタン (tailwind-variants スタイル)。\n *\n * 原本 (quants-react-router components/elements/GoogleForm.tsx) を挙動保存で移植。\n * ラベル・action 値は props 化し、既定値は原本と同一 (\"Sign In with Google\")。\n */\n// react-icons はコレクション単位の exports キー (\"./fa\" など) だけを公開しており、ワイルドカードを持たない。\n// `react-icons/fa/index.js` は実ファイルが存在しても exports マップに無いため、TypeScript (bundler/nodenext)・\n// Node ESM・esbuild/Vite の全てが解決を拒否する。この指定子に拡張子を足してはならない (#2044 の再発防止)。\nimport { FaGoogle } from \"react-icons/fa\"\nimport { Form } from \"react-router\"\nimport { tv } from \"tailwind-variants\"\n\nconst googleFormStyles = tv({\n slots: {\n googleForm: \"my-4\",\n btn: \"w-full flex items-center justify-center gap-3 rounded-2xl bg-gradient-to-r from-blue-500 to-indigo-500 px-6 py-3 text-white font-bold shadow-lg transition-all duration-300 hover:from-blue-600 hover:to-indigo-600 hover:scale-105 focus:outline-none focus:ring-2 focus:ring-blue-300\",\n icon: \"\",\n btnText: \"text-base tracking-wide\",\n },\n})\n\n/** GoogleForm のプロパティ。 */\nexport type GoogleFormProps = {\n /** フォーム action の値 (loginAction の switch と一致させる)。既定 \"Sign In with Google\"。 */\n actionValue?: string\n /** ボタンの表示ラベル。既定 \"Sign in with Google\"。 */\n label?: string\n}\n\n/**\n * Renders the Google sign-in form (POST with `_action`).\n * Google サインインフォーム (`_action` 付き POST) を描画する。\n */\nexport const GoogleForm = ({ actionValue = \"Sign In with Google\", label = \"Sign in with Google\" }: GoogleFormProps = {}) => {\n const { googleForm, btn, icon: _icon, btnText } = googleFormStyles()\n return (\n <Form method=\"POST\" className={googleForm()}>\n <button type=\"submit\" name=\"_action\" value={actionValue} className={btn()}>\n <FaGoogle size={24} className=\"text-white drop-shadow\" />\n <span className={btnText()}>{label}</span>\n </button>\n </Form>\n )\n}\n"],"mappings":"AAWA,OAAS,YAAAA,MAAgB,iBACzB,OAAS,QAAAC,MAAY,eACrB,OAAS,MAAAC,MAAU,oBA2BP,OACI,OAAAC,EADJ,QAAAC,MAAA,oBAzBZ,IAAMC,EAAmBH,EAAG,CACxB,MAAO,CACH,WAAY,OACZ,IAAK,0RACL,KAAM,GACN,QAAS,yBACb,CACJ,CAAC,EAcYI,EAAa,CAAC,CAAE,YAAAC,EAAc,sBAAuB,MAAAC,EAAQ,qBAAsB,EAAqB,CAAC,IAAM,CACxH,GAAM,CAAE,WAAAC,EAAY,IAAAC,EAAK,KAAMC,EAAO,QAAAC,CAAQ,EAAIP,EAAiB,EACnE,OACIF,EAACF,EAAA,CAAK,OAAO,OAAO,UAAWQ,EAAW,EACtC,SAAAL,EAAC,UAAO,KAAK,SAAS,KAAK,UAAU,MAAOG,EAAa,UAAWG,EAAI,EACpE,UAAAP,EAACH,EAAA,CAAS,KAAM,GAAI,UAAU,yBAAyB,EACvDG,EAAC,QAAK,UAAWS,EAAQ,EAAI,SAAAJ,EAAM,GACvC,EACJ,CAER","names":["FaGoogle","Form","tv","jsx","jsxs","googleFormStyles","GoogleForm","actionValue","label","googleForm","btn","_icon","btnText"]}
package/dist/server.d.mts CHANGED
@@ -143,7 +143,7 @@ declare const createAuthServer: (config: AuthServerConfig) => {
143
143
  };
144
144
  authFns: {
145
145
  authenticate: (request: Request, session: react_router.Session, options?: {
146
- failureRedirect?: string;
146
+ failureRedirect?: string | null;
147
147
  }) => Promise<{
148
148
  accessToken: any;
149
149
  refreshToken: any;
@@ -164,7 +164,7 @@ declare const createAuthServer: (config: AuthServerConfig) => {
164
164
  commitSession: (session: react_router.Session<react_router.SessionData, react_router.SessionData>, options?: react_router.CookieSerializeOptions) => Promise<string>;
165
165
  destroySession: (session: react_router.Session<react_router.SessionData, react_router.SessionData>, options?: react_router.CookieSerializeOptions) => Promise<string>;
166
166
  authenticate: (request: Request, session: react_router.Session, options?: {
167
- failureRedirect?: string;
167
+ failureRedirect?: string | null;
168
168
  }) => Promise<{
169
169
  accessToken: any;
170
170
  refreshToken: any;
@@ -219,6 +219,19 @@ declare class TokenRefreshError extends Error {
219
219
  declare class GoogleInvalidGrantError extends TokenRefreshError {
220
220
  constructor();
221
221
  }
222
+ /**
223
+ * Signals that authentication is required when the caller opted out of redirecting.
224
+ * 呼び出し側がリダイレクトを拒否した (`failureRedirect: null`) 際に認証要求を伝えるエラー。
225
+ *
226
+ * `redirect()` を投げてしまうと React Router がそれを本物の 302 として扱うため、
227
+ * 「リダイレクトしない」という契約を型で表現できるこのエラーへ置き換える。
228
+ * セッション破棄が発生した場合はその `Set-Cookie` を `setCookie` で運ぶ
229
+ * (捨てると無効セッションがブラウザに残り続ける)。
230
+ */
231
+ declare class AuthRequiredError extends Error {
232
+ readonly setCookie?: string;
233
+ constructor(setCookie?: string);
234
+ }
222
235
  /** createAuthenticate が返す認証関数束。 */
223
236
  type AuthenticateFns = ReturnType<typeof createAuthenticate>;
224
237
  /**
@@ -227,7 +240,7 @@ type AuthenticateFns = ReturnType<typeof createAuthenticate>;
227
240
  */
228
241
  declare const createAuthenticate: (config: AuthServerConfig, storage: AuthSessionStorage, logger: LoggerPort) => {
229
242
  authenticate: (request: Request, session: Session, options?: {
230
- failureRedirect?: string;
243
+ failureRedirect?: string | null;
231
244
  }) => Promise<{
232
245
  accessToken: any;
233
246
  refreshToken: any;
@@ -315,4 +328,4 @@ declare const createAuthHandlers: (config: AuthServerConfig, storage: AuthSessio
315
328
  }) => Promise<never>;
316
329
  };
317
330
 
318
- export { type AuthServer, type AuthServerConfig, type AuthSessionStorage, type BackendAuthPort, GoogleInvalidGrantError, type GoogleOAuthConfig, type LoggerPort, type MockUserPort, type SessionConfig, TokenRefreshError, type UserPhotoPort, type WarmupPort, createAuthHandlers, createAuthServer, createAuthSessionStorage, createAuthenticate, createAuthenticateInLoader, createGoogleAuthenticator };
331
+ export { AuthRequiredError, type AuthServer, type AuthServerConfig, type AuthSessionStorage, type BackendAuthPort, GoogleInvalidGrantError, type GoogleOAuthConfig, type LoggerPort, type MockUserPort, type SessionConfig, TokenRefreshError, type UserPhotoPort, type WarmupPort, createAuthHandlers, createAuthServer, createAuthSessionStorage, createAuthenticate, createAuthenticateInLoader, createGoogleAuthenticator };
package/dist/server.d.ts CHANGED
@@ -143,7 +143,7 @@ declare const createAuthServer: (config: AuthServerConfig) => {
143
143
  };
144
144
  authFns: {
145
145
  authenticate: (request: Request, session: react_router.Session, options?: {
146
- failureRedirect?: string;
146
+ failureRedirect?: string | null;
147
147
  }) => Promise<{
148
148
  accessToken: any;
149
149
  refreshToken: any;
@@ -164,7 +164,7 @@ declare const createAuthServer: (config: AuthServerConfig) => {
164
164
  commitSession: (session: react_router.Session<react_router.SessionData, react_router.SessionData>, options?: react_router.CookieSerializeOptions) => Promise<string>;
165
165
  destroySession: (session: react_router.Session<react_router.SessionData, react_router.SessionData>, options?: react_router.CookieSerializeOptions) => Promise<string>;
166
166
  authenticate: (request: Request, session: react_router.Session, options?: {
167
- failureRedirect?: string;
167
+ failureRedirect?: string | null;
168
168
  }) => Promise<{
169
169
  accessToken: any;
170
170
  refreshToken: any;
@@ -219,6 +219,19 @@ declare class TokenRefreshError extends Error {
219
219
  declare class GoogleInvalidGrantError extends TokenRefreshError {
220
220
  constructor();
221
221
  }
222
+ /**
223
+ * Signals that authentication is required when the caller opted out of redirecting.
224
+ * 呼び出し側がリダイレクトを拒否した (`failureRedirect: null`) 際に認証要求を伝えるエラー。
225
+ *
226
+ * `redirect()` を投げてしまうと React Router がそれを本物の 302 として扱うため、
227
+ * 「リダイレクトしない」という契約を型で表現できるこのエラーへ置き換える。
228
+ * セッション破棄が発生した場合はその `Set-Cookie` を `setCookie` で運ぶ
229
+ * (捨てると無効セッションがブラウザに残り続ける)。
230
+ */
231
+ declare class AuthRequiredError extends Error {
232
+ readonly setCookie?: string;
233
+ constructor(setCookie?: string);
234
+ }
222
235
  /** createAuthenticate が返す認証関数束。 */
223
236
  type AuthenticateFns = ReturnType<typeof createAuthenticate>;
224
237
  /**
@@ -227,7 +240,7 @@ type AuthenticateFns = ReturnType<typeof createAuthenticate>;
227
240
  */
228
241
  declare const createAuthenticate: (config: AuthServerConfig, storage: AuthSessionStorage, logger: LoggerPort) => {
229
242
  authenticate: (request: Request, session: Session, options?: {
230
- failureRedirect?: string;
243
+ failureRedirect?: string | null;
231
244
  }) => Promise<{
232
245
  accessToken: any;
233
246
  refreshToken: any;
@@ -315,4 +328,4 @@ declare const createAuthHandlers: (config: AuthServerConfig, storage: AuthSessio
315
328
  }) => Promise<never>;
316
329
  };
317
330
 
318
- export { type AuthServer, type AuthServerConfig, type AuthSessionStorage, type BackendAuthPort, GoogleInvalidGrantError, type GoogleOAuthConfig, type LoggerPort, type MockUserPort, type SessionConfig, TokenRefreshError, type UserPhotoPort, type WarmupPort, createAuthHandlers, createAuthServer, createAuthSessionStorage, createAuthenticate, createAuthenticateInLoader, createGoogleAuthenticator };
331
+ export { AuthRequiredError, type AuthServer, type AuthServerConfig, type AuthSessionStorage, type BackendAuthPort, GoogleInvalidGrantError, type GoogleOAuthConfig, type LoggerPort, type MockUserPort, type SessionConfig, TokenRefreshError, type UserPhotoPort, type WarmupPort, createAuthHandlers, createAuthServer, createAuthSessionStorage, createAuthenticate, createAuthenticateInLoader, createGoogleAuthenticator };
package/dist/server.js CHANGED
@@ -1,2 +1,2 @@
1
- "use strict";var $=Object.defineProperty;var V=Object.getOwnPropertyDescriptor;var W=Object.getOwnPropertyNames;var J=Object.prototype.hasOwnProperty;var Q=(e,r)=>{for(var o in r)$(e,o,{get:r[o],enumerable:!0})},X=(e,r,o,u)=>{if(r&&typeof r=="object"||typeof r=="function")for(let n of W(r))!J.call(e,n)&&n!==o&&$(e,n,{get:()=>r[n],enumerable:!(u=V(r,n))||u.enumerable});return e};var Z=e=>X($({},"__esModule",{value:!0}),e);var re={};Q(re,{GoogleInvalidGrantError:()=>x,TokenRefreshError:()=>k,createAuthHandlers:()=>D,createAuthServer:()=>z,createAuthSessionStorage:()=>O,createAuthenticate:()=>b,createAuthenticateInLoader:()=>I,createGoogleAuthenticator:()=>q});module.exports=Z(re);var E=require("react-router"),k=class extends Error{constructor(r){super(r),this.name="TokenRefreshError"}},x=class extends k{constructor(){super("Google refresh token invalid or revoked"),this.name="GoogleInvalidGrantError"}},b=(e,r,o)=>{let u=e.loginPath??"/auth/login",n=r.sessionKey,m=async a=>{let l=a.get(n)?.refreshToken;if(!l)throw new Error("Refresh token not found in session");try{let h="https://www.googleapis.com/oauth2/v4/token",c={client_id:e.google.clientId,client_secret:e.google.clientSecret,grant_type:"refresh_token",refresh_token:l},t=await fetch(h,{headers:{"Content-Type":"application/json"},method:"POST",body:JSON.stringify(c)});if(!t.ok){let p=await t.json().catch(()=>{});if(t.status===400&&typeof p=="object"&&!!p&&"error"in p&&p.error==="invalid_grant")throw new x;let S=typeof p=="object"&&p&&"error_description"in p?String(p.error_description):"Unknown error";throw new k(`Google token refresh failed: ${t.status} ${S}`)}let s=await t.json(),{access_token:i,expires_in:g}=s;if(!(i&&g))throw o.error("Invalid token response from Google:",s),new Error("Invalid token response from Google");return{accessToken:i,refreshToken:l,expirationDateMs:Number(Date.now())+g*1e3}}catch(h){if(h instanceof k)throw h;let c=h instanceof Error?h.message:"Unknown error";throw new k(`Failed to refresh Google access token: ${c}`)}},y=async a=>{let l=a.get(n).provider;if(!l)throw new Error("Provider not found in session");if(l==="google")return await m(a);throw new Error("Provider not supported")};return{authenticate:async(a,d,l)=>{let{failureRedirect:h}=l||{};try{if(e.mockUser?.enabled(a))return{accessToken:"mock-access-token",refreshToken:"mock-refresh-token",expirationDateMs:Date.now()+36e5};let c=d.get(n);if(!(c?.accessToken&&c.expirationDateMs))throw(0,E.redirect)(h||u);let{accessToken:t,refreshToken:s,expirationDateMs:i}=c;if(i<Date.now())throw s?new Error("Expired"):(0,E.redirect)(h||u);return{accessToken:t,refreshToken:s,expirationDateMs:i}}catch(c){if(c instanceof Error&&c.message==="Expired")try{let t=await y(d),s=d.get(n);return d.set(n,{...s,...t}),t}catch(t){if(t instanceof x)o.warn("Google refresh token invalid or revoked");else{let i=t instanceof Error?t.message:"Unknown error";o.error("Failed to refresh token:",i)}let s=await r.destroySession(d);throw(0,E.redirect)(h||u,{headers:{"Set-Cookie":s}})}throw c}},refreshedAccessToken:y}};var C=require("react-router"),I=(e,r,o,u)=>{let n=new Map,m=new Map,y=async a=>{let d=n.get(a);if(d!==void 0)return d;let l=m.get(a);if(l!==void 0)return l;let h=(async()=>{try{let c=e.getUserPhoto?await e.getUserPhoto(a):"";return n.set(a,c),c}catch(c){throw n.delete(a),c}finally{m.delete(a)}})();return m.set(a,h),h};return async(a,d)=>{e.warmup&&await e.warmup();let{failureRedirect:l}=d,h=await r.getSession(a),c=await r.getSessionUser(a);if(c===void 0){if(l===null)return{};throw(0,C.redirect)(l)}try{let t=await o.authenticate(a,h,{failureRedirect:l??void 0}),s={accessToken:t.accessToken,refreshToken:t.refreshToken,expirationDateMs:t.expirationDateMs,provider:c.provider},i={id:c.id,displayName:c.displayName,name:c.name,emails:c.emails,...s,role:c.role},g=await y(c.id),p={...c,...s,photo:g},S=t.accessToken!==c.accessToken?await r.saveSession(a,i):void 0;return{user:p,cookie:S}}catch(t){if(t instanceof Response&&t.headers.get("Location"))throw t;if(u.error("Error in authenticateInLoader:",t),l!==null){let s;try{s=await r.destroySession(h)}catch(i){u.error("Failed to destroy session during auth error:",i)}throw s!==void 0?(0,C.redirect)(l,{headers:{"Set-Cookie":s}}):(0,C.redirect)(l)}throw t}}};var B=require("@aiquants/auth-core"),T=require("react-router"),K=require("remix-auth");var H=require("remix-auth-oauth2"),ee=["openid","https://www.googleapis.com/auth/userinfo.profile","https://www.googleapis.com/auth/userinfo.email"],oe="google",te="https://www.googleapis.com/oauth2/v3/userinfo",U=class extends H.OAuth2Strategy{constructor({clientId:o,clientSecret:u,redirectURI:n,scopes:m,accessType:y,includeGrantedScopes:w,prompt:a,hd:d,loginHint:l},h){let c=m??ee;super({clientId:o,clientSecret:u,redirectURI:n,authorizationEndpoint:"https://accounts.google.com/o/oauth2/v2/auth",tokenEndpoint:"https://oauth2.googleapis.com/token",scopes:c},h);this.name=oe;this.scopes=c,this.accessType=y??"online",this.includeGrantedScopes=w??!1,this.prompt=a,this.hd=d,this.loginHint=l}authorizationParams(o,u){return o.set("access_type",this.accessType),o.set("include_granted_scopes",String(this.includeGrantedScopes)),this.scopes&&this.scopes.length>0&&o.set("scope",this.scopes.join(" ")),this.prompt&&o.set("prompt",this.prompt),this.hd&&o.set("hd",this.hd),this.loginHint&&o.set("login_hint",this.loginHint),o}static async userProfile(o){let u=await fetch(te,{headers:{Authorization:`Bearer ${o.accessToken()}`}});if(!u.ok)throw new Error(`Failed to fetch user profile: ${u.statusText}`);let n=await u.json();return{id:n.sub,displayName:n.name,name:{familyName:n.family_name,givenName:n.given_name},emails:[{value:n.email}],photos:[{value:n.picture}],_json:n}}};var N={info:(...e)=>console.info("[Auth]",...e),warn:(...e)=>console.warn("[Auth]",...e),error:(...e)=>console.error("[Auth]",...e)};var q=e=>{let r=e.logger??N,o=(0,B.resolveAuthMessages)(e.messages),u=e.loginPath??"/auth/login",n=new U({clientId:e.google.clientId,clientSecret:e.google.clientSecret,redirectURI:e.google.redirectURI,scopes:e.google.scopes,accessType:e.google.accessType??"offline",includeGrantedScopes:e.google.includeGrantedScopes??!0,prompt:e.google.prompt??"consent"},async({request:y,tokens:w})=>{let{access_token:a,refresh_token:d,expires_in:l,scope:h,id_token:c}=w.data,{_json:{sub:t,email:s,picture:i,hd:g,email_verified:p},photos:L,...S}=await U.userProfile(w),{id:F,displayName:Y,name:{familyName:se,givenName:ne},emails:ie}=S,j="google";if(a===void 0||d===void 0||l===void 0||h===void 0||F===void 0||Y===void 0||t===void 0||s===void 0||p===void 0){if(p===!1){let v=encodeURIComponent(o.emailNotVerified);throw(0,T.redirect)(`${u}?error=${v}`)}let f=encodeURIComponent(o.googleFailed);throw(0,T.redirect)(`${u}?error=${f}`)}r.info("Authenticated on Google: ",S.displayName);try{if(e.backendAuth){if(!await e.backendAuth.verify(j,a)){r.error("Backend verification returned false for user:",S.displayName);let P=encodeURIComponent(o.backendFailed);throw(0,T.redirect)(`${u}?error=${P}`)}r.info("Authenticated on Backend: ",S.displayName)}let f=S.emails[0]?.value;if(!await e.isEmailAllowed(f)){let A=new Error("You are not allowed to access this application");throw r.error(A),A}if(e.isUserActive&&!await e.isUserActive(S)){let A=new Error("You are not allowed to access this application");throw r.error("Deactivated account attempted to sign in:",f),A}e.backendAuth&&await e.backendAuth.signup(j,a),r.info("Authenticated complete: ",S.displayName);let v=Date.now()+l*1e3;return{...S,...{accessToken:a,refreshToken:d,expirationDateMs:v,provider:j},photo:i}}catch(f){let v=f;typeof f=="object"&&f!==null&&"cause"in f&&(v=f.cause||f),r.error("Error during backend authentication or subsequent processing:",v);let M=o.unexpected,R=!1;if(typeof f=="object"&&f!==null&&"cause"in f){let P=f.cause;P&&typeof P=="object"&&"message"in P&&typeof P.message=="string"&&P.message.includes("getaddrinfo ENOTFOUND")&&(M=o.backendUnreachable,R=!0)}if(!R&&f instanceof Error&&f.message.includes("You are not allowed to access this application")&&(M=o.notAllowed,R=!0),!R&&f instanceof Response&&f.headers.get("Location")?.startsWith(u))throw f;let A=encodeURIComponent(M);throw(0,T.redirect)(`${u}?error=${A}`)}}),m=new K.Authenticator;return m.use(n),m};var G=require("react-router"),D=(e,r,o,u)=>{let n=e.loginPath??"/auth/login",m=e.postLoginRedirect??"/",y=e.postLogoutRedirect??"/",w=async({request:t})=>{if(await r.getSessionUser(t))throw(0,G.redirect)(m);return{error:new URL(t.url).searchParams.get("error")}},a=async({request:t})=>{let s=await t.clone().formData(),i=String(s.get("_action")),g=null;return i==="Sign In with Google"?g=await o.authenticate("google",t):g=null,g},d=async t=>await r.logout(t),l=async t=>{let s=await d(t),i=new Headers;return i.append("Set-Cookie",s),(0,G.redirect)(y,{headers:i})};return{loginLoader:w,loginAction:a,logoutLoader:async({request:t})=>{throw await l(t)},logout:d,logoutAndRedirect:l,googleCallbackLoader:async({request:t})=>{try{let s=await o.authenticate("google",t),p=(await r.getSession(t)).get(r.sessionKey)?.refreshToken,L={accessToken:s.accessToken,refreshToken:s.refreshToken||p,expirationDateMs:s.expirationDateMs,provider:s.provider},S={id:s.id,displayName:s.displayName,name:s.name,emails:s.emails,...L,role:s.role},F=await r.saveSession(t,S);throw(0,G.redirect)(m,{headers:{"Set-Cookie":F}})}catch(s){throw s instanceof Response?s:(u.error("Authentication error:",s),(0,G.redirect)(n))}}}};var _=require("react-router"),O=(e,r)=>{if(!e.secrets||e.secrets.length===0||e.secrets.some(i=>!i))throw new Error("REMIX_SESSION_SECRET must be set");let o=e.sessionKey??"user",u=e.maxAge??3600*24*30,n=r?.loginPath??"/auth/login",m=r?.postLoginRedirect??"/",y=r?.mockUser,w=r?.isUserActive,a=(0,_.createCookieSessionStorage)({cookie:{name:e.cookieName??"__session",httpOnly:!0,path:"/",sameSite:"lax",secrets:e.secrets,secure:e.secure??process.env.NODE_ENV==="production",maxAge:u}}),d=async i=>await a.getSession(i.headers.get("Cookie")),l=async i=>{if(y?.enabled(i))return y.resolve(i);let p=(await d(i)).get(o);if(p!==void 0&&!(w&&!await w(p)))return p},h=async(i,g)=>{let p=await d(i);return p.set(o,g),await a.commitSession(p)},c=async i=>{let g=await l(i);if(g===void 0)throw(0,_.redirect)(n);return g};return{sessionStorage:a,sessionKey:o,getSession:d,getSessionUser:l,saveSession:h,requireUser:c,requireAdminUser:async i=>{let g=await c(i);if(g?.role!=="admin")throw(0,_.redirect)(m);return g},logout:async i=>{let g=await d(i);return await a.destroySession(g)},commitSession:a.commitSession,destroySession:a.destroySession}};var z=e=>{let r=e.logger??N,o=O(e.session,{mockUser:e.mockUser,loginPath:e.loginPath,postLoginRedirect:e.postLoginRedirect,isUserActive:e.isUserActive}),u=q(e),n=b(e,o,r),m=I(e,o,n,r),y=D(e,o,u,r);return{authenticator:u,storage:o,authFns:n,sessionStorage:o.sessionStorage,getSession:o.getSession,getSessionUser:o.getSessionUser,saveSession:o.saveSession,requireUser:o.requireUser,requireAdminUser:o.requireAdminUser,commitSession:o.commitSession,destroySession:o.destroySession,authenticate:n.authenticate,refreshedAccessToken:n.refreshedAccessToken,authenticateInLoader:m,...y}};0&&(module.exports={GoogleInvalidGrantError,TokenRefreshError,createAuthHandlers,createAuthServer,createAuthSessionStorage,createAuthenticate,createAuthenticateInLoader,createGoogleAuthenticator});
1
+ "use strict";var $=Object.defineProperty;var W=Object.getOwnPropertyDescriptor;var J=Object.getOwnPropertyNames;var Q=Object.prototype.hasOwnProperty;var X=(e,r)=>{for(var t in r)$(e,t,{get:r[t],enumerable:!0})},Z=(e,r,t,l)=>{if(r&&typeof r=="object"||typeof r=="function")for(let i of J(r))!Q.call(e,i)&&i!==t&&$(e,i,{get:()=>r[i],enumerable:!(l=W(r,i))||l.enumerable});return e};var ee=e=>Z($({},"__esModule",{value:!0}),e);var se={};X(se,{AuthRequiredError:()=>v,GoogleInvalidGrantError:()=>R,TokenRefreshError:()=>k,createAuthHandlers:()=>D,createAuthServer:()=>Y,createAuthSessionStorage:()=>O,createAuthenticate:()=>C,createAuthenticateInLoader:()=>I,createGoogleAuthenticator:()=>N});module.exports=ee(se);var H=require("react-router"),k=class extends Error{constructor(r){super(r),this.name="TokenRefreshError"}},R=class extends k{constructor(){super("Google refresh token invalid or revoked"),this.name="GoogleInvalidGrantError"}},v=class extends Error{constructor(r){super("Authentication required"),this.name="AuthRequiredError",this.setCookie=r}},C=(e,r,t)=>{let l=e.loginPath??"/auth/login",i=r.sessionKey,m=async a=>{let u=a.get(i)?.refreshToken;if(!u)throw new Error("Refresh token not found in session");try{let p="https://www.googleapis.com/oauth2/v4/token",c={client_id:e.google.clientId,client_secret:e.google.clientSecret,grant_type:"refresh_token",refresh_token:u},o=await fetch(p,{headers:{"Content-Type":"application/json"},method:"POST",body:JSON.stringify(c)});if(!o.ok){let g=await o.json().catch(()=>{});if(o.status===400&&typeof g=="object"&&!!g&&"error"in g&&g.error==="invalid_grant")throw new R;let S=typeof g=="object"&&g&&"error_description"in g?String(g.error_description):"Unknown error";throw new k(`Google token refresh failed: ${o.status} ${S}`)}let s=await o.json(),{access_token:n,expires_in:d}=s;if(!(n&&d))throw t.error("Invalid token response from Google:",s),new Error("Invalid token response from Google");return{accessToken:n,refreshToken:u,expirationDateMs:Number(Date.now())+d*1e3}}catch(p){if(p instanceof k)throw p;let c=p instanceof Error?p.message:"Unknown error";throw new k(`Failed to refresh Google access token: ${c}`)}},y=async a=>{let u=a.get(i).provider;if(!u)throw new Error("Provider not found in session");if(u==="google")return await m(a);throw new Error("Provider not supported")};return{authenticate:async(a,h,u)=>{let{failureRedirect:p}=u||{},c=o=>p===null?new v(o):o!==void 0?(0,H.redirect)(p||l,{headers:{"Set-Cookie":o}}):(0,H.redirect)(p||l);try{if(e.mockUser?.enabled(a))return{accessToken:"mock-access-token",refreshToken:"mock-refresh-token",expirationDateMs:Date.now()+36e5};let o=h.get(i);if(!(o?.accessToken&&o.expirationDateMs))throw c();let{accessToken:s,refreshToken:n,expirationDateMs:d}=o;if(d<Date.now())throw n?new Error("Expired"):c();return{accessToken:s,refreshToken:n,expirationDateMs:d}}catch(o){if(o instanceof Error&&o.message==="Expired")try{let s=await y(h),n=h.get(i);return h.set(i,{...n,...s}),s}catch(s){if(s instanceof R)t.warn("Google refresh token invalid or revoked");else{let d=s instanceof Error?s.message:"Unknown error";t.error("Failed to refresh token:",d)}let n=await r.destroySession(h);throw c(n)}throw o}},refreshedAccessToken:y}};var b=require("react-router");var I=(e,r,t,l)=>{let i=new Map,m=new Map,y=async a=>{let h=i.get(a);if(h!==void 0)return h;let u=m.get(a);if(u!==void 0)return u;let p=(async()=>{try{let c=e.getUserPhoto?await e.getUserPhoto(a):"";return i.set(a,c),c}catch(c){throw i.delete(a),c}finally{m.delete(a)}})();return m.set(a,p),p};return async(a,h)=>{e.warmup&&await e.warmup();let{failureRedirect:u}=h,p=await r.getSession(a),c=await r.getSessionUser(a);if(c===void 0){if(u===null)return{};throw(0,b.redirect)(u)}try{let o=await t.authenticate(a,p,{failureRedirect:u}),s={accessToken:o.accessToken,refreshToken:o.refreshToken,expirationDateMs:o.expirationDateMs,provider:c.provider},n={id:c.id,displayName:c.displayName,name:c.name,emails:c.emails,...s,role:c.role},d=await y(c.id),g={...c,...s,photo:d},S=o.accessToken!==c.accessToken?await r.saveSession(a,n):void 0;return{user:g,cookie:S}}catch(o){if(o instanceof v)return o.setCookie!==void 0?{cookie:o.setCookie}:{};if(o instanceof Response&&o.headers.get("Location"))throw o;if(l.error("Error in authenticateInLoader:",o),u!==null){let s;try{s=await r.destroySession(p)}catch(n){l.error("Failed to destroy session during auth error:",n)}throw s!==void 0?(0,b.redirect)(u,{headers:{"Set-Cookie":s}}):(0,b.redirect)(u)}throw o}}};var K=require("@aiquants/auth-core"),G=require("react-router"),z=require("remix-auth");var B=require("remix-auth-oauth2"),oe=["openid","https://www.googleapis.com/auth/userinfo.profile","https://www.googleapis.com/auth/userinfo.email"],te="google",re="https://www.googleapis.com/oauth2/v3/userinfo",T=class extends B.OAuth2Strategy{constructor({clientId:t,clientSecret:l,redirectURI:i,scopes:m,accessType:y,includeGrantedScopes:w,prompt:a,hd:h,loginHint:u},p){let c=m??oe;super({clientId:t,clientSecret:l,redirectURI:i,authorizationEndpoint:"https://accounts.google.com/o/oauth2/v2/auth",tokenEndpoint:"https://oauth2.googleapis.com/token",scopes:c},p);this.name=te;this.scopes=c,this.accessType=y??"online",this.includeGrantedScopes=w??!1,this.prompt=a,this.hd=h,this.loginHint=u}authorizationParams(t,l){return t.set("access_type",this.accessType),t.set("include_granted_scopes",String(this.includeGrantedScopes)),this.scopes&&this.scopes.length>0&&t.set("scope",this.scopes.join(" ")),this.prompt&&t.set("prompt",this.prompt),this.hd&&t.set("hd",this.hd),this.loginHint&&t.set("login_hint",this.loginHint),t}static async userProfile(t){let l=await fetch(re,{headers:{Authorization:`Bearer ${t.accessToken()}`}});if(!l.ok)throw new Error(`Failed to fetch user profile: ${l.statusText}`);let i=await l.json();return{id:i.sub,displayName:i.name,name:{familyName:i.family_name,givenName:i.given_name},emails:[{value:i.email}],photos:[{value:i.picture}],_json:i}}};var q={info:(...e)=>console.info("[Auth]",...e),warn:(...e)=>console.warn("[Auth]",...e),error:(...e)=>console.error("[Auth]",...e)};var N=e=>{let r=e.logger??q,t=(0,K.resolveAuthMessages)(e.messages),l=e.loginPath??"/auth/login",i=new T({clientId:e.google.clientId,clientSecret:e.google.clientSecret,redirectURI:e.google.redirectURI,scopes:e.google.scopes,accessType:e.google.accessType??"offline",includeGrantedScopes:e.google.includeGrantedScopes??!0,prompt:e.google.prompt??"consent"},async({request:y,tokens:w})=>{let{access_token:a,refresh_token:h,expires_in:u,scope:p,id_token:c}=w.data,{_json:{sub:o,email:s,picture:n,hd:d,email_verified:g},photos:E,...S}=await T.userProfile(w),{id:F,displayName:V,name:{familyName:ne,givenName:ie},emails:ae}=S,j="google";if(a===void 0||h===void 0||u===void 0||p===void 0||F===void 0||V===void 0||o===void 0||s===void 0||g===void 0){if(g===!1){let x=encodeURIComponent(t.emailNotVerified);throw(0,G.redirect)(`${l}?error=${x}`)}let f=encodeURIComponent(t.googleFailed);throw(0,G.redirect)(`${l}?error=${f}`)}r.info("Authenticated on Google: ",S.displayName);try{if(e.backendAuth){if(!await e.backendAuth.verify(j,a)){r.error("Backend verification returned false for user:",S.displayName);let P=encodeURIComponent(t.backendFailed);throw(0,G.redirect)(`${l}?error=${P}`)}r.info("Authenticated on Backend: ",S.displayName)}let f=S.emails[0]?.value;if(!await e.isEmailAllowed(f)){let A=new Error("You are not allowed to access this application");throw r.error(A),A}if(e.isUserActive&&!await e.isUserActive(S)){let A=new Error("You are not allowed to access this application");throw r.error("Deactivated account attempted to sign in:",f),A}e.backendAuth&&await e.backendAuth.signup(j,a),r.info("Authenticated complete: ",S.displayName);let x=Date.now()+u*1e3;return{...S,...{accessToken:a,refreshToken:h,expirationDateMs:x,provider:j},photo:n}}catch(f){let x=f;typeof f=="object"&&f!==null&&"cause"in f&&(x=f.cause||f),r.error("Error during backend authentication or subsequent processing:",x);let M=t.unexpected,U=!1;if(typeof f=="object"&&f!==null&&"cause"in f){let P=f.cause;P&&typeof P=="object"&&"message"in P&&typeof P.message=="string"&&P.message.includes("getaddrinfo ENOTFOUND")&&(M=t.backendUnreachable,U=!0)}if(!U&&f instanceof Error&&f.message.includes("You are not allowed to access this application")&&(M=t.notAllowed,U=!0),!U&&f instanceof Response&&f.headers.get("Location")?.startsWith(l))throw f;let A=encodeURIComponent(M);throw(0,G.redirect)(`${l}?error=${A}`)}}),m=new z.Authenticator;return m.use(i),m};var _=require("react-router"),D=(e,r,t,l)=>{let i=e.loginPath??"/auth/login",m=e.postLoginRedirect??"/",y=e.postLogoutRedirect??"/",w=async({request:o})=>{if(await r.getSessionUser(o))throw(0,_.redirect)(m);return{error:new URL(o.url).searchParams.get("error")}},a=async({request:o})=>{let s=await o.clone().formData(),n=String(s.get("_action")),d=null;return n==="Sign In with Google"?d=await t.authenticate("google",o):d=null,d},h=async o=>await r.logout(o),u=async o=>{let s=await h(o),n=new Headers;return n.append("Set-Cookie",s),(0,_.redirect)(y,{headers:n})};return{loginLoader:w,loginAction:a,logoutLoader:async({request:o})=>{throw await u(o)},logout:h,logoutAndRedirect:u,googleCallbackLoader:async({request:o})=>{try{let s=await t.authenticate("google",o),g=(await r.getSession(o)).get(r.sessionKey)?.refreshToken,E={accessToken:s.accessToken,refreshToken:s.refreshToken||g,expirationDateMs:s.expirationDateMs,provider:s.provider},S={id:s.id,displayName:s.displayName,name:s.name,emails:s.emails,...E,role:s.role},F=await r.saveSession(o,S);throw(0,_.redirect)(m,{headers:{"Set-Cookie":F}})}catch(s){throw s instanceof Response?s:(l.error("Authentication error:",s),(0,_.redirect)(i))}}}};var L=require("react-router"),O=(e,r)=>{if(!e.secrets||e.secrets.length===0||e.secrets.some(n=>!n))throw new Error("REMIX_SESSION_SECRET must be set");let t=e.sessionKey??"user",l=e.maxAge??3600*24*30,i=r?.loginPath??"/auth/login",m=r?.postLoginRedirect??"/",y=r?.mockUser,w=r?.isUserActive,a=(0,L.createCookieSessionStorage)({cookie:{name:e.cookieName??"__session",httpOnly:!0,path:"/",sameSite:"lax",secrets:e.secrets,secure:e.secure??process.env.NODE_ENV==="production",maxAge:l}}),h=async n=>await a.getSession(n.headers.get("Cookie")),u=async n=>{if(y?.enabled(n))return y.resolve(n);let g=(await h(n)).get(t);if(g!==void 0&&!(w&&!await w(g)))return g},p=async(n,d)=>{let g=await h(n);return g.set(t,d),await a.commitSession(g)},c=async n=>{let d=await u(n);if(d===void 0)throw(0,L.redirect)(i);return d};return{sessionStorage:a,sessionKey:t,getSession:h,getSessionUser:u,saveSession:p,requireUser:c,requireAdminUser:async n=>{let d=await c(n);if(d?.role!=="admin")throw(0,L.redirect)(m);return d},logout:async n=>{let d=await h(n);return await a.destroySession(d)},commitSession:a.commitSession,destroySession:a.destroySession}};var Y=e=>{let r=e.logger??q,t=O(e.session,{mockUser:e.mockUser,loginPath:e.loginPath,postLoginRedirect:e.postLoginRedirect,isUserActive:e.isUserActive}),l=N(e),i=C(e,t,r),m=I(e,t,i,r),y=D(e,t,l,r);return{authenticator:l,storage:t,authFns:i,sessionStorage:t.sessionStorage,getSession:t.getSession,getSessionUser:t.getSessionUser,saveSession:t.saveSession,requireUser:t.requireUser,requireAdminUser:t.requireAdminUser,commitSession:t.commitSession,destroySession:t.destroySession,authenticate:i.authenticate,refreshedAccessToken:i.refreshedAccessToken,authenticateInLoader:m,...y}};0&&(module.exports={AuthRequiredError,GoogleInvalidGrantError,TokenRefreshError,createAuthHandlers,createAuthServer,createAuthSessionStorage,createAuthenticate,createAuthenticateInLoader,createGoogleAuthenticator});
2
2
  //# sourceMappingURL=server.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/server.ts","../src/server/authenticate.ts","../src/server/authenticate-in-loader.ts","../src/server/google-strategy.ts","../src/server/google-strategy-impl.ts","../src/server/logger.ts","../src/server/handlers.ts","../src/server/session.ts","../src/server/auth-server.ts"],"sourcesContent":["/**\n * @aiquants/auth-react-router/server — server-side auth adapter barrel.\n * サーバーサイド認証アダプタのバレル。\n */\n\nexport type { AuthServer } from \"./server/auth-server\"\nexport { createAuthServer } from \"./server/auth-server\"\nexport { createAuthenticate, GoogleInvalidGrantError, TokenRefreshError } from \"./server/authenticate\"\nexport { createAuthenticateInLoader } from \"./server/authenticate-in-loader\"\nexport { createGoogleAuthenticator } from \"./server/google-strategy\"\nexport { createAuthHandlers } from \"./server/handlers\"\nexport type { AuthServerConfig, BackendAuthPort, GoogleOAuthConfig, LoggerPort, MockUserPort, SessionConfig, UserPhotoPort, WarmupPort } from \"./server/ports\"\nexport type { AuthSessionStorage } from \"./server/session\"\nexport { createAuthSessionStorage } from \"./server/session\"\n","/**\n * @module authenticate\n * @description Session token guard with Google refresh-token rotation.\n * Google リフレッシュトークンによるセッショントークンガード。\n *\n * 原本 (quants-react-router authenticate.server.ts) を挙動保存で移植。\n * env (GOOGLE_CLIENT_ID/SECRET) は Google 設定注入に、destroySession はセッションストレージ\n * 注入に、テストバックドアは mockUser ポート注入に置き換えた。\n * 期限判定 → リフレッシュ → 失敗時セッション破棄 + ログインへリダイレクトの流れは原本と等価。\n *\n * 参考: https://sergiodxa.com/articles/working-with-refresh-tokens-in-remix\n */\nimport type { SessionProfile } from \"@aiquants/auth-core\"\nimport { redirect, type Session } from \"react-router\"\nimport type { AuthServerConfig, LoggerPort } from \"./ports\"\nimport type { AuthSessionStorage } from \"./session\"\n\n/** トークンリフレッシュ失敗の基底エラー。 */\nexport class TokenRefreshError extends Error {\n constructor(message: string) {\n super(message)\n this.name = \"TokenRefreshError\"\n }\n}\n\n/** Google の invalid_grant (失効/取り消し済みリフレッシュトークン) エラー。 */\nexport class GoogleInvalidGrantError extends TokenRefreshError {\n constructor() {\n super(\"Google refresh token invalid or revoked\")\n this.name = \"GoogleInvalidGrantError\"\n }\n}\n\n/** createAuthenticate が返す認証関数束。 */\nexport type AuthenticateFns = ReturnType<typeof createAuthenticate>\n\n/**\n * Builds the `authenticate` / `refreshedAccessToken` pair wired to injected config.\n * 注入設定に結線した `authenticate` / `refreshedAccessToken` を生成する。\n */\nexport const createAuthenticate = (config: AuthServerConfig, storage: AuthSessionStorage, logger: LoggerPort) => {\n const loginPath = config.loginPath ?? \"/auth/login\"\n const sessionKey = storage.sessionKey\n\n /**\n * Refreshes the Google access token using the refresh token in the session.\n * セッション内のリフレッシュトークンで Google アクセストークンを更新する。\n */\n const getRefreshedGoogleAccessToken = async (session: Session) => {\n const userFromSession = session.get(sessionKey)\n const refreshTokenFromSession = userFromSession?.refreshToken\n if (!refreshTokenFromSession) {\n throw new Error(\"Refresh token not found in session\")\n }\n try {\n const url = \"https://www.googleapis.com/oauth2/v4/token\"\n const body = {\n client_id: config.google.clientId,\n client_secret: config.google.clientSecret,\n grant_type: \"refresh_token\",\n refresh_token: refreshTokenFromSession,\n }\n\n const response = await fetch(url, {\n headers: {\n \"Content-Type\": \"application/json\",\n },\n method: \"POST\",\n body: JSON.stringify(body),\n })\n\n if (!response.ok) {\n // Google API からエラーが返された場合 (JSON パース失敗も考慮)\n const errorData = await response.json().catch(() => undefined)\n const isInvalidGrant = response.status === 400 && typeof errorData === \"object\" && !!errorData && \"error\" in errorData && errorData.error === \"invalid_grant\"\n if (isInvalidGrant) {\n throw new GoogleInvalidGrantError()\n }\n const errorDescription = typeof errorData === \"object\" && errorData && \"error_description\" in errorData ? String(errorData.error_description) : \"Unknown error\"\n throw new TokenRefreshError(`Google token refresh failed: ${response.status} ${errorDescription}`)\n }\n\n const refreshedTokens = await response.json()\n const { access_token, expires_in } = refreshedTokens // id_token, scope, token_type は不要\n\n if (!(access_token && expires_in)) {\n logger.error(\"Invalid token response from Google:\", refreshedTokens)\n throw new Error(\"Invalid token response from Google\")\n }\n\n return {\n accessToken: access_token,\n refreshToken: refreshTokenFromSession, // Google は通常リフレッシュトークンを再発行しない\n expirationDateMs: Number(Date.now()) + expires_in * 1000,\n }\n } catch (error) {\n if (error instanceof TokenRefreshError) {\n throw error\n }\n const description = error instanceof Error ? error.message : \"Unknown error\"\n throw new TokenRefreshError(`Failed to refresh Google access token: ${description}`)\n }\n }\n\n /**\n * Dispatches the token refresh by session provider (currently Google only).\n * セッションのプロバイダー別にトークンリフレッシュを振り分ける (現状 Google のみ)。\n */\n const refreshedAccessToken = async (session: Session) => {\n const sessionProfile: SessionProfile = session.get(sessionKey)\n const provider = sessionProfile.provider\n if (!provider) {\n throw new Error(\"Provider not found in session\")\n }\n switch (provider) {\n case \"google\":\n return await getRefreshedGoogleAccessToken(session)\n default:\n throw new Error(\"Provider not supported\")\n }\n }\n\n /**\n * Validates session tokens, refreshing them when expired; destroys the session on refresh failure.\n * セッショントークンを検証し、期限切れならリフレッシュする。失敗時はセッションを破棄する。\n */\n const authenticate = async (request: Request, session: Session, options?: { failureRedirect?: string }) => {\n const { failureRedirect } = options || {}\n try {\n // テストバックドア (ポート注入時のみ有効)\n if (config.mockUser?.enabled(request)) {\n return {\n accessToken: \"mock-access-token\",\n refreshToken: \"mock-refresh-token\",\n expirationDateMs: Date.now() + 3600000,\n }\n }\n\n // セッションから認証データを取得\n const user = session.get(sessionKey)\n if (!(user?.accessToken && user.expirationDateMs)) {\n throw redirect(failureRedirect || loginPath)\n }\n\n const { accessToken, refreshToken, expirationDateMs } = user\n\n // 期限切れならエラーを投げてリフレッシュ経路へ\n if (expirationDateMs < Date.now()) {\n if (!refreshToken) {\n // リフレッシュトークンがない場合は更新できないのでログインページへ\n throw redirect(failureRedirect || loginPath)\n }\n throw new Error(\"Expired\")\n }\n\n // 期限内ならそのまま返す\n return { accessToken, refreshToken, expirationDateMs }\n } catch (error) {\n // 期限切れエラーのみリフレッシュを試みる\n if (error instanceof Error && error.message === \"Expired\") {\n try {\n const refreshed = await refreshedAccessToken(session)\n\n // セッションを新しいトークンで更新\n const user = session.get(sessionKey)\n session.set(sessionKey, { ...user, ...refreshed })\n\n // アクセストークンを返す (commit は呼び出し側の責務)\n return refreshed\n } catch (refreshError) {\n if (refreshError instanceof GoogleInvalidGrantError) {\n logger.warn(\"Google refresh token invalid or revoked\")\n } else {\n const message = refreshError instanceof Error ? refreshError.message : \"Unknown error\"\n logger.error(\"Failed to refresh token:\", message)\n }\n // リフレッシュに失敗した場合はセッションを破棄してログインページへ\n const cookie = await storage.destroySession(session)\n throw redirect(failureRedirect || loginPath, {\n headers: { \"Set-Cookie\": cookie },\n })\n }\n }\n\n // その他の予期せぬエラーは再 throw\n throw error\n }\n }\n\n return { authenticate, refreshedAccessToken }\n}\n","/**\n * @module authenticate-in-loader\n * @description Per-request loader authentication guard with photo cache and warmup.\n * 写真キャッシュとウォームアップ付きの毎リクエストローダー認証ガード。\n *\n * 原本 (quants-react-router authenticateInLoader.server.ts) を挙動保存で移植。\n * DB クエリ (getOpenids) は `getUserPhoto` ポートへ、DB プールウォームアップは `warmup`\n * ポートへ置き換えた。写真キャッシュ (Map + pending dedup) はファクトリインスタンスごとに\n * 保持する (module-level mutable state の排除)。\n */\nimport type { AuthenticationPayload, MyGoogleProfile, SessionProfile } from \"@aiquants/auth-core\"\nimport { redirect } from \"react-router\"\nimport type { AuthenticateFns } from \"./authenticate\"\nimport type { AuthServerConfig, LoggerPort } from \"./ports\"\nimport type { AuthSessionStorage } from \"./session\"\n\n/** createAuthenticateInLoader が返すローダーガード関数。 */\nexport type AuthenticateInLoader = ReturnType<typeof createAuthenticateInLoader>\n\n/**\n * Builds the per-request loader guard wired to injected ports.\n * 注入ポートに結線した毎リクエストローダーガードを生成する。\n */\nexport const createAuthenticateInLoader = (config: AuthServerConfig, storage: AuthSessionStorage, authFns: AuthenticateFns, logger: LoggerPort) => {\n // 写真キャッシュ (インスタンススコープ。同時リクエストは pending dedup で 1 クエリに集約)\n const openidPhotoCache = new Map<string, string>()\n const pendingOpenidPhotoQueries = new Map<string, Promise<string>>()\n\n /**\n * Resolves the user photo with cache + in-flight dedup, delegating the query to the port.\n * キャッシュ + 実行中 dedup 付きでユーザー写真を解決する (実クエリはポートへ委譲)。\n */\n const getCachedOpenidPhoto = async (openid: string): Promise<string> => {\n const cached = openidPhotoCache.get(openid)\n if (cached !== undefined) {\n return cached\n }\n\n const pending = pendingOpenidPhotoQueries.get(openid)\n if (pending !== undefined) {\n return pending\n }\n\n const fetchPromise = (async () => {\n try {\n const photo = config.getUserPhoto ? await config.getUserPhoto(openid) : \"\"\n openidPhotoCache.set(openid, photo)\n return photo\n } catch (error) {\n openidPhotoCache.delete(openid)\n throw error\n } finally {\n pendingOpenidPhotoQueries.delete(openid)\n }\n })()\n\n pendingOpenidPhotoQueries.set(openid, fetchPromise)\n return fetchPromise\n }\n\n /**\n * Handles authentication within loader requests by refreshing session tokens when needed.\n * ローダーリクエスト内で必要に応じてセッショントークンを更新する認証処理。\n *\n * @param request - リクエストオブジェクト。\n * @param options - 認証オプション。`failureRedirect: null` でリダイレクトを明示的にスキップ。\n * @returns 認証済みユーザープロファイルとオプショナルな Set-Cookie 値。\n */\n const authenticateInLoader = async (request: Request, options: { failureRedirect: string } | { failureRedirect: null }): Promise<{ user?: MyGoogleProfile; cookie?: string }> => {\n // DB 接続プールが確実にオープンするのを待ってから処理を開始する (E2E 等でのコールドスタート競合回避)\n if (config.warmup) {\n await config.warmup()\n }\n\n const { failureRedirect } = options\n const session = await storage.getSession(request)\n const currentUser = await storage.getSessionUser(request)\n\n if (currentUser === undefined) {\n if (failureRedirect === null) {\n return {}\n }\n throw redirect(failureRedirect)\n }\n\n try {\n // トークンの有効期限を確認し、必要に応じて更新\n const authenticatedTokens = await authFns.authenticate(request, session, {\n failureRedirect: failureRedirect ?? undefined,\n })\n\n // 認証ペイロードの構築\n const authenticationPayload: AuthenticationPayload = {\n accessToken: authenticatedTokens.accessToken,\n refreshToken: authenticatedTokens.refreshToken,\n expirationDateMs: authenticatedTokens.expirationDateMs,\n provider: currentUser.provider,\n }\n // セッション内のユーザ情報\n const updatedSessionUser: SessionProfile = {\n id: currentUser.id,\n displayName: currentUser.displayName,\n name: currentUser.name,\n emails: currentUser.emails,\n ...authenticationPayload,\n role: currentUser.role,\n }\n\n // ユーザー写真をキャッシュまたはポートから取得\n const photo = await getCachedOpenidPhoto(currentUser.id)\n\n // ユーザープロファイル情報\n const updatedUser: MyGoogleProfile = {\n ...currentUser,\n ...authenticationPayload,\n photo,\n }\n\n // トークンが更新 (リフレッシュ) された場合のみ、セッションを保存して Cookie ヘッダーを設定する\n const isRefreshed = authenticatedTokens.accessToken !== currentUser.accessToken\n const cookie = isRefreshed ? await storage.saveSession(request, updatedSessionUser) : undefined\n\n return { user: updatedUser, cookie }\n } catch (error) {\n if (error instanceof Response && error.headers.get(\"Location\")) {\n throw error\n }\n logger.error(\"Error in authenticateInLoader:\", error)\n // 認証エラー時はセッションを破棄してリダイレクト\n if (failureRedirect !== null) {\n // セッション破棄のみを try で保護する。redirect の throw を try 内に置くと\n // 同じ catch(destroyError) に捕捉されて Set-Cookie 無しの fallback に差し替わってしまうため、\n // throw redirect は必ず try の外で行う。\n let destroyedCookie: string | undefined\n try {\n destroyedCookie = await storage.destroySession(session)\n } catch (destroyError) {\n logger.error(\"Failed to destroy session during auth error:\", destroyError)\n }\n // 破棄に成功していれば Set-Cookie を付与し、失敗してもとにかくリダイレクトは試みる\n throw destroyedCookie !== undefined ? redirect(failureRedirect, { headers: { \"Set-Cookie\": destroyedCookie } }) : redirect(failureRedirect)\n }\n throw error\n }\n }\n\n return authenticateInLoader\n}\n","/**\n * @module google-strategy\n * @description Google OAuth authenticator factory (remix-auth v4 + local GoogleStrategy).\n * Google OAuth 認証器ファクトリ (remix-auth v4 + 同梱の GoogleStrategy)。\n *\n * 原本 (quants-react-router auth.server.ts) の verify コールバックを挙動保存で移植。\n * env 読取り・backend HTTP・allowlist・エラーメッセージはすべてポート/設定注入に置き換えた。\n * 検証順序 (Google プロフィール検証 → backend verify → allowlist → backend signup)、\n * リダイレクト先 (`<loginPath>?error=<encoded>`)、エラーメッセージ文言は原本と等価。\n */\nimport type { MyGoogleProfile } from \"@aiquants/auth-core\"\nimport { type AuthMessages, resolveAuthMessages } from \"@aiquants/auth-core\"\nimport { redirect } from \"react-router\"\nimport { Authenticator } from \"remix-auth\"\nimport { GoogleStrategy } from \"./google-strategy-impl\"\nimport { defaultLogger } from \"./logger\"\nimport type { AuthServerConfig } from \"./ports\"\n\ninterface GoogleToken {\n id_token?: string\n access_token?: string\n refresh_token?: string\n expires_in?: number\n scope?: string\n}\n\n/**\n * Builds a remix-auth Authenticator with the Google strategy wired to injected ports.\n * 注入ポートに結線した Google ストラテジー付き Authenticator を生成する。\n */\nexport const createGoogleAuthenticator = (config: AuthServerConfig): Authenticator<MyGoogleProfile> => {\n const logger = config.logger ?? defaultLogger\n const messages: AuthMessages = resolveAuthMessages(config.messages)\n const loginPath = config.loginPath ?? \"/auth/login\"\n\n const googleStrategy = new GoogleStrategy<MyGoogleProfile>(\n {\n clientId: config.google.clientId,\n clientSecret: config.google.clientSecret,\n redirectURI: config.google.redirectURI,\n scopes: config.google.scopes,\n accessType: config.google.accessType ?? \"offline\",\n includeGrantedScopes: config.google.includeGrantedScopes ?? true,\n prompt: config.google.prompt ?? \"consent\",\n },\n /**\n * Google 認証後のコールバック。Google のトークンとプロフィールを検証し、\n * backend verify → allowlist → backend signup を経て MyGoogleProfile を返す。\n * 失敗時は `<loginPath>?error=<encoded>` へのリダイレクト Response を throw する。\n */\n async ({ request: _request, tokens }) => {\n const { access_token: accessToken, refresh_token: refreshToken, expires_in: expiresIn, scope, id_token: _idToken } = tokens.data as GoogleToken\n const {\n _json: { sub, email, picture, hd: _hd, email_verified },\n photos: _photos,\n ...profile\n } = await GoogleStrategy.userProfile(tokens)\n const {\n id,\n displayName,\n name: { familyName: _familyName, givenName: _givenName },\n emails: _emails,\n } = profile\n const provider = \"google\"\n\n // first verify (トークン/プロフィール欠落は Google 認証失敗として扱う)\n if (\n accessToken === undefined ||\n refreshToken === undefined ||\n expiresIn === undefined ||\n scope === undefined ||\n id === undefined ||\n displayName === undefined ||\n sub === undefined ||\n email === undefined ||\n email_verified === undefined\n // hd === undefined // Google Workspace などの組織制限をする場合は有効化\n ) {\n if (email_verified === false) {\n const errorMessage = encodeURIComponent(messages.emailNotVerified)\n throw redirect(`${loginPath}?error=${errorMessage}`)\n }\n const errorMessage = encodeURIComponent(messages.googleFailed)\n throw redirect(`${loginPath}?error=${errorMessage}`)\n }\n\n logger.info(\"Authenticated on Google: \", profile.displayName)\n\n // Backend Authentication\n try {\n // backendAuth ポート未指定時は verify/signup をスキップ (フロント単独運用)\n if (config.backendAuth) {\n const ok = await config.backendAuth.verify(provider, accessToken)\n\n if (!ok) {\n logger.error(\"Backend verification returned false for user:\", profile.displayName)\n const errorMessage = encodeURIComponent(messages.backendFailed)\n throw redirect(`${loginPath}?error=${errorMessage}`)\n }\n\n logger.info(\"Authenticated on Backend: \", profile.displayName)\n }\n\n // メールアドレスまたはドメインベースでのアクセス制限チェック (論理和は allowlist 実装側)\n // ⚠️ `await` 必須。非同期実装を await せずに評価すると Promise が truthy 判定され\n // 許可判定を素通りする (fail-open)。この await は spec で門番されている。\n const userEmail = profile.emails[0]?.value\n if (!(await config.isEmailAllowed(userEmail))) {\n const error = new Error(\"You are not allowed to access this application\")\n logger.error(error)\n throw error\n }\n\n // 無効化済みアカウントはログインさせない (allowlist とは別軸の判定)\n if (config.isUserActive && !(await config.isUserActive(profile))) {\n const error = new Error(\"You are not allowed to access this application\")\n logger.error(\"Deactivated account attempted to sign in:\", userEmail)\n throw error\n }\n\n // バックエンドでのサインアップ処理 (毎ログイン実行の挙動を保存)\n if (config.backendAuth) {\n await config.backendAuth.signup(provider, accessToken)\n }\n\n logger.info(\"Authenticated complete: \", profile.displayName)\n\n // 認証ペイロードの構築\n const expirationDateMs = Date.now() + expiresIn * 1000\n const authenticationPayload = {\n accessToken: accessToken,\n refreshToken: refreshToken,\n expirationDateMs: expirationDateMs,\n provider: provider,\n }\n // ユーザープロファイル情報\n const user: MyGoogleProfile = {\n ...profile,\n ...authenticationPayload,\n photo: picture,\n }\n return user\n } catch (error: unknown) {\n let loggable = error\n if (typeof error === \"object\" && error !== null && \"cause\" in error) {\n // cause があればそれを優先してログに残す (falsy なら元のエラー)\n loggable = (error as { cause: unknown }).cause || error\n }\n logger.error(\"Error during backend authentication or subsequent processing:\", loggable)\n\n let friendlyErrorMessage = messages.unexpected\n let conditionMet = false\n\n // ネットワーク到達不可 (getaddrinfo ENOTFOUND) の判定\n if (typeof error === \"object\" && error !== null && \"cause\" in error) {\n const cause = (error as { cause: unknown }).cause\n if (cause && typeof cause === \"object\" && \"message\" in cause && typeof (cause as { message: string }).message === \"string\" && (cause as { message: string }).message.includes(\"getaddrinfo ENOTFOUND\")) {\n friendlyErrorMessage = messages.backendUnreachable\n conditionMet = true\n }\n }\n\n // allowlist 拒否の判定\n if (!conditionMet && error instanceof Error && error.message.includes(\"You are not allowed to access this application\")) {\n friendlyErrorMessage = messages.notAllowed\n conditionMet = true\n }\n\n // 既にリダイレクト Response が throw されている場合はそのまま再 throw\n if (!conditionMet && error instanceof Response && error.headers.get(\"Location\")?.startsWith(loginPath)) {\n throw error\n }\n\n const encodedErrorMessage = encodeURIComponent(friendlyErrorMessage)\n throw redirect(`${loginPath}?error=${encodedErrorMessage}`)\n }\n },\n )\n\n const authenticator = new Authenticator<MyGoogleProfile>()\n authenticator.use(googleStrategy)\n return authenticator\n}\n","/**\n * @module google-strategy-impl\n * @description Self-contained Google OAuth2 strategy (ported from @coji/remix-auth-google).\n * 自己完結型の Google OAuth2 ストラテジー (@coji/remix-auth-google から移植)。\n *\n * @coji/remix-auth-google@4.2.0 の実装 (68 行) を挙動保存で移植し、外部依存を除去する。\n * OAuth2 の本体機構 (PKCE / state / トークン交換) は継承元の remix-auth-oauth2 が担うため\n * 移植対象ではない。ここに置くのは Google 固有の設定 (エンドポイント URL・既定スコープ)、\n * 認可パラメータの上書き (access_type / include_granted_scopes / prompt / hd / login_hint)、\n * および userinfo (v3) からプロファイルを取得する静的メソッドのみ。\n *\n * 既定スコープ・URL・プロファイル整形 (sub→id、picture→photos、_json=raw) は原本と byte 一致。\n */\nimport { OAuth2Strategy } from \"remix-auth-oauth2\"\n\n/** Google の既定スコープ (原本と同一。フル URL 形式)。 */\nexport const GoogleStrategyDefaultScopes = [\"openid\", \"https://www.googleapis.com/auth/userinfo.profile\", \"https://www.googleapis.com/auth/userinfo.email\"]\n\n/** ストラテジー名 (remix-auth の Authenticator 登録キー)。 */\nexport const GoogleStrategyDefaultName = \"google\"\n\n// userinfo エンドポイント (v3)。プロファイル取得のみに用いる\nconst userInfoURL = \"https://www.googleapis.com/oauth2/v3/userinfo\"\n\n/** @see https://developers.google.com/identity/protocols/oauth2/scopes */\nexport type GoogleScope = string\n\n/** GoogleStrategy のコンストラクタオプション (原本 @coji と同一)。 */\nexport type GoogleStrategyOptions = {\n clientId: string\n clientSecret: string\n redirectURI: string\n /** @default \"openid profile email\" 相当 (GoogleStrategyDefaultScopes)。 */\n scopes?: GoogleScope[]\n accessType?: \"online\" | \"offline\"\n includeGrantedScopes?: boolean\n prompt?: \"none\" | \"consent\" | \"select_account\"\n hd?: string\n loginHint?: string\n}\n\n/** userinfo から取得・整形した Google プロファイル (原本 GoogleProfile と同一形状)。 */\nexport type GoogleProfile = {\n id: string\n displayName: string\n name: {\n familyName: string\n givenName: string\n }\n emails: [{ value: string }]\n photos: [{ value: string }]\n _json: {\n sub: string\n name: string\n given_name: string\n family_name: string\n picture: string\n locale: string\n email: string\n email_verified: boolean\n hd: string\n }\n}\n\n// userProfile が必要とする最小のトークン形状 (arctic の OAuth2Tokens の部分集合)。\n// 直接 arctic へ依存しないための構造的型。実際に呼ぶのは accessToken() のみ。\ntype OAuth2AccessTokenSource = { accessToken(): string }\n\n/**\n * Google OAuth2 strategy for remix-auth (extends remix-auth-oauth2's OAuth2Strategy).\n * remix-auth 用の Google OAuth2 ストラテジー (remix-auth-oauth2 の OAuth2Strategy を継承)。\n */\nexport class GoogleStrategy<User> extends OAuth2Strategy<User> {\n name = GoogleStrategyDefaultName\n private readonly scopes: GoogleScope[]\n private readonly accessType: \"online\" | \"offline\"\n private readonly prompt?: \"none\" | \"consent\" | \"select_account\"\n private readonly includeGrantedScopes: boolean\n private readonly hd?: string\n private readonly loginHint?: string\n\n constructor({ clientId, clientSecret, redirectURI, scopes, accessType, includeGrantedScopes, prompt, hd, loginHint }: GoogleStrategyOptions, verify: OAuth2Strategy<User>[\"verify\"]) {\n const scopeList = scopes ?? GoogleStrategyDefaultScopes\n super(\n {\n clientId,\n clientSecret,\n redirectURI,\n authorizationEndpoint: \"https://accounts.google.com/o/oauth2/v2/auth\",\n tokenEndpoint: \"https://oauth2.googleapis.com/token\",\n scopes: scopeList,\n },\n verify,\n )\n this.scopes = scopeList\n this.accessType = accessType ?? \"online\"\n this.includeGrantedScopes = includeGrantedScopes ?? false\n this.prompt = prompt\n this.hd = hd\n this.loginHint = loginHint\n }\n\n /**\n * Adds Google-specific authorization parameters to the redirect URL.\n * 認可リダイレクト URL へ Google 固有パラメータを付与する。\n */\n protected authorizationParams(params: URLSearchParams, _request?: Request): URLSearchParams {\n params.set(\"access_type\", this.accessType)\n params.set(\"include_granted_scopes\", String(this.includeGrantedScopes))\n if (this.scopes && this.scopes.length > 0) {\n params.set(\"scope\", this.scopes.join(\" \"))\n }\n if (this.prompt) {\n params.set(\"prompt\", this.prompt)\n }\n if (this.hd) {\n params.set(\"hd\", this.hd)\n }\n if (this.loginHint) {\n params.set(\"login_hint\", this.loginHint)\n }\n return params\n }\n\n /**\n * Fetches the Google user profile from the userinfo (v3) endpoint.\n * userinfo (v3) エンドポイントから Google ユーザープロファイルを取得する。\n *\n * @throws プロファイル取得に失敗した場合 (レスポンスが ok でない)。\n */\n static async userProfile(tokens: OAuth2AccessTokenSource): Promise<GoogleProfile> {\n const response = await fetch(userInfoURL, {\n headers: {\n Authorization: `Bearer ${tokens.accessToken()}`,\n },\n })\n if (!response.ok) {\n throw new Error(`Failed to fetch user profile: ${response.statusText}`)\n }\n const raw = await response.json()\n const profile: GoogleProfile = {\n id: raw.sub,\n displayName: raw.name,\n name: {\n familyName: raw.family_name,\n givenName: raw.given_name,\n },\n emails: [{ value: raw.email }],\n photos: [{ value: raw.picture }],\n _json: raw,\n }\n return profile\n }\n}\n","/**\n * @module logger\n * @description Default console-backed logger used when no logger port is injected.\n * ロガーポート未注入時に使う console ベースの既定ロガー。\n */\nimport type { LoggerPort } from \"./ports\"\n\n/** console ベースの既定ロガー (ライブラリ既定のフォールバックとして意図的に console を使用)。 */\nexport const defaultLogger: LoggerPort = {\n // biome-ignore lint/suspicious/noConsole: ロガー未注入時の意図的な console フォールバック\n info: (...args: unknown[]) => console.info(\"[Auth]\", ...args),\n warn: (...args: unknown[]) => console.warn(\"[Auth]\", ...args),\n error: (...args: unknown[]) => console.error(\"[Auth]\", ...args),\n}\n","/**\n * @module handlers\n * @description Route handler factories for login / logout / Google callback.\n * login / logout / Google コールバックのルートハンドラ工場。\n *\n * 原本 (quants-react-router routes/_auth+ の 4 ルート + features/logoutAndRedirect.ts) の\n * サーバー側ロジックを挙動保存で移植。UI (JSX) はアプリ側に残す。\n * コールバックのセッション保存形状 (SessionProfile、photo/_json/photos を含まない) は\n * cookie byte 互換のため厳密に保存する。\n */\nimport type { AuthenticationPayload, MyGoogleProfile, SessionProfile } from \"@aiquants/auth-core\"\nimport { redirect } from \"react-router\"\nimport type { Authenticator } from \"remix-auth\"\nimport type { AuthServerConfig, LoggerPort } from \"./ports\"\nimport type { AuthSessionStorage } from \"./session\"\n\n/** createAuthHandlers が返すハンドラ束。 */\nexport type AuthHandlers = ReturnType<typeof createAuthHandlers>\n\n/**\n * Builds loader/action handlers for the auth routes.\n * 認証ルート用の loader/action ハンドラ群を生成する。\n */\nexport const createAuthHandlers = (config: AuthServerConfig, storage: AuthSessionStorage, authenticator: Authenticator<MyGoogleProfile>, logger: LoggerPort) => {\n const loginPath = config.loginPath ?? \"/auth/login\"\n const postLoginRedirect = config.postLoginRedirect ?? \"/\"\n const postLogoutRedirect = config.postLogoutRedirect ?? \"/\"\n\n /**\n * Login-page loader: redirects authenticated users, passes the error message through.\n * ログインページ loader。認証済みならリダイレクトし、エラーメッセージを data として返す。\n */\n const loginLoader = async ({ request }: { request: Request }): Promise<{ error: string | null }> => {\n const user = await storage.getSessionUser(request)\n if (user) {\n // 認証済みならトップページにリダイレクト\n throw redirect(postLoginRedirect)\n }\n const url = new URL(request.url)\n const error = url.searchParams.get(\"error\")\n return { error } // エラーメッセージを loader data として渡す\n }\n\n /**\n * Login action: starts the Google OAuth flow for the \"Sign In with Google\" form action.\n * ログイン action。\"Sign In with Google\" フォーム action で Google OAuth フローを開始する。\n */\n const loginAction = async ({ request }: { request: Request }): Promise<MyGoogleProfile | null> => {\n const formData = await request.clone().formData()\n const action = String(formData.get(\"_action\"))\n\n let user: MyGoogleProfile | null = null\n switch (action) {\n case \"Sign In with Google\":\n user = await authenticator.authenticate(\"google\", request)\n break\n default:\n user = null\n }\n return user\n }\n\n /**\n * Destroys the session and returns the Set-Cookie value (logout primitive).\n * セッションを破棄して Set-Cookie 値を返す (ログアウトの基本操作)。\n */\n const logout = async (request: Request) => {\n return await storage.logout(request)\n }\n\n /**\n * Logs out and redirects with the destroyed-session cookie attached.\n * ログアウトし、破棄済みセッション Cookie を付与してリダイレクトする。\n */\n const logoutAndRedirect = async (request: Request) => {\n const cookie = await logout(request)\n const headers = new Headers()\n headers.append(\"Set-Cookie\", cookie)\n return redirect(postLogoutRedirect, { headers })\n }\n\n /**\n * Logout-route loader: performs logout and throws the redirect.\n * ログアウトルート loader。ログアウトを実行してリダイレクトを throw する。\n */\n const logoutLoader = async ({ request }: { request: Request }): Promise<never> => {\n throw await logoutAndRedirect(request)\n }\n\n /**\n * Google OAuth callback loader: authenticates, saves the session and redirects home.\n * Google OAuth コールバック loader。認証してセッションを保存し、ホームへリダイレクトする。\n */\n const googleCallbackLoader = async ({ request }: { request: Request }): Promise<never> => {\n try {\n const googleProfile = await authenticator.authenticate(\"google\", request)\n\n // 既存のセッションから前回のユーザー情報を取得して refresh_token を保護する\n const existingSession = await storage.getSession(request)\n const existingUser = existingSession.get(storage.sessionKey) as SessionProfile | undefined\n const previousRefreshToken = existingUser?.refreshToken\n\n // 認証ペイロードの構築\n const authenticationPayload: AuthenticationPayload = {\n accessToken: googleProfile.accessToken,\n refreshToken: googleProfile.refreshToken || previousRefreshToken,\n expirationDateMs: googleProfile.expirationDateMs,\n provider: googleProfile.provider,\n }\n\n // セッション内のユーザ情報 (photo/_json/photos を含まない byte 互換形状)\n const user: SessionProfile = {\n id: googleProfile.id,\n displayName: googleProfile.displayName,\n name: googleProfile.name,\n emails: googleProfile.emails,\n ...authenticationPayload,\n role: googleProfile.role,\n }\n\n // 認証後のユーザ情報をセッションに保存\n const cookie = await storage.saveSession(request, user)\n\n // ホームへリダイレクト\n throw redirect(postLoginRedirect, { headers: { \"Set-Cookie\": cookie } })\n } catch (error) {\n // authenticator.authenticate は認証失敗時に Response か Error を throw する。\n // access_denied のような OAuth エラーでは Error の可能性があるため、\n // どちらの場合でもログインページへのリダイレクトが安全 (Response はそのまま再 throw)。\n if (error instanceof Response) {\n throw error\n }\n logger.error(\"Authentication error:\", error)\n throw redirect(loginPath)\n }\n }\n\n return { loginLoader, loginAction, logoutLoader, logout, logoutAndRedirect, googleCallbackLoader }\n}\n","/**\n * @module session\n * @description Cookie session storage factory with a DI test backdoor.\n * DI テストバックドア付きの cookie セッションストレージファクトリ。\n *\n * 原本 (quants-react-router session.server.ts) を挙動保存で移植。相違点は\n * (1) module-level の env throw をファクトリ引数検証へ移した、\n * (2) テストバックドア (mock_user cookie) をハードコードから `mockUser` ポート注入に変えた、\n * の 2 点のみ。cookie 形状 (`__session`/httpOnly/lax/30d、キー \"user\") は byte 互換。\n */\nimport type { GoogleProfileBase, SessionProfile } from \"@aiquants/auth-core\"\nimport { createCookieSessionStorage, redirect } from \"react-router\"\nimport type { MockUserPort, SessionConfig } from \"./ports\"\n\n/** createAuthSessionStorage が返すセッション操作束。 */\nexport type AuthSessionStorage = ReturnType<typeof createAuthSessionStorage>\n\n/**\n * Builds the cookie session storage and session helpers.\n * cookie セッションストレージとセッションヘルパー群を生成する。\n *\n * @throws secrets が空の場合 (fail-closed の設定ガード)。\n */\nexport const createAuthSessionStorage = (config: SessionConfig, options?: { mockUser?: MockUserPort; loginPath?: string; postLoginRedirect?: string; isUserActive?: (profile: GoogleProfileBase) => boolean | Promise<boolean> }) => {\n // ファクトリ実行時検証 (import 時副作用の排除)\n if (!config.secrets || config.secrets.length === 0 || config.secrets.some((s) => !s)) {\n throw new Error(\"REMIX_SESSION_SECRET must be set\")\n }\n\n const sessionKey = config.sessionKey ?? \"user\"\n const maxAge = config.maxAge ?? 60 * 60 * 24 * 30\n const loginPath = options?.loginPath ?? \"/auth/login\"\n const postLoginRedirect = options?.postLoginRedirect ?? \"/\"\n const mockUser = options?.mockUser\n const isUserActive = options?.isUserActive\n\n const sessionStorage = createCookieSessionStorage({\n cookie: {\n name: config.cookieName ?? \"__session\",\n httpOnly: true, // for security reasons, make this cookie http only\n path: \"/\", // remember to add this so the cookie will work in all routes\n sameSite: \"lax\", // this helps with CSRF\n secrets: config.secrets,\n secure: config.secure ?? process.env.NODE_ENV === \"production\",\n maxAge,\n },\n })\n\n /**\n * Reads the session from the request cookie.\n * リクエストの Cookie からセッションを取得する。\n */\n const getSession = async (request: Request) => {\n return await sessionStorage.getSession(request.headers.get(\"Cookie\"))\n }\n\n /**\n * Reads the session user, honoring the injected test backdoor when enabled.\n * セッションからユーザー情報を取得する (注入されたテストバックドアが有効ならそれを返す)。\n */\n const getSessionUser = async (request: Request): Promise<SessionProfile | undefined> => {\n // テストバックドア (ポート注入時のみ有効。判定条件はアプリ側の enabled に委譲)\n if (mockUser?.enabled(request)) {\n return mockUser.resolve(request)\n }\n\n const session = await getSession(request)\n const user = session.get(sessionKey) as SessionProfile | undefined\n if (user === undefined) return undefined\n // 無効化済みアカウントは「未認証」として扱う。これにより、既に発行済みのセッション Cookie を\n // 持っていてもガードは通らず、無効化がその場で全ルートへ波及する。\n if (isUserActive && !(await isUserActive(user))) return undefined\n return user\n }\n\n /**\n * Saves the user into the session and returns the Set-Cookie value.\n * セッションへユーザー情報を保存し、Set-Cookie 値を返す。\n */\n const saveSession = async (request: Request, user: SessionProfile) => {\n const session = await getSession(request)\n session.set(sessionKey, user)\n return await sessionStorage.commitSession(session)\n }\n\n /**\n * Authenticated-user guard: redirects to the login page when absent.\n * 認証ユーザーガード。未認証ならログインページへリダイレクトする。\n */\n const requireUser = async (request: Request) => {\n const sessionUser = await getSessionUser(request)\n if (sessionUser === undefined) {\n throw redirect(loginPath)\n }\n return sessionUser as SessionProfile\n }\n\n /**\n * Admin-user guard: redirects to the app root when the role is not \"admin\".\n * 管理者ユーザーガード。role が \"admin\" でなければルートへリダイレクトする。\n */\n const requireAdminUser = async (request: Request) => {\n const sessionUser = await requireUser(request)\n if (sessionUser?.role !== \"admin\") {\n throw redirect(postLoginRedirect)\n }\n return sessionUser\n }\n\n /**\n * Destroys the session and returns the Set-Cookie value (logout primitive).\n * セッションを破棄して Set-Cookie 値を返す (ログアウトの基本操作)。\n */\n const logout = async (request: Request) => {\n const session = await getSession(request)\n const cookie = await sessionStorage.destroySession(session)\n return cookie\n }\n\n return {\n sessionStorage,\n sessionKey,\n getSession,\n getSessionUser,\n saveSession,\n requireUser,\n requireAdminUser,\n logout,\n commitSession: sessionStorage.commitSession,\n destroySession: sessionStorage.destroySession,\n }\n}\n","/**\n * @module auth-server\n * @description Aggregate factory bundling session, authenticator, guards and handlers.\n * セッション・認証器・ガード・ハンドラを束ねる一括ファクトリ。\n *\n * daily-report の createDailyReportServer と同型の単一結線点。アプリは\n * `createAuthServer(config)` を 1 回呼び、返り値を各 `*.server.ts` shim から再輸出する。\n */\nimport { createAuthenticate } from \"./authenticate\"\nimport { createAuthenticateInLoader } from \"./authenticate-in-loader\"\nimport { createGoogleAuthenticator } from \"./google-strategy\"\nimport { createAuthHandlers } from \"./handlers\"\nimport { defaultLogger } from \"./logger\"\nimport type { AuthServerConfig } from \"./ports\"\nimport { createAuthSessionStorage } from \"./session\"\n\n/** createAuthServer が返す認証サーバー束。 */\nexport type AuthServer = ReturnType<typeof createAuthServer>\n\n/**\n * Builds the full auth server bundle from a single config.\n * 単一の設定から認証サーバー束一式を生成する。\n */\nexport const createAuthServer = (config: AuthServerConfig) => {\n const logger = config.logger ?? defaultLogger\n\n // セッションストレージ + ヘルパー (テストバックドアはポート委譲)\n const storage = createAuthSessionStorage(config.session, {\n mockUser: config.mockUser,\n loginPath: config.loginPath,\n postLoginRedirect: config.postLoginRedirect,\n isUserActive: config.isUserActive,\n })\n\n // Google 認証器 (verify コールバックはポート結線済み)\n const authenticator = createGoogleAuthenticator(config)\n\n // トークンリフレッシュ\n const authFns = createAuthenticate(config, storage, logger)\n\n // 毎リクエストのローダーガード (写真キャッシュ内包)\n const authenticateInLoader = createAuthenticateInLoader(config, storage, authFns, logger)\n\n // ルートハンドラ (login/logout/callback)\n const handlers = createAuthHandlers(config, storage, authenticator, logger)\n\n return {\n // 認証器\n authenticator,\n // セッション操作/トークンガードの内部束 (テストでの失敗注入等に使う。プロパティ差し替えは\n // authenticate / authenticateInLoader の内部呼び出しへ即時反映される)\n storage,\n authFns,\n sessionStorage: storage.sessionStorage,\n getSession: storage.getSession,\n getSessionUser: storage.getSessionUser,\n saveSession: storage.saveSession,\n requireUser: storage.requireUser,\n requireAdminUser: storage.requireAdminUser,\n commitSession: storage.commitSession,\n destroySession: storage.destroySession,\n // トークンガード\n authenticate: authFns.authenticate,\n refreshedAccessToken: authFns.refreshedAccessToken,\n authenticateInLoader,\n // ルートハンドラ\n ...handlers,\n }\n}\n"],"mappings":"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","names":["server_exports","__export","GoogleInvalidGrantError","TokenRefreshError","createAuthHandlers","createAuthServer","createAuthSessionStorage","createAuthenticate","createAuthenticateInLoader","createGoogleAuthenticator","__toCommonJS","import_react_router","TokenRefreshError","message","GoogleInvalidGrantError","createAuthenticate","config","storage","logger","loginPath","sessionKey","getRefreshedGoogleAccessToken","session","refreshTokenFromSession","url","body","response","errorData","errorDescription","refreshedTokens","access_token","expires_in","error","description","refreshedAccessToken","provider","request","options","failureRedirect","user","accessToken","refreshToken","expirationDateMs","refreshed","refreshError","cookie","import_react_router","createAuthenticateInLoader","config","storage","authFns","logger","openidPhotoCache","pendingOpenidPhotoQueries","getCachedOpenidPhoto","openid","cached","pending","fetchPromise","photo","error","request","options","failureRedirect","session","currentUser","authenticatedTokens","authenticationPayload","updatedSessionUser","updatedUser","cookie","destroyedCookie","destroyError","import_auth_core","import_react_router","import_remix_auth","import_remix_auth_oauth2","GoogleStrategyDefaultScopes","GoogleStrategyDefaultName","userInfoURL","GoogleStrategy","clientId","clientSecret","redirectURI","scopes","accessType","includeGrantedScopes","prompt","hd","loginHint","verify","scopeList","params","_request","tokens","response","raw","defaultLogger","args","createGoogleAuthenticator","config","logger","defaultLogger","messages","loginPath","googleStrategy","GoogleStrategy","_request","tokens","accessToken","refreshToken","expiresIn","scope","_idToken","sub","email","picture","_hd","email_verified","_photos","profile","id","displayName","_familyName","_givenName","_emails","provider","errorMessage","userEmail","error","expirationDateMs","loggable","friendlyErrorMessage","conditionMet","cause","encodedErrorMessage","authenticator","import_react_router","createAuthHandlers","config","storage","authenticator","logger","loginPath","postLoginRedirect","postLogoutRedirect","loginLoader","request","loginAction","formData","action","user","logout","logoutAndRedirect","cookie","headers","googleProfile","previousRefreshToken","authenticationPayload","error","import_react_router","createAuthSessionStorage","config","options","s","sessionKey","maxAge","loginPath","postLoginRedirect","mockUser","isUserActive","sessionStorage","getSession","request","getSessionUser","user","saveSession","session","requireUser","sessionUser","createAuthServer","config","logger","defaultLogger","storage","createAuthSessionStorage","authenticator","createGoogleAuthenticator","authFns","createAuthenticate","authenticateInLoader","createAuthenticateInLoader","handlers","createAuthHandlers"]}
1
+ {"version":3,"sources":["../src/server.ts","../src/server/authenticate.ts","../src/server/authenticate-in-loader.ts","../src/server/google-strategy.ts","../src/server/google-strategy-impl.ts","../src/server/logger.ts","../src/server/handlers.ts","../src/server/session.ts","../src/server/auth-server.ts"],"sourcesContent":["/**\n * @aiquants/auth-react-router/server — server-side auth adapter barrel.\n * サーバーサイド認証アダプタのバレル。\n */\n\nexport type { AuthServer } from \"./server/auth-server\"\nexport { createAuthServer } from \"./server/auth-server\"\nexport { AuthRequiredError, createAuthenticate, GoogleInvalidGrantError, TokenRefreshError } from \"./server/authenticate\"\nexport { createAuthenticateInLoader } from \"./server/authenticate-in-loader\"\nexport { createGoogleAuthenticator } from \"./server/google-strategy\"\nexport { createAuthHandlers } from \"./server/handlers\"\nexport type { AuthServerConfig, BackendAuthPort, GoogleOAuthConfig, LoggerPort, MockUserPort, SessionConfig, UserPhotoPort, WarmupPort } from \"./server/ports\"\nexport type { AuthSessionStorage } from \"./server/session\"\nexport { createAuthSessionStorage } from \"./server/session\"\n","/**\n * @module authenticate\n * @description Session token guard with Google refresh-token rotation.\n * Google リフレッシュトークンによるセッショントークンガード。\n *\n * 原本 (quants-react-router authenticate.server.ts) を挙動保存で移植。\n * env (GOOGLE_CLIENT_ID/SECRET) は Google 設定注入に、destroySession はセッションストレージ\n * 注入に、テストバックドアは mockUser ポート注入に置き換えた。\n * 期限判定 → リフレッシュ → 失敗時セッション破棄 + ログインへリダイレクトの流れは原本と等価。\n *\n * 参考: https://sergiodxa.com/articles/working-with-refresh-tokens-in-remix\n */\nimport type { SessionProfile } from \"@aiquants/auth-core\"\nimport { redirect, type Session } from \"react-router\"\nimport type { AuthServerConfig, LoggerPort } from \"./ports\"\nimport type { AuthSessionStorage } from \"./session\"\n\n/** トークンリフレッシュ失敗の基底エラー。 */\nexport class TokenRefreshError extends Error {\n constructor(message: string) {\n super(message)\n this.name = \"TokenRefreshError\"\n }\n}\n\n/** Google の invalid_grant (失効/取り消し済みリフレッシュトークン) エラー。 */\nexport class GoogleInvalidGrantError extends TokenRefreshError {\n constructor() {\n super(\"Google refresh token invalid or revoked\")\n this.name = \"GoogleInvalidGrantError\"\n }\n}\n\n/**\n * Signals that authentication is required when the caller opted out of redirecting.\n * 呼び出し側がリダイレクトを拒否した (`failureRedirect: null`) 際に認証要求を伝えるエラー。\n *\n * `redirect()` を投げてしまうと React Router がそれを本物の 302 として扱うため、\n * 「リダイレクトしない」という契約を型で表現できるこのエラーへ置き換える。\n * セッション破棄が発生した場合はその `Set-Cookie` を `setCookie` で運ぶ\n * (捨てると無効セッションがブラウザに残り続ける)。\n */\nexport class AuthRequiredError extends Error {\n readonly setCookie?: string\n\n constructor(setCookie?: string) {\n super(\"Authentication required\")\n this.name = \"AuthRequiredError\"\n this.setCookie = setCookie\n }\n}\n\n/** createAuthenticate が返す認証関数束。 */\nexport type AuthenticateFns = ReturnType<typeof createAuthenticate>\n\n/**\n * Builds the `authenticate` / `refreshedAccessToken` pair wired to injected config.\n * 注入設定に結線した `authenticate` / `refreshedAccessToken` を生成する。\n */\nexport const createAuthenticate = (config: AuthServerConfig, storage: AuthSessionStorage, logger: LoggerPort) => {\n const loginPath = config.loginPath ?? \"/auth/login\"\n const sessionKey = storage.sessionKey\n\n /**\n * Refreshes the Google access token using the refresh token in the session.\n * セッション内のリフレッシュトークンで Google アクセストークンを更新する。\n */\n const getRefreshedGoogleAccessToken = async (session: Session) => {\n const userFromSession = session.get(sessionKey)\n const refreshTokenFromSession = userFromSession?.refreshToken\n if (!refreshTokenFromSession) {\n throw new Error(\"Refresh token not found in session\")\n }\n try {\n const url = \"https://www.googleapis.com/oauth2/v4/token\"\n const body = {\n client_id: config.google.clientId,\n client_secret: config.google.clientSecret,\n grant_type: \"refresh_token\",\n refresh_token: refreshTokenFromSession,\n }\n\n const response = await fetch(url, {\n headers: {\n \"Content-Type\": \"application/json\",\n },\n method: \"POST\",\n body: JSON.stringify(body),\n })\n\n if (!response.ok) {\n // Google API からエラーが返された場合 (JSON パース失敗も考慮)\n const errorData = await response.json().catch(() => undefined)\n const isInvalidGrant = response.status === 400 && typeof errorData === \"object\" && !!errorData && \"error\" in errorData && errorData.error === \"invalid_grant\"\n if (isInvalidGrant) {\n throw new GoogleInvalidGrantError()\n }\n const errorDescription = typeof errorData === \"object\" && errorData && \"error_description\" in errorData ? String(errorData.error_description) : \"Unknown error\"\n throw new TokenRefreshError(`Google token refresh failed: ${response.status} ${errorDescription}`)\n }\n\n const refreshedTokens = await response.json()\n const { access_token, expires_in } = refreshedTokens // id_token, scope, token_type は不要\n\n if (!(access_token && expires_in)) {\n logger.error(\"Invalid token response from Google:\", refreshedTokens)\n throw new Error(\"Invalid token response from Google\")\n }\n\n return {\n accessToken: access_token,\n refreshToken: refreshTokenFromSession, // Google は通常リフレッシュトークンを再発行しない\n expirationDateMs: Number(Date.now()) + expires_in * 1000,\n }\n } catch (error) {\n if (error instanceof TokenRefreshError) {\n throw error\n }\n const description = error instanceof Error ? error.message : \"Unknown error\"\n throw new TokenRefreshError(`Failed to refresh Google access token: ${description}`)\n }\n }\n\n /**\n * Dispatches the token refresh by session provider (currently Google only).\n * セッションのプロバイダー別にトークンリフレッシュを振り分ける (現状 Google のみ)。\n */\n const refreshedAccessToken = async (session: Session) => {\n const sessionProfile: SessionProfile = session.get(sessionKey)\n const provider = sessionProfile.provider\n if (!provider) {\n throw new Error(\"Provider not found in session\")\n }\n switch (provider) {\n case \"google\":\n return await getRefreshedGoogleAccessToken(session)\n default:\n throw new Error(\"Provider not supported\")\n }\n }\n\n /**\n * Validates session tokens, refreshing them when expired; destroys the session on refresh failure.\n * セッショントークンを検証し、期限切れならリフレッシュする。失敗時はセッションを破棄する。\n */\n const authenticate = async (request: Request, session: Session, options?: { failureRedirect?: string | null }) => {\n const { failureRedirect } = options || {}\n\n // failureRedirect が null のときはリダイレクトを作らず AuthRequiredError を投げる。\n // undefined / 文字列のときは従来どおり redirect() を投げる。\n const authFailure = (setCookie?: string) => {\n if (failureRedirect === null) {\n return new AuthRequiredError(setCookie)\n }\n return setCookie !== undefined ? redirect(failureRedirect || loginPath, { headers: { \"Set-Cookie\": setCookie } }) : redirect(failureRedirect || loginPath)\n }\n try {\n // テストバックドア (ポート注入時のみ有効)\n if (config.mockUser?.enabled(request)) {\n return {\n accessToken: \"mock-access-token\",\n refreshToken: \"mock-refresh-token\",\n expirationDateMs: Date.now() + 3600000,\n }\n }\n\n // セッションから認証データを取得\n const user = session.get(sessionKey)\n if (!(user?.accessToken && user.expirationDateMs)) {\n throw authFailure()\n }\n\n const { accessToken, refreshToken, expirationDateMs } = user\n\n // 期限切れならエラーを投げてリフレッシュ経路へ\n if (expirationDateMs < Date.now()) {\n if (!refreshToken) {\n // リフレッシュトークンがない場合は更新できないので認証失敗として扱う\n throw authFailure()\n }\n throw new Error(\"Expired\")\n }\n\n // 期限内ならそのまま返す\n return { accessToken, refreshToken, expirationDateMs }\n } catch (error) {\n // 期限切れエラーのみリフレッシュを試みる\n if (error instanceof Error && error.message === \"Expired\") {\n try {\n const refreshed = await refreshedAccessToken(session)\n\n // セッションを新しいトークンで更新\n const user = session.get(sessionKey)\n session.set(sessionKey, { ...user, ...refreshed })\n\n // アクセストークンを返す (commit は呼び出し側の責務)\n return refreshed\n } catch (refreshError) {\n if (refreshError instanceof GoogleInvalidGrantError) {\n logger.warn(\"Google refresh token invalid or revoked\")\n } else {\n const message = refreshError instanceof Error ? refreshError.message : \"Unknown error\"\n logger.error(\"Failed to refresh token:\", message)\n }\n // リフレッシュに失敗した場合はセッションを破棄してログインページへ\n const cookie = await storage.destroySession(session)\n throw authFailure(cookie)\n }\n }\n\n // その他の予期せぬエラーは再 throw\n throw error\n }\n }\n\n return { authenticate, refreshedAccessToken }\n}\n","/**\n * @module authenticate-in-loader\n * @description Per-request loader authentication guard with photo cache and warmup.\n * 写真キャッシュとウォームアップ付きの毎リクエストローダー認証ガード。\n *\n * 原本 (quants-react-router authenticateInLoader.server.ts) を挙動保存で移植。\n * DB クエリ (getOpenids) は `getUserPhoto` ポートへ、DB プールウォームアップは `warmup`\n * ポートへ置き換えた。写真キャッシュ (Map + pending dedup) はファクトリインスタンスごとに\n * 保持する (module-level mutable state の排除)。\n */\nimport type { AuthenticationPayload, MyGoogleProfile, SessionProfile } from \"@aiquants/auth-core\"\nimport { redirect } from \"react-router\"\nimport { AuthRequiredError, type AuthenticateFns } from \"./authenticate\"\nimport type { AuthServerConfig, LoggerPort } from \"./ports\"\nimport type { AuthSessionStorage } from \"./session\"\n\n/** createAuthenticateInLoader が返すローダーガード関数。 */\nexport type AuthenticateInLoader = ReturnType<typeof createAuthenticateInLoader>\n\n/**\n * Builds the per-request loader guard wired to injected ports.\n * 注入ポートに結線した毎リクエストローダーガードを生成する。\n */\nexport const createAuthenticateInLoader = (config: AuthServerConfig, storage: AuthSessionStorage, authFns: AuthenticateFns, logger: LoggerPort) => {\n // 写真キャッシュ (インスタンススコープ。同時リクエストは pending dedup で 1 クエリに集約)\n const openidPhotoCache = new Map<string, string>()\n const pendingOpenidPhotoQueries = new Map<string, Promise<string>>()\n\n /**\n * Resolves the user photo with cache + in-flight dedup, delegating the query to the port.\n * キャッシュ + 実行中 dedup 付きでユーザー写真を解決する (実クエリはポートへ委譲)。\n */\n const getCachedOpenidPhoto = async (openid: string): Promise<string> => {\n const cached = openidPhotoCache.get(openid)\n if (cached !== undefined) {\n return cached\n }\n\n const pending = pendingOpenidPhotoQueries.get(openid)\n if (pending !== undefined) {\n return pending\n }\n\n const fetchPromise = (async () => {\n try {\n const photo = config.getUserPhoto ? await config.getUserPhoto(openid) : \"\"\n openidPhotoCache.set(openid, photo)\n return photo\n } catch (error) {\n openidPhotoCache.delete(openid)\n throw error\n } finally {\n pendingOpenidPhotoQueries.delete(openid)\n }\n })()\n\n pendingOpenidPhotoQueries.set(openid, fetchPromise)\n return fetchPromise\n }\n\n /**\n * Handles authentication within loader requests by refreshing session tokens when needed.\n * ローダーリクエスト内で必要に応じてセッショントークンを更新する認証処理。\n *\n * @param request - リクエストオブジェクト。\n * @param options - 認証オプション。`failureRedirect: null` でリダイレクトを明示的にスキップ。\n * @returns 認証済みユーザープロファイルとオプショナルな Set-Cookie 値。\n */\n const authenticateInLoader = async (request: Request, options: { failureRedirect: string } | { failureRedirect: null }): Promise<{ user?: MyGoogleProfile; cookie?: string }> => {\n // DB 接続プールが確実にオープンするのを待ってから処理を開始する (E2E 等でのコールドスタート競合回避)\n if (config.warmup) {\n await config.warmup()\n }\n\n const { failureRedirect } = options\n const session = await storage.getSession(request)\n const currentUser = await storage.getSessionUser(request)\n\n if (currentUser === undefined) {\n if (failureRedirect === null) {\n return {}\n }\n throw redirect(failureRedirect)\n }\n\n try {\n // トークンの有効期限を確認し、必要に応じて更新\n const authenticatedTokens = await authFns.authenticate(request, session, {\n failureRedirect,\n })\n\n // 認証ペイロードの構築\n const authenticationPayload: AuthenticationPayload = {\n accessToken: authenticatedTokens.accessToken,\n refreshToken: authenticatedTokens.refreshToken,\n expirationDateMs: authenticatedTokens.expirationDateMs,\n provider: currentUser.provider,\n }\n // セッション内のユーザ情報\n const updatedSessionUser: SessionProfile = {\n id: currentUser.id,\n displayName: currentUser.displayName,\n name: currentUser.name,\n emails: currentUser.emails,\n ...authenticationPayload,\n role: currentUser.role,\n }\n\n // ユーザー写真をキャッシュまたはポートから取得\n const photo = await getCachedOpenidPhoto(currentUser.id)\n\n // ユーザープロファイル情報\n const updatedUser: MyGoogleProfile = {\n ...currentUser,\n ...authenticationPayload,\n photo,\n }\n\n // トークンが更新 (リフレッシュ) された場合のみ、セッションを保存して Cookie ヘッダーを設定する\n const isRefreshed = authenticatedTokens.accessToken !== currentUser.accessToken\n const cookie = isRefreshed ? await storage.saveSession(request, updatedSessionUser) : undefined\n\n return { user: updatedUser, cookie }\n } catch (error) {\n // failureRedirect: null の契約 (リダイレクトせず未認証として返す) を守る。\n // 以前は authenticate() が投げた redirect Response を下の Location 判定が\n // 無条件に再スローしており、null 指定でも /auth/login へ飛んでいた。\n if (error instanceof AuthRequiredError) {\n return error.setCookie !== undefined ? { cookie: error.setCookie } : {}\n }\n if (error instanceof Response && error.headers.get(\"Location\")) {\n throw error\n }\n logger.error(\"Error in authenticateInLoader:\", error)\n // 認証エラー時はセッションを破棄してリダイレクト\n if (failureRedirect !== null) {\n // セッション破棄のみを try で保護する。redirect の throw を try 内に置くと\n // 同じ catch(destroyError) に捕捉されて Set-Cookie 無しの fallback に差し替わってしまうため、\n // throw redirect は必ず try の外で行う。\n let destroyedCookie: string | undefined\n try {\n destroyedCookie = await storage.destroySession(session)\n } catch (destroyError) {\n logger.error(\"Failed to destroy session during auth error:\", destroyError)\n }\n // 破棄に成功していれば Set-Cookie を付与し、失敗してもとにかくリダイレクトは試みる\n throw destroyedCookie !== undefined ? redirect(failureRedirect, { headers: { \"Set-Cookie\": destroyedCookie } }) : redirect(failureRedirect)\n }\n throw error\n }\n }\n\n return authenticateInLoader\n}\n","/**\n * @module google-strategy\n * @description Google OAuth authenticator factory (remix-auth v4 + local GoogleStrategy).\n * Google OAuth 認証器ファクトリ (remix-auth v4 + 同梱の GoogleStrategy)。\n *\n * 原本 (quants-react-router auth.server.ts) の verify コールバックを挙動保存で移植。\n * env 読取り・backend HTTP・allowlist・エラーメッセージはすべてポート/設定注入に置き換えた。\n * 検証順序 (Google プロフィール検証 → backend verify → allowlist → backend signup)、\n * リダイレクト先 (`<loginPath>?error=<encoded>`)、エラーメッセージ文言は原本と等価。\n */\nimport type { MyGoogleProfile } from \"@aiquants/auth-core\"\nimport { type AuthMessages, resolveAuthMessages } from \"@aiquants/auth-core\"\nimport { redirect } from \"react-router\"\nimport { Authenticator } from \"remix-auth\"\nimport { GoogleStrategy } from \"./google-strategy-impl\"\nimport { defaultLogger } from \"./logger\"\nimport type { AuthServerConfig } from \"./ports\"\n\ninterface GoogleToken {\n id_token?: string\n access_token?: string\n refresh_token?: string\n expires_in?: number\n scope?: string\n}\n\n/**\n * Builds a remix-auth Authenticator with the Google strategy wired to injected ports.\n * 注入ポートに結線した Google ストラテジー付き Authenticator を生成する。\n */\nexport const createGoogleAuthenticator = (config: AuthServerConfig): Authenticator<MyGoogleProfile> => {\n const logger = config.logger ?? defaultLogger\n const messages: AuthMessages = resolveAuthMessages(config.messages)\n const loginPath = config.loginPath ?? \"/auth/login\"\n\n const googleStrategy = new GoogleStrategy<MyGoogleProfile>(\n {\n clientId: config.google.clientId,\n clientSecret: config.google.clientSecret,\n redirectURI: config.google.redirectURI,\n scopes: config.google.scopes,\n accessType: config.google.accessType ?? \"offline\",\n includeGrantedScopes: config.google.includeGrantedScopes ?? true,\n prompt: config.google.prompt ?? \"consent\",\n },\n /**\n * Google 認証後のコールバック。Google のトークンとプロフィールを検証し、\n * backend verify → allowlist → backend signup を経て MyGoogleProfile を返す。\n * 失敗時は `<loginPath>?error=<encoded>` へのリダイレクト Response を throw する。\n */\n async ({ request: _request, tokens }) => {\n const { access_token: accessToken, refresh_token: refreshToken, expires_in: expiresIn, scope, id_token: _idToken } = tokens.data as GoogleToken\n const {\n _json: { sub, email, picture, hd: _hd, email_verified },\n photos: _photos,\n ...profile\n } = await GoogleStrategy.userProfile(tokens)\n const {\n id,\n displayName,\n name: { familyName: _familyName, givenName: _givenName },\n emails: _emails,\n } = profile\n const provider = \"google\"\n\n // first verify (トークン/プロフィール欠落は Google 認証失敗として扱う)\n if (\n accessToken === undefined ||\n refreshToken === undefined ||\n expiresIn === undefined ||\n scope === undefined ||\n id === undefined ||\n displayName === undefined ||\n sub === undefined ||\n email === undefined ||\n email_verified === undefined\n // hd === undefined // Google Workspace などの組織制限をする場合は有効化\n ) {\n if (email_verified === false) {\n const errorMessage = encodeURIComponent(messages.emailNotVerified)\n throw redirect(`${loginPath}?error=${errorMessage}`)\n }\n const errorMessage = encodeURIComponent(messages.googleFailed)\n throw redirect(`${loginPath}?error=${errorMessage}`)\n }\n\n logger.info(\"Authenticated on Google: \", profile.displayName)\n\n // Backend Authentication\n try {\n // backendAuth ポート未指定時は verify/signup をスキップ (フロント単独運用)\n if (config.backendAuth) {\n const ok = await config.backendAuth.verify(provider, accessToken)\n\n if (!ok) {\n logger.error(\"Backend verification returned false for user:\", profile.displayName)\n const errorMessage = encodeURIComponent(messages.backendFailed)\n throw redirect(`${loginPath}?error=${errorMessage}`)\n }\n\n logger.info(\"Authenticated on Backend: \", profile.displayName)\n }\n\n // メールアドレスまたはドメインベースでのアクセス制限チェック (論理和は allowlist 実装側)\n // ⚠️ `await` 必須。非同期実装を await せずに評価すると Promise が truthy 判定され\n // 許可判定を素通りする (fail-open)。この await は spec で門番されている。\n const userEmail = profile.emails[0]?.value\n if (!(await config.isEmailAllowed(userEmail))) {\n const error = new Error(\"You are not allowed to access this application\")\n logger.error(error)\n throw error\n }\n\n // 無効化済みアカウントはログインさせない (allowlist とは別軸の判定)\n if (config.isUserActive && !(await config.isUserActive(profile))) {\n const error = new Error(\"You are not allowed to access this application\")\n logger.error(\"Deactivated account attempted to sign in:\", userEmail)\n throw error\n }\n\n // バックエンドでのサインアップ処理 (毎ログイン実行の挙動を保存)\n if (config.backendAuth) {\n await config.backendAuth.signup(provider, accessToken)\n }\n\n logger.info(\"Authenticated complete: \", profile.displayName)\n\n // 認証ペイロードの構築\n const expirationDateMs = Date.now() + expiresIn * 1000\n const authenticationPayload = {\n accessToken: accessToken,\n refreshToken: refreshToken,\n expirationDateMs: expirationDateMs,\n provider: provider,\n }\n // ユーザープロファイル情報\n const user: MyGoogleProfile = {\n ...profile,\n ...authenticationPayload,\n photo: picture,\n }\n return user\n } catch (error: unknown) {\n let loggable = error\n if (typeof error === \"object\" && error !== null && \"cause\" in error) {\n // cause があればそれを優先してログに残す (falsy なら元のエラー)\n loggable = (error as { cause: unknown }).cause || error\n }\n logger.error(\"Error during backend authentication or subsequent processing:\", loggable)\n\n let friendlyErrorMessage = messages.unexpected\n let conditionMet = false\n\n // ネットワーク到達不可 (getaddrinfo ENOTFOUND) の判定\n if (typeof error === \"object\" && error !== null && \"cause\" in error) {\n const cause = (error as { cause: unknown }).cause\n if (cause && typeof cause === \"object\" && \"message\" in cause && typeof (cause as { message: string }).message === \"string\" && (cause as { message: string }).message.includes(\"getaddrinfo ENOTFOUND\")) {\n friendlyErrorMessage = messages.backendUnreachable\n conditionMet = true\n }\n }\n\n // allowlist 拒否の判定\n if (!conditionMet && error instanceof Error && error.message.includes(\"You are not allowed to access this application\")) {\n friendlyErrorMessage = messages.notAllowed\n conditionMet = true\n }\n\n // 既にリダイレクト Response が throw されている場合はそのまま再 throw\n if (!conditionMet && error instanceof Response && error.headers.get(\"Location\")?.startsWith(loginPath)) {\n throw error\n }\n\n const encodedErrorMessage = encodeURIComponent(friendlyErrorMessage)\n throw redirect(`${loginPath}?error=${encodedErrorMessage}`)\n }\n },\n )\n\n const authenticator = new Authenticator<MyGoogleProfile>()\n authenticator.use(googleStrategy)\n return authenticator\n}\n","/**\n * @module google-strategy-impl\n * @description Self-contained Google OAuth2 strategy (ported from @coji/remix-auth-google).\n * 自己完結型の Google OAuth2 ストラテジー (@coji/remix-auth-google から移植)。\n *\n * @coji/remix-auth-google@4.2.0 の実装 (68 行) を挙動保存で移植し、外部依存を除去する。\n * OAuth2 の本体機構 (PKCE / state / トークン交換) は継承元の remix-auth-oauth2 が担うため\n * 移植対象ではない。ここに置くのは Google 固有の設定 (エンドポイント URL・既定スコープ)、\n * 認可パラメータの上書き (access_type / include_granted_scopes / prompt / hd / login_hint)、\n * および userinfo (v3) からプロファイルを取得する静的メソッドのみ。\n *\n * 既定スコープ・URL・プロファイル整形 (sub→id、picture→photos、_json=raw) は原本と byte 一致。\n */\nimport { OAuth2Strategy } from \"remix-auth-oauth2\"\n\n/** Google の既定スコープ (原本と同一。フル URL 形式)。 */\nexport const GoogleStrategyDefaultScopes = [\"openid\", \"https://www.googleapis.com/auth/userinfo.profile\", \"https://www.googleapis.com/auth/userinfo.email\"]\n\n/** ストラテジー名 (remix-auth の Authenticator 登録キー)。 */\nexport const GoogleStrategyDefaultName = \"google\"\n\n// userinfo エンドポイント (v3)。プロファイル取得のみに用いる\nconst userInfoURL = \"https://www.googleapis.com/oauth2/v3/userinfo\"\n\n/** @see https://developers.google.com/identity/protocols/oauth2/scopes */\nexport type GoogleScope = string\n\n/** GoogleStrategy のコンストラクタオプション (原本 @coji と同一)。 */\nexport type GoogleStrategyOptions = {\n clientId: string\n clientSecret: string\n redirectURI: string\n /** @default \"openid profile email\" 相当 (GoogleStrategyDefaultScopes)。 */\n scopes?: GoogleScope[]\n accessType?: \"online\" | \"offline\"\n includeGrantedScopes?: boolean\n prompt?: \"none\" | \"consent\" | \"select_account\"\n hd?: string\n loginHint?: string\n}\n\n/** userinfo から取得・整形した Google プロファイル (原本 GoogleProfile と同一形状)。 */\nexport type GoogleProfile = {\n id: string\n displayName: string\n name: {\n familyName: string\n givenName: string\n }\n emails: [{ value: string }]\n photos: [{ value: string }]\n _json: {\n sub: string\n name: string\n given_name: string\n family_name: string\n picture: string\n locale: string\n email: string\n email_verified: boolean\n hd: string\n }\n}\n\n// userProfile が必要とする最小のトークン形状 (arctic の OAuth2Tokens の部分集合)。\n// 直接 arctic へ依存しないための構造的型。実際に呼ぶのは accessToken() のみ。\ntype OAuth2AccessTokenSource = { accessToken(): string }\n\n/**\n * Google OAuth2 strategy for remix-auth (extends remix-auth-oauth2's OAuth2Strategy).\n * remix-auth 用の Google OAuth2 ストラテジー (remix-auth-oauth2 の OAuth2Strategy を継承)。\n */\nexport class GoogleStrategy<User> extends OAuth2Strategy<User> {\n name = GoogleStrategyDefaultName\n private readonly scopes: GoogleScope[]\n private readonly accessType: \"online\" | \"offline\"\n private readonly prompt?: \"none\" | \"consent\" | \"select_account\"\n private readonly includeGrantedScopes: boolean\n private readonly hd?: string\n private readonly loginHint?: string\n\n constructor({ clientId, clientSecret, redirectURI, scopes, accessType, includeGrantedScopes, prompt, hd, loginHint }: GoogleStrategyOptions, verify: OAuth2Strategy<User>[\"verify\"]) {\n const scopeList = scopes ?? GoogleStrategyDefaultScopes\n super(\n {\n clientId,\n clientSecret,\n redirectURI,\n authorizationEndpoint: \"https://accounts.google.com/o/oauth2/v2/auth\",\n tokenEndpoint: \"https://oauth2.googleapis.com/token\",\n scopes: scopeList,\n },\n verify,\n )\n this.scopes = scopeList\n this.accessType = accessType ?? \"online\"\n this.includeGrantedScopes = includeGrantedScopes ?? false\n this.prompt = prompt\n this.hd = hd\n this.loginHint = loginHint\n }\n\n /**\n * Adds Google-specific authorization parameters to the redirect URL.\n * 認可リダイレクト URL へ Google 固有パラメータを付与する。\n */\n protected authorizationParams(params: URLSearchParams, _request?: Request): URLSearchParams {\n params.set(\"access_type\", this.accessType)\n params.set(\"include_granted_scopes\", String(this.includeGrantedScopes))\n if (this.scopes && this.scopes.length > 0) {\n params.set(\"scope\", this.scopes.join(\" \"))\n }\n if (this.prompt) {\n params.set(\"prompt\", this.prompt)\n }\n if (this.hd) {\n params.set(\"hd\", this.hd)\n }\n if (this.loginHint) {\n params.set(\"login_hint\", this.loginHint)\n }\n return params\n }\n\n /**\n * Fetches the Google user profile from the userinfo (v3) endpoint.\n * userinfo (v3) エンドポイントから Google ユーザープロファイルを取得する。\n *\n * @throws プロファイル取得に失敗した場合 (レスポンスが ok でない)。\n */\n static async userProfile(tokens: OAuth2AccessTokenSource): Promise<GoogleProfile> {\n const response = await fetch(userInfoURL, {\n headers: {\n Authorization: `Bearer ${tokens.accessToken()}`,\n },\n })\n if (!response.ok) {\n throw new Error(`Failed to fetch user profile: ${response.statusText}`)\n }\n const raw = await response.json()\n const profile: GoogleProfile = {\n id: raw.sub,\n displayName: raw.name,\n name: {\n familyName: raw.family_name,\n givenName: raw.given_name,\n },\n emails: [{ value: raw.email }],\n photos: [{ value: raw.picture }],\n _json: raw,\n }\n return profile\n }\n}\n","/**\n * @module logger\n * @description Default console-backed logger used when no logger port is injected.\n * ロガーポート未注入時に使う console ベースの既定ロガー。\n */\nimport type { LoggerPort } from \"./ports\"\n\n/** console ベースの既定ロガー (ライブラリ既定のフォールバックとして意図的に console を使用)。 */\nexport const defaultLogger: LoggerPort = {\n // biome-ignore lint/suspicious/noConsole: ロガー未注入時の意図的な console フォールバック\n info: (...args: unknown[]) => console.info(\"[Auth]\", ...args),\n warn: (...args: unknown[]) => console.warn(\"[Auth]\", ...args),\n error: (...args: unknown[]) => console.error(\"[Auth]\", ...args),\n}\n","/**\n * @module handlers\n * @description Route handler factories for login / logout / Google callback.\n * login / logout / Google コールバックのルートハンドラ工場。\n *\n * 原本 (quants-react-router routes/_auth+ の 4 ルート + features/logoutAndRedirect.ts) の\n * サーバー側ロジックを挙動保存で移植。UI (JSX) はアプリ側に残す。\n * コールバックのセッション保存形状 (SessionProfile、photo/_json/photos を含まない) は\n * cookie byte 互換のため厳密に保存する。\n */\nimport type { AuthenticationPayload, MyGoogleProfile, SessionProfile } from \"@aiquants/auth-core\"\nimport { redirect } from \"react-router\"\nimport type { Authenticator } from \"remix-auth\"\nimport type { AuthServerConfig, LoggerPort } from \"./ports\"\nimport type { AuthSessionStorage } from \"./session\"\n\n/** createAuthHandlers が返すハンドラ束。 */\nexport type AuthHandlers = ReturnType<typeof createAuthHandlers>\n\n/**\n * Builds loader/action handlers for the auth routes.\n * 認証ルート用の loader/action ハンドラ群を生成する。\n */\nexport const createAuthHandlers = (config: AuthServerConfig, storage: AuthSessionStorage, authenticator: Authenticator<MyGoogleProfile>, logger: LoggerPort) => {\n const loginPath = config.loginPath ?? \"/auth/login\"\n const postLoginRedirect = config.postLoginRedirect ?? \"/\"\n const postLogoutRedirect = config.postLogoutRedirect ?? \"/\"\n\n /**\n * Login-page loader: redirects authenticated users, passes the error message through.\n * ログインページ loader。認証済みならリダイレクトし、エラーメッセージを data として返す。\n */\n const loginLoader = async ({ request }: { request: Request }): Promise<{ error: string | null }> => {\n const user = await storage.getSessionUser(request)\n if (user) {\n // 認証済みならトップページにリダイレクト\n throw redirect(postLoginRedirect)\n }\n const url = new URL(request.url)\n const error = url.searchParams.get(\"error\")\n return { error } // エラーメッセージを loader data として渡す\n }\n\n /**\n * Login action: starts the Google OAuth flow for the \"Sign In with Google\" form action.\n * ログイン action。\"Sign In with Google\" フォーム action で Google OAuth フローを開始する。\n */\n const loginAction = async ({ request }: { request: Request }): Promise<MyGoogleProfile | null> => {\n const formData = await request.clone().formData()\n const action = String(formData.get(\"_action\"))\n\n let user: MyGoogleProfile | null = null\n switch (action) {\n case \"Sign In with Google\":\n user = await authenticator.authenticate(\"google\", request)\n break\n default:\n user = null\n }\n return user\n }\n\n /**\n * Destroys the session and returns the Set-Cookie value (logout primitive).\n * セッションを破棄して Set-Cookie 値を返す (ログアウトの基本操作)。\n */\n const logout = async (request: Request) => {\n return await storage.logout(request)\n }\n\n /**\n * Logs out and redirects with the destroyed-session cookie attached.\n * ログアウトし、破棄済みセッション Cookie を付与してリダイレクトする。\n */\n const logoutAndRedirect = async (request: Request) => {\n const cookie = await logout(request)\n const headers = new Headers()\n headers.append(\"Set-Cookie\", cookie)\n return redirect(postLogoutRedirect, { headers })\n }\n\n /**\n * Logout-route loader: performs logout and throws the redirect.\n * ログアウトルート loader。ログアウトを実行してリダイレクトを throw する。\n */\n const logoutLoader = async ({ request }: { request: Request }): Promise<never> => {\n throw await logoutAndRedirect(request)\n }\n\n /**\n * Google OAuth callback loader: authenticates, saves the session and redirects home.\n * Google OAuth コールバック loader。認証してセッションを保存し、ホームへリダイレクトする。\n */\n const googleCallbackLoader = async ({ request }: { request: Request }): Promise<never> => {\n try {\n const googleProfile = await authenticator.authenticate(\"google\", request)\n\n // 既存のセッションから前回のユーザー情報を取得して refresh_token を保護する\n const existingSession = await storage.getSession(request)\n const existingUser = existingSession.get(storage.sessionKey) as SessionProfile | undefined\n const previousRefreshToken = existingUser?.refreshToken\n\n // 認証ペイロードの構築\n const authenticationPayload: AuthenticationPayload = {\n accessToken: googleProfile.accessToken,\n refreshToken: googleProfile.refreshToken || previousRefreshToken,\n expirationDateMs: googleProfile.expirationDateMs,\n provider: googleProfile.provider,\n }\n\n // セッション内のユーザ情報 (photo/_json/photos を含まない byte 互換形状)\n const user: SessionProfile = {\n id: googleProfile.id,\n displayName: googleProfile.displayName,\n name: googleProfile.name,\n emails: googleProfile.emails,\n ...authenticationPayload,\n role: googleProfile.role,\n }\n\n // 認証後のユーザ情報をセッションに保存\n const cookie = await storage.saveSession(request, user)\n\n // ホームへリダイレクト\n throw redirect(postLoginRedirect, { headers: { \"Set-Cookie\": cookie } })\n } catch (error) {\n // authenticator.authenticate は認証失敗時に Response か Error を throw する。\n // access_denied のような OAuth エラーでは Error の可能性があるため、\n // どちらの場合でもログインページへのリダイレクトが安全 (Response はそのまま再 throw)。\n if (error instanceof Response) {\n throw error\n }\n logger.error(\"Authentication error:\", error)\n throw redirect(loginPath)\n }\n }\n\n return { loginLoader, loginAction, logoutLoader, logout, logoutAndRedirect, googleCallbackLoader }\n}\n","/**\n * @module session\n * @description Cookie session storage factory with a DI test backdoor.\n * DI テストバックドア付きの cookie セッションストレージファクトリ。\n *\n * 原本 (quants-react-router session.server.ts) を挙動保存で移植。相違点は\n * (1) module-level の env throw をファクトリ引数検証へ移した、\n * (2) テストバックドア (mock_user cookie) をハードコードから `mockUser` ポート注入に変えた、\n * の 2 点のみ。cookie 形状 (`__session`/httpOnly/lax/30d、キー \"user\") は byte 互換。\n */\nimport type { GoogleProfileBase, SessionProfile } from \"@aiquants/auth-core\"\nimport { createCookieSessionStorage, redirect } from \"react-router\"\nimport type { MockUserPort, SessionConfig } from \"./ports\"\n\n/** createAuthSessionStorage が返すセッション操作束。 */\nexport type AuthSessionStorage = ReturnType<typeof createAuthSessionStorage>\n\n/**\n * Builds the cookie session storage and session helpers.\n * cookie セッションストレージとセッションヘルパー群を生成する。\n *\n * @throws secrets が空の場合 (fail-closed の設定ガード)。\n */\nexport const createAuthSessionStorage = (config: SessionConfig, options?: { mockUser?: MockUserPort; loginPath?: string; postLoginRedirect?: string; isUserActive?: (profile: GoogleProfileBase) => boolean | Promise<boolean> }) => {\n // ファクトリ実行時検証 (import 時副作用の排除)\n if (!config.secrets || config.secrets.length === 0 || config.secrets.some((s) => !s)) {\n throw new Error(\"REMIX_SESSION_SECRET must be set\")\n }\n\n const sessionKey = config.sessionKey ?? \"user\"\n const maxAge = config.maxAge ?? 60 * 60 * 24 * 30\n const loginPath = options?.loginPath ?? \"/auth/login\"\n const postLoginRedirect = options?.postLoginRedirect ?? \"/\"\n const mockUser = options?.mockUser\n const isUserActive = options?.isUserActive\n\n const sessionStorage = createCookieSessionStorage({\n cookie: {\n name: config.cookieName ?? \"__session\",\n httpOnly: true, // for security reasons, make this cookie http only\n path: \"/\", // remember to add this so the cookie will work in all routes\n sameSite: \"lax\", // this helps with CSRF\n secrets: config.secrets,\n secure: config.secure ?? process.env.NODE_ENV === \"production\",\n maxAge,\n },\n })\n\n /**\n * Reads the session from the request cookie.\n * リクエストの Cookie からセッションを取得する。\n */\n const getSession = async (request: Request) => {\n return await sessionStorage.getSession(request.headers.get(\"Cookie\"))\n }\n\n /**\n * Reads the session user, honoring the injected test backdoor when enabled.\n * セッションからユーザー情報を取得する (注入されたテストバックドアが有効ならそれを返す)。\n */\n const getSessionUser = async (request: Request): Promise<SessionProfile | undefined> => {\n // テストバックドア (ポート注入時のみ有効。判定条件はアプリ側の enabled に委譲)\n if (mockUser?.enabled(request)) {\n return mockUser.resolve(request)\n }\n\n const session = await getSession(request)\n const user = session.get(sessionKey) as SessionProfile | undefined\n if (user === undefined) return undefined\n // 無効化済みアカウントは「未認証」として扱う。これにより、既に発行済みのセッション Cookie を\n // 持っていてもガードは通らず、無効化がその場で全ルートへ波及する。\n if (isUserActive && !(await isUserActive(user))) return undefined\n return user\n }\n\n /**\n * Saves the user into the session and returns the Set-Cookie value.\n * セッションへユーザー情報を保存し、Set-Cookie 値を返す。\n */\n const saveSession = async (request: Request, user: SessionProfile) => {\n const session = await getSession(request)\n session.set(sessionKey, user)\n return await sessionStorage.commitSession(session)\n }\n\n /**\n * Authenticated-user guard: redirects to the login page when absent.\n * 認証ユーザーガード。未認証ならログインページへリダイレクトする。\n */\n const requireUser = async (request: Request) => {\n const sessionUser = await getSessionUser(request)\n if (sessionUser === undefined) {\n throw redirect(loginPath)\n }\n return sessionUser as SessionProfile\n }\n\n /**\n * Admin-user guard: redirects to the app root when the role is not \"admin\".\n * 管理者ユーザーガード。role が \"admin\" でなければルートへリダイレクトする。\n */\n const requireAdminUser = async (request: Request) => {\n const sessionUser = await requireUser(request)\n if (sessionUser?.role !== \"admin\") {\n throw redirect(postLoginRedirect)\n }\n return sessionUser\n }\n\n /**\n * Destroys the session and returns the Set-Cookie value (logout primitive).\n * セッションを破棄して Set-Cookie 値を返す (ログアウトの基本操作)。\n */\n const logout = async (request: Request) => {\n const session = await getSession(request)\n const cookie = await sessionStorage.destroySession(session)\n return cookie\n }\n\n return {\n sessionStorage,\n sessionKey,\n getSession,\n getSessionUser,\n saveSession,\n requireUser,\n requireAdminUser,\n logout,\n commitSession: sessionStorage.commitSession,\n destroySession: sessionStorage.destroySession,\n }\n}\n","/**\n * @module auth-server\n * @description Aggregate factory bundling session, authenticator, guards and handlers.\n * セッション・認証器・ガード・ハンドラを束ねる一括ファクトリ。\n *\n * daily-report の createDailyReportServer と同型の単一結線点。アプリは\n * `createAuthServer(config)` を 1 回呼び、返り値を各 `*.server.ts` shim から再輸出する。\n */\nimport { createAuthenticate } from \"./authenticate\"\nimport { createAuthenticateInLoader } from \"./authenticate-in-loader\"\nimport { createGoogleAuthenticator } from \"./google-strategy\"\nimport { createAuthHandlers } from \"./handlers\"\nimport { defaultLogger } from \"./logger\"\nimport type { AuthServerConfig } from \"./ports\"\nimport { createAuthSessionStorage } from \"./session\"\n\n/** createAuthServer が返す認証サーバー束。 */\nexport type AuthServer = ReturnType<typeof createAuthServer>\n\n/**\n * Builds the full auth server bundle from a single config.\n * 単一の設定から認証サーバー束一式を生成する。\n */\nexport const createAuthServer = (config: AuthServerConfig) => {\n const logger = config.logger ?? defaultLogger\n\n // セッションストレージ + ヘルパー (テストバックドアはポート委譲)\n const storage = createAuthSessionStorage(config.session, {\n mockUser: config.mockUser,\n loginPath: config.loginPath,\n postLoginRedirect: config.postLoginRedirect,\n isUserActive: config.isUserActive,\n })\n\n // Google 認証器 (verify コールバックはポート結線済み)\n const authenticator = createGoogleAuthenticator(config)\n\n // トークンリフレッシュ\n const authFns = createAuthenticate(config, storage, logger)\n\n // 毎リクエストのローダーガード (写真キャッシュ内包)\n const authenticateInLoader = createAuthenticateInLoader(config, storage, authFns, logger)\n\n // ルートハンドラ (login/logout/callback)\n const handlers = createAuthHandlers(config, storage, authenticator, logger)\n\n return {\n // 認証器\n authenticator,\n // セッション操作/トークンガードの内部束 (テストでの失敗注入等に使う。プロパティ差し替えは\n // authenticate / authenticateInLoader の内部呼び出しへ即時反映される)\n storage,\n authFns,\n sessionStorage: storage.sessionStorage,\n getSession: storage.getSession,\n getSessionUser: storage.getSessionUser,\n saveSession: storage.saveSession,\n requireUser: storage.requireUser,\n requireAdminUser: storage.requireAdminUser,\n commitSession: storage.commitSession,\n destroySession: storage.destroySession,\n // トークンガード\n authenticate: authFns.authenticate,\n refreshedAccessToken: authFns.refreshedAccessToken,\n authenticateInLoader,\n // ルートハンドラ\n ...handlers,\n }\n}\n"],"mappings":"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","names":["server_exports","__export","AuthRequiredError","GoogleInvalidGrantError","TokenRefreshError","createAuthHandlers","createAuthServer","createAuthSessionStorage","createAuthenticate","createAuthenticateInLoader","createGoogleAuthenticator","__toCommonJS","import_react_router","TokenRefreshError","message","GoogleInvalidGrantError","AuthRequiredError","setCookie","createAuthenticate","config","storage","logger","loginPath","sessionKey","getRefreshedGoogleAccessToken","session","refreshTokenFromSession","url","body","response","errorData","errorDescription","refreshedTokens","access_token","expires_in","error","description","refreshedAccessToken","provider","request","options","failureRedirect","authFailure","user","accessToken","refreshToken","expirationDateMs","refreshed","refreshError","cookie","import_react_router","createAuthenticateInLoader","config","storage","authFns","logger","openidPhotoCache","pendingOpenidPhotoQueries","getCachedOpenidPhoto","openid","cached","pending","fetchPromise","photo","error","request","options","failureRedirect","session","currentUser","authenticatedTokens","authenticationPayload","updatedSessionUser","updatedUser","cookie","AuthRequiredError","destroyedCookie","destroyError","import_auth_core","import_react_router","import_remix_auth","import_remix_auth_oauth2","GoogleStrategyDefaultScopes","GoogleStrategyDefaultName","userInfoURL","GoogleStrategy","clientId","clientSecret","redirectURI","scopes","accessType","includeGrantedScopes","prompt","hd","loginHint","verify","scopeList","params","_request","tokens","response","raw","defaultLogger","args","createGoogleAuthenticator","config","logger","defaultLogger","messages","loginPath","googleStrategy","GoogleStrategy","_request","tokens","accessToken","refreshToken","expiresIn","scope","_idToken","sub","email","picture","_hd","email_verified","_photos","profile","id","displayName","_familyName","_givenName","_emails","provider","errorMessage","userEmail","error","expirationDateMs","loggable","friendlyErrorMessage","conditionMet","cause","encodedErrorMessage","authenticator","import_react_router","createAuthHandlers","config","storage","authenticator","logger","loginPath","postLoginRedirect","postLogoutRedirect","loginLoader","request","loginAction","formData","action","user","logout","logoutAndRedirect","cookie","headers","googleProfile","previousRefreshToken","authenticationPayload","error","import_react_router","createAuthSessionStorage","config","options","s","sessionKey","maxAge","loginPath","postLoginRedirect","mockUser","isUserActive","sessionStorage","getSession","request","getSessionUser","user","saveSession","session","requireUser","sessionUser","createAuthServer","config","logger","defaultLogger","storage","createAuthSessionStorage","authenticator","createGoogleAuthenticator","authFns","createAuthenticate","authenticateInLoader","createAuthenticateInLoader","handlers","createAuthHandlers"]}
package/dist/server.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import{redirect as C}from"react-router";var P=class extends Error{constructor(a){super(a),this.name="TokenRefreshError"}},R=class extends P{constructor(){super("Google refresh token invalid or revoked"),this.name="GoogleInvalidGrantError"}},I=(e,a,o)=>{let l=e.loginPath??"/auth/login",c=a.sessionKey,m=async n=>{let u=n.get(c)?.refreshToken;if(!u)throw new Error("Refresh token not found in session");try{let h="https://www.googleapis.com/oauth2/v4/token",i={client_id:e.google.clientId,client_secret:e.google.clientSecret,grant_type:"refresh_token",refresh_token:u},t=await fetch(h,{headers:{"Content-Type":"application/json"},method:"POST",body:JSON.stringify(i)});if(!t.ok){let p=await t.json().catch(()=>{});if(t.status===400&&typeof p=="object"&&!!p&&"error"in p&&p.error==="invalid_grant")throw new R;let S=typeof p=="object"&&p&&"error_description"in p?String(p.error_description):"Unknown error";throw new P(`Google token refresh failed: ${t.status} ${S}`)}let r=await t.json(),{access_token:s,expires_in:g}=r;if(!(s&&g))throw o.error("Invalid token response from Google:",r),new Error("Invalid token response from Google");return{accessToken:s,refreshToken:u,expirationDateMs:Number(Date.now())+g*1e3}}catch(h){if(h instanceof P)throw h;let i=h instanceof Error?h.message:"Unknown error";throw new P(`Failed to refresh Google access token: ${i}`)}},y=async n=>{let u=n.get(c).provider;if(!u)throw new Error("Provider not found in session");if(u==="google")return await m(n);throw new Error("Provider not supported")};return{authenticate:async(n,d,u)=>{let{failureRedirect:h}=u||{};try{if(e.mockUser?.enabled(n))return{accessToken:"mock-access-token",refreshToken:"mock-refresh-token",expirationDateMs:Date.now()+36e5};let i=d.get(c);if(!(i?.accessToken&&i.expirationDateMs))throw C(h||l);let{accessToken:t,refreshToken:r,expirationDateMs:s}=i;if(s<Date.now())throw r?new Error("Expired"):C(h||l);return{accessToken:t,refreshToken:r,expirationDateMs:s}}catch(i){if(i instanceof Error&&i.message==="Expired")try{let t=await y(d),r=d.get(c);return d.set(c,{...r,...t}),t}catch(t){if(t instanceof R)o.warn("Google refresh token invalid or revoked");else{let s=t instanceof Error?t.message:"Unknown error";o.error("Failed to refresh token:",s)}let r=await a.destroySession(d);throw C(h||l,{headers:{"Set-Cookie":r}})}throw i}},refreshedAccessToken:y}};import{redirect as N}from"react-router";var q=(e,a,o,l)=>{let c=new Map,m=new Map,y=async n=>{let d=c.get(n);if(d!==void 0)return d;let u=m.get(n);if(u!==void 0)return u;let h=(async()=>{try{let i=e.getUserPhoto?await e.getUserPhoto(n):"";return c.set(n,i),i}catch(i){throw c.delete(n),i}finally{m.delete(n)}})();return m.set(n,h),h};return async(n,d)=>{e.warmup&&await e.warmup();let{failureRedirect:u}=d,h=await a.getSession(n),i=await a.getSessionUser(n);if(i===void 0){if(u===null)return{};throw N(u)}try{let t=await o.authenticate(n,h,{failureRedirect:u??void 0}),r={accessToken:t.accessToken,refreshToken:t.refreshToken,expirationDateMs:t.expirationDateMs,provider:i.provider},s={id:i.id,displayName:i.displayName,name:i.name,emails:i.emails,...r,role:i.role},g=await y(i.id),p={...i,...r,photo:g},S=t.accessToken!==i.accessToken?await a.saveSession(n,s):void 0;return{user:p,cookie:S}}catch(t){if(t instanceof Response&&t.headers.get("Location"))throw t;if(l.error("Error in authenticateInLoader:",t),u!==null){let r;try{r=await a.destroySession(h)}catch(s){l.error("Failed to destroy session during auth error:",s)}throw r!==void 0?N(u,{headers:{"Set-Cookie":r}}):N(u)}throw t}}};import{resolveAuthMessages as Y}from"@aiquants/auth-core";import{redirect as L}from"react-router";import{Authenticator as V}from"remix-auth";import{OAuth2Strategy as H}from"remix-auth-oauth2";var B=["openid","https://www.googleapis.com/auth/userinfo.profile","https://www.googleapis.com/auth/userinfo.email"],K="google",z="https://www.googleapis.com/oauth2/v3/userinfo",U=class extends H{constructor({clientId:o,clientSecret:l,redirectURI:c,scopes:m,accessType:y,includeGrantedScopes:w,prompt:n,hd:d,loginHint:u},h){let i=m??B;super({clientId:o,clientSecret:l,redirectURI:c,authorizationEndpoint:"https://accounts.google.com/o/oauth2/v2/auth",tokenEndpoint:"https://oauth2.googleapis.com/token",scopes:i},h);this.name=K;this.scopes=i,this.accessType=y??"online",this.includeGrantedScopes=w??!1,this.prompt=n,this.hd=d,this.loginHint=u}authorizationParams(o,l){return o.set("access_type",this.accessType),o.set("include_granted_scopes",String(this.includeGrantedScopes)),this.scopes&&this.scopes.length>0&&o.set("scope",this.scopes.join(" ")),this.prompt&&o.set("prompt",this.prompt),this.hd&&o.set("hd",this.hd),this.loginHint&&o.set("login_hint",this.loginHint),o}static async userProfile(o){let l=await fetch(z,{headers:{Authorization:`Bearer ${o.accessToken()}`}});if(!l.ok)throw new Error(`Failed to fetch user profile: ${l.statusText}`);let c=await l.json();return{id:c.sub,displayName:c.name,name:{familyName:c.family_name,givenName:c.given_name},emails:[{value:c.email}],photos:[{value:c.picture}],_json:c}}};var _={info:(...e)=>console.info("[Auth]",...e),warn:(...e)=>console.warn("[Auth]",...e),error:(...e)=>console.error("[Auth]",...e)};var D=e=>{let a=e.logger??_,o=Y(e.messages),l=e.loginPath??"/auth/login",c=new U({clientId:e.google.clientId,clientSecret:e.google.clientSecret,redirectURI:e.google.redirectURI,scopes:e.google.scopes,accessType:e.google.accessType??"offline",includeGrantedScopes:e.google.includeGrantedScopes??!0,prompt:e.google.prompt??"consent"},async({request:y,tokens:w})=>{let{access_token:n,refresh_token:d,expires_in:u,scope:h,id_token:i}=w.data,{_json:{sub:t,email:r,picture:s,hd:g,email_verified:p},photos:T,...S}=await U.userProfile(w),{id:E,displayName:$,name:{familyName:Q,givenName:X},emails:Z}=S,b="google";if(n===void 0||d===void 0||u===void 0||h===void 0||E===void 0||$===void 0||t===void 0||r===void 0||p===void 0){if(p===!1){let v=encodeURIComponent(o.emailNotVerified);throw L(`${l}?error=${v}`)}let f=encodeURIComponent(o.googleFailed);throw L(`${l}?error=${f}`)}a.info("Authenticated on Google: ",S.displayName);try{if(e.backendAuth){if(!await e.backendAuth.verify(b,n)){a.error("Backend verification returned false for user:",S.displayName);let A=encodeURIComponent(o.backendFailed);throw L(`${l}?error=${A}`)}a.info("Authenticated on Backend: ",S.displayName)}let f=S.emails[0]?.value;if(!await e.isEmailAllowed(f)){let k=new Error("You are not allowed to access this application");throw a.error(k),k}if(e.isUserActive&&!await e.isUserActive(S)){let k=new Error("You are not allowed to access this application");throw a.error("Deactivated account attempted to sign in:",f),k}e.backendAuth&&await e.backendAuth.signup(b,n),a.info("Authenticated complete: ",S.displayName);let v=Date.now()+u*1e3;return{...S,...{accessToken:n,refreshToken:d,expirationDateMs:v,provider:b},photo:s}}catch(f){let v=f;typeof f=="object"&&f!==null&&"cause"in f&&(v=f.cause||f),a.error("Error during backend authentication or subsequent processing:",v);let G=o.unexpected,x=!1;if(typeof f=="object"&&f!==null&&"cause"in f){let A=f.cause;A&&typeof A=="object"&&"message"in A&&typeof A.message=="string"&&A.message.includes("getaddrinfo ENOTFOUND")&&(G=o.backendUnreachable,x=!0)}if(!x&&f instanceof Error&&f.message.includes("You are not allowed to access this application")&&(G=o.notAllowed,x=!0),!x&&f instanceof Response&&f.headers.get("Location")?.startsWith(l))throw f;let k=encodeURIComponent(G);throw L(`${l}?error=${k}`)}}),m=new V;return m.use(c),m};import{redirect as M}from"react-router";var O=(e,a,o,l)=>{let c=e.loginPath??"/auth/login",m=e.postLoginRedirect??"/",y=e.postLogoutRedirect??"/",w=async({request:t})=>{if(await a.getSessionUser(t))throw M(m);return{error:new URL(t.url).searchParams.get("error")}},n=async({request:t})=>{let r=await t.clone().formData(),s=String(r.get("_action")),g=null;return s==="Sign In with Google"?g=await o.authenticate("google",t):g=null,g},d=async t=>await a.logout(t),u=async t=>{let r=await d(t),s=new Headers;return s.append("Set-Cookie",r),M(y,{headers:s})};return{loginLoader:w,loginAction:n,logoutLoader:async({request:t})=>{throw await u(t)},logout:d,logoutAndRedirect:u,googleCallbackLoader:async({request:t})=>{try{let r=await o.authenticate("google",t),p=(await a.getSession(t)).get(a.sessionKey)?.refreshToken,T={accessToken:r.accessToken,refreshToken:r.refreshToken||p,expirationDateMs:r.expirationDateMs,provider:r.provider},S={id:r.id,displayName:r.displayName,name:r.name,emails:r.emails,...T,role:r.role},E=await a.saveSession(t,S);throw M(m,{headers:{"Set-Cookie":E}})}catch(r){throw r instanceof Response?r:(l.error("Authentication error:",r),M(c))}}}};import{createCookieSessionStorage as W,redirect as j}from"react-router";var F=(e,a)=>{if(!e.secrets||e.secrets.length===0||e.secrets.some(s=>!s))throw new Error("REMIX_SESSION_SECRET must be set");let o=e.sessionKey??"user",l=e.maxAge??3600*24*30,c=a?.loginPath??"/auth/login",m=a?.postLoginRedirect??"/",y=a?.mockUser,w=a?.isUserActive,n=W({cookie:{name:e.cookieName??"__session",httpOnly:!0,path:"/",sameSite:"lax",secrets:e.secrets,secure:e.secure??process.env.NODE_ENV==="production",maxAge:l}}),d=async s=>await n.getSession(s.headers.get("Cookie")),u=async s=>{if(y?.enabled(s))return y.resolve(s);let p=(await d(s)).get(o);if(p!==void 0&&!(w&&!await w(p)))return p},h=async(s,g)=>{let p=await d(s);return p.set(o,g),await n.commitSession(p)},i=async s=>{let g=await u(s);if(g===void 0)throw j(c);return g};return{sessionStorage:n,sessionKey:o,getSession:d,getSessionUser:u,saveSession:h,requireUser:i,requireAdminUser:async s=>{let g=await i(s);if(g?.role!=="admin")throw j(m);return g},logout:async s=>{let g=await d(s);return await n.destroySession(g)},commitSession:n.commitSession,destroySession:n.destroySession}};var J=e=>{let a=e.logger??_,o=F(e.session,{mockUser:e.mockUser,loginPath:e.loginPath,postLoginRedirect:e.postLoginRedirect,isUserActive:e.isUserActive}),l=D(e),c=I(e,o,a),m=q(e,o,c,a),y=O(e,o,l,a);return{authenticator:l,storage:o,authFns:c,sessionStorage:o.sessionStorage,getSession:o.getSession,getSessionUser:o.getSessionUser,saveSession:o.saveSession,requireUser:o.requireUser,requireAdminUser:o.requireAdminUser,commitSession:o.commitSession,destroySession:o.destroySession,authenticate:c.authenticate,refreshedAccessToken:c.refreshedAccessToken,authenticateInLoader:m,...y}};export{R as GoogleInvalidGrantError,P as TokenRefreshError,O as createAuthHandlers,J as createAuthServer,F as createAuthSessionStorage,I as createAuthenticate,q as createAuthenticateInLoader,D as createGoogleAuthenticator};
1
+ import{redirect as j}from"react-router";var P=class extends Error{constructor(n){super(n),this.name="TokenRefreshError"}},U=class extends P{constructor(){super("Google refresh token invalid or revoked"),this.name="GoogleInvalidGrantError"}},x=class extends Error{constructor(n){super("Authentication required"),this.name="AuthRequiredError",this.setCookie=n}},I=(e,n,t)=>{let d=e.loginPath??"/auth/login",c=n.sessionKey,m=async i=>{let u=i.get(c)?.refreshToken;if(!u)throw new Error("Refresh token not found in session");try{let p="https://www.googleapis.com/oauth2/v4/token",a={client_id:e.google.clientId,client_secret:e.google.clientSecret,grant_type:"refresh_token",refresh_token:u},o=await fetch(p,{headers:{"Content-Type":"application/json"},method:"POST",body:JSON.stringify(a)});if(!o.ok){let g=await o.json().catch(()=>{});if(o.status===400&&typeof g=="object"&&!!g&&"error"in g&&g.error==="invalid_grant")throw new U;let S=typeof g=="object"&&g&&"error_description"in g?String(g.error_description):"Unknown error";throw new P(`Google token refresh failed: ${o.status} ${S}`)}let r=await o.json(),{access_token:s,expires_in:l}=r;if(!(s&&l))throw t.error("Invalid token response from Google:",r),new Error("Invalid token response from Google");return{accessToken:s,refreshToken:u,expirationDateMs:Number(Date.now())+l*1e3}}catch(p){if(p instanceof P)throw p;let a=p instanceof Error?p.message:"Unknown error";throw new P(`Failed to refresh Google access token: ${a}`)}},y=async i=>{let u=i.get(c).provider;if(!u)throw new Error("Provider not found in session");if(u==="google")return await m(i);throw new Error("Provider not supported")};return{authenticate:async(i,h,u)=>{let{failureRedirect:p}=u||{},a=o=>p===null?new x(o):o!==void 0?j(p||d,{headers:{"Set-Cookie":o}}):j(p||d);try{if(e.mockUser?.enabled(i))return{accessToken:"mock-access-token",refreshToken:"mock-refresh-token",expirationDateMs:Date.now()+36e5};let o=h.get(c);if(!(o?.accessToken&&o.expirationDateMs))throw a();let{accessToken:r,refreshToken:s,expirationDateMs:l}=o;if(l<Date.now())throw s?new Error("Expired"):a();return{accessToken:r,refreshToken:s,expirationDateMs:l}}catch(o){if(o instanceof Error&&o.message==="Expired")try{let r=await y(h),s=h.get(c);return h.set(c,{...s,...r}),r}catch(r){if(r instanceof U)t.warn("Google refresh token invalid or revoked");else{let l=r instanceof Error?r.message:"Unknown error";t.error("Failed to refresh token:",l)}let s=await n.destroySession(h);throw a(s)}throw o}},refreshedAccessToken:y}};import{redirect as q}from"react-router";var N=(e,n,t,d)=>{let c=new Map,m=new Map,y=async i=>{let h=c.get(i);if(h!==void 0)return h;let u=m.get(i);if(u!==void 0)return u;let p=(async()=>{try{let a=e.getUserPhoto?await e.getUserPhoto(i):"";return c.set(i,a),a}catch(a){throw c.delete(i),a}finally{m.delete(i)}})();return m.set(i,p),p};return async(i,h)=>{e.warmup&&await e.warmup();let{failureRedirect:u}=h,p=await n.getSession(i),a=await n.getSessionUser(i);if(a===void 0){if(u===null)return{};throw q(u)}try{let o=await t.authenticate(i,p,{failureRedirect:u}),r={accessToken:o.accessToken,refreshToken:o.refreshToken,expirationDateMs:o.expirationDateMs,provider:a.provider},s={id:a.id,displayName:a.displayName,name:a.name,emails:a.emails,...r,role:a.role},l=await y(a.id),g={...a,...r,photo:l},S=o.accessToken!==a.accessToken?await n.saveSession(i,s):void 0;return{user:g,cookie:S}}catch(o){if(o instanceof x)return o.setCookie!==void 0?{cookie:o.setCookie}:{};if(o instanceof Response&&o.headers.get("Location"))throw o;if(d.error("Error in authenticateInLoader:",o),u!==null){let r;try{r=await n.destroySession(p)}catch(s){d.error("Failed to destroy session during auth error:",s)}throw r!==void 0?q(u,{headers:{"Set-Cookie":r}}):q(u)}throw o}}};import{resolveAuthMessages as V}from"@aiquants/auth-core";import{redirect as E}from"react-router";import{Authenticator as W}from"remix-auth";import{OAuth2Strategy as B}from"remix-auth-oauth2";var K=["openid","https://www.googleapis.com/auth/userinfo.profile","https://www.googleapis.com/auth/userinfo.email"],z="google",Y="https://www.googleapis.com/oauth2/v3/userinfo",T=class extends B{constructor({clientId:t,clientSecret:d,redirectURI:c,scopes:m,accessType:y,includeGrantedScopes:w,prompt:i,hd:h,loginHint:u},p){let a=m??K;super({clientId:t,clientSecret:d,redirectURI:c,authorizationEndpoint:"https://accounts.google.com/o/oauth2/v2/auth",tokenEndpoint:"https://oauth2.googleapis.com/token",scopes:a},p);this.name=z;this.scopes=a,this.accessType=y??"online",this.includeGrantedScopes=w??!1,this.prompt=i,this.hd=h,this.loginHint=u}authorizationParams(t,d){return t.set("access_type",this.accessType),t.set("include_granted_scopes",String(this.includeGrantedScopes)),this.scopes&&this.scopes.length>0&&t.set("scope",this.scopes.join(" ")),this.prompt&&t.set("prompt",this.prompt),this.hd&&t.set("hd",this.hd),this.loginHint&&t.set("login_hint",this.loginHint),t}static async userProfile(t){let d=await fetch(Y,{headers:{Authorization:`Bearer ${t.accessToken()}`}});if(!d.ok)throw new Error(`Failed to fetch user profile: ${d.statusText}`);let c=await d.json();return{id:c.sub,displayName:c.name,name:{familyName:c.family_name,givenName:c.given_name},emails:[{value:c.email}],photos:[{value:c.picture}],_json:c}}};var L={info:(...e)=>console.info("[Auth]",...e),warn:(...e)=>console.warn("[Auth]",...e),error:(...e)=>console.error("[Auth]",...e)};var D=e=>{let n=e.logger??L,t=V(e.messages),d=e.loginPath??"/auth/login",c=new T({clientId:e.google.clientId,clientSecret:e.google.clientSecret,redirectURI:e.google.redirectURI,scopes:e.google.scopes,accessType:e.google.accessType??"offline",includeGrantedScopes:e.google.includeGrantedScopes??!0,prompt:e.google.prompt??"consent"},async({request:y,tokens:w})=>{let{access_token:i,refresh_token:h,expires_in:u,scope:p,id_token:a}=w.data,{_json:{sub:o,email:r,picture:s,hd:l,email_verified:g},photos:G,...S}=await T.userProfile(w),{id:C,displayName:H,name:{familyName:X,givenName:Z},emails:ee}=S,b="google";if(i===void 0||h===void 0||u===void 0||p===void 0||C===void 0||H===void 0||o===void 0||r===void 0||g===void 0){if(g===!1){let v=encodeURIComponent(t.emailNotVerified);throw E(`${d}?error=${v}`)}let f=encodeURIComponent(t.googleFailed);throw E(`${d}?error=${f}`)}n.info("Authenticated on Google: ",S.displayName);try{if(e.backendAuth){if(!await e.backendAuth.verify(b,i)){n.error("Backend verification returned false for user:",S.displayName);let A=encodeURIComponent(t.backendFailed);throw E(`${d}?error=${A}`)}n.info("Authenticated on Backend: ",S.displayName)}let f=S.emails[0]?.value;if(!await e.isEmailAllowed(f)){let k=new Error("You are not allowed to access this application");throw n.error(k),k}if(e.isUserActive&&!await e.isUserActive(S)){let k=new Error("You are not allowed to access this application");throw n.error("Deactivated account attempted to sign in:",f),k}e.backendAuth&&await e.backendAuth.signup(b,i),n.info("Authenticated complete: ",S.displayName);let v=Date.now()+u*1e3;return{...S,...{accessToken:i,refreshToken:h,expirationDateMs:v,provider:b},photo:s}}catch(f){let v=f;typeof f=="object"&&f!==null&&"cause"in f&&(v=f.cause||f),n.error("Error during backend authentication or subsequent processing:",v);let _=t.unexpected,R=!1;if(typeof f=="object"&&f!==null&&"cause"in f){let A=f.cause;A&&typeof A=="object"&&"message"in A&&typeof A.message=="string"&&A.message.includes("getaddrinfo ENOTFOUND")&&(_=t.backendUnreachable,R=!0)}if(!R&&f instanceof Error&&f.message.includes("You are not allowed to access this application")&&(_=t.notAllowed,R=!0),!R&&f instanceof Response&&f.headers.get("Location")?.startsWith(d))throw f;let k=encodeURIComponent(_);throw E(`${d}?error=${k}`)}}),m=new W;return m.use(c),m};import{redirect as M}from"react-router";var O=(e,n,t,d)=>{let c=e.loginPath??"/auth/login",m=e.postLoginRedirect??"/",y=e.postLogoutRedirect??"/",w=async({request:o})=>{if(await n.getSessionUser(o))throw M(m);return{error:new URL(o.url).searchParams.get("error")}},i=async({request:o})=>{let r=await o.clone().formData(),s=String(r.get("_action")),l=null;return s==="Sign In with Google"?l=await t.authenticate("google",o):l=null,l},h=async o=>await n.logout(o),u=async o=>{let r=await h(o),s=new Headers;return s.append("Set-Cookie",r),M(y,{headers:s})};return{loginLoader:w,loginAction:i,logoutLoader:async({request:o})=>{throw await u(o)},logout:h,logoutAndRedirect:u,googleCallbackLoader:async({request:o})=>{try{let r=await t.authenticate("google",o),g=(await n.getSession(o)).get(n.sessionKey)?.refreshToken,G={accessToken:r.accessToken,refreshToken:r.refreshToken||g,expirationDateMs:r.expirationDateMs,provider:r.provider},S={id:r.id,displayName:r.displayName,name:r.name,emails:r.emails,...G,role:r.role},C=await n.saveSession(o,S);throw M(m,{headers:{"Set-Cookie":C}})}catch(r){throw r instanceof Response?r:(d.error("Authentication error:",r),M(c))}}}};import{createCookieSessionStorage as J,redirect as $}from"react-router";var F=(e,n)=>{if(!e.secrets||e.secrets.length===0||e.secrets.some(s=>!s))throw new Error("REMIX_SESSION_SECRET must be set");let t=e.sessionKey??"user",d=e.maxAge??3600*24*30,c=n?.loginPath??"/auth/login",m=n?.postLoginRedirect??"/",y=n?.mockUser,w=n?.isUserActive,i=J({cookie:{name:e.cookieName??"__session",httpOnly:!0,path:"/",sameSite:"lax",secrets:e.secrets,secure:e.secure??process.env.NODE_ENV==="production",maxAge:d}}),h=async s=>await i.getSession(s.headers.get("Cookie")),u=async s=>{if(y?.enabled(s))return y.resolve(s);let g=(await h(s)).get(t);if(g!==void 0&&!(w&&!await w(g)))return g},p=async(s,l)=>{let g=await h(s);return g.set(t,l),await i.commitSession(g)},a=async s=>{let l=await u(s);if(l===void 0)throw $(c);return l};return{sessionStorage:i,sessionKey:t,getSession:h,getSessionUser:u,saveSession:p,requireUser:a,requireAdminUser:async s=>{let l=await a(s);if(l?.role!=="admin")throw $(m);return l},logout:async s=>{let l=await h(s);return await i.destroySession(l)},commitSession:i.commitSession,destroySession:i.destroySession}};var Q=e=>{let n=e.logger??L,t=F(e.session,{mockUser:e.mockUser,loginPath:e.loginPath,postLoginRedirect:e.postLoginRedirect,isUserActive:e.isUserActive}),d=D(e),c=I(e,t,n),m=N(e,t,c,n),y=O(e,t,d,n);return{authenticator:d,storage:t,authFns:c,sessionStorage:t.sessionStorage,getSession:t.getSession,getSessionUser:t.getSessionUser,saveSession:t.saveSession,requireUser:t.requireUser,requireAdminUser:t.requireAdminUser,commitSession:t.commitSession,destroySession:t.destroySession,authenticate:c.authenticate,refreshedAccessToken:c.refreshedAccessToken,authenticateInLoader:m,...y}};export{x as AuthRequiredError,U as GoogleInvalidGrantError,P as TokenRefreshError,O as createAuthHandlers,Q as createAuthServer,F as createAuthSessionStorage,I as createAuthenticate,N as createAuthenticateInLoader,D as createGoogleAuthenticator};
2
2
  //# sourceMappingURL=server.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/server/authenticate.ts","../src/server/authenticate-in-loader.ts","../src/server/google-strategy.ts","../src/server/google-strategy-impl.ts","../src/server/logger.ts","../src/server/handlers.ts","../src/server/session.ts","../src/server/auth-server.ts"],"sourcesContent":["/**\n * @module authenticate\n * @description Session token guard with Google refresh-token rotation.\n * Google リフレッシュトークンによるセッショントークンガード。\n *\n * 原本 (quants-react-router authenticate.server.ts) を挙動保存で移植。\n * env (GOOGLE_CLIENT_ID/SECRET) は Google 設定注入に、destroySession はセッションストレージ\n * 注入に、テストバックドアは mockUser ポート注入に置き換えた。\n * 期限判定 → リフレッシュ → 失敗時セッション破棄 + ログインへリダイレクトの流れは原本と等価。\n *\n * 参考: https://sergiodxa.com/articles/working-with-refresh-tokens-in-remix\n */\nimport type { SessionProfile } from \"@aiquants/auth-core\"\nimport { redirect, type Session } from \"react-router\"\nimport type { AuthServerConfig, LoggerPort } from \"./ports\"\nimport type { AuthSessionStorage } from \"./session\"\n\n/** トークンリフレッシュ失敗の基底エラー。 */\nexport class TokenRefreshError extends Error {\n constructor(message: string) {\n super(message)\n this.name = \"TokenRefreshError\"\n }\n}\n\n/** Google の invalid_grant (失効/取り消し済みリフレッシュトークン) エラー。 */\nexport class GoogleInvalidGrantError extends TokenRefreshError {\n constructor() {\n super(\"Google refresh token invalid or revoked\")\n this.name = \"GoogleInvalidGrantError\"\n }\n}\n\n/** createAuthenticate が返す認証関数束。 */\nexport type AuthenticateFns = ReturnType<typeof createAuthenticate>\n\n/**\n * Builds the `authenticate` / `refreshedAccessToken` pair wired to injected config.\n * 注入設定に結線した `authenticate` / `refreshedAccessToken` を生成する。\n */\nexport const createAuthenticate = (config: AuthServerConfig, storage: AuthSessionStorage, logger: LoggerPort) => {\n const loginPath = config.loginPath ?? \"/auth/login\"\n const sessionKey = storage.sessionKey\n\n /**\n * Refreshes the Google access token using the refresh token in the session.\n * セッション内のリフレッシュトークンで Google アクセストークンを更新する。\n */\n const getRefreshedGoogleAccessToken = async (session: Session) => {\n const userFromSession = session.get(sessionKey)\n const refreshTokenFromSession = userFromSession?.refreshToken\n if (!refreshTokenFromSession) {\n throw new Error(\"Refresh token not found in session\")\n }\n try {\n const url = \"https://www.googleapis.com/oauth2/v4/token\"\n const body = {\n client_id: config.google.clientId,\n client_secret: config.google.clientSecret,\n grant_type: \"refresh_token\",\n refresh_token: refreshTokenFromSession,\n }\n\n const response = await fetch(url, {\n headers: {\n \"Content-Type\": \"application/json\",\n },\n method: \"POST\",\n body: JSON.stringify(body),\n })\n\n if (!response.ok) {\n // Google API からエラーが返された場合 (JSON パース失敗も考慮)\n const errorData = await response.json().catch(() => undefined)\n const isInvalidGrant = response.status === 400 && typeof errorData === \"object\" && !!errorData && \"error\" in errorData && errorData.error === \"invalid_grant\"\n if (isInvalidGrant) {\n throw new GoogleInvalidGrantError()\n }\n const errorDescription = typeof errorData === \"object\" && errorData && \"error_description\" in errorData ? String(errorData.error_description) : \"Unknown error\"\n throw new TokenRefreshError(`Google token refresh failed: ${response.status} ${errorDescription}`)\n }\n\n const refreshedTokens = await response.json()\n const { access_token, expires_in } = refreshedTokens // id_token, scope, token_type は不要\n\n if (!(access_token && expires_in)) {\n logger.error(\"Invalid token response from Google:\", refreshedTokens)\n throw new Error(\"Invalid token response from Google\")\n }\n\n return {\n accessToken: access_token,\n refreshToken: refreshTokenFromSession, // Google は通常リフレッシュトークンを再発行しない\n expirationDateMs: Number(Date.now()) + expires_in * 1000,\n }\n } catch (error) {\n if (error instanceof TokenRefreshError) {\n throw error\n }\n const description = error instanceof Error ? error.message : \"Unknown error\"\n throw new TokenRefreshError(`Failed to refresh Google access token: ${description}`)\n }\n }\n\n /**\n * Dispatches the token refresh by session provider (currently Google only).\n * セッションのプロバイダー別にトークンリフレッシュを振り分ける (現状 Google のみ)。\n */\n const refreshedAccessToken = async (session: Session) => {\n const sessionProfile: SessionProfile = session.get(sessionKey)\n const provider = sessionProfile.provider\n if (!provider) {\n throw new Error(\"Provider not found in session\")\n }\n switch (provider) {\n case \"google\":\n return await getRefreshedGoogleAccessToken(session)\n default:\n throw new Error(\"Provider not supported\")\n }\n }\n\n /**\n * Validates session tokens, refreshing them when expired; destroys the session on refresh failure.\n * セッショントークンを検証し、期限切れならリフレッシュする。失敗時はセッションを破棄する。\n */\n const authenticate = async (request: Request, session: Session, options?: { failureRedirect?: string }) => {\n const { failureRedirect } = options || {}\n try {\n // テストバックドア (ポート注入時のみ有効)\n if (config.mockUser?.enabled(request)) {\n return {\n accessToken: \"mock-access-token\",\n refreshToken: \"mock-refresh-token\",\n expirationDateMs: Date.now() + 3600000,\n }\n }\n\n // セッションから認証データを取得\n const user = session.get(sessionKey)\n if (!(user?.accessToken && user.expirationDateMs)) {\n throw redirect(failureRedirect || loginPath)\n }\n\n const { accessToken, refreshToken, expirationDateMs } = user\n\n // 期限切れならエラーを投げてリフレッシュ経路へ\n if (expirationDateMs < Date.now()) {\n if (!refreshToken) {\n // リフレッシュトークンがない場合は更新できないのでログインページへ\n throw redirect(failureRedirect || loginPath)\n }\n throw new Error(\"Expired\")\n }\n\n // 期限内ならそのまま返す\n return { accessToken, refreshToken, expirationDateMs }\n } catch (error) {\n // 期限切れエラーのみリフレッシュを試みる\n if (error instanceof Error && error.message === \"Expired\") {\n try {\n const refreshed = await refreshedAccessToken(session)\n\n // セッションを新しいトークンで更新\n const user = session.get(sessionKey)\n session.set(sessionKey, { ...user, ...refreshed })\n\n // アクセストークンを返す (commit は呼び出し側の責務)\n return refreshed\n } catch (refreshError) {\n if (refreshError instanceof GoogleInvalidGrantError) {\n logger.warn(\"Google refresh token invalid or revoked\")\n } else {\n const message = refreshError instanceof Error ? refreshError.message : \"Unknown error\"\n logger.error(\"Failed to refresh token:\", message)\n }\n // リフレッシュに失敗した場合はセッションを破棄してログインページへ\n const cookie = await storage.destroySession(session)\n throw redirect(failureRedirect || loginPath, {\n headers: { \"Set-Cookie\": cookie },\n })\n }\n }\n\n // その他の予期せぬエラーは再 throw\n throw error\n }\n }\n\n return { authenticate, refreshedAccessToken }\n}\n","/**\n * @module authenticate-in-loader\n * @description Per-request loader authentication guard with photo cache and warmup.\n * 写真キャッシュとウォームアップ付きの毎リクエストローダー認証ガード。\n *\n * 原本 (quants-react-router authenticateInLoader.server.ts) を挙動保存で移植。\n * DB クエリ (getOpenids) は `getUserPhoto` ポートへ、DB プールウォームアップは `warmup`\n * ポートへ置き換えた。写真キャッシュ (Map + pending dedup) はファクトリインスタンスごとに\n * 保持する (module-level mutable state の排除)。\n */\nimport type { AuthenticationPayload, MyGoogleProfile, SessionProfile } from \"@aiquants/auth-core\"\nimport { redirect } from \"react-router\"\nimport type { AuthenticateFns } from \"./authenticate\"\nimport type { AuthServerConfig, LoggerPort } from \"./ports\"\nimport type { AuthSessionStorage } from \"./session\"\n\n/** createAuthenticateInLoader が返すローダーガード関数。 */\nexport type AuthenticateInLoader = ReturnType<typeof createAuthenticateInLoader>\n\n/**\n * Builds the per-request loader guard wired to injected ports.\n * 注入ポートに結線した毎リクエストローダーガードを生成する。\n */\nexport const createAuthenticateInLoader = (config: AuthServerConfig, storage: AuthSessionStorage, authFns: AuthenticateFns, logger: LoggerPort) => {\n // 写真キャッシュ (インスタンススコープ。同時リクエストは pending dedup で 1 クエリに集約)\n const openidPhotoCache = new Map<string, string>()\n const pendingOpenidPhotoQueries = new Map<string, Promise<string>>()\n\n /**\n * Resolves the user photo with cache + in-flight dedup, delegating the query to the port.\n * キャッシュ + 実行中 dedup 付きでユーザー写真を解決する (実クエリはポートへ委譲)。\n */\n const getCachedOpenidPhoto = async (openid: string): Promise<string> => {\n const cached = openidPhotoCache.get(openid)\n if (cached !== undefined) {\n return cached\n }\n\n const pending = pendingOpenidPhotoQueries.get(openid)\n if (pending !== undefined) {\n return pending\n }\n\n const fetchPromise = (async () => {\n try {\n const photo = config.getUserPhoto ? await config.getUserPhoto(openid) : \"\"\n openidPhotoCache.set(openid, photo)\n return photo\n } catch (error) {\n openidPhotoCache.delete(openid)\n throw error\n } finally {\n pendingOpenidPhotoQueries.delete(openid)\n }\n })()\n\n pendingOpenidPhotoQueries.set(openid, fetchPromise)\n return fetchPromise\n }\n\n /**\n * Handles authentication within loader requests by refreshing session tokens when needed.\n * ローダーリクエスト内で必要に応じてセッショントークンを更新する認証処理。\n *\n * @param request - リクエストオブジェクト。\n * @param options - 認証オプション。`failureRedirect: null` でリダイレクトを明示的にスキップ。\n * @returns 認証済みユーザープロファイルとオプショナルな Set-Cookie 値。\n */\n const authenticateInLoader = async (request: Request, options: { failureRedirect: string } | { failureRedirect: null }): Promise<{ user?: MyGoogleProfile; cookie?: string }> => {\n // DB 接続プールが確実にオープンするのを待ってから処理を開始する (E2E 等でのコールドスタート競合回避)\n if (config.warmup) {\n await config.warmup()\n }\n\n const { failureRedirect } = options\n const session = await storage.getSession(request)\n const currentUser = await storage.getSessionUser(request)\n\n if (currentUser === undefined) {\n if (failureRedirect === null) {\n return {}\n }\n throw redirect(failureRedirect)\n }\n\n try {\n // トークンの有効期限を確認し、必要に応じて更新\n const authenticatedTokens = await authFns.authenticate(request, session, {\n failureRedirect: failureRedirect ?? undefined,\n })\n\n // 認証ペイロードの構築\n const authenticationPayload: AuthenticationPayload = {\n accessToken: authenticatedTokens.accessToken,\n refreshToken: authenticatedTokens.refreshToken,\n expirationDateMs: authenticatedTokens.expirationDateMs,\n provider: currentUser.provider,\n }\n // セッション内のユーザ情報\n const updatedSessionUser: SessionProfile = {\n id: currentUser.id,\n displayName: currentUser.displayName,\n name: currentUser.name,\n emails: currentUser.emails,\n ...authenticationPayload,\n role: currentUser.role,\n }\n\n // ユーザー写真をキャッシュまたはポートから取得\n const photo = await getCachedOpenidPhoto(currentUser.id)\n\n // ユーザープロファイル情報\n const updatedUser: MyGoogleProfile = {\n ...currentUser,\n ...authenticationPayload,\n photo,\n }\n\n // トークンが更新 (リフレッシュ) された場合のみ、セッションを保存して Cookie ヘッダーを設定する\n const isRefreshed = authenticatedTokens.accessToken !== currentUser.accessToken\n const cookie = isRefreshed ? await storage.saveSession(request, updatedSessionUser) : undefined\n\n return { user: updatedUser, cookie }\n } catch (error) {\n if (error instanceof Response && error.headers.get(\"Location\")) {\n throw error\n }\n logger.error(\"Error in authenticateInLoader:\", error)\n // 認証エラー時はセッションを破棄してリダイレクト\n if (failureRedirect !== null) {\n // セッション破棄のみを try で保護する。redirect の throw を try 内に置くと\n // 同じ catch(destroyError) に捕捉されて Set-Cookie 無しの fallback に差し替わってしまうため、\n // throw redirect は必ず try の外で行う。\n let destroyedCookie: string | undefined\n try {\n destroyedCookie = await storage.destroySession(session)\n } catch (destroyError) {\n logger.error(\"Failed to destroy session during auth error:\", destroyError)\n }\n // 破棄に成功していれば Set-Cookie を付与し、失敗してもとにかくリダイレクトは試みる\n throw destroyedCookie !== undefined ? redirect(failureRedirect, { headers: { \"Set-Cookie\": destroyedCookie } }) : redirect(failureRedirect)\n }\n throw error\n }\n }\n\n return authenticateInLoader\n}\n","/**\n * @module google-strategy\n * @description Google OAuth authenticator factory (remix-auth v4 + local GoogleStrategy).\n * Google OAuth 認証器ファクトリ (remix-auth v4 + 同梱の GoogleStrategy)。\n *\n * 原本 (quants-react-router auth.server.ts) の verify コールバックを挙動保存で移植。\n * env 読取り・backend HTTP・allowlist・エラーメッセージはすべてポート/設定注入に置き換えた。\n * 検証順序 (Google プロフィール検証 → backend verify → allowlist → backend signup)、\n * リダイレクト先 (`<loginPath>?error=<encoded>`)、エラーメッセージ文言は原本と等価。\n */\nimport type { MyGoogleProfile } from \"@aiquants/auth-core\"\nimport { type AuthMessages, resolveAuthMessages } from \"@aiquants/auth-core\"\nimport { redirect } from \"react-router\"\nimport { Authenticator } from \"remix-auth\"\nimport { GoogleStrategy } from \"./google-strategy-impl\"\nimport { defaultLogger } from \"./logger\"\nimport type { AuthServerConfig } from \"./ports\"\n\ninterface GoogleToken {\n id_token?: string\n access_token?: string\n refresh_token?: string\n expires_in?: number\n scope?: string\n}\n\n/**\n * Builds a remix-auth Authenticator with the Google strategy wired to injected ports.\n * 注入ポートに結線した Google ストラテジー付き Authenticator を生成する。\n */\nexport const createGoogleAuthenticator = (config: AuthServerConfig): Authenticator<MyGoogleProfile> => {\n const logger = config.logger ?? defaultLogger\n const messages: AuthMessages = resolveAuthMessages(config.messages)\n const loginPath = config.loginPath ?? \"/auth/login\"\n\n const googleStrategy = new GoogleStrategy<MyGoogleProfile>(\n {\n clientId: config.google.clientId,\n clientSecret: config.google.clientSecret,\n redirectURI: config.google.redirectURI,\n scopes: config.google.scopes,\n accessType: config.google.accessType ?? \"offline\",\n includeGrantedScopes: config.google.includeGrantedScopes ?? true,\n prompt: config.google.prompt ?? \"consent\",\n },\n /**\n * Google 認証後のコールバック。Google のトークンとプロフィールを検証し、\n * backend verify → allowlist → backend signup を経て MyGoogleProfile を返す。\n * 失敗時は `<loginPath>?error=<encoded>` へのリダイレクト Response を throw する。\n */\n async ({ request: _request, tokens }) => {\n const { access_token: accessToken, refresh_token: refreshToken, expires_in: expiresIn, scope, id_token: _idToken } = tokens.data as GoogleToken\n const {\n _json: { sub, email, picture, hd: _hd, email_verified },\n photos: _photos,\n ...profile\n } = await GoogleStrategy.userProfile(tokens)\n const {\n id,\n displayName,\n name: { familyName: _familyName, givenName: _givenName },\n emails: _emails,\n } = profile\n const provider = \"google\"\n\n // first verify (トークン/プロフィール欠落は Google 認証失敗として扱う)\n if (\n accessToken === undefined ||\n refreshToken === undefined ||\n expiresIn === undefined ||\n scope === undefined ||\n id === undefined ||\n displayName === undefined ||\n sub === undefined ||\n email === undefined ||\n email_verified === undefined\n // hd === undefined // Google Workspace などの組織制限をする場合は有効化\n ) {\n if (email_verified === false) {\n const errorMessage = encodeURIComponent(messages.emailNotVerified)\n throw redirect(`${loginPath}?error=${errorMessage}`)\n }\n const errorMessage = encodeURIComponent(messages.googleFailed)\n throw redirect(`${loginPath}?error=${errorMessage}`)\n }\n\n logger.info(\"Authenticated on Google: \", profile.displayName)\n\n // Backend Authentication\n try {\n // backendAuth ポート未指定時は verify/signup をスキップ (フロント単独運用)\n if (config.backendAuth) {\n const ok = await config.backendAuth.verify(provider, accessToken)\n\n if (!ok) {\n logger.error(\"Backend verification returned false for user:\", profile.displayName)\n const errorMessage = encodeURIComponent(messages.backendFailed)\n throw redirect(`${loginPath}?error=${errorMessage}`)\n }\n\n logger.info(\"Authenticated on Backend: \", profile.displayName)\n }\n\n // メールアドレスまたはドメインベースでのアクセス制限チェック (論理和は allowlist 実装側)\n // ⚠️ `await` 必須。非同期実装を await せずに評価すると Promise が truthy 判定され\n // 許可判定を素通りする (fail-open)。この await は spec で門番されている。\n const userEmail = profile.emails[0]?.value\n if (!(await config.isEmailAllowed(userEmail))) {\n const error = new Error(\"You are not allowed to access this application\")\n logger.error(error)\n throw error\n }\n\n // 無効化済みアカウントはログインさせない (allowlist とは別軸の判定)\n if (config.isUserActive && !(await config.isUserActive(profile))) {\n const error = new Error(\"You are not allowed to access this application\")\n logger.error(\"Deactivated account attempted to sign in:\", userEmail)\n throw error\n }\n\n // バックエンドでのサインアップ処理 (毎ログイン実行の挙動を保存)\n if (config.backendAuth) {\n await config.backendAuth.signup(provider, accessToken)\n }\n\n logger.info(\"Authenticated complete: \", profile.displayName)\n\n // 認証ペイロードの構築\n const expirationDateMs = Date.now() + expiresIn * 1000\n const authenticationPayload = {\n accessToken: accessToken,\n refreshToken: refreshToken,\n expirationDateMs: expirationDateMs,\n provider: provider,\n }\n // ユーザープロファイル情報\n const user: MyGoogleProfile = {\n ...profile,\n ...authenticationPayload,\n photo: picture,\n }\n return user\n } catch (error: unknown) {\n let loggable = error\n if (typeof error === \"object\" && error !== null && \"cause\" in error) {\n // cause があればそれを優先してログに残す (falsy なら元のエラー)\n loggable = (error as { cause: unknown }).cause || error\n }\n logger.error(\"Error during backend authentication or subsequent processing:\", loggable)\n\n let friendlyErrorMessage = messages.unexpected\n let conditionMet = false\n\n // ネットワーク到達不可 (getaddrinfo ENOTFOUND) の判定\n if (typeof error === \"object\" && error !== null && \"cause\" in error) {\n const cause = (error as { cause: unknown }).cause\n if (cause && typeof cause === \"object\" && \"message\" in cause && typeof (cause as { message: string }).message === \"string\" && (cause as { message: string }).message.includes(\"getaddrinfo ENOTFOUND\")) {\n friendlyErrorMessage = messages.backendUnreachable\n conditionMet = true\n }\n }\n\n // allowlist 拒否の判定\n if (!conditionMet && error instanceof Error && error.message.includes(\"You are not allowed to access this application\")) {\n friendlyErrorMessage = messages.notAllowed\n conditionMet = true\n }\n\n // 既にリダイレクト Response が throw されている場合はそのまま再 throw\n if (!conditionMet && error instanceof Response && error.headers.get(\"Location\")?.startsWith(loginPath)) {\n throw error\n }\n\n const encodedErrorMessage = encodeURIComponent(friendlyErrorMessage)\n throw redirect(`${loginPath}?error=${encodedErrorMessage}`)\n }\n },\n )\n\n const authenticator = new Authenticator<MyGoogleProfile>()\n authenticator.use(googleStrategy)\n return authenticator\n}\n","/**\n * @module google-strategy-impl\n * @description Self-contained Google OAuth2 strategy (ported from @coji/remix-auth-google).\n * 自己完結型の Google OAuth2 ストラテジー (@coji/remix-auth-google から移植)。\n *\n * @coji/remix-auth-google@4.2.0 の実装 (68 行) を挙動保存で移植し、外部依存を除去する。\n * OAuth2 の本体機構 (PKCE / state / トークン交換) は継承元の remix-auth-oauth2 が担うため\n * 移植対象ではない。ここに置くのは Google 固有の設定 (エンドポイント URL・既定スコープ)、\n * 認可パラメータの上書き (access_type / include_granted_scopes / prompt / hd / login_hint)、\n * および userinfo (v3) からプロファイルを取得する静的メソッドのみ。\n *\n * 既定スコープ・URL・プロファイル整形 (sub→id、picture→photos、_json=raw) は原本と byte 一致。\n */\nimport { OAuth2Strategy } from \"remix-auth-oauth2\"\n\n/** Google の既定スコープ (原本と同一。フル URL 形式)。 */\nexport const GoogleStrategyDefaultScopes = [\"openid\", \"https://www.googleapis.com/auth/userinfo.profile\", \"https://www.googleapis.com/auth/userinfo.email\"]\n\n/** ストラテジー名 (remix-auth の Authenticator 登録キー)。 */\nexport const GoogleStrategyDefaultName = \"google\"\n\n// userinfo エンドポイント (v3)。プロファイル取得のみに用いる\nconst userInfoURL = \"https://www.googleapis.com/oauth2/v3/userinfo\"\n\n/** @see https://developers.google.com/identity/protocols/oauth2/scopes */\nexport type GoogleScope = string\n\n/** GoogleStrategy のコンストラクタオプション (原本 @coji と同一)。 */\nexport type GoogleStrategyOptions = {\n clientId: string\n clientSecret: string\n redirectURI: string\n /** @default \"openid profile email\" 相当 (GoogleStrategyDefaultScopes)。 */\n scopes?: GoogleScope[]\n accessType?: \"online\" | \"offline\"\n includeGrantedScopes?: boolean\n prompt?: \"none\" | \"consent\" | \"select_account\"\n hd?: string\n loginHint?: string\n}\n\n/** userinfo から取得・整形した Google プロファイル (原本 GoogleProfile と同一形状)。 */\nexport type GoogleProfile = {\n id: string\n displayName: string\n name: {\n familyName: string\n givenName: string\n }\n emails: [{ value: string }]\n photos: [{ value: string }]\n _json: {\n sub: string\n name: string\n given_name: string\n family_name: string\n picture: string\n locale: string\n email: string\n email_verified: boolean\n hd: string\n }\n}\n\n// userProfile が必要とする最小のトークン形状 (arctic の OAuth2Tokens の部分集合)。\n// 直接 arctic へ依存しないための構造的型。実際に呼ぶのは accessToken() のみ。\ntype OAuth2AccessTokenSource = { accessToken(): string }\n\n/**\n * Google OAuth2 strategy for remix-auth (extends remix-auth-oauth2's OAuth2Strategy).\n * remix-auth 用の Google OAuth2 ストラテジー (remix-auth-oauth2 の OAuth2Strategy を継承)。\n */\nexport class GoogleStrategy<User> extends OAuth2Strategy<User> {\n name = GoogleStrategyDefaultName\n private readonly scopes: GoogleScope[]\n private readonly accessType: \"online\" | \"offline\"\n private readonly prompt?: \"none\" | \"consent\" | \"select_account\"\n private readonly includeGrantedScopes: boolean\n private readonly hd?: string\n private readonly loginHint?: string\n\n constructor({ clientId, clientSecret, redirectURI, scopes, accessType, includeGrantedScopes, prompt, hd, loginHint }: GoogleStrategyOptions, verify: OAuth2Strategy<User>[\"verify\"]) {\n const scopeList = scopes ?? GoogleStrategyDefaultScopes\n super(\n {\n clientId,\n clientSecret,\n redirectURI,\n authorizationEndpoint: \"https://accounts.google.com/o/oauth2/v2/auth\",\n tokenEndpoint: \"https://oauth2.googleapis.com/token\",\n scopes: scopeList,\n },\n verify,\n )\n this.scopes = scopeList\n this.accessType = accessType ?? \"online\"\n this.includeGrantedScopes = includeGrantedScopes ?? false\n this.prompt = prompt\n this.hd = hd\n this.loginHint = loginHint\n }\n\n /**\n * Adds Google-specific authorization parameters to the redirect URL.\n * 認可リダイレクト URL へ Google 固有パラメータを付与する。\n */\n protected authorizationParams(params: URLSearchParams, _request?: Request): URLSearchParams {\n params.set(\"access_type\", this.accessType)\n params.set(\"include_granted_scopes\", String(this.includeGrantedScopes))\n if (this.scopes && this.scopes.length > 0) {\n params.set(\"scope\", this.scopes.join(\" \"))\n }\n if (this.prompt) {\n params.set(\"prompt\", this.prompt)\n }\n if (this.hd) {\n params.set(\"hd\", this.hd)\n }\n if (this.loginHint) {\n params.set(\"login_hint\", this.loginHint)\n }\n return params\n }\n\n /**\n * Fetches the Google user profile from the userinfo (v3) endpoint.\n * userinfo (v3) エンドポイントから Google ユーザープロファイルを取得する。\n *\n * @throws プロファイル取得に失敗した場合 (レスポンスが ok でない)。\n */\n static async userProfile(tokens: OAuth2AccessTokenSource): Promise<GoogleProfile> {\n const response = await fetch(userInfoURL, {\n headers: {\n Authorization: `Bearer ${tokens.accessToken()}`,\n },\n })\n if (!response.ok) {\n throw new Error(`Failed to fetch user profile: ${response.statusText}`)\n }\n const raw = await response.json()\n const profile: GoogleProfile = {\n id: raw.sub,\n displayName: raw.name,\n name: {\n familyName: raw.family_name,\n givenName: raw.given_name,\n },\n emails: [{ value: raw.email }],\n photos: [{ value: raw.picture }],\n _json: raw,\n }\n return profile\n }\n}\n","/**\n * @module logger\n * @description Default console-backed logger used when no logger port is injected.\n * ロガーポート未注入時に使う console ベースの既定ロガー。\n */\nimport type { LoggerPort } from \"./ports\"\n\n/** console ベースの既定ロガー (ライブラリ既定のフォールバックとして意図的に console を使用)。 */\nexport const defaultLogger: LoggerPort = {\n // biome-ignore lint/suspicious/noConsole: ロガー未注入時の意図的な console フォールバック\n info: (...args: unknown[]) => console.info(\"[Auth]\", ...args),\n warn: (...args: unknown[]) => console.warn(\"[Auth]\", ...args),\n error: (...args: unknown[]) => console.error(\"[Auth]\", ...args),\n}\n","/**\n * @module handlers\n * @description Route handler factories for login / logout / Google callback.\n * login / logout / Google コールバックのルートハンドラ工場。\n *\n * 原本 (quants-react-router routes/_auth+ の 4 ルート + features/logoutAndRedirect.ts) の\n * サーバー側ロジックを挙動保存で移植。UI (JSX) はアプリ側に残す。\n * コールバックのセッション保存形状 (SessionProfile、photo/_json/photos を含まない) は\n * cookie byte 互換のため厳密に保存する。\n */\nimport type { AuthenticationPayload, MyGoogleProfile, SessionProfile } from \"@aiquants/auth-core\"\nimport { redirect } from \"react-router\"\nimport type { Authenticator } from \"remix-auth\"\nimport type { AuthServerConfig, LoggerPort } from \"./ports\"\nimport type { AuthSessionStorage } from \"./session\"\n\n/** createAuthHandlers が返すハンドラ束。 */\nexport type AuthHandlers = ReturnType<typeof createAuthHandlers>\n\n/**\n * Builds loader/action handlers for the auth routes.\n * 認証ルート用の loader/action ハンドラ群を生成する。\n */\nexport const createAuthHandlers = (config: AuthServerConfig, storage: AuthSessionStorage, authenticator: Authenticator<MyGoogleProfile>, logger: LoggerPort) => {\n const loginPath = config.loginPath ?? \"/auth/login\"\n const postLoginRedirect = config.postLoginRedirect ?? \"/\"\n const postLogoutRedirect = config.postLogoutRedirect ?? \"/\"\n\n /**\n * Login-page loader: redirects authenticated users, passes the error message through.\n * ログインページ loader。認証済みならリダイレクトし、エラーメッセージを data として返す。\n */\n const loginLoader = async ({ request }: { request: Request }): Promise<{ error: string | null }> => {\n const user = await storage.getSessionUser(request)\n if (user) {\n // 認証済みならトップページにリダイレクト\n throw redirect(postLoginRedirect)\n }\n const url = new URL(request.url)\n const error = url.searchParams.get(\"error\")\n return { error } // エラーメッセージを loader data として渡す\n }\n\n /**\n * Login action: starts the Google OAuth flow for the \"Sign In with Google\" form action.\n * ログイン action。\"Sign In with Google\" フォーム action で Google OAuth フローを開始する。\n */\n const loginAction = async ({ request }: { request: Request }): Promise<MyGoogleProfile | null> => {\n const formData = await request.clone().formData()\n const action = String(formData.get(\"_action\"))\n\n let user: MyGoogleProfile | null = null\n switch (action) {\n case \"Sign In with Google\":\n user = await authenticator.authenticate(\"google\", request)\n break\n default:\n user = null\n }\n return user\n }\n\n /**\n * Destroys the session and returns the Set-Cookie value (logout primitive).\n * セッションを破棄して Set-Cookie 値を返す (ログアウトの基本操作)。\n */\n const logout = async (request: Request) => {\n return await storage.logout(request)\n }\n\n /**\n * Logs out and redirects with the destroyed-session cookie attached.\n * ログアウトし、破棄済みセッション Cookie を付与してリダイレクトする。\n */\n const logoutAndRedirect = async (request: Request) => {\n const cookie = await logout(request)\n const headers = new Headers()\n headers.append(\"Set-Cookie\", cookie)\n return redirect(postLogoutRedirect, { headers })\n }\n\n /**\n * Logout-route loader: performs logout and throws the redirect.\n * ログアウトルート loader。ログアウトを実行してリダイレクトを throw する。\n */\n const logoutLoader = async ({ request }: { request: Request }): Promise<never> => {\n throw await logoutAndRedirect(request)\n }\n\n /**\n * Google OAuth callback loader: authenticates, saves the session and redirects home.\n * Google OAuth コールバック loader。認証してセッションを保存し、ホームへリダイレクトする。\n */\n const googleCallbackLoader = async ({ request }: { request: Request }): Promise<never> => {\n try {\n const googleProfile = await authenticator.authenticate(\"google\", request)\n\n // 既存のセッションから前回のユーザー情報を取得して refresh_token を保護する\n const existingSession = await storage.getSession(request)\n const existingUser = existingSession.get(storage.sessionKey) as SessionProfile | undefined\n const previousRefreshToken = existingUser?.refreshToken\n\n // 認証ペイロードの構築\n const authenticationPayload: AuthenticationPayload = {\n accessToken: googleProfile.accessToken,\n refreshToken: googleProfile.refreshToken || previousRefreshToken,\n expirationDateMs: googleProfile.expirationDateMs,\n provider: googleProfile.provider,\n }\n\n // セッション内のユーザ情報 (photo/_json/photos を含まない byte 互換形状)\n const user: SessionProfile = {\n id: googleProfile.id,\n displayName: googleProfile.displayName,\n name: googleProfile.name,\n emails: googleProfile.emails,\n ...authenticationPayload,\n role: googleProfile.role,\n }\n\n // 認証後のユーザ情報をセッションに保存\n const cookie = await storage.saveSession(request, user)\n\n // ホームへリダイレクト\n throw redirect(postLoginRedirect, { headers: { \"Set-Cookie\": cookie } })\n } catch (error) {\n // authenticator.authenticate は認証失敗時に Response か Error を throw する。\n // access_denied のような OAuth エラーでは Error の可能性があるため、\n // どちらの場合でもログインページへのリダイレクトが安全 (Response はそのまま再 throw)。\n if (error instanceof Response) {\n throw error\n }\n logger.error(\"Authentication error:\", error)\n throw redirect(loginPath)\n }\n }\n\n return { loginLoader, loginAction, logoutLoader, logout, logoutAndRedirect, googleCallbackLoader }\n}\n","/**\n * @module session\n * @description Cookie session storage factory with a DI test backdoor.\n * DI テストバックドア付きの cookie セッションストレージファクトリ。\n *\n * 原本 (quants-react-router session.server.ts) を挙動保存で移植。相違点は\n * (1) module-level の env throw をファクトリ引数検証へ移した、\n * (2) テストバックドア (mock_user cookie) をハードコードから `mockUser` ポート注入に変えた、\n * の 2 点のみ。cookie 形状 (`__session`/httpOnly/lax/30d、キー \"user\") は byte 互換。\n */\nimport type { GoogleProfileBase, SessionProfile } from \"@aiquants/auth-core\"\nimport { createCookieSessionStorage, redirect } from \"react-router\"\nimport type { MockUserPort, SessionConfig } from \"./ports\"\n\n/** createAuthSessionStorage が返すセッション操作束。 */\nexport type AuthSessionStorage = ReturnType<typeof createAuthSessionStorage>\n\n/**\n * Builds the cookie session storage and session helpers.\n * cookie セッションストレージとセッションヘルパー群を生成する。\n *\n * @throws secrets が空の場合 (fail-closed の設定ガード)。\n */\nexport const createAuthSessionStorage = (config: SessionConfig, options?: { mockUser?: MockUserPort; loginPath?: string; postLoginRedirect?: string; isUserActive?: (profile: GoogleProfileBase) => boolean | Promise<boolean> }) => {\n // ファクトリ実行時検証 (import 時副作用の排除)\n if (!config.secrets || config.secrets.length === 0 || config.secrets.some((s) => !s)) {\n throw new Error(\"REMIX_SESSION_SECRET must be set\")\n }\n\n const sessionKey = config.sessionKey ?? \"user\"\n const maxAge = config.maxAge ?? 60 * 60 * 24 * 30\n const loginPath = options?.loginPath ?? \"/auth/login\"\n const postLoginRedirect = options?.postLoginRedirect ?? \"/\"\n const mockUser = options?.mockUser\n const isUserActive = options?.isUserActive\n\n const sessionStorage = createCookieSessionStorage({\n cookie: {\n name: config.cookieName ?? \"__session\",\n httpOnly: true, // for security reasons, make this cookie http only\n path: \"/\", // remember to add this so the cookie will work in all routes\n sameSite: \"lax\", // this helps with CSRF\n secrets: config.secrets,\n secure: config.secure ?? process.env.NODE_ENV === \"production\",\n maxAge,\n },\n })\n\n /**\n * Reads the session from the request cookie.\n * リクエストの Cookie からセッションを取得する。\n */\n const getSession = async (request: Request) => {\n return await sessionStorage.getSession(request.headers.get(\"Cookie\"))\n }\n\n /**\n * Reads the session user, honoring the injected test backdoor when enabled.\n * セッションからユーザー情報を取得する (注入されたテストバックドアが有効ならそれを返す)。\n */\n const getSessionUser = async (request: Request): Promise<SessionProfile | undefined> => {\n // テストバックドア (ポート注入時のみ有効。判定条件はアプリ側の enabled に委譲)\n if (mockUser?.enabled(request)) {\n return mockUser.resolve(request)\n }\n\n const session = await getSession(request)\n const user = session.get(sessionKey) as SessionProfile | undefined\n if (user === undefined) return undefined\n // 無効化済みアカウントは「未認証」として扱う。これにより、既に発行済みのセッション Cookie を\n // 持っていてもガードは通らず、無効化がその場で全ルートへ波及する。\n if (isUserActive && !(await isUserActive(user))) return undefined\n return user\n }\n\n /**\n * Saves the user into the session and returns the Set-Cookie value.\n * セッションへユーザー情報を保存し、Set-Cookie 値を返す。\n */\n const saveSession = async (request: Request, user: SessionProfile) => {\n const session = await getSession(request)\n session.set(sessionKey, user)\n return await sessionStorage.commitSession(session)\n }\n\n /**\n * Authenticated-user guard: redirects to the login page when absent.\n * 認証ユーザーガード。未認証ならログインページへリダイレクトする。\n */\n const requireUser = async (request: Request) => {\n const sessionUser = await getSessionUser(request)\n if (sessionUser === undefined) {\n throw redirect(loginPath)\n }\n return sessionUser as SessionProfile\n }\n\n /**\n * Admin-user guard: redirects to the app root when the role is not \"admin\".\n * 管理者ユーザーガード。role が \"admin\" でなければルートへリダイレクトする。\n */\n const requireAdminUser = async (request: Request) => {\n const sessionUser = await requireUser(request)\n if (sessionUser?.role !== \"admin\") {\n throw redirect(postLoginRedirect)\n }\n return sessionUser\n }\n\n /**\n * Destroys the session and returns the Set-Cookie value (logout primitive).\n * セッションを破棄して Set-Cookie 値を返す (ログアウトの基本操作)。\n */\n const logout = async (request: Request) => {\n const session = await getSession(request)\n const cookie = await sessionStorage.destroySession(session)\n return cookie\n }\n\n return {\n sessionStorage,\n sessionKey,\n getSession,\n getSessionUser,\n saveSession,\n requireUser,\n requireAdminUser,\n logout,\n commitSession: sessionStorage.commitSession,\n destroySession: sessionStorage.destroySession,\n }\n}\n","/**\n * @module auth-server\n * @description Aggregate factory bundling session, authenticator, guards and handlers.\n * セッション・認証器・ガード・ハンドラを束ねる一括ファクトリ。\n *\n * daily-report の createDailyReportServer と同型の単一結線点。アプリは\n * `createAuthServer(config)` を 1 回呼び、返り値を各 `*.server.ts` shim から再輸出する。\n */\nimport { createAuthenticate } from \"./authenticate\"\nimport { createAuthenticateInLoader } from \"./authenticate-in-loader\"\nimport { createGoogleAuthenticator } from \"./google-strategy\"\nimport { createAuthHandlers } from \"./handlers\"\nimport { defaultLogger } from \"./logger\"\nimport type { AuthServerConfig } from \"./ports\"\nimport { createAuthSessionStorage } from \"./session\"\n\n/** createAuthServer が返す認証サーバー束。 */\nexport type AuthServer = ReturnType<typeof createAuthServer>\n\n/**\n * Builds the full auth server bundle from a single config.\n * 単一の設定から認証サーバー束一式を生成する。\n */\nexport const createAuthServer = (config: AuthServerConfig) => {\n const logger = config.logger ?? defaultLogger\n\n // セッションストレージ + ヘルパー (テストバックドアはポート委譲)\n const storage = createAuthSessionStorage(config.session, {\n mockUser: config.mockUser,\n loginPath: config.loginPath,\n postLoginRedirect: config.postLoginRedirect,\n isUserActive: config.isUserActive,\n })\n\n // Google 認証器 (verify コールバックはポート結線済み)\n const authenticator = createGoogleAuthenticator(config)\n\n // トークンリフレッシュ\n const authFns = createAuthenticate(config, storage, logger)\n\n // 毎リクエストのローダーガード (写真キャッシュ内包)\n const authenticateInLoader = createAuthenticateInLoader(config, storage, authFns, logger)\n\n // ルートハンドラ (login/logout/callback)\n const handlers = createAuthHandlers(config, storage, authenticator, logger)\n\n return {\n // 認証器\n authenticator,\n // セッション操作/トークンガードの内部束 (テストでの失敗注入等に使う。プロパティ差し替えは\n // authenticate / authenticateInLoader の内部呼び出しへ即時反映される)\n storage,\n authFns,\n sessionStorage: storage.sessionStorage,\n getSession: storage.getSession,\n getSessionUser: storage.getSessionUser,\n saveSession: storage.saveSession,\n requireUser: storage.requireUser,\n requireAdminUser: storage.requireAdminUser,\n commitSession: storage.commitSession,\n destroySession: storage.destroySession,\n // トークンガード\n authenticate: authFns.authenticate,\n refreshedAccessToken: authFns.refreshedAccessToken,\n authenticateInLoader,\n // ルートハンドラ\n ...handlers,\n }\n}\n"],"mappings":"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","names":["redirect","TokenRefreshError","message","GoogleInvalidGrantError","createAuthenticate","config","storage","logger","loginPath","sessionKey","getRefreshedGoogleAccessToken","session","refreshTokenFromSession","url","body","response","errorData","errorDescription","refreshedTokens","access_token","expires_in","error","description","refreshedAccessToken","provider","request","options","failureRedirect","user","accessToken","refreshToken","expirationDateMs","refreshed","refreshError","cookie","redirect","createAuthenticateInLoader","config","storage","authFns","logger","openidPhotoCache","pendingOpenidPhotoQueries","getCachedOpenidPhoto","openid","cached","pending","fetchPromise","photo","error","request","options","failureRedirect","session","currentUser","authenticatedTokens","authenticationPayload","updatedSessionUser","updatedUser","cookie","destroyedCookie","destroyError","resolveAuthMessages","redirect","Authenticator","OAuth2Strategy","GoogleStrategyDefaultScopes","GoogleStrategyDefaultName","userInfoURL","GoogleStrategy","clientId","clientSecret","redirectURI","scopes","accessType","includeGrantedScopes","prompt","hd","loginHint","verify","scopeList","params","_request","tokens","response","raw","defaultLogger","args","createGoogleAuthenticator","config","logger","defaultLogger","messages","resolveAuthMessages","loginPath","googleStrategy","GoogleStrategy","_request","tokens","accessToken","refreshToken","expiresIn","scope","_idToken","sub","email","picture","_hd","email_verified","_photos","profile","id","displayName","_familyName","_givenName","_emails","provider","errorMessage","redirect","userEmail","error","expirationDateMs","loggable","friendlyErrorMessage","conditionMet","cause","encodedErrorMessage","authenticator","Authenticator","redirect","createAuthHandlers","config","storage","authenticator","logger","loginPath","postLoginRedirect","postLogoutRedirect","loginLoader","request","loginAction","formData","action","user","logout","logoutAndRedirect","cookie","headers","googleProfile","previousRefreshToken","authenticationPayload","error","createCookieSessionStorage","redirect","createAuthSessionStorage","config","options","sessionKey","maxAge","loginPath","postLoginRedirect","mockUser","isUserActive","sessionStorage","getSession","request","getSessionUser","user","saveSession","session","requireUser","sessionUser","createAuthServer","config","logger","defaultLogger","storage","createAuthSessionStorage","authenticator","createGoogleAuthenticator","authFns","createAuthenticate","authenticateInLoader","createAuthenticateInLoader","handlers","createAuthHandlers"]}
1
+ {"version":3,"sources":["../src/server/authenticate.ts","../src/server/authenticate-in-loader.ts","../src/server/google-strategy.ts","../src/server/google-strategy-impl.ts","../src/server/logger.ts","../src/server/handlers.ts","../src/server/session.ts","../src/server/auth-server.ts"],"sourcesContent":["/**\n * @module authenticate\n * @description Session token guard with Google refresh-token rotation.\n * Google リフレッシュトークンによるセッショントークンガード。\n *\n * 原本 (quants-react-router authenticate.server.ts) を挙動保存で移植。\n * env (GOOGLE_CLIENT_ID/SECRET) は Google 設定注入に、destroySession はセッションストレージ\n * 注入に、テストバックドアは mockUser ポート注入に置き換えた。\n * 期限判定 → リフレッシュ → 失敗時セッション破棄 + ログインへリダイレクトの流れは原本と等価。\n *\n * 参考: https://sergiodxa.com/articles/working-with-refresh-tokens-in-remix\n */\nimport type { SessionProfile } from \"@aiquants/auth-core\"\nimport { redirect, type Session } from \"react-router\"\nimport type { AuthServerConfig, LoggerPort } from \"./ports\"\nimport type { AuthSessionStorage } from \"./session\"\n\n/** トークンリフレッシュ失敗の基底エラー。 */\nexport class TokenRefreshError extends Error {\n constructor(message: string) {\n super(message)\n this.name = \"TokenRefreshError\"\n }\n}\n\n/** Google の invalid_grant (失効/取り消し済みリフレッシュトークン) エラー。 */\nexport class GoogleInvalidGrantError extends TokenRefreshError {\n constructor() {\n super(\"Google refresh token invalid or revoked\")\n this.name = \"GoogleInvalidGrantError\"\n }\n}\n\n/**\n * Signals that authentication is required when the caller opted out of redirecting.\n * 呼び出し側がリダイレクトを拒否した (`failureRedirect: null`) 際に認証要求を伝えるエラー。\n *\n * `redirect()` を投げてしまうと React Router がそれを本物の 302 として扱うため、\n * 「リダイレクトしない」という契約を型で表現できるこのエラーへ置き換える。\n * セッション破棄が発生した場合はその `Set-Cookie` を `setCookie` で運ぶ\n * (捨てると無効セッションがブラウザに残り続ける)。\n */\nexport class AuthRequiredError extends Error {\n readonly setCookie?: string\n\n constructor(setCookie?: string) {\n super(\"Authentication required\")\n this.name = \"AuthRequiredError\"\n this.setCookie = setCookie\n }\n}\n\n/** createAuthenticate が返す認証関数束。 */\nexport type AuthenticateFns = ReturnType<typeof createAuthenticate>\n\n/**\n * Builds the `authenticate` / `refreshedAccessToken` pair wired to injected config.\n * 注入設定に結線した `authenticate` / `refreshedAccessToken` を生成する。\n */\nexport const createAuthenticate = (config: AuthServerConfig, storage: AuthSessionStorage, logger: LoggerPort) => {\n const loginPath = config.loginPath ?? \"/auth/login\"\n const sessionKey = storage.sessionKey\n\n /**\n * Refreshes the Google access token using the refresh token in the session.\n * セッション内のリフレッシュトークンで Google アクセストークンを更新する。\n */\n const getRefreshedGoogleAccessToken = async (session: Session) => {\n const userFromSession = session.get(sessionKey)\n const refreshTokenFromSession = userFromSession?.refreshToken\n if (!refreshTokenFromSession) {\n throw new Error(\"Refresh token not found in session\")\n }\n try {\n const url = \"https://www.googleapis.com/oauth2/v4/token\"\n const body = {\n client_id: config.google.clientId,\n client_secret: config.google.clientSecret,\n grant_type: \"refresh_token\",\n refresh_token: refreshTokenFromSession,\n }\n\n const response = await fetch(url, {\n headers: {\n \"Content-Type\": \"application/json\",\n },\n method: \"POST\",\n body: JSON.stringify(body),\n })\n\n if (!response.ok) {\n // Google API からエラーが返された場合 (JSON パース失敗も考慮)\n const errorData = await response.json().catch(() => undefined)\n const isInvalidGrant = response.status === 400 && typeof errorData === \"object\" && !!errorData && \"error\" in errorData && errorData.error === \"invalid_grant\"\n if (isInvalidGrant) {\n throw new GoogleInvalidGrantError()\n }\n const errorDescription = typeof errorData === \"object\" && errorData && \"error_description\" in errorData ? String(errorData.error_description) : \"Unknown error\"\n throw new TokenRefreshError(`Google token refresh failed: ${response.status} ${errorDescription}`)\n }\n\n const refreshedTokens = await response.json()\n const { access_token, expires_in } = refreshedTokens // id_token, scope, token_type は不要\n\n if (!(access_token && expires_in)) {\n logger.error(\"Invalid token response from Google:\", refreshedTokens)\n throw new Error(\"Invalid token response from Google\")\n }\n\n return {\n accessToken: access_token,\n refreshToken: refreshTokenFromSession, // Google は通常リフレッシュトークンを再発行しない\n expirationDateMs: Number(Date.now()) + expires_in * 1000,\n }\n } catch (error) {\n if (error instanceof TokenRefreshError) {\n throw error\n }\n const description = error instanceof Error ? error.message : \"Unknown error\"\n throw new TokenRefreshError(`Failed to refresh Google access token: ${description}`)\n }\n }\n\n /**\n * Dispatches the token refresh by session provider (currently Google only).\n * セッションのプロバイダー別にトークンリフレッシュを振り分ける (現状 Google のみ)。\n */\n const refreshedAccessToken = async (session: Session) => {\n const sessionProfile: SessionProfile = session.get(sessionKey)\n const provider = sessionProfile.provider\n if (!provider) {\n throw new Error(\"Provider not found in session\")\n }\n switch (provider) {\n case \"google\":\n return await getRefreshedGoogleAccessToken(session)\n default:\n throw new Error(\"Provider not supported\")\n }\n }\n\n /**\n * Validates session tokens, refreshing them when expired; destroys the session on refresh failure.\n * セッショントークンを検証し、期限切れならリフレッシュする。失敗時はセッションを破棄する。\n */\n const authenticate = async (request: Request, session: Session, options?: { failureRedirect?: string | null }) => {\n const { failureRedirect } = options || {}\n\n // failureRedirect が null のときはリダイレクトを作らず AuthRequiredError を投げる。\n // undefined / 文字列のときは従来どおり redirect() を投げる。\n const authFailure = (setCookie?: string) => {\n if (failureRedirect === null) {\n return new AuthRequiredError(setCookie)\n }\n return setCookie !== undefined ? redirect(failureRedirect || loginPath, { headers: { \"Set-Cookie\": setCookie } }) : redirect(failureRedirect || loginPath)\n }\n try {\n // テストバックドア (ポート注入時のみ有効)\n if (config.mockUser?.enabled(request)) {\n return {\n accessToken: \"mock-access-token\",\n refreshToken: \"mock-refresh-token\",\n expirationDateMs: Date.now() + 3600000,\n }\n }\n\n // セッションから認証データを取得\n const user = session.get(sessionKey)\n if (!(user?.accessToken && user.expirationDateMs)) {\n throw authFailure()\n }\n\n const { accessToken, refreshToken, expirationDateMs } = user\n\n // 期限切れならエラーを投げてリフレッシュ経路へ\n if (expirationDateMs < Date.now()) {\n if (!refreshToken) {\n // リフレッシュトークンがない場合は更新できないので認証失敗として扱う\n throw authFailure()\n }\n throw new Error(\"Expired\")\n }\n\n // 期限内ならそのまま返す\n return { accessToken, refreshToken, expirationDateMs }\n } catch (error) {\n // 期限切れエラーのみリフレッシュを試みる\n if (error instanceof Error && error.message === \"Expired\") {\n try {\n const refreshed = await refreshedAccessToken(session)\n\n // セッションを新しいトークンで更新\n const user = session.get(sessionKey)\n session.set(sessionKey, { ...user, ...refreshed })\n\n // アクセストークンを返す (commit は呼び出し側の責務)\n return refreshed\n } catch (refreshError) {\n if (refreshError instanceof GoogleInvalidGrantError) {\n logger.warn(\"Google refresh token invalid or revoked\")\n } else {\n const message = refreshError instanceof Error ? refreshError.message : \"Unknown error\"\n logger.error(\"Failed to refresh token:\", message)\n }\n // リフレッシュに失敗した場合はセッションを破棄してログインページへ\n const cookie = await storage.destroySession(session)\n throw authFailure(cookie)\n }\n }\n\n // その他の予期せぬエラーは再 throw\n throw error\n }\n }\n\n return { authenticate, refreshedAccessToken }\n}\n","/**\n * @module authenticate-in-loader\n * @description Per-request loader authentication guard with photo cache and warmup.\n * 写真キャッシュとウォームアップ付きの毎リクエストローダー認証ガード。\n *\n * 原本 (quants-react-router authenticateInLoader.server.ts) を挙動保存で移植。\n * DB クエリ (getOpenids) は `getUserPhoto` ポートへ、DB プールウォームアップは `warmup`\n * ポートへ置き換えた。写真キャッシュ (Map + pending dedup) はファクトリインスタンスごとに\n * 保持する (module-level mutable state の排除)。\n */\nimport type { AuthenticationPayload, MyGoogleProfile, SessionProfile } from \"@aiquants/auth-core\"\nimport { redirect } from \"react-router\"\nimport { AuthRequiredError, type AuthenticateFns } from \"./authenticate\"\nimport type { AuthServerConfig, LoggerPort } from \"./ports\"\nimport type { AuthSessionStorage } from \"./session\"\n\n/** createAuthenticateInLoader が返すローダーガード関数。 */\nexport type AuthenticateInLoader = ReturnType<typeof createAuthenticateInLoader>\n\n/**\n * Builds the per-request loader guard wired to injected ports.\n * 注入ポートに結線した毎リクエストローダーガードを生成する。\n */\nexport const createAuthenticateInLoader = (config: AuthServerConfig, storage: AuthSessionStorage, authFns: AuthenticateFns, logger: LoggerPort) => {\n // 写真キャッシュ (インスタンススコープ。同時リクエストは pending dedup で 1 クエリに集約)\n const openidPhotoCache = new Map<string, string>()\n const pendingOpenidPhotoQueries = new Map<string, Promise<string>>()\n\n /**\n * Resolves the user photo with cache + in-flight dedup, delegating the query to the port.\n * キャッシュ + 実行中 dedup 付きでユーザー写真を解決する (実クエリはポートへ委譲)。\n */\n const getCachedOpenidPhoto = async (openid: string): Promise<string> => {\n const cached = openidPhotoCache.get(openid)\n if (cached !== undefined) {\n return cached\n }\n\n const pending = pendingOpenidPhotoQueries.get(openid)\n if (pending !== undefined) {\n return pending\n }\n\n const fetchPromise = (async () => {\n try {\n const photo = config.getUserPhoto ? await config.getUserPhoto(openid) : \"\"\n openidPhotoCache.set(openid, photo)\n return photo\n } catch (error) {\n openidPhotoCache.delete(openid)\n throw error\n } finally {\n pendingOpenidPhotoQueries.delete(openid)\n }\n })()\n\n pendingOpenidPhotoQueries.set(openid, fetchPromise)\n return fetchPromise\n }\n\n /**\n * Handles authentication within loader requests by refreshing session tokens when needed.\n * ローダーリクエスト内で必要に応じてセッショントークンを更新する認証処理。\n *\n * @param request - リクエストオブジェクト。\n * @param options - 認証オプション。`failureRedirect: null` でリダイレクトを明示的にスキップ。\n * @returns 認証済みユーザープロファイルとオプショナルな Set-Cookie 値。\n */\n const authenticateInLoader = async (request: Request, options: { failureRedirect: string } | { failureRedirect: null }): Promise<{ user?: MyGoogleProfile; cookie?: string }> => {\n // DB 接続プールが確実にオープンするのを待ってから処理を開始する (E2E 等でのコールドスタート競合回避)\n if (config.warmup) {\n await config.warmup()\n }\n\n const { failureRedirect } = options\n const session = await storage.getSession(request)\n const currentUser = await storage.getSessionUser(request)\n\n if (currentUser === undefined) {\n if (failureRedirect === null) {\n return {}\n }\n throw redirect(failureRedirect)\n }\n\n try {\n // トークンの有効期限を確認し、必要に応じて更新\n const authenticatedTokens = await authFns.authenticate(request, session, {\n failureRedirect,\n })\n\n // 認証ペイロードの構築\n const authenticationPayload: AuthenticationPayload = {\n accessToken: authenticatedTokens.accessToken,\n refreshToken: authenticatedTokens.refreshToken,\n expirationDateMs: authenticatedTokens.expirationDateMs,\n provider: currentUser.provider,\n }\n // セッション内のユーザ情報\n const updatedSessionUser: SessionProfile = {\n id: currentUser.id,\n displayName: currentUser.displayName,\n name: currentUser.name,\n emails: currentUser.emails,\n ...authenticationPayload,\n role: currentUser.role,\n }\n\n // ユーザー写真をキャッシュまたはポートから取得\n const photo = await getCachedOpenidPhoto(currentUser.id)\n\n // ユーザープロファイル情報\n const updatedUser: MyGoogleProfile = {\n ...currentUser,\n ...authenticationPayload,\n photo,\n }\n\n // トークンが更新 (リフレッシュ) された場合のみ、セッションを保存して Cookie ヘッダーを設定する\n const isRefreshed = authenticatedTokens.accessToken !== currentUser.accessToken\n const cookie = isRefreshed ? await storage.saveSession(request, updatedSessionUser) : undefined\n\n return { user: updatedUser, cookie }\n } catch (error) {\n // failureRedirect: null の契約 (リダイレクトせず未認証として返す) を守る。\n // 以前は authenticate() が投げた redirect Response を下の Location 判定が\n // 無条件に再スローしており、null 指定でも /auth/login へ飛んでいた。\n if (error instanceof AuthRequiredError) {\n return error.setCookie !== undefined ? { cookie: error.setCookie } : {}\n }\n if (error instanceof Response && error.headers.get(\"Location\")) {\n throw error\n }\n logger.error(\"Error in authenticateInLoader:\", error)\n // 認証エラー時はセッションを破棄してリダイレクト\n if (failureRedirect !== null) {\n // セッション破棄のみを try で保護する。redirect の throw を try 内に置くと\n // 同じ catch(destroyError) に捕捉されて Set-Cookie 無しの fallback に差し替わってしまうため、\n // throw redirect は必ず try の外で行う。\n let destroyedCookie: string | undefined\n try {\n destroyedCookie = await storage.destroySession(session)\n } catch (destroyError) {\n logger.error(\"Failed to destroy session during auth error:\", destroyError)\n }\n // 破棄に成功していれば Set-Cookie を付与し、失敗してもとにかくリダイレクトは試みる\n throw destroyedCookie !== undefined ? redirect(failureRedirect, { headers: { \"Set-Cookie\": destroyedCookie } }) : redirect(failureRedirect)\n }\n throw error\n }\n }\n\n return authenticateInLoader\n}\n","/**\n * @module google-strategy\n * @description Google OAuth authenticator factory (remix-auth v4 + local GoogleStrategy).\n * Google OAuth 認証器ファクトリ (remix-auth v4 + 同梱の GoogleStrategy)。\n *\n * 原本 (quants-react-router auth.server.ts) の verify コールバックを挙動保存で移植。\n * env 読取り・backend HTTP・allowlist・エラーメッセージはすべてポート/設定注入に置き換えた。\n * 検証順序 (Google プロフィール検証 → backend verify → allowlist → backend signup)、\n * リダイレクト先 (`<loginPath>?error=<encoded>`)、エラーメッセージ文言は原本と等価。\n */\nimport type { MyGoogleProfile } from \"@aiquants/auth-core\"\nimport { type AuthMessages, resolveAuthMessages } from \"@aiquants/auth-core\"\nimport { redirect } from \"react-router\"\nimport { Authenticator } from \"remix-auth\"\nimport { GoogleStrategy } from \"./google-strategy-impl\"\nimport { defaultLogger } from \"./logger\"\nimport type { AuthServerConfig } from \"./ports\"\n\ninterface GoogleToken {\n id_token?: string\n access_token?: string\n refresh_token?: string\n expires_in?: number\n scope?: string\n}\n\n/**\n * Builds a remix-auth Authenticator with the Google strategy wired to injected ports.\n * 注入ポートに結線した Google ストラテジー付き Authenticator を生成する。\n */\nexport const createGoogleAuthenticator = (config: AuthServerConfig): Authenticator<MyGoogleProfile> => {\n const logger = config.logger ?? defaultLogger\n const messages: AuthMessages = resolveAuthMessages(config.messages)\n const loginPath = config.loginPath ?? \"/auth/login\"\n\n const googleStrategy = new GoogleStrategy<MyGoogleProfile>(\n {\n clientId: config.google.clientId,\n clientSecret: config.google.clientSecret,\n redirectURI: config.google.redirectURI,\n scopes: config.google.scopes,\n accessType: config.google.accessType ?? \"offline\",\n includeGrantedScopes: config.google.includeGrantedScopes ?? true,\n prompt: config.google.prompt ?? \"consent\",\n },\n /**\n * Google 認証後のコールバック。Google のトークンとプロフィールを検証し、\n * backend verify → allowlist → backend signup を経て MyGoogleProfile を返す。\n * 失敗時は `<loginPath>?error=<encoded>` へのリダイレクト Response を throw する。\n */\n async ({ request: _request, tokens }) => {\n const { access_token: accessToken, refresh_token: refreshToken, expires_in: expiresIn, scope, id_token: _idToken } = tokens.data as GoogleToken\n const {\n _json: { sub, email, picture, hd: _hd, email_verified },\n photos: _photos,\n ...profile\n } = await GoogleStrategy.userProfile(tokens)\n const {\n id,\n displayName,\n name: { familyName: _familyName, givenName: _givenName },\n emails: _emails,\n } = profile\n const provider = \"google\"\n\n // first verify (トークン/プロフィール欠落は Google 認証失敗として扱う)\n if (\n accessToken === undefined ||\n refreshToken === undefined ||\n expiresIn === undefined ||\n scope === undefined ||\n id === undefined ||\n displayName === undefined ||\n sub === undefined ||\n email === undefined ||\n email_verified === undefined\n // hd === undefined // Google Workspace などの組織制限をする場合は有効化\n ) {\n if (email_verified === false) {\n const errorMessage = encodeURIComponent(messages.emailNotVerified)\n throw redirect(`${loginPath}?error=${errorMessage}`)\n }\n const errorMessage = encodeURIComponent(messages.googleFailed)\n throw redirect(`${loginPath}?error=${errorMessage}`)\n }\n\n logger.info(\"Authenticated on Google: \", profile.displayName)\n\n // Backend Authentication\n try {\n // backendAuth ポート未指定時は verify/signup をスキップ (フロント単独運用)\n if (config.backendAuth) {\n const ok = await config.backendAuth.verify(provider, accessToken)\n\n if (!ok) {\n logger.error(\"Backend verification returned false for user:\", profile.displayName)\n const errorMessage = encodeURIComponent(messages.backendFailed)\n throw redirect(`${loginPath}?error=${errorMessage}`)\n }\n\n logger.info(\"Authenticated on Backend: \", profile.displayName)\n }\n\n // メールアドレスまたはドメインベースでのアクセス制限チェック (論理和は allowlist 実装側)\n // ⚠️ `await` 必須。非同期実装を await せずに評価すると Promise が truthy 判定され\n // 許可判定を素通りする (fail-open)。この await は spec で門番されている。\n const userEmail = profile.emails[0]?.value\n if (!(await config.isEmailAllowed(userEmail))) {\n const error = new Error(\"You are not allowed to access this application\")\n logger.error(error)\n throw error\n }\n\n // 無効化済みアカウントはログインさせない (allowlist とは別軸の判定)\n if (config.isUserActive && !(await config.isUserActive(profile))) {\n const error = new Error(\"You are not allowed to access this application\")\n logger.error(\"Deactivated account attempted to sign in:\", userEmail)\n throw error\n }\n\n // バックエンドでのサインアップ処理 (毎ログイン実行の挙動を保存)\n if (config.backendAuth) {\n await config.backendAuth.signup(provider, accessToken)\n }\n\n logger.info(\"Authenticated complete: \", profile.displayName)\n\n // 認証ペイロードの構築\n const expirationDateMs = Date.now() + expiresIn * 1000\n const authenticationPayload = {\n accessToken: accessToken,\n refreshToken: refreshToken,\n expirationDateMs: expirationDateMs,\n provider: provider,\n }\n // ユーザープロファイル情報\n const user: MyGoogleProfile = {\n ...profile,\n ...authenticationPayload,\n photo: picture,\n }\n return user\n } catch (error: unknown) {\n let loggable = error\n if (typeof error === \"object\" && error !== null && \"cause\" in error) {\n // cause があればそれを優先してログに残す (falsy なら元のエラー)\n loggable = (error as { cause: unknown }).cause || error\n }\n logger.error(\"Error during backend authentication or subsequent processing:\", loggable)\n\n let friendlyErrorMessage = messages.unexpected\n let conditionMet = false\n\n // ネットワーク到達不可 (getaddrinfo ENOTFOUND) の判定\n if (typeof error === \"object\" && error !== null && \"cause\" in error) {\n const cause = (error as { cause: unknown }).cause\n if (cause && typeof cause === \"object\" && \"message\" in cause && typeof (cause as { message: string }).message === \"string\" && (cause as { message: string }).message.includes(\"getaddrinfo ENOTFOUND\")) {\n friendlyErrorMessage = messages.backendUnreachable\n conditionMet = true\n }\n }\n\n // allowlist 拒否の判定\n if (!conditionMet && error instanceof Error && error.message.includes(\"You are not allowed to access this application\")) {\n friendlyErrorMessage = messages.notAllowed\n conditionMet = true\n }\n\n // 既にリダイレクト Response が throw されている場合はそのまま再 throw\n if (!conditionMet && error instanceof Response && error.headers.get(\"Location\")?.startsWith(loginPath)) {\n throw error\n }\n\n const encodedErrorMessage = encodeURIComponent(friendlyErrorMessage)\n throw redirect(`${loginPath}?error=${encodedErrorMessage}`)\n }\n },\n )\n\n const authenticator = new Authenticator<MyGoogleProfile>()\n authenticator.use(googleStrategy)\n return authenticator\n}\n","/**\n * @module google-strategy-impl\n * @description Self-contained Google OAuth2 strategy (ported from @coji/remix-auth-google).\n * 自己完結型の Google OAuth2 ストラテジー (@coji/remix-auth-google から移植)。\n *\n * @coji/remix-auth-google@4.2.0 の実装 (68 行) を挙動保存で移植し、外部依存を除去する。\n * OAuth2 の本体機構 (PKCE / state / トークン交換) は継承元の remix-auth-oauth2 が担うため\n * 移植対象ではない。ここに置くのは Google 固有の設定 (エンドポイント URL・既定スコープ)、\n * 認可パラメータの上書き (access_type / include_granted_scopes / prompt / hd / login_hint)、\n * および userinfo (v3) からプロファイルを取得する静的メソッドのみ。\n *\n * 既定スコープ・URL・プロファイル整形 (sub→id、picture→photos、_json=raw) は原本と byte 一致。\n */\nimport { OAuth2Strategy } from \"remix-auth-oauth2\"\n\n/** Google の既定スコープ (原本と同一。フル URL 形式)。 */\nexport const GoogleStrategyDefaultScopes = [\"openid\", \"https://www.googleapis.com/auth/userinfo.profile\", \"https://www.googleapis.com/auth/userinfo.email\"]\n\n/** ストラテジー名 (remix-auth の Authenticator 登録キー)。 */\nexport const GoogleStrategyDefaultName = \"google\"\n\n// userinfo エンドポイント (v3)。プロファイル取得のみに用いる\nconst userInfoURL = \"https://www.googleapis.com/oauth2/v3/userinfo\"\n\n/** @see https://developers.google.com/identity/protocols/oauth2/scopes */\nexport type GoogleScope = string\n\n/** GoogleStrategy のコンストラクタオプション (原本 @coji と同一)。 */\nexport type GoogleStrategyOptions = {\n clientId: string\n clientSecret: string\n redirectURI: string\n /** @default \"openid profile email\" 相当 (GoogleStrategyDefaultScopes)。 */\n scopes?: GoogleScope[]\n accessType?: \"online\" | \"offline\"\n includeGrantedScopes?: boolean\n prompt?: \"none\" | \"consent\" | \"select_account\"\n hd?: string\n loginHint?: string\n}\n\n/** userinfo から取得・整形した Google プロファイル (原本 GoogleProfile と同一形状)。 */\nexport type GoogleProfile = {\n id: string\n displayName: string\n name: {\n familyName: string\n givenName: string\n }\n emails: [{ value: string }]\n photos: [{ value: string }]\n _json: {\n sub: string\n name: string\n given_name: string\n family_name: string\n picture: string\n locale: string\n email: string\n email_verified: boolean\n hd: string\n }\n}\n\n// userProfile が必要とする最小のトークン形状 (arctic の OAuth2Tokens の部分集合)。\n// 直接 arctic へ依存しないための構造的型。実際に呼ぶのは accessToken() のみ。\ntype OAuth2AccessTokenSource = { accessToken(): string }\n\n/**\n * Google OAuth2 strategy for remix-auth (extends remix-auth-oauth2's OAuth2Strategy).\n * remix-auth 用の Google OAuth2 ストラテジー (remix-auth-oauth2 の OAuth2Strategy を継承)。\n */\nexport class GoogleStrategy<User> extends OAuth2Strategy<User> {\n name = GoogleStrategyDefaultName\n private readonly scopes: GoogleScope[]\n private readonly accessType: \"online\" | \"offline\"\n private readonly prompt?: \"none\" | \"consent\" | \"select_account\"\n private readonly includeGrantedScopes: boolean\n private readonly hd?: string\n private readonly loginHint?: string\n\n constructor({ clientId, clientSecret, redirectURI, scopes, accessType, includeGrantedScopes, prompt, hd, loginHint }: GoogleStrategyOptions, verify: OAuth2Strategy<User>[\"verify\"]) {\n const scopeList = scopes ?? GoogleStrategyDefaultScopes\n super(\n {\n clientId,\n clientSecret,\n redirectURI,\n authorizationEndpoint: \"https://accounts.google.com/o/oauth2/v2/auth\",\n tokenEndpoint: \"https://oauth2.googleapis.com/token\",\n scopes: scopeList,\n },\n verify,\n )\n this.scopes = scopeList\n this.accessType = accessType ?? \"online\"\n this.includeGrantedScopes = includeGrantedScopes ?? false\n this.prompt = prompt\n this.hd = hd\n this.loginHint = loginHint\n }\n\n /**\n * Adds Google-specific authorization parameters to the redirect URL.\n * 認可リダイレクト URL へ Google 固有パラメータを付与する。\n */\n protected authorizationParams(params: URLSearchParams, _request?: Request): URLSearchParams {\n params.set(\"access_type\", this.accessType)\n params.set(\"include_granted_scopes\", String(this.includeGrantedScopes))\n if (this.scopes && this.scopes.length > 0) {\n params.set(\"scope\", this.scopes.join(\" \"))\n }\n if (this.prompt) {\n params.set(\"prompt\", this.prompt)\n }\n if (this.hd) {\n params.set(\"hd\", this.hd)\n }\n if (this.loginHint) {\n params.set(\"login_hint\", this.loginHint)\n }\n return params\n }\n\n /**\n * Fetches the Google user profile from the userinfo (v3) endpoint.\n * userinfo (v3) エンドポイントから Google ユーザープロファイルを取得する。\n *\n * @throws プロファイル取得に失敗した場合 (レスポンスが ok でない)。\n */\n static async userProfile(tokens: OAuth2AccessTokenSource): Promise<GoogleProfile> {\n const response = await fetch(userInfoURL, {\n headers: {\n Authorization: `Bearer ${tokens.accessToken()}`,\n },\n })\n if (!response.ok) {\n throw new Error(`Failed to fetch user profile: ${response.statusText}`)\n }\n const raw = await response.json()\n const profile: GoogleProfile = {\n id: raw.sub,\n displayName: raw.name,\n name: {\n familyName: raw.family_name,\n givenName: raw.given_name,\n },\n emails: [{ value: raw.email }],\n photos: [{ value: raw.picture }],\n _json: raw,\n }\n return profile\n }\n}\n","/**\n * @module logger\n * @description Default console-backed logger used when no logger port is injected.\n * ロガーポート未注入時に使う console ベースの既定ロガー。\n */\nimport type { LoggerPort } from \"./ports\"\n\n/** console ベースの既定ロガー (ライブラリ既定のフォールバックとして意図的に console を使用)。 */\nexport const defaultLogger: LoggerPort = {\n // biome-ignore lint/suspicious/noConsole: ロガー未注入時の意図的な console フォールバック\n info: (...args: unknown[]) => console.info(\"[Auth]\", ...args),\n warn: (...args: unknown[]) => console.warn(\"[Auth]\", ...args),\n error: (...args: unknown[]) => console.error(\"[Auth]\", ...args),\n}\n","/**\n * @module handlers\n * @description Route handler factories for login / logout / Google callback.\n * login / logout / Google コールバックのルートハンドラ工場。\n *\n * 原本 (quants-react-router routes/_auth+ の 4 ルート + features/logoutAndRedirect.ts) の\n * サーバー側ロジックを挙動保存で移植。UI (JSX) はアプリ側に残す。\n * コールバックのセッション保存形状 (SessionProfile、photo/_json/photos を含まない) は\n * cookie byte 互換のため厳密に保存する。\n */\nimport type { AuthenticationPayload, MyGoogleProfile, SessionProfile } from \"@aiquants/auth-core\"\nimport { redirect } from \"react-router\"\nimport type { Authenticator } from \"remix-auth\"\nimport type { AuthServerConfig, LoggerPort } from \"./ports\"\nimport type { AuthSessionStorage } from \"./session\"\n\n/** createAuthHandlers が返すハンドラ束。 */\nexport type AuthHandlers = ReturnType<typeof createAuthHandlers>\n\n/**\n * Builds loader/action handlers for the auth routes.\n * 認証ルート用の loader/action ハンドラ群を生成する。\n */\nexport const createAuthHandlers = (config: AuthServerConfig, storage: AuthSessionStorage, authenticator: Authenticator<MyGoogleProfile>, logger: LoggerPort) => {\n const loginPath = config.loginPath ?? \"/auth/login\"\n const postLoginRedirect = config.postLoginRedirect ?? \"/\"\n const postLogoutRedirect = config.postLogoutRedirect ?? \"/\"\n\n /**\n * Login-page loader: redirects authenticated users, passes the error message through.\n * ログインページ loader。認証済みならリダイレクトし、エラーメッセージを data として返す。\n */\n const loginLoader = async ({ request }: { request: Request }): Promise<{ error: string | null }> => {\n const user = await storage.getSessionUser(request)\n if (user) {\n // 認証済みならトップページにリダイレクト\n throw redirect(postLoginRedirect)\n }\n const url = new URL(request.url)\n const error = url.searchParams.get(\"error\")\n return { error } // エラーメッセージを loader data として渡す\n }\n\n /**\n * Login action: starts the Google OAuth flow for the \"Sign In with Google\" form action.\n * ログイン action。\"Sign In with Google\" フォーム action で Google OAuth フローを開始する。\n */\n const loginAction = async ({ request }: { request: Request }): Promise<MyGoogleProfile | null> => {\n const formData = await request.clone().formData()\n const action = String(formData.get(\"_action\"))\n\n let user: MyGoogleProfile | null = null\n switch (action) {\n case \"Sign In with Google\":\n user = await authenticator.authenticate(\"google\", request)\n break\n default:\n user = null\n }\n return user\n }\n\n /**\n * Destroys the session and returns the Set-Cookie value (logout primitive).\n * セッションを破棄して Set-Cookie 値を返す (ログアウトの基本操作)。\n */\n const logout = async (request: Request) => {\n return await storage.logout(request)\n }\n\n /**\n * Logs out and redirects with the destroyed-session cookie attached.\n * ログアウトし、破棄済みセッション Cookie を付与してリダイレクトする。\n */\n const logoutAndRedirect = async (request: Request) => {\n const cookie = await logout(request)\n const headers = new Headers()\n headers.append(\"Set-Cookie\", cookie)\n return redirect(postLogoutRedirect, { headers })\n }\n\n /**\n * Logout-route loader: performs logout and throws the redirect.\n * ログアウトルート loader。ログアウトを実行してリダイレクトを throw する。\n */\n const logoutLoader = async ({ request }: { request: Request }): Promise<never> => {\n throw await logoutAndRedirect(request)\n }\n\n /**\n * Google OAuth callback loader: authenticates, saves the session and redirects home.\n * Google OAuth コールバック loader。認証してセッションを保存し、ホームへリダイレクトする。\n */\n const googleCallbackLoader = async ({ request }: { request: Request }): Promise<never> => {\n try {\n const googleProfile = await authenticator.authenticate(\"google\", request)\n\n // 既存のセッションから前回のユーザー情報を取得して refresh_token を保護する\n const existingSession = await storage.getSession(request)\n const existingUser = existingSession.get(storage.sessionKey) as SessionProfile | undefined\n const previousRefreshToken = existingUser?.refreshToken\n\n // 認証ペイロードの構築\n const authenticationPayload: AuthenticationPayload = {\n accessToken: googleProfile.accessToken,\n refreshToken: googleProfile.refreshToken || previousRefreshToken,\n expirationDateMs: googleProfile.expirationDateMs,\n provider: googleProfile.provider,\n }\n\n // セッション内のユーザ情報 (photo/_json/photos を含まない byte 互換形状)\n const user: SessionProfile = {\n id: googleProfile.id,\n displayName: googleProfile.displayName,\n name: googleProfile.name,\n emails: googleProfile.emails,\n ...authenticationPayload,\n role: googleProfile.role,\n }\n\n // 認証後のユーザ情報をセッションに保存\n const cookie = await storage.saveSession(request, user)\n\n // ホームへリダイレクト\n throw redirect(postLoginRedirect, { headers: { \"Set-Cookie\": cookie } })\n } catch (error) {\n // authenticator.authenticate は認証失敗時に Response か Error を throw する。\n // access_denied のような OAuth エラーでは Error の可能性があるため、\n // どちらの場合でもログインページへのリダイレクトが安全 (Response はそのまま再 throw)。\n if (error instanceof Response) {\n throw error\n }\n logger.error(\"Authentication error:\", error)\n throw redirect(loginPath)\n }\n }\n\n return { loginLoader, loginAction, logoutLoader, logout, logoutAndRedirect, googleCallbackLoader }\n}\n","/**\n * @module session\n * @description Cookie session storage factory with a DI test backdoor.\n * DI テストバックドア付きの cookie セッションストレージファクトリ。\n *\n * 原本 (quants-react-router session.server.ts) を挙動保存で移植。相違点は\n * (1) module-level の env throw をファクトリ引数検証へ移した、\n * (2) テストバックドア (mock_user cookie) をハードコードから `mockUser` ポート注入に変えた、\n * の 2 点のみ。cookie 形状 (`__session`/httpOnly/lax/30d、キー \"user\") は byte 互換。\n */\nimport type { GoogleProfileBase, SessionProfile } from \"@aiquants/auth-core\"\nimport { createCookieSessionStorage, redirect } from \"react-router\"\nimport type { MockUserPort, SessionConfig } from \"./ports\"\n\n/** createAuthSessionStorage が返すセッション操作束。 */\nexport type AuthSessionStorage = ReturnType<typeof createAuthSessionStorage>\n\n/**\n * Builds the cookie session storage and session helpers.\n * cookie セッションストレージとセッションヘルパー群を生成する。\n *\n * @throws secrets が空の場合 (fail-closed の設定ガード)。\n */\nexport const createAuthSessionStorage = (config: SessionConfig, options?: { mockUser?: MockUserPort; loginPath?: string; postLoginRedirect?: string; isUserActive?: (profile: GoogleProfileBase) => boolean | Promise<boolean> }) => {\n // ファクトリ実行時検証 (import 時副作用の排除)\n if (!config.secrets || config.secrets.length === 0 || config.secrets.some((s) => !s)) {\n throw new Error(\"REMIX_SESSION_SECRET must be set\")\n }\n\n const sessionKey = config.sessionKey ?? \"user\"\n const maxAge = config.maxAge ?? 60 * 60 * 24 * 30\n const loginPath = options?.loginPath ?? \"/auth/login\"\n const postLoginRedirect = options?.postLoginRedirect ?? \"/\"\n const mockUser = options?.mockUser\n const isUserActive = options?.isUserActive\n\n const sessionStorage = createCookieSessionStorage({\n cookie: {\n name: config.cookieName ?? \"__session\",\n httpOnly: true, // for security reasons, make this cookie http only\n path: \"/\", // remember to add this so the cookie will work in all routes\n sameSite: \"lax\", // this helps with CSRF\n secrets: config.secrets,\n secure: config.secure ?? process.env.NODE_ENV === \"production\",\n maxAge,\n },\n })\n\n /**\n * Reads the session from the request cookie.\n * リクエストの Cookie からセッションを取得する。\n */\n const getSession = async (request: Request) => {\n return await sessionStorage.getSession(request.headers.get(\"Cookie\"))\n }\n\n /**\n * Reads the session user, honoring the injected test backdoor when enabled.\n * セッションからユーザー情報を取得する (注入されたテストバックドアが有効ならそれを返す)。\n */\n const getSessionUser = async (request: Request): Promise<SessionProfile | undefined> => {\n // テストバックドア (ポート注入時のみ有効。判定条件はアプリ側の enabled に委譲)\n if (mockUser?.enabled(request)) {\n return mockUser.resolve(request)\n }\n\n const session = await getSession(request)\n const user = session.get(sessionKey) as SessionProfile | undefined\n if (user === undefined) return undefined\n // 無効化済みアカウントは「未認証」として扱う。これにより、既に発行済みのセッション Cookie を\n // 持っていてもガードは通らず、無効化がその場で全ルートへ波及する。\n if (isUserActive && !(await isUserActive(user))) return undefined\n return user\n }\n\n /**\n * Saves the user into the session and returns the Set-Cookie value.\n * セッションへユーザー情報を保存し、Set-Cookie 値を返す。\n */\n const saveSession = async (request: Request, user: SessionProfile) => {\n const session = await getSession(request)\n session.set(sessionKey, user)\n return await sessionStorage.commitSession(session)\n }\n\n /**\n * Authenticated-user guard: redirects to the login page when absent.\n * 認証ユーザーガード。未認証ならログインページへリダイレクトする。\n */\n const requireUser = async (request: Request) => {\n const sessionUser = await getSessionUser(request)\n if (sessionUser === undefined) {\n throw redirect(loginPath)\n }\n return sessionUser as SessionProfile\n }\n\n /**\n * Admin-user guard: redirects to the app root when the role is not \"admin\".\n * 管理者ユーザーガード。role が \"admin\" でなければルートへリダイレクトする。\n */\n const requireAdminUser = async (request: Request) => {\n const sessionUser = await requireUser(request)\n if (sessionUser?.role !== \"admin\") {\n throw redirect(postLoginRedirect)\n }\n return sessionUser\n }\n\n /**\n * Destroys the session and returns the Set-Cookie value (logout primitive).\n * セッションを破棄して Set-Cookie 値を返す (ログアウトの基本操作)。\n */\n const logout = async (request: Request) => {\n const session = await getSession(request)\n const cookie = await sessionStorage.destroySession(session)\n return cookie\n }\n\n return {\n sessionStorage,\n sessionKey,\n getSession,\n getSessionUser,\n saveSession,\n requireUser,\n requireAdminUser,\n logout,\n commitSession: sessionStorage.commitSession,\n destroySession: sessionStorage.destroySession,\n }\n}\n","/**\n * @module auth-server\n * @description Aggregate factory bundling session, authenticator, guards and handlers.\n * セッション・認証器・ガード・ハンドラを束ねる一括ファクトリ。\n *\n * daily-report の createDailyReportServer と同型の単一結線点。アプリは\n * `createAuthServer(config)` を 1 回呼び、返り値を各 `*.server.ts` shim から再輸出する。\n */\nimport { createAuthenticate } from \"./authenticate\"\nimport { createAuthenticateInLoader } from \"./authenticate-in-loader\"\nimport { createGoogleAuthenticator } from \"./google-strategy\"\nimport { createAuthHandlers } from \"./handlers\"\nimport { defaultLogger } from \"./logger\"\nimport type { AuthServerConfig } from \"./ports\"\nimport { createAuthSessionStorage } from \"./session\"\n\n/** createAuthServer が返す認証サーバー束。 */\nexport type AuthServer = ReturnType<typeof createAuthServer>\n\n/**\n * Builds the full auth server bundle from a single config.\n * 単一の設定から認証サーバー束一式を生成する。\n */\nexport const createAuthServer = (config: AuthServerConfig) => {\n const logger = config.logger ?? defaultLogger\n\n // セッションストレージ + ヘルパー (テストバックドアはポート委譲)\n const storage = createAuthSessionStorage(config.session, {\n mockUser: config.mockUser,\n loginPath: config.loginPath,\n postLoginRedirect: config.postLoginRedirect,\n isUserActive: config.isUserActive,\n })\n\n // Google 認証器 (verify コールバックはポート結線済み)\n const authenticator = createGoogleAuthenticator(config)\n\n // トークンリフレッシュ\n const authFns = createAuthenticate(config, storage, logger)\n\n // 毎リクエストのローダーガード (写真キャッシュ内包)\n const authenticateInLoader = createAuthenticateInLoader(config, storage, authFns, logger)\n\n // ルートハンドラ (login/logout/callback)\n const handlers = createAuthHandlers(config, storage, authenticator, logger)\n\n return {\n // 認証器\n authenticator,\n // セッション操作/トークンガードの内部束 (テストでの失敗注入等に使う。プロパティ差し替えは\n // authenticate / authenticateInLoader の内部呼び出しへ即時反映される)\n storage,\n authFns,\n sessionStorage: storage.sessionStorage,\n getSession: storage.getSession,\n getSessionUser: storage.getSessionUser,\n saveSession: storage.saveSession,\n requireUser: storage.requireUser,\n requireAdminUser: storage.requireAdminUser,\n commitSession: storage.commitSession,\n destroySession: storage.destroySession,\n // トークンガード\n authenticate: authFns.authenticate,\n refreshedAccessToken: authFns.refreshedAccessToken,\n authenticateInLoader,\n // ルートハンドラ\n ...handlers,\n }\n}\n"],"mappings":"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","names":["redirect","TokenRefreshError","message","GoogleInvalidGrantError","AuthRequiredError","setCookie","createAuthenticate","config","storage","logger","loginPath","sessionKey","getRefreshedGoogleAccessToken","session","refreshTokenFromSession","url","body","response","errorData","errorDescription","refreshedTokens","access_token","expires_in","error","description","refreshedAccessToken","provider","request","options","failureRedirect","authFailure","user","accessToken","refreshToken","expirationDateMs","refreshed","refreshError","cookie","redirect","createAuthenticateInLoader","config","storage","authFns","logger","openidPhotoCache","pendingOpenidPhotoQueries","getCachedOpenidPhoto","openid","cached","pending","fetchPromise","photo","error","request","options","failureRedirect","session","currentUser","redirect","authenticatedTokens","authenticationPayload","updatedSessionUser","updatedUser","cookie","AuthRequiredError","destroyedCookie","destroyError","resolveAuthMessages","redirect","Authenticator","OAuth2Strategy","GoogleStrategyDefaultScopes","GoogleStrategyDefaultName","userInfoURL","GoogleStrategy","clientId","clientSecret","redirectURI","scopes","accessType","includeGrantedScopes","prompt","hd","loginHint","verify","scopeList","params","_request","tokens","response","raw","defaultLogger","args","createGoogleAuthenticator","config","logger","defaultLogger","messages","resolveAuthMessages","loginPath","googleStrategy","GoogleStrategy","_request","tokens","accessToken","refreshToken","expiresIn","scope","_idToken","sub","email","picture","_hd","email_verified","_photos","profile","id","displayName","_familyName","_givenName","_emails","provider","errorMessage","redirect","userEmail","error","expirationDateMs","loggable","friendlyErrorMessage","conditionMet","cause","encodedErrorMessage","authenticator","Authenticator","redirect","createAuthHandlers","config","storage","authenticator","logger","loginPath","postLoginRedirect","postLogoutRedirect","loginLoader","request","loginAction","formData","action","user","logout","logoutAndRedirect","cookie","headers","googleProfile","previousRefreshToken","authenticationPayload","error","createCookieSessionStorage","redirect","createAuthSessionStorage","config","options","sessionKey","maxAge","loginPath","postLoginRedirect","mockUser","isUserActive","sessionStorage","getSession","request","getSessionUser","user","saveSession","session","requireUser","sessionUser","createAuthServer","config","logger","defaultLogger","storage","createAuthSessionStorage","authenticator","createGoogleAuthenticator","authFns","createAuthenticate","authenticateInLoader","createAuthenticateInLoader","handlers","createAuthHandlers"]}
@@ -1,3 +1,3 @@
1
1
  /*! tailwindcss v4.3.0 | MIT License | https://tailwindcss.com */
2
2
  /*! @aiquants/auth-react-router standalone CSS — 非 Tailwind ホスト専用。ホストの Tailwind ビルドと混在させないこと */
3
- @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-duration:initial;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1}}}@layer theme{:root,:host{--color-blue-300:oklch(80.9% .105 251.813);--color-blue-500:oklch(62.3% .214 259.815);--color-blue-600:oklch(54.6% .245 262.881);--color-indigo-500:oklch(58.5% .233 277.117);--color-indigo-600:oklch(51.1% .262 276.966);--color-white:#fff;--spacing:.25rem;--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--font-weight-bold:700;--tracking-wide:.025em;--radius-2xl:1rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1)}}@layer base,components;@layer utilities{.collapse{visibility:collapse}.fixed{position:fixed}.static{position:static}.my-4{margin-block:calc(var(--spacing) * 4)}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline-flex{display:inline-flex}.w-full{width:100%}.items-center{align-items:center}.justify-center{justify-content:center}.gap-3{gap:calc(var(--spacing) * 3)}.rounded-2xl{border-radius:var(--radius-2xl)}.border{border-style:var(--tw-border-style);border-width:1px}.bg-gradient-to-r{--tw-gradient-position:to right in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.from-blue-500{--tw-gradient-from:var(--color-blue-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-indigo-500{--tw-gradient-to:var(--color-indigo-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.px-6{padding-inline:calc(var(--spacing) * 6)}.py-3{padding-block:calc(var(--spacing) * 3)}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.text-white{color:var(--color-white)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.drop-shadow{--tw-drop-shadow-size:drop-shadow(0 1px 2px var(--tw-drop-shadow-color,#0000001a)) drop-shadow(0 1px 1px var(--tw-drop-shadow-color,#0000000f));--tw-drop-shadow:drop-shadow(0 1px 2px #0000001a) drop-shadow(0 1px 1px #0000000f);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-300{--tw-duration:.3s;transition-duration:.3s}@media (hover:hover){.hover\:scale-105:hover{--tw-scale-x:105%;--tw-scale-y:105%;--tw-scale-z:105%;scale:var(--tw-scale-x) var(--tw-scale-y)}.hover\:from-blue-600:hover{--tw-gradient-from:var(--color-blue-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.hover\:to-indigo-600:hover{--tw-gradient-to:var(--color-indigo-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus\:ring-blue-300:focus{--tw-ring-color:var(--color-blue-300)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-gradient-position{syntax:"*";inherits:false}@property --tw-gradient-from{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-via{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-to{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-stops{syntax:"*";inherits:false}@property --tw-gradient-via-stops{syntax:"*";inherits:false}@property --tw-gradient-from-position{syntax:"<length-percentage>";inherits:false;initial-value:0%}@property --tw-gradient-via-position{syntax:"<length-percentage>";inherits:false;initial-value:50%}@property --tw-gradient-to-position{syntax:"<length-percentage>";inherits:false;initial-value:100%}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}
3
+ @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-duration:initial;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1}}}@layer theme{:root,:host{--color-blue-300:oklch(80.9% .105 251.813);--color-blue-500:oklch(62.3% .214 259.815);--color-blue-600:oklch(54.6% .245 262.881);--color-indigo-500:oklch(58.5% .233 277.117);--color-indigo-600:oklch(51.1% .262 276.966);--color-white:#fff;--spacing:.25rem;--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--font-weight-bold:700;--tracking-wide:.025em;--radius-2xl:1rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1)}}@layer base,components;@layer utilities{.collapse{visibility:collapse}.fixed{position:fixed}.static{position:static}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.my-4{margin-block:calc(var(--spacing) * 4)}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline-flex{display:inline-flex}.w-full{width:100%}.items-center{align-items:center}.justify-center{justify-content:center}.gap-3{gap:calc(var(--spacing) * 3)}.rounded-2xl{border-radius:var(--radius-2xl)}.border{border-style:var(--tw-border-style);border-width:1px}.bg-gradient-to-r{--tw-gradient-position:to right in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.from-blue-500{--tw-gradient-from:var(--color-blue-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-indigo-500{--tw-gradient-to:var(--color-indigo-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.px-6{padding-inline:calc(var(--spacing) * 6)}.py-3{padding-block:calc(var(--spacing) * 3)}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.text-white{color:var(--color-white)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.drop-shadow{--tw-drop-shadow-size:drop-shadow(0 1px 2px var(--tw-drop-shadow-color,#0000001a)) drop-shadow(0 1px 1px var(--tw-drop-shadow-color,#0000000f));--tw-drop-shadow:drop-shadow(0 1px 2px #0000001a) drop-shadow(0 1px 1px #0000000f);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-300{--tw-duration:.3s;transition-duration:.3s}@media (hover:hover){.hover\:scale-105:hover{--tw-scale-x:105%;--tw-scale-y:105%;--tw-scale-z:105%;scale:var(--tw-scale-x) var(--tw-scale-y)}.hover\:from-blue-600:hover{--tw-gradient-from:var(--color-blue-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.hover\:to-indigo-600:hover{--tw-gradient-to:var(--color-indigo-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus\:ring-blue-300:focus{--tw-ring-color:var(--color-blue-300)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-gradient-position{syntax:"*";inherits:false}@property --tw-gradient-from{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-via{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-to{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-stops{syntax:"*";inherits:false}@property --tw-gradient-via-stops{syntax:"*";inherits:false}@property --tw-gradient-from-position{syntax:"<length-percentage>";inherits:false;initial-value:0%}@property --tw-gradient-via-position{syntax:"<length-percentage>";inherits:false;initial-value:50%}@property --tw-gradient-to-position{syntax:"<length-percentage>";inherits:false;initial-value:100%}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@aiquants/auth-react-router",
3
- "version": "0.7.0",
4
- "description": "React Router v7 auth adapter for @aiquants/auth-core: Google OAuth strategy factory (remix-auth), cookie session storage with DI test backdoor, per-request loader guard with token refresh + photo cache, auth route handlers, and a <GoogleForm> login button.",
3
+ "version": "0.8.0",
4
+ "description": "React Router v7 / v8 auth adapter for @aiquants/auth-core: Google OAuth strategy factory (remix-auth), cookie session storage with DI test backdoor, per-request loader guard with token refresh + photo cache, auth route handlers, and a <GoogleForm> login button.",
5
5
  "sideEffects": [
6
6
  "**/*.css"
7
7
  ],
@@ -59,7 +59,7 @@
59
59
  "jsdom": "^30.0.1",
60
60
  "react": "^19.2.7",
61
61
  "react-icons": "^5.5.0",
62
- "react-router": "^7.13.0",
62
+ "react-router": "^8.3.0",
63
63
  "remix-auth": "^4.2.0",
64
64
  "remix-auth-oauth2": "^3.4.1",
65
65
  "rimraf": "^6.1.2",
@@ -72,7 +72,7 @@
72
72
  "peerDependencies": {
73
73
  "react": ">=18",
74
74
  "react-icons": ">=5",
75
- "react-router": "^7.0.0",
75
+ "react-router": "^7.0.0 || ^8.0.0",
76
76
  "remix-auth": "^4.0.0",
77
77
  "remix-auth-oauth2": "^3.0.0",
78
78
  "@aiquants/auth-core": "^0.2.0"
@@ -6,6 +6,9 @@
6
6
  * 原本 (quants-react-router components/elements/GoogleForm.tsx) を挙動保存で移植。
7
7
  * ラベル・action 値は props 化し、既定値は原本と同一 ("Sign In with Google")。
8
8
  */
9
+ // react-icons はコレクション単位の exports キー ("./fa" など) だけを公開しており、ワイルドカードを持たない。
10
+ // `react-icons/fa/index.js` は実ファイルが存在しても exports マップに無いため、TypeScript (bundler/nodenext)・
11
+ // Node ESM・esbuild/Vite の全てが解決を拒否する。この指定子に拡張子を足してはならない (#2044 の再発防止)。
9
12
  import { FaGoogle } from "react-icons/fa"
10
13
  import { Form } from "react-router"
11
14
  import { tv } from "tailwind-variants"
@@ -10,7 +10,7 @@
10
10
  */
11
11
  import type { AuthenticationPayload, MyGoogleProfile, SessionProfile } from "@aiquants/auth-core"
12
12
  import { redirect } from "react-router"
13
- import type { AuthenticateFns } from "./authenticate"
13
+ import { AuthRequiredError, type AuthenticateFns } from "./authenticate"
14
14
  import type { AuthServerConfig, LoggerPort } from "./ports"
15
15
  import type { AuthSessionStorage } from "./session"
16
16
 
@@ -86,7 +86,7 @@ export const createAuthenticateInLoader = (config: AuthServerConfig, storage: Au
86
86
  try {
87
87
  // トークンの有効期限を確認し、必要に応じて更新
88
88
  const authenticatedTokens = await authFns.authenticate(request, session, {
89
- failureRedirect: failureRedirect ?? undefined,
89
+ failureRedirect,
90
90
  })
91
91
 
92
92
  // 認証ペイロードの構築
@@ -122,6 +122,12 @@ export const createAuthenticateInLoader = (config: AuthServerConfig, storage: Au
122
122
 
123
123
  return { user: updatedUser, cookie }
124
124
  } catch (error) {
125
+ // failureRedirect: null の契約 (リダイレクトせず未認証として返す) を守る。
126
+ // 以前は authenticate() が投げた redirect Response を下の Location 判定が
127
+ // 無条件に再スローしており、null 指定でも /auth/login へ飛んでいた。
128
+ if (error instanceof AuthRequiredError) {
129
+ return error.setCookie !== undefined ? { cookie: error.setCookie } : {}
130
+ }
125
131
  if (error instanceof Response && error.headers.get("Location")) {
126
132
  throw error
127
133
  }
@@ -31,6 +31,25 @@ export class GoogleInvalidGrantError extends TokenRefreshError {
31
31
  }
32
32
  }
33
33
 
34
+ /**
35
+ * Signals that authentication is required when the caller opted out of redirecting.
36
+ * 呼び出し側がリダイレクトを拒否した (`failureRedirect: null`) 際に認証要求を伝えるエラー。
37
+ *
38
+ * `redirect()` を投げてしまうと React Router がそれを本物の 302 として扱うため、
39
+ * 「リダイレクトしない」という契約を型で表現できるこのエラーへ置き換える。
40
+ * セッション破棄が発生した場合はその `Set-Cookie` を `setCookie` で運ぶ
41
+ * (捨てると無効セッションがブラウザに残り続ける)。
42
+ */
43
+ export class AuthRequiredError extends Error {
44
+ readonly setCookie?: string
45
+
46
+ constructor(setCookie?: string) {
47
+ super("Authentication required")
48
+ this.name = "AuthRequiredError"
49
+ this.setCookie = setCookie
50
+ }
51
+ }
52
+
34
53
  /** createAuthenticate が返す認証関数束。 */
35
54
  export type AuthenticateFns = ReturnType<typeof createAuthenticate>
36
55
 
@@ -124,8 +143,17 @@ export const createAuthenticate = (config: AuthServerConfig, storage: AuthSessio
124
143
  * Validates session tokens, refreshing them when expired; destroys the session on refresh failure.
125
144
  * セッショントークンを検証し、期限切れならリフレッシュする。失敗時はセッションを破棄する。
126
145
  */
127
- const authenticate = async (request: Request, session: Session, options?: { failureRedirect?: string }) => {
146
+ const authenticate = async (request: Request, session: Session, options?: { failureRedirect?: string | null }) => {
128
147
  const { failureRedirect } = options || {}
148
+
149
+ // failureRedirect が null のときはリダイレクトを作らず AuthRequiredError を投げる。
150
+ // undefined / 文字列のときは従来どおり redirect() を投げる。
151
+ const authFailure = (setCookie?: string) => {
152
+ if (failureRedirect === null) {
153
+ return new AuthRequiredError(setCookie)
154
+ }
155
+ return setCookie !== undefined ? redirect(failureRedirect || loginPath, { headers: { "Set-Cookie": setCookie } }) : redirect(failureRedirect || loginPath)
156
+ }
129
157
  try {
130
158
  // テストバックドア (ポート注入時のみ有効)
131
159
  if (config.mockUser?.enabled(request)) {
@@ -139,7 +167,7 @@ export const createAuthenticate = (config: AuthServerConfig, storage: AuthSessio
139
167
  // セッションから認証データを取得
140
168
  const user = session.get(sessionKey)
141
169
  if (!(user?.accessToken && user.expirationDateMs)) {
142
- throw redirect(failureRedirect || loginPath)
170
+ throw authFailure()
143
171
  }
144
172
 
145
173
  const { accessToken, refreshToken, expirationDateMs } = user
@@ -147,8 +175,8 @@ export const createAuthenticate = (config: AuthServerConfig, storage: AuthSessio
147
175
  // 期限切れならエラーを投げてリフレッシュ経路へ
148
176
  if (expirationDateMs < Date.now()) {
149
177
  if (!refreshToken) {
150
- // リフレッシュトークンがない場合は更新できないのでログインページへ
151
- throw redirect(failureRedirect || loginPath)
178
+ // リフレッシュトークンがない場合は更新できないので認証失敗として扱う
179
+ throw authFailure()
152
180
  }
153
181
  throw new Error("Expired")
154
182
  }
@@ -176,9 +204,7 @@ export const createAuthenticate = (config: AuthServerConfig, storage: AuthSessio
176
204
  }
177
205
  // リフレッシュに失敗した場合はセッションを破棄してログインページへ
178
206
  const cookie = await storage.destroySession(session)
179
- throw redirect(failureRedirect || loginPath, {
180
- headers: { "Set-Cookie": cookie },
181
- })
207
+ throw authFailure(cookie)
182
208
  }
183
209
  }
184
210
 
package/src/server.ts CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  export type { AuthServer } from "./server/auth-server"
7
7
  export { createAuthServer } from "./server/auth-server"
8
- export { createAuthenticate, GoogleInvalidGrantError, TokenRefreshError } from "./server/authenticate"
8
+ export { AuthRequiredError, createAuthenticate, GoogleInvalidGrantError, TokenRefreshError } from "./server/authenticate"
9
9
  export { createAuthenticateInLoader } from "./server/authenticate-in-loader"
10
10
  export { createGoogleAuthenticator } from "./server/google-strategy"
11
11
  export { createAuthHandlers } from "./server/handlers"