{"version":3,"file":"entra-47nBhMK-.mjs","names":["ConfidentialClientApplication","CryptoProvider","InteractionRequiredAuthError","ServerError","TokenPayload","serverEnv","LOGIN_SCOPES","ENTRA_AUTHORITY","ENTRA_TENANT_ID","cryptoProvider","createConfidentialClient","auth","clientId","ENTRA_CLIENT_ID","clientSecret","ENTRA_CLIENT_SECRET","authority","extractRefreshToken","client","cache","JSON","parse","getTokenCache","serialize","RefreshToken","Record","secret","Object","values","buildAuthCodeUrl","opts","redirectUri","state","codeChallenge","Promise","getAuthCodeUrl","scopes","responseMode","codeChallengeMethod","exchangeCodeForSession","code","codeVerifier","refreshToken","claims","result","acquireTokenByCode","Error","idTokenClaims","SessionExpiredError","acquireDelegatedToken","ReadonlyArray","accessToken","acquireTokenByRefreshToken","forceCache","error","errorCode","message","buildLogoutUrl","postLogoutRedirectUri","base","encodeURIComponent"],"sources":["../src/lib/auth/entra.ts"],"sourcesContent":["import {\r\n  ConfidentialClientApplication,\r\n  CryptoProvider,\r\n  InteractionRequiredAuthError,\r\n  ServerError,\r\n} from \"@azure/msal-node\";\r\nimport type { TokenPayload } from \"~/models/TokenPayload\";\r\nimport { serverEnv } from \"~/env\";\r\n\r\n/**\r\n * Scopes requested during the interactive login. `offline_access` yields the\r\n * refresh token that the session is built around; `User.Read` lets us read the\r\n * user's Graph profile (e.g. avatar). Resource API scopes (file, approval, …) are\r\n * acquired on demand via {@link acquireDelegatedToken} using the multi-resource\r\n * refresh token.\r\n */\r\nexport const LOGIN_SCOPES = [\"openid\", \"profile\", \"offline_access\", \"User.Read\"];\r\n\r\n/** Entra authority for this tenant. */\r\nexport const ENTRA_AUTHORITY = `https://login.microsoftonline.com/${serverEnv.ENTRA_TENANT_ID}`;\r\n\r\n/** Shared across login/callback/refresh calls — stateless and cheap to reuse. */\r\nexport const cryptoProvider = new CryptoProvider();\r\n\r\n/**\r\n * Creates a confidential client (no persistent cache plugin).\r\n *\r\n * The interactive/refresh flows below deliberately use a FRESH client per token\r\n * operation rather than a singleton: the client's in-memory cache then holds only\r\n * that operation's tokens, so {@link extractRefreshToken} reads the correct user's\r\n * refresh token — a shared client would accumulate (and leak) every user's tokens.\r\n * Persisting just the refresh token (not the whole multi-KB MSAL cache) is what\r\n * keeps the session in a small cookie with no server-side token store.\r\n */\r\nexport const createConfidentialClient = (): ConfidentialClientApplication =>\r\n  new ConfidentialClientApplication({\r\n    auth: {\r\n      clientId: serverEnv.ENTRA_CLIENT_ID,\r\n      clientSecret: serverEnv.ENTRA_CLIENT_SECRET,\r\n      authority: ENTRA_AUTHORITY,\r\n    },\r\n  });\r\n\r\n/**\r\n * Pulls the refresh-token secret out of a client's in-memory MSAL cache. MSAL\r\n * intentionally hides refresh tokens, so reading the serialized cache is the only\r\n * way to persist just the RT (instead of the whole cache) in the session cookie.\r\n */\r\nfunction extractRefreshToken(client: ConfidentialClientApplication): string | undefined {\r\n  const cache = JSON.parse(client.getTokenCache().serialize()) as {\r\n    RefreshToken?: Record<string, { secret?: string }>;\r\n  };\r\n  return Object.values(cache.RefreshToken ?? {})[0]?.secret;\r\n}\r\n\r\n/** First leg of the auth-code flow: the URL to redirect the browser to. */\r\nexport function buildAuthCodeUrl(opts: {\r\n  redirectUri: string;\r\n  state: string;\r\n  codeChallenge: string;\r\n}): Promise<string> {\r\n  return createConfidentialClient().getAuthCodeUrl({\r\n    scopes: LOGIN_SCOPES,\r\n    redirectUri: opts.redirectUri,\r\n    responseMode: \"query\",\r\n    state: opts.state,\r\n    codeChallenge: opts.codeChallenge,\r\n    codeChallengeMethod: \"S256\",\r\n  });\r\n}\r\n\r\n/**\r\n * Second leg of the auth-code flow: exchange the code for tokens, returning the\r\n * refresh token and the id-token claims to persist in the session.\r\n */\r\nexport async function exchangeCodeForSession(opts: {\r\n  code: string;\r\n  redirectUri: string;\r\n  codeVerifier: string;\r\n}): Promise<{ refreshToken: string; claims: TokenPayload }> {\r\n  const client = createConfidentialClient();\r\n  const result = await client.acquireTokenByCode({\r\n    code: opts.code,\r\n    scopes: LOGIN_SCOPES,\r\n    redirectUri: opts.redirectUri,\r\n    codeVerifier: opts.codeVerifier,\r\n  });\r\n\r\n  const refreshToken = extractRefreshToken(client);\r\n  if (!refreshToken)\r\n    throw new Error(\"No refresh token returned (is the offline_access scope granted?)\");\r\n\r\n  return { refreshToken, claims: (result.idTokenClaims ?? {}) as TokenPayload };\r\n}\r\n\r\n/**\r\n * Acquires a delegated access token for `scopes` from the stored refresh token.\r\n * Entra rotates the refresh token on each use, so the (possibly new) refresh\r\n * token is returned for the caller to persist. Throws when the refresh token is\r\n * expired/revoked and interaction is required.\r\n */\r\n/** The refresh token is expired/revoked — the user must sign in interactively. */\r\nexport class SessionExpiredError extends Error {}\r\n\r\nexport async function acquireDelegatedToken(opts: {\r\n  refreshToken: string;\r\n  scopes: ReadonlyArray<string>;\r\n}): Promise<{ accessToken: string; refreshToken: string }> {\r\n  const client = createConfidentialClient();\r\n  let result;\r\n  try {\r\n    result = await client.acquireTokenByRefreshToken({\r\n      refreshToken: opts.refreshToken,\r\n      scopes: [...opts.scopes],\r\n      forceCache: true,\r\n    });\r\n  } catch (error) {\r\n    // Revocation, inactivity expiry (AADSTS70008/700082/50173) and Conditional\r\n    // Access sign-in frequency (AADSTS70043) all surface as interaction-required\r\n    // or invalid_grant — the session is dead, only an interactive login helps.\r\n    if (\r\n      error instanceof InteractionRequiredAuthError ||\r\n      (error instanceof ServerError && error.errorCode === \"invalid_grant\")\r\n    )\r\n      throw new SessionExpiredError(error.message);\r\n    throw error;\r\n  }\r\n  if (!result) throw new Error(\"Failed to acquire token from refresh token\");\r\n\r\n  return {\r\n    accessToken: result.accessToken,\r\n    refreshToken: extractRefreshToken(client) ?? opts.refreshToken,\r\n  };\r\n}\r\n\r\n/** Entra's front-channel logout endpoint, which clears the user's IdP session. */\r\nexport function buildLogoutUrl(postLogoutRedirectUri: string): string {\r\n  const base = `${ENTRA_AUTHORITY}/oauth2/v2.0/logout`;\r\n  return `${base}?post_logout_redirect_uri=${encodeURIComponent(postLogoutRedirectUri)}`;\r\n}\r\n"],"mappings":";;;;;;;;;;AAgBA,MAAaM,eAAe;CAAC;CAAU;CAAW;CAAkB;AAAW;;AAG/E,MAAaC,kBAAkB,qCAAqCF,UAAUG;;AAG9E,MAAaC,iBAAiB,IAAIR,eAAe;;;;;;;;;;;AAYjD,MAAaS,iCACX,IAAIV,8BAA8B,EAChCW,MAAM;CACJC,UAAUP,UAAUQ;CACpBC,cAAcT,UAAUU;CACxBC,WAAWT;AACb,EACF,CAAC;;;;;;AAOH,SAASU,oBAAoBC,QAA2D;CACtF,MAAMC,QAAQC,KAAKC,MAAMH,OAAOI,cAAc,CAAC,CAACC,UAAU,CAAC;CAG3D,OAAOI,OAAOC,OAAOT,MAAMK,gBAAgB,CAAC,CAAC,CAAC,CAAC,EAAE,EAAEE;AACrD;;AAGA,SAAgBG,iBAAiBC,MAIb;CAClB,OAAOpB,yBAAyB,CAAC,CAACyB,eAAe;EAC/CC,QAAQ9B;EACRyB,aAAaD,KAAKC;EAClBM,cAAc;EACdL,OAAOF,KAAKE;EACZC,eAAeH,KAAKG;EACpBK,qBAAqB;CACvB,CAAC;AACH;;;;;AAMA,eAAsBC,uBAAuBT,MAIe;CAC1D,MAAMZ,SAASR,yBAAyB;CACxC,MAAMkC,SAAS,MAAM1B,OAAO2B,mBAAmB;EAC7CL,MAAMV,KAAKU;EACXJ,QAAQ9B;EACRyB,aAAaD,KAAKC;EAClBU,cAAcX,KAAKW;CACrB,CAAC;CAED,MAAMC,eAAezB,oBAAoBC,MAAM;CAC/C,IAAI,CAACwB,cACH,MAAM,IAAII,MAAM,kEAAkE;CAEpF,OAAO;EAAEJ;EAAcC,QAASC,OAAOG,iBAAiB,CAAC;CAAmB;AAC9E;;;;;;;;AASA,IAAaC,sBAAb,cAAyCF,MAAM,CAAA;AAE/C,eAAsBG,sBAAsBnB,MAGe;CACzD,MAAMZ,SAASR,yBAAyB;CACxC,IAAIkC;CACJ,IAAI;EACFA,SAAS,MAAM1B,OAAOkC,2BAA2B;GAC/CV,cAAcZ,KAAKY;GACnBN,QAAQ,CAAC,GAAGN,KAAKM,MAAM;GACvBiB,YAAY;EACd,CAAC;CACH,SAASC,OAAO;EAId,IACEA,iBAAiBpD,gCAChBoD,iBAAiBnD,eAAemD,MAAMC,cAAc,iBAErD,MAAM,IAAIP,oBAAoBM,MAAME,OAAO;EAC7C,MAAMF;CACR;CACA,IAAI,CAACV,QAAQ,MAAM,IAAIE,MAAM,4CAA4C;CAEzE,OAAO;EACLK,aAAaP,OAAOO;EACpBT,cAAczB,oBAAoBC,MAAM,KAAKY,KAAKY;CACpD;AACF;;AAGA,SAAgBe,eAAeC,uBAAuC;CAEpE,OAAO,GAAGC,GADMpD,gBAAe,qBACjB,4BAA6BqD,mBAAmBF,qBAAqB;AACrF"}