@aiquants/auth-react-router 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/dist/server.d.mts +28 -16
- package/dist/server.d.ts +28 -16
- package/dist/server.js +1 -1
- package/dist/server.js.map +1 -1
- package/dist/server.mjs +1 -1
- package/dist/server.mjs.map +1 -1
- package/dist/styles/auth-react-router.standalone.css +1 -1
- package/package.json +3 -2
- package/src/server/authenticate-in-loader.ts +157 -68
- package/src/server/authenticate.ts +8 -1
package/README.md
CHANGED
|
@@ -106,7 +106,10 @@ Never mix the two: importing the standalone utility CSS next to a host Tailwind
|
|
|
106
106
|
- **Session cookie byte compatibility**: `__session` (httpOnly/lax/30d), key `"user"`, value = `{ id, displayName, name, emails, accessToken, refreshToken?, expirationDateMs?, provider, role? }` — never `photo`/`_json`/`photos`. Changing this invalidates all live 30-day sessions.
|
|
107
107
|
- **Verify order**: Google profile check → `backendAuth.verify` → allowlist → `backendAuth.signup` (every login). Failures redirect to `<loginPath>?error=<encoded error message>` (messages overridable via `messages`).
|
|
108
108
|
- **Token refresh**: expiry check → Google `oauth2/v4/token` rotate → session update; `invalid_grant` destroys the session and redirects to login with `Set-Cookie`.
|
|
109
|
-
- **Loader guard**: `authenticateInLoader(request, { failureRedirect })` returns `{ user
|
|
109
|
+
- **Loader guard**: `authenticateInLoader(request, { failureRedirect })` returns `{ user, cookie? }` with `user` **required** when `failureRedirect` is a string (any authentication failure throws a redirect), and `{ user?, cookie? }` when it is `null` (no-redirect mode). Passing `""` throws a `TypeError` before any work — it is a mistake, not a request for the default.
|
|
110
|
+
- **One authentication per request**: the guard performs its work at most once per `Request` instance and shares a mode-neutral result, so a root loader (`null`) and a leaf loader (`"/auth/login"`) in the same React Router data pass share a single pass — one warmup, one token refresh, one `Set-Cookie`. The failure policy is applied per caller on top of the shared result, so it must never be part of the cache key.
|
|
111
|
+
- **Infrastructure failures are never laundered into logouts**: anything that is not an authentication failure (an unexpected error inside the auth step, a `getUserPhoto` port outage, a `saveSession` failure) propagates unchanged in **both** modes. The session is not destroyed and no redirect is produced.
|
|
112
|
+
- Photo lookups are cached with in-flight dedup per factory instance.
|
|
110
113
|
|
|
111
114
|
## DI Ports Absorbing Per-App Differences
|
|
112
115
|
|
package/dist/server.d.mts
CHANGED
|
@@ -175,14 +175,20 @@ declare const createAuthServer: (config: AuthServerConfig) => {
|
|
|
175
175
|
refreshToken: any;
|
|
176
176
|
expirationDateMs: number;
|
|
177
177
|
}>;
|
|
178
|
-
authenticateInLoader:
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
178
|
+
authenticateInLoader: {
|
|
179
|
+
(request: Request, options: {
|
|
180
|
+
failureRedirect: string;
|
|
181
|
+
}): Promise<{
|
|
182
|
+
user: _aiquants_auth_core.MyGoogleProfile;
|
|
183
|
+
cookie?: string;
|
|
184
|
+
}>;
|
|
185
|
+
(request: Request, options: {
|
|
186
|
+
failureRedirect: null;
|
|
187
|
+
}): Promise<{
|
|
188
|
+
user?: _aiquants_auth_core.MyGoogleProfile;
|
|
189
|
+
cookie?: string;
|
|
190
|
+
}>;
|
|
191
|
+
};
|
|
186
192
|
};
|
|
187
193
|
|
|
188
194
|
/** createAuthSessionStorage が返すセッション操作束。 */
|
|
@@ -268,14 +274,20 @@ declare const createAuthenticate: (config: AuthServerConfig, storage: AuthSessio
|
|
|
268
274
|
* Builds the per-request loader guard wired to injected ports.
|
|
269
275
|
* 注入ポートに結線した毎リクエストローダーガードを生成する。
|
|
270
276
|
*/
|
|
271
|
-
declare const createAuthenticateInLoader: (config: AuthServerConfig, storage: AuthSessionStorage, authFns: AuthenticateFns, logger: LoggerPort) =>
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
277
|
+
declare const createAuthenticateInLoader: (config: AuthServerConfig, storage: AuthSessionStorage, authFns: AuthenticateFns, logger: LoggerPort) => {
|
|
278
|
+
(request: Request, options: {
|
|
279
|
+
failureRedirect: string;
|
|
280
|
+
}): Promise<{
|
|
281
|
+
user: MyGoogleProfile;
|
|
282
|
+
cookie?: string;
|
|
283
|
+
}>;
|
|
284
|
+
(request: Request, options: {
|
|
285
|
+
failureRedirect: null;
|
|
286
|
+
}): Promise<{
|
|
287
|
+
user?: MyGoogleProfile;
|
|
288
|
+
cookie?: string;
|
|
289
|
+
}>;
|
|
290
|
+
};
|
|
279
291
|
|
|
280
292
|
/**
|
|
281
293
|
* @module google-strategy
|
package/dist/server.d.ts
CHANGED
|
@@ -175,14 +175,20 @@ declare const createAuthServer: (config: AuthServerConfig) => {
|
|
|
175
175
|
refreshToken: any;
|
|
176
176
|
expirationDateMs: number;
|
|
177
177
|
}>;
|
|
178
|
-
authenticateInLoader:
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
178
|
+
authenticateInLoader: {
|
|
179
|
+
(request: Request, options: {
|
|
180
|
+
failureRedirect: string;
|
|
181
|
+
}): Promise<{
|
|
182
|
+
user: _aiquants_auth_core.MyGoogleProfile;
|
|
183
|
+
cookie?: string;
|
|
184
|
+
}>;
|
|
185
|
+
(request: Request, options: {
|
|
186
|
+
failureRedirect: null;
|
|
187
|
+
}): Promise<{
|
|
188
|
+
user?: _aiquants_auth_core.MyGoogleProfile;
|
|
189
|
+
cookie?: string;
|
|
190
|
+
}>;
|
|
191
|
+
};
|
|
186
192
|
};
|
|
187
193
|
|
|
188
194
|
/** createAuthSessionStorage が返すセッション操作束。 */
|
|
@@ -268,14 +274,20 @@ declare const createAuthenticate: (config: AuthServerConfig, storage: AuthSessio
|
|
|
268
274
|
* Builds the per-request loader guard wired to injected ports.
|
|
269
275
|
* 注入ポートに結線した毎リクエストローダーガードを生成する。
|
|
270
276
|
*/
|
|
271
|
-
declare const createAuthenticateInLoader: (config: AuthServerConfig, storage: AuthSessionStorage, authFns: AuthenticateFns, logger: LoggerPort) =>
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
277
|
+
declare const createAuthenticateInLoader: (config: AuthServerConfig, storage: AuthSessionStorage, authFns: AuthenticateFns, logger: LoggerPort) => {
|
|
278
|
+
(request: Request, options: {
|
|
279
|
+
failureRedirect: string;
|
|
280
|
+
}): Promise<{
|
|
281
|
+
user: MyGoogleProfile;
|
|
282
|
+
cookie?: string;
|
|
283
|
+
}>;
|
|
284
|
+
(request: Request, options: {
|
|
285
|
+
failureRedirect: null;
|
|
286
|
+
}): Promise<{
|
|
287
|
+
user?: MyGoogleProfile;
|
|
288
|
+
cookie?: string;
|
|
289
|
+
}>;
|
|
290
|
+
};
|
|
279
291
|
|
|
280
292
|
/**
|
|
281
293
|
* @module google-strategy
|
package/dist/server.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
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});
|
|
1
|
+
"use strict";var $=Object.defineProperty;var V=Object.getOwnPropertyDescriptor;var J=Object.getOwnPropertyNames;var Q=Object.prototype.hasOwnProperty;var X=(e,s)=>{for(var r in s)$(e,r,{get:s[r],enumerable:!0})},Z=(e,s,r,u)=>{if(s&&typeof s=="object"||typeof s=="function")for(let i of J(s))!Q.call(e,i)&&i!==r&&$(e,i,{get:()=>s[i],enumerable:!(u=V(s,i))||u.enumerable});return e};var ee=e=>Z($({},"__esModule",{value:!0}),e);var se={};X(se,{AuthRequiredError:()=>R,GoogleInvalidGrantError:()=>G,TokenRefreshError:()=>w,createAuthHandlers:()=>O,createAuthServer:()=>Y,createAuthSessionStorage:()=>F,createAuthenticate:()=>C,createAuthenticateInLoader:()=>I,createGoogleAuthenticator:()=>D});module.exports=ee(se);var H=require("react-router"),w=class extends Error{constructor(s){super(s),this.name="TokenRefreshError"}},G=class extends w{constructor(){super("Google refresh token invalid or revoked"),this.name="GoogleInvalidGrantError"}},R=class extends Error{constructor(s){super("Authentication required"),this.name="AuthRequiredError",this.setCookie=s}},C=(e,s,r)=>{let u=e.loginPath??"/auth/login",i=s.sessionKey,m=async d=>{let g=d.get(i)?.refreshToken;if(!g)throw new Error("Refresh token not found in session");try{let a="https://www.googleapis.com/oauth2/v4/token",l={client_id:e.google.clientId,client_secret:e.google.clientSecret,grant_type:"refresh_token",refresh_token:g},t=await fetch(a,{headers:{"Content-Type":"application/json"},method:"POST",body:JSON.stringify(l)});if(!t.ok){let h=await t.json().catch(()=>{});if(t.status===400&&typeof h=="object"&&!!h&&"error"in h&&h.error==="invalid_grant")throw new G;let S=typeof h=="object"&&h&&"error_description"in h?String(h.error_description):"Unknown error";throw new w(`Google token refresh failed: ${t.status} ${S}`)}let o=await t.json(),{access_token:n,expires_in:c}=o;if(!(n&&c))throw r.error("Invalid token response from Google:",o),new Error("Invalid token response from Google");return{accessToken:n,refreshToken:g,expirationDateMs:Number(Date.now())+c*1e3}}catch(a){if(a instanceof w)throw a;let l=a instanceof Error?a.message:"Unknown error";throw new w(`Failed to refresh Google access token: ${l}`)}},y=async d=>{let g=d.get(i).provider;if(!g)throw new Error("Provider not found in session");if(g==="google")return await m(d);throw new Error("Provider not supported")};return{authenticate:async(d,p,g)=>{let{failureRedirect:a}=g||{},l=t=>{if(a===null)return new R(t);if(a==="")throw new TypeError("failureRedirect must not be an empty string; pass null to skip redirecting");let o=a??u;return t!==void 0?(0,H.redirect)(o,{headers:{"Set-Cookie":t}}):(0,H.redirect)(o)};try{if(e.mockUser?.enabled(d))return{accessToken:"mock-access-token",refreshToken:"mock-refresh-token",expirationDateMs:Date.now()+36e5};let t=p.get(i);if(!(t?.accessToken&&t.expirationDateMs))throw l();let{accessToken:o,refreshToken:n,expirationDateMs:c}=t;if(c<Date.now())throw n?new Error("Expired"):l();return{accessToken:o,refreshToken:n,expirationDateMs:c}}catch(t){if(t instanceof Error&&t.message==="Expired")try{let o=await y(p),n=p.get(i);return p.set(i,{...n,...o}),o}catch(o){if(o instanceof G)r.warn("Google refresh token invalid or revoked");else{let c=o instanceof Error?o.message:"Unknown error";r.error("Failed to refresh token:",c)}let n=await s.destroySession(p);throw l(n)}throw t}},refreshedAccessToken:y}};var B=require("react-router");var I=(e,s,r,u)=>{let i=new Map,m=new Map,y=async a=>{let l=i.get(a);if(l!==void 0)return l;let t=m.get(a);if(t!==void 0)return t;let o=(async()=>{try{let n=e.getUserPhoto?await e.getUserPhoto(a):"";return i.set(a,n),n}catch(n){throw i.delete(a),n}finally{m.delete(a)}})();return m.set(a,o),o},k=new WeakMap,d=async a=>{e.warmup&&await e.warmup();let l=await s.getSession(a),t=await s.getSessionUser(a);if(t===void 0)return{kind:"unauthenticated"};let o;try{o=await r.authenticate(a,l,{failureRedirect:null})}catch(A){if(A instanceof R)return A.setCookie!==void 0?{kind:"unauthenticated",cookie:A.setCookie}:{kind:"unauthenticated"};throw u.error("Error in authenticateInLoader:",A),A}let n={accessToken:o.accessToken,refreshToken:o.refreshToken,expirationDateMs:o.expirationDateMs,provider:t.provider},c={id:t.id,displayName:t.displayName,name:t.name,emails:t.emails,...n,role:t.role},h=await y(t.id),x={...t,...n,photo:h},T=o.accessToken!==t.accessToken?await s.saveSession(a,c):void 0;return T!==void 0?{kind:"authenticated",user:x,cookie:T}:{kind:"authenticated",user:x}},p=a=>{let l=k.get(a);if(l!==void 0)return l;let t=d(a);return k.set(a,t),t};async function g(a,l){let{failureRedirect:t}=l;if(t==="")throw new TypeError("failureRedirect must not be an empty string; pass null to skip redirecting");let o=await p(a);if(o.kind==="authenticated")return o.cookie!==void 0?{user:o.user,cookie:o.cookie}:{user:o.user};if(t===null)return o.cookie!==void 0?{cookie:o.cookie}:{};throw o.cookie!==void 0?(0,B.redirect)(t,{headers:{"Set-Cookie":o.cookie}}):(0,B.redirect)(t)}return g};var z=require("@aiquants/auth-core"),M=require("react-router"),W=require("remix-auth");var K=require("remix-auth-oauth2"),te=["openid","https://www.googleapis.com/auth/userinfo.profile","https://www.googleapis.com/auth/userinfo.email"],oe="google",re="https://www.googleapis.com/oauth2/v3/userinfo",L=class extends K.OAuth2Strategy{constructor({clientId:r,clientSecret:u,redirectURI:i,scopes:m,accessType:y,includeGrantedScopes:k,prompt:d,hd:p,loginHint:g},a){let l=m??te;super({clientId:r,clientSecret:u,redirectURI:i,authorizationEndpoint:"https://accounts.google.com/o/oauth2/v2/auth",tokenEndpoint:"https://oauth2.googleapis.com/token",scopes:l},a);this.name=oe;this.scopes=l,this.accessType=y??"online",this.includeGrantedScopes=k??!1,this.prompt=d,this.hd=p,this.loginHint=g}authorizationParams(r,u){return r.set("access_type",this.accessType),r.set("include_granted_scopes",String(this.includeGrantedScopes)),this.scopes&&this.scopes.length>0&&r.set("scope",this.scopes.join(" ")),this.prompt&&r.set("prompt",this.prompt),this.hd&&r.set("hd",this.hd),this.loginHint&&r.set("login_hint",this.loginHint),r}static async userProfile(r){let u=await fetch(re,{headers:{Authorization:`Bearer ${r.accessToken()}`}});if(!u.ok)throw new Error(`Failed to fetch user profile: ${u.statusText}`);let i=await u.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 N={info:(...e)=>console.info("[Auth]",...e),warn:(...e)=>console.warn("[Auth]",...e),error:(...e)=>console.error("[Auth]",...e)};var D=e=>{let s=e.logger??N,r=(0,z.resolveAuthMessages)(e.messages),u=e.loginPath??"/auth/login",i=new L({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:k})=>{let{access_token:d,refresh_token:p,expires_in:g,scope:a,id_token:l}=k.data,{_json:{sub:t,email:o,picture:n,hd:c,email_verified:h},photos:x,...S}=await L.userProfile(k),{id:T,displayName:A,name:{familyName:ne,givenName:ie},emails:ae}=S,j="google";if(d===void 0||p===void 0||g===void 0||a===void 0||T===void 0||A===void 0||t===void 0||o===void 0||h===void 0){if(h===!1){let U=encodeURIComponent(r.emailNotVerified);throw(0,M.redirect)(`${u}?error=${U}`)}let f=encodeURIComponent(r.googleFailed);throw(0,M.redirect)(`${u}?error=${f}`)}s.info("Authenticated on Google: ",S.displayName);try{if(e.backendAuth){if(!await e.backendAuth.verify(j,d)){s.error("Backend verification returned false for user:",S.displayName);let v=encodeURIComponent(r.backendFailed);throw(0,M.redirect)(`${u}?error=${v}`)}s.info("Authenticated on Backend: ",S.displayName)}let f=S.emails[0]?.value;if(!await e.isEmailAllowed(f)){let P=new Error("You are not allowed to access this application");throw s.error(P),P}if(e.isUserActive&&!await e.isUserActive(S)){let P=new Error("You are not allowed to access this application");throw s.error("Deactivated account attempted to sign in:",f),P}e.backendAuth&&await e.backendAuth.signup(j,d),s.info("Authenticated complete: ",S.displayName);let U=Date.now()+g*1e3;return{...S,...{accessToken:d,refreshToken:p,expirationDateMs:U,provider:j},photo:n}}catch(f){let U=f;typeof f=="object"&&f!==null&&"cause"in f&&(U=f.cause||f),s.error("Error during backend authentication or subsequent processing:",U);let q=r.unexpected,_=!1;if(typeof f=="object"&&f!==null&&"cause"in f){let v=f.cause;v&&typeof v=="object"&&"message"in v&&typeof v.message=="string"&&v.message.includes("getaddrinfo ENOTFOUND")&&(q=r.backendUnreachable,_=!0)}if(!_&&f instanceof Error&&f.message.includes("You are not allowed to access this application")&&(q=r.notAllowed,_=!0),!_&&f instanceof Response&&f.headers.get("Location")?.startsWith(u))throw f;let P=encodeURIComponent(q);throw(0,M.redirect)(`${u}?error=${P}`)}}),m=new W.Authenticator;return m.use(i),m};var E=require("react-router"),O=(e,s,r,u)=>{let i=e.loginPath??"/auth/login",m=e.postLoginRedirect??"/",y=e.postLogoutRedirect??"/",k=async({request:t})=>{if(await s.getSessionUser(t))throw(0,E.redirect)(m);return{error:new URL(t.url).searchParams.get("error")}},d=async({request:t})=>{let o=await t.clone().formData(),n=String(o.get("_action")),c=null;return n==="Sign In with Google"?c=await r.authenticate("google",t):c=null,c},p=async t=>await s.logout(t),g=async t=>{let o=await p(t),n=new Headers;return n.append("Set-Cookie",o),(0,E.redirect)(y,{headers:n})};return{loginLoader:k,loginAction:d,logoutLoader:async({request:t})=>{throw await g(t)},logout:p,logoutAndRedirect:g,googleCallbackLoader:async({request:t})=>{try{let o=await r.authenticate("google",t),h=(await s.getSession(t)).get(s.sessionKey)?.refreshToken,x={accessToken:o.accessToken,refreshToken:o.refreshToken||h,expirationDateMs:o.expirationDateMs,provider:o.provider},S={id:o.id,displayName:o.displayName,name:o.name,emails:o.emails,...x,role:o.role},T=await s.saveSession(t,S);throw(0,E.redirect)(m,{headers:{"Set-Cookie":T}})}catch(o){throw o instanceof Response?o:(u.error("Authentication error:",o),(0,E.redirect)(i))}}}};var b=require("react-router"),F=(e,s)=>{if(!e.secrets||e.secrets.length===0||e.secrets.some(n=>!n))throw new Error("REMIX_SESSION_SECRET must be set");let r=e.sessionKey??"user",u=e.maxAge??3600*24*30,i=s?.loginPath??"/auth/login",m=s?.postLoginRedirect??"/",y=s?.mockUser,k=s?.isUserActive,d=(0,b.createCookieSessionStorage)({cookie:{name:e.cookieName??"__session",httpOnly:!0,path:"/",sameSite:"lax",secrets:e.secrets,secure:e.secure??process.env.NODE_ENV==="production",maxAge:u}}),p=async n=>await d.getSession(n.headers.get("Cookie")),g=async n=>{if(y?.enabled(n))return y.resolve(n);let h=(await p(n)).get(r);if(h!==void 0&&!(k&&!await k(h)))return h},a=async(n,c)=>{let h=await p(n);return h.set(r,c),await d.commitSession(h)},l=async n=>{let c=await g(n);if(c===void 0)throw(0,b.redirect)(i);return c};return{sessionStorage:d,sessionKey:r,getSession:p,getSessionUser:g,saveSession:a,requireUser:l,requireAdminUser:async n=>{let c=await l(n);if(c?.role!=="admin")throw(0,b.redirect)(m);return c},logout:async n=>{let c=await p(n);return await d.destroySession(c)},commitSession:d.commitSession,destroySession:d.destroySession}};var Y=e=>{let s=e.logger??N,r=F(e.session,{mockUser:e.mockUser,loginPath:e.loginPath,postLoginRedirect:e.postLoginRedirect,isUserActive:e.isUserActive}),u=D(e),i=C(e,r,s),m=I(e,r,i,s),y=O(e,r,u,s);return{authenticator:u,storage:r,authFns:i,sessionStorage:r.sessionStorage,getSession:r.getSession,getSessionUser:r.getSessionUser,saveSession:r.saveSession,requireUser:r.requireUser,requireAdminUser:r.requireAdminUser,commitSession:r.commitSession,destroySession:r.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
|
package/dist/server.js.map
CHANGED
|
@@ -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 { 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"]}
|
|
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 // 未指定 (undefined) は「既定のログイン先で良い」という表明なので loginPath へ倒す。\n // 空文字は表明ではなく取り違えであり、既定へ倒すと意図しない遷移先を黙って使うことに\n // なるため失敗させる (`||` で両者を同一視しない)\n if (failureRedirect === \"\") {\n throw new TypeError(\"failureRedirect must not be an empty string; pass null to skip redirecting\")\n }\n const target = failureRedirect ?? loginPath\n return setCookie !== undefined ? redirect(target, { headers: { \"Set-Cookie\": setCookie } }) : redirect(target)\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/**\n * Mode-neutral result of the authentication work, carrying no failure policy.\n * 失敗時の方針を含まない、モード中立な認証作業の結果。\n *\n * リダイレクトするか否かは呼び出し元ごとの方針であり、認証**作業**の一部ではない。\n * 結果をこの形に保つことで、`failureRedirect` の指定が異なる呼び出し元どうしが\n * 1 リクエスト 1 回の作業を共有できる。\n */\ntype LoaderAuthOutcome = { readonly kind: \"authenticated\"; readonly user: MyGoogleProfile; readonly cookie?: string } | { readonly kind: \"unauthenticated\"; readonly cookie?: string }\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 // 1 リクエスト = 1 認証。React Router は 1 データパス内の全ローダーへ**同一の** Request\n // インスタンスを渡す (callLoaderOrAction が単一の request をクローズオーバーする) ため、\n // Request 同一性をそのままキーにできる。\n //\n // キーに `failureRedirect` を含めてはならない。あれは失敗時の**方針**であって認証**作業**では\n // なく、含めると root (null) と leaf (文字列) が別スロットに落ちて 1 ナビゲーションで二重に\n // 認証する。期限切れ時には Google のトークン更新が 2 本走り、内容の異なる Set-Cookie が\n // 2 本返る (React Router の Set-Cookie 重複排除はバイト一致のみ)。\n //\n // 保持は Request の寿命と同じで TTL も無効化 API も持たない。ストリーミング応答は Request を\n // コネクション終了まで掴むため、エントリもその間生き残る。リソースルートはローダーが 1 本\n // なのでエントリは一度書かれて二度と読まれず、陳腐化の危険は無い。ただしストリーム途中で\n // 再認証する実装を将来足すと、何時間も前のトークンを掴むことになる。\n const outcomeByRequest = new WeakMap<Request, Promise<LoaderAuthOutcome>>()\n\n /**\n * Performs the authentication work once, independent of any failure policy.\n * 失敗時の方針から独立した認証作業そのもの。\n *\n * 戻り値は**モード中立**で、リダイレクトするか否かの判断を含まない。これにより\n * `failureRedirect` の指定が異なる複数の呼び出し元が 1 つの結果を共有できる。\n *\n * @param request - リクエストオブジェクト。\n * @returns 認証済みか未認証かを判別できる結果。\n */\n const resolveOutcome = async (request: Request): Promise<LoaderAuthOutcome> => {\n // DB 接続プールが確実にオープンするのを待ってから処理を開始する (E2E 等でのコールドスタート競合回避)\n if (config.warmup) {\n await config.warmup()\n }\n\n const session = await storage.getSession(request)\n const currentUser = await storage.getSessionUser(request)\n\n if (currentUser === undefined) {\n return { kind: \"unauthenticated\" }\n }\n\n // try で覆うのは**認証そのもの**だけに限る。写真取得ポートやセッション保存まで含めると、\n // DB プールの一時的な失敗が「認証失敗」に化け、有効なセッションを持つ利用者が\n // 無関係な障害で再ログインを強いられる\n let authenticatedTokens: Awaited<ReturnType<typeof authFns.authenticate>>\n try {\n // 常に null モードで呼ぶ。ここで失敗方針を渡すと結果がモード依存になり共有できない\n authenticatedTokens = await authFns.authenticate(request, session, { failureRedirect: null })\n } catch (error) {\n if (error instanceof AuthRequiredError) {\n // 認証失敗。セッション破棄を伴う経路では破棄 Set-Cookie を運ぶ\n // (捨てると無効セッションがブラウザに残り続ける)\n return error.setCookie !== undefined ? { kind: \"unauthenticated\", cookie: error.setCookie } : { kind: \"unauthenticated\" }\n }\n // 認証失敗ではない予期せぬ失敗。セッションを破棄してログインへ飛ばすとインフラ障害が\n // ログアウトに化けて真因が消えるため、どのモードでも素通しする\n logger.error(\"Error in authenticateInLoader:\", error)\n throw error\n }\n\n // ここから先は認証済み。写真取得やセッション保存の失敗は「認証失敗」ではないため畳まず、\n // そのまま送出して呼び出し側 (ErrorBoundary / 500) に本当の原因を見せる\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 cookie !== undefined ? { kind: \"authenticated\", user: updatedUser, cookie } : { kind: \"authenticated\", user: updatedUser }\n }\n\n /**\n * Returns the per-request authentication outcome, computing it at most once.\n * リクエスト単位の認証結果を返す (計算は高々 1 回)。\n *\n * この関数を `async` にしてはならない。`get` と `set` の間に await 境界が入ると、\n * 並行する 2 つの呼び出しが両方とも miss して二重認証に戻る。\n *\n * @param request - リクエストオブジェクト。\n * @returns 共有される認証結果の Promise。\n */\n const sharedOutcome = (request: Request): Promise<LoaderAuthOutcome> => {\n const cached = outcomeByRequest.get(request)\n if (cached !== undefined) {\n return cached\n }\n const pending = resolveOutcome(request)\n outcomeByRequest.set(request, pending)\n return pending\n }\n\n /**\n * Handles authentication within loader requests, redirecting on any authentication failure.\n * ローダーリクエスト内の認証処理 (認証失敗時は必ずリダイレクトを throw)。\n *\n * `failureRedirect` に文字列を渡した場合、認証失敗は例外 (redirect Response) として送出される\n * ため、正常に戻った時点で `user` は必ず存在する。呼び出し側に到達不能な `!user` ガードを\n * 書かせないよう、戻り値の `user` を必須として型付けする。\n *\n * 認証以外の失敗 (認証処理内部の予期せぬ例外・写真取得ポートの障害・セッション保存の失敗) は\n * リダイレクトへ畳まず送出する。認証の可否とは無関係な障害であり、ログアウトに化けさせると\n * 有効なセッションが失われたうえ真因も消える。\n *\n * @param request - リクエストオブジェクト。\n * @param options - 認証オプション。`failureRedirect` は認証失敗時のリダイレクト先。\n * @returns 認証済みユーザープロファイルとオプショナルな Set-Cookie 値。\n */\n async function authenticateInLoader(request: Request, options: { failureRedirect: string }): Promise<{ user: MyGoogleProfile; cookie?: string }>\n /**\n * Handles authentication within loader requests without ever redirecting.\n * ローダーリクエスト内の認証処理 (リダイレクトを一切発生させない)。\n *\n * `failureRedirect: null` では全ての**認証**失敗経路が `{}` を返す。セッション破棄を伴う失敗\n * (期限切れ + リフレッシュ失敗) では破棄 Set-Cookie を `cookie` に載せて返すため、呼び出し側は\n * 未認証応答にもこれを転送すること (捨てると無効セッションがブラウザに残り続ける)。\n *\n * 認証以外の失敗は文字列モードと同じく送出する。両モードの観測可能な差はリダイレクトの\n * 有無だけであり、それ以外に差を作らない。\n *\n * @param request - リクエストオブジェクト。\n * @param options - 認証オプション。`failureRedirect: null` でリダイレクトを明示的にスキップ。\n * @returns 未認証なら `user` を持たない結果、認証済みならユーザープロファイル。\n */\n async function authenticateInLoader(request: Request, options: { failureRedirect: null }): Promise<{ user?: MyGoogleProfile; cookie?: string }>\n /**\n * Implementation signature shared by both overloads (not part of the public API).\n * 両オーバーロードが共有する実装シグネチャ (公開 API ではない)。\n */\n async function authenticateInLoader(request: Request, options: { failureRedirect: string } | { failureRedirect: null }): Promise<{ user?: MyGoogleProfile; cookie?: string }> {\n const { failureRedirect } = options\n // 空文字は「既定でよい」という表明ではなく取り違えである。既定へ倒すと意図しない遷移先を\n // 黙って使うことになるため、認証の成否を待たず入口で失敗させる\n if (failureRedirect === \"\") {\n throw new TypeError(\"failureRedirect must not be an empty string; pass null to skip redirecting\")\n }\n\n const outcome = await sharedOutcome(request)\n if (outcome.kind === \"authenticated\") {\n return outcome.cookie !== undefined ? { user: outcome.user, cookie: outcome.cookie } : { user: outcome.user }\n }\n\n // 未認証。ここから先が呼び出し元ごとの失敗方針であり、共有される作業には含めない\n if (failureRedirect === null) {\n return outcome.cookie !== undefined ? { cookie: outcome.cookie } : {}\n }\n throw outcome.cookie !== undefined ? redirect(failureRedirect, { headers: { \"Set-Cookie\": outcome.cookie } }) : redirect(failureRedirect)\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","target","user","accessToken","refreshToken","expirationDateMs","refreshed","refreshError","cookie","import_react_router","createAuthenticateInLoader","config","storage","authFns","logger","openidPhotoCache","pendingOpenidPhotoQueries","getCachedOpenidPhoto","openid","cached","pending","fetchPromise","photo","error","outcomeByRequest","resolveOutcome","request","session","currentUser","authenticatedTokens","AuthRequiredError","authenticationPayload","updatedSessionUser","updatedUser","cookie","sharedOutcome","authenticateInLoader","options","failureRedirect","outcome","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"]}
|