@webority/web-core 0.51.3 → 0.52.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -111,6 +111,19 @@ server's reason from the `X-Sign-Out-Reason` header (`signOutReasonHeader` renam
111
111
  off). A 401 that names a sign-out reason is a revoked session, so no refresh is tried. Cross-origin,
112
112
  the API lists the header in `Access-Control-Expose-Headers`.
113
113
 
114
+ A request never runs in a workspace other than the one it was made in. `initUserAuth`'s `accountId`
115
+ provider is read once, when a request is made, and sent as `X-Account-Id`; a retry, a replay after a
116
+ refresh and a replay after a session replacement send that same id and never read the provider again.
117
+ A request refused (401) for a session that has since been replaced is sent again with the stored
118
+ token only when the provider still names the account it was made in (or neither names one). When the
119
+ session moved to another workspace, it is not sent again: it rejects with `WorkspaceChangedError`
120
+ (`code: 'workspace_changed'`, `status: 409`) and the session is kept. An API answer of 409 with
121
+ `{ code: 'workspace_changed' }` rejects the same way, is never retried and never logs out.
122
+ `extractApiError` maps both to `code: 'workspace_changed'`; branch with `isWorkspaceChangedError(error)`,
123
+ which accepts the error or the `ApiError` made of it. Mark a call that ends the session and returns a
124
+ new one (a workspace switch) with `replacesSession: true`, so requests refused while it is open wait
125
+ for its tokens.
126
+
114
127
  `legacyKeys` moves a session an app kept under its own keys before it adopted the library: the first
115
128
  token read that finds the library's keys empty moves it (bare token strings are fine; an access
116
129
  token's expiry comes from its JWT) and deletes the old keys, so the deploy signs nobody out. Each move
@@ -1 +1 @@
1
- import{getConfigValue as e}from"../config/index.js";var t=`X-Client-Build`,n=`X-Client-Kind`,r=`X-Account-Id`,i=`Browser`,a=()=>{let t=e(`appVersion`);if(!t)return null;let n=e(`commit`);return n?`${t}+${n}`:t},o=(e,t,n)=>{n&&!e.has(t)&&e.set(t,n)},s=e=>{e.interceptors.request.use(e=>(o(e.headers,n,i),o(e.headers,t,a()),e))},c=async(e,t)=>{let n=t?await t():null;n&&e.set(r,n)};export{r as ACCOUNT_ID_HEADER,t as CLIENT_BUILD_HEADER,i as CLIENT_KIND,n as CLIENT_KIND_HEADER,c as applyAccountIdHeader,s as applyClientHeaders,a as getClientBuild};
1
+ import{getConfigValue as e}from"../config/index.js";var t=`X-Client-Build`,n=`X-Client-Kind`,r=`X-Account-Id`,i=`Browser`,a=()=>{let t=e(`appVersion`);if(!t)return null;let n=e(`commit`);return n?`${t}+${n}`:t},o=(e,t,n)=>{n&&!e.has(t)&&e.set(t,n)},s=e=>{e.interceptors.request.use(e=>(o(e.headers,n,i),o(e.headers,t,a()),e))},c=async e=>(e?await e():null)||null,l=async(e,t)=>{if(e._accountId!==void 0){e._accountId&&e.headers.set(r,e._accountId);return}let n=e.headers.get(r);if(typeof n==`string`&&n!==``){e._accountId=n;return}let i=await c(t);e._accountId=i,i&&e.headers.set(r,i)};export{r as ACCOUNT_ID_HEADER,t as CLIENT_BUILD_HEADER,i as CLIENT_KIND,n as CLIENT_KIND_HEADER,s as applyClientHeaders,a as getClientBuild,c as readAccountId,l as stampAccountId};
package/dist/api/index.js CHANGED
@@ -1 +1 @@
1
- import{ACCOUNT_ID_HEADER as e,CLIENT_BUILD_HEADER as t,CLIENT_KIND as n,CLIENT_KIND_HEADER as r,getClientBuild as i}from"./clientHeaders.js";import{MIN_APP_VERSION_HEADER as a,addMinAppVersionListener as o,getMinAppVersion as s}from"./minAppVersion.js";import{applyRequestDefaults as c,newCorrelationId as l}from"./requestDefaults.js";import{IDEMPOTENCY_KEY_HEADER as u,attachRetry as d,isNeverSentError as f,isRetryableFailure as p,isRetryableStatus as m,retryAfterMs as h}from"./retry.js";import{REFRESH_LOCK_NAME as g,withCrossTabRefreshLock as _}from"./crossTabRefreshLock.js";import v from"./publicClient.js";import{SIGN_OUT_REASON_HEADER as y,injectStore as b,injectUserAuth as x,refreshAccessToken as S,resetRefreshState as C}from"./tokenRefresh.js";import w,{RequestOfflineError as T,SESSION_TERMINATION_CODES as E}from"./userClient.js";import{initUserAuth as D}from"../initUserAuth.js";export{e as ACCOUNT_ID_HEADER,t as CLIENT_BUILD_HEADER,n as CLIENT_KIND,r as CLIENT_KIND_HEADER,u as IDEMPOTENCY_KEY_HEADER,a as MIN_APP_VERSION_HEADER,g as REFRESH_LOCK_NAME,T as RequestOfflineError,E as SESSION_TERMINATION_CODES,y as SIGN_OUT_REASON_HEADER,o as addMinAppVersionListener,c as applyRequestDefaults,d as attachRetry,i as getClientBuild,s as getMinAppVersion,D as initUserAuth,b as injectStore,x as injectUserAuth,f as isNeverSentError,p as isRetryableFailure,m as isRetryableStatus,l as newCorrelationId,v as publicClient,S as refreshAccessToken,C as resetRefreshState,h as retryAfterMs,w as userClient,_ as withCrossTabRefreshLock};
1
+ import{ACCOUNT_ID_HEADER as e,CLIENT_BUILD_HEADER as t,CLIENT_KIND as n,CLIENT_KIND_HEADER as r,getClientBuild as i}from"./clientHeaders.js";import{MIN_APP_VERSION_HEADER as a,addMinAppVersionListener as o,getMinAppVersion as s}from"./minAppVersion.js";import{applyRequestDefaults as c,newCorrelationId as l}from"./requestDefaults.js";import{WORKSPACE_CHANGED_CODE as u,WorkspaceChangedError as d,isWorkspaceChangedError as f}from"./workspaceChanged.js";import{IDEMPOTENCY_KEY_HEADER as p,attachRetry as m,isNeverSentError as h,isRetryableFailure as g,isRetryableStatus as _,retryAfterMs as v}from"./retry.js";import{REFRESH_LOCK_NAME as y,withCrossTabRefreshLock as b}from"./crossTabRefreshLock.js";import x from"./publicClient.js";import{SIGN_OUT_REASON_HEADER as S,injectStore as C,injectUserAuth as w,refreshAccessToken as T,resetRefreshState as E}from"./tokenRefresh.js";import D,{RequestOfflineError as O,SESSION_TERMINATION_CODES as k}from"./userClient.js";import{initUserAuth as A}from"../initUserAuth.js";export{e as ACCOUNT_ID_HEADER,t as CLIENT_BUILD_HEADER,n as CLIENT_KIND,r as CLIENT_KIND_HEADER,p as IDEMPOTENCY_KEY_HEADER,a as MIN_APP_VERSION_HEADER,y as REFRESH_LOCK_NAME,O as RequestOfflineError,k as SESSION_TERMINATION_CODES,S as SIGN_OUT_REASON_HEADER,u as WORKSPACE_CHANGED_CODE,d as WorkspaceChangedError,o as addMinAppVersionListener,c as applyRequestDefaults,m as attachRetry,i as getClientBuild,s as getMinAppVersion,A as initUserAuth,C as injectStore,w as injectUserAuth,h as isNeverSentError,g as isRetryableFailure,_ as isRetryableStatus,f as isWorkspaceChangedError,l as newCorrelationId,x as publicClient,T as refreshAccessToken,E as resetRefreshState,v as retryAfterMs,D as userClient,b as withCrossTabRefreshLock};
package/dist/api/retry.js CHANGED
@@ -1 +1 @@
1
- import{getConfigValue as e}from"../config/index.js";import t from"axios";var n=`Idempotency-Key`,r=e=>typeof e==`number`&&(e===429||e>=500&&e<=599),i=new Set([`get`,`head`,`options`,`put`,`delete`]),a=new Set([`ECONNREFUSED`,`ENOTFOUND`,`EAI_AGAIN`,`ENETUNREACH`,`EHOSTUNREACH`]),o=e=>!e.response&&typeof e.code==`string`&&a.has(e.code),s=3e4,c=e=>{let t=e.response;if(!t)return null;let n=t.data,r=t.headers,i=typeof r?.get==`function`?r.get(`retry-after`):r?.[`retry-after`];for(let e of[n?.retryAfterSeconds,i]){let t=Number(e);if(e!=null&&Number.isFinite(t)&&t>0)return t*1e3}return null},l=e=>{let t=e?.headers,r=typeof t?.get==`function`?t.get(n):t?.[n];return typeof r==`string`&&r!==``},u=e=>{if(t.isCancel(e)||e.code===`ERR_CANCELED`)return!1;if(o(e))return!0;let n=(e.config?.method??`get`).toLowerCase(),a=i.has(n),u=e.response?.status;if(u===void 0)return a;if(!r(u))return!1;let d=c(e);return d!==null&&d>s?!1:a||(u===429||u===503)&&l(e.config)},d={retryDelayMs:e=>e<=1?0:2**e*1e3,shouldRetry:u,onRetry:()=>void 0},f=(e,t)=>new Promise(n=>{let r=()=>{clearTimeout(i),n(!1)},i=setTimeout(()=>{t?.removeEventListener?.(`abort`,r),n(!0)},e);t?.addEventListener?.(`abort`,r,{once:!0})}),p=(t,n={})=>{t.interceptors.response.use(void 0,async r=>{let i=r,a=i?.config;if(!a||a.retry===!1)return Promise.reject(r);let o=typeof a.retry==`object`?a.retry:{},s={...d,retries:e(`retries`),...n,...o},l=(a.__retryAttempt??0)+1;return l>s.retries||!s.shouldRetry(i)||a.signal?.aborted||(a.__retryAttempt=l,a.transformRequest=[e=>e],s.onRetry(l,i),!await f(c(i)??s.retryDelayMs(l),a.signal))?Promise.reject(r):t.request(a)})};export{n as IDEMPOTENCY_KEY_HEADER,p as attachRetry,o as isNeverSentError,u as isRetryableFailure,r as isRetryableStatus,c as retryAfterMs};
1
+ import{getConfigValue as e}from"../config/index.js";import{isWorkspaceChangedResponse as t}from"./workspaceChanged.js";import n from"axios";var r=`Idempotency-Key`,i=e=>typeof e==`number`&&(e===429||e>=500&&e<=599),a=new Set([`get`,`head`,`options`,`put`,`delete`]),o=new Set([`ECONNREFUSED`,`ENOTFOUND`,`EAI_AGAIN`,`ENETUNREACH`,`EHOSTUNREACH`]),s=e=>!e.response&&typeof e.code==`string`&&o.has(e.code),c=3e4,l=e=>{let t=e.response;if(!t)return null;let n=t.data,r=t.headers,i=typeof r?.get==`function`?r.get(`retry-after`):r?.[`retry-after`];for(let e of[n?.retryAfterSeconds,i]){let t=Number(e);if(e!=null&&Number.isFinite(t)&&t>0)return t*1e3}return null},u=e=>{let t=e?.headers,n=typeof t?.get==`function`?t.get(r):t?.[r];return typeof n==`string`&&n!==``},d=e=>{if(n.isCancel(e)||e.code===`ERR_CANCELED`)return!1;if(s(e))return!0;let t=(e.config?.method??`get`).toLowerCase(),r=a.has(t),o=e.response?.status;if(o===void 0)return r;if(!i(o))return!1;let d=l(e);return d!==null&&d>c?!1:r||(o===429||o===503)&&u(e.config)},f={retryDelayMs:e=>e<=1?0:2**e*1e3,shouldRetry:d,onRetry:()=>void 0},p=(e,t)=>new Promise(n=>{let r=()=>{clearTimeout(i),n(!1)},i=setTimeout(()=>{t?.removeEventListener?.(`abort`,r),n(!0)},e);t?.addEventListener?.(`abort`,r,{once:!0})}),m=(n,r={})=>{n.interceptors.response.use(void 0,async i=>{let a=i,o=a?.config;if(!o||o.retry===!1)return Promise.reject(i);let s=typeof o.retry==`object`?o.retry:{},c={...f,retries:e(`retries`),...r,...s},u=(o.__retryAttempt??0)+1;return t(a)||u>c.retries||!c.shouldRetry(a)||o.signal?.aborted||(o.__retryAttempt=u,o.transformRequest=[e=>e],c.onRetry(u,a),!await p(l(a)??c.retryDelayMs(u),o.signal))?Promise.reject(i):n.request(o)})};export{r as IDEMPOTENCY_KEY_HEADER,m as attachRetry,s as isNeverSentError,d as isRetryableFailure,i as isRetryableStatus,l as retryAfterMs};
@@ -1 +1 @@
1
- import{Logger as e}from"../logger/logger.js";import{ACCESS_TOKEN_KEY as t,REFRESH_TOKEN_KEY as n,clearSession as r,getToken as i,storeToken as a}from"../auth/authStore.js";import{withCrossTabRefreshLock as o}from"./crossTabRefreshLock.js";import s from"./publicClient.js";import c from"axios";var l,u,d=e=>{l=e},f=e=>{u=e},p=null,m=[],h=(e,t=null)=>{m.forEach(({resolve:n,reject:r})=>{e?r(e):t!==null&&n(t)}),m=[]},g=()=>{p=null,h(Error(`[@webority/web-core] Session ended`))},_=e=>e instanceof Error?e.message:typeof e==`string`?e:`Unknown error`,v=()=>{if(!l||!u)throw Error(`User package store is not initialized. Call initUserAuth first.`);return{store:l,auth:u}},y=()=>u?.accountId,b=()=>u?.refresh.path,x=async()=>{let e=v();await e.store.dispatch(e.auth.logout())},S=`X-Sign-Out-Reason`,C=e=>{let t=u?.signOutReasonHeader===void 0?S:u.signOutReasonHeader;if(!t||!e||typeof e!=`object`)return null;let n=e.get,r=typeof n==`function`?n.call(e,t):e[t.toLowerCase()];return typeof r==`string`&&r.trim()!==``?r.trim():null},w=async(e,t)=>{await r(e,{signOutReason:t}),await x()},T=()=>p!==null,E=()=>new Promise((e,t)=>{m.push({resolve:e,reject:t})}),D=class e extends Error{constructor(t){super(t),this.name=`SessionEndedError`,Object.setPrototypeOf(this,e.prototype)}},O=e=>e instanceof D||c.isAxiosError(e)&&(e.response?.status===400||e.response?.status===401),k=(e,t)=>{if(typeof e!=`string`||e===``)throw Error(`The refresh response has no "${t}".`);return e},A=e=>{let t=e??{};return{accessToken:k(t.accessToken,`accessToken`),accessTokenExpiry:k(t.accessTokenExpiration,`accessTokenExpiration`),refreshToken:k(t.refreshToken,`refreshToken`),refreshTokenExpiry:k(t.refreshTokenExpiration,`refreshTokenExpiration`)}},j=async r=>{let o=await i(n);if(!o?.token)throw new D(`No refresh token available`);if(Date.parse(o.expiry)<=Date.now())throw new D(`Refresh token expired`);let c={accessToken:(await i(`accessToken`))?.token??null,refreshToken:o.token},l=r.body?await r.body(c):c,u=r.headers?await r.headers():void 0;e.info(`Refreshing the access token`);let{data:d}=await s.post(r.path,l,u?{headers:u}:void 0),f=(r.readTokens??A)(d);return await a(t,f.accessToken,f.accessTokenExpiry),await a(n,f.refreshToken,f.refreshTokenExpiry),e.info(`Access token refreshed`),f.accessToken},M=30*1e3,N=async(n,r)=>{let a=await i(t);return a?.token&&a.token!==r&&Date.parse(a.expiry)>Date.now()+M?(e.info(`Adopting the access token another tab refreshed`),a.token):j(n)},P=()=>{if(p)return p;let t=(async()=>{try{let{refresh:e}=v().auth,t=(await i(`accessToken`))?.token??null,n=await o(()=>N(e,t));return h(null,n),n}catch(t){let n=c.isAxiosError(t)?t.response?.data:void 0;throw e.error(`Token refresh failed:`,n??_(t)),h(t,null),O(t)&&await w(`rejected`,c.isAxiosError(t)?C(t.response?.headers):null),t}})().finally(()=>{p===t&&(p=null)});return p=t,t};export{S as SIGN_OUT_REASON_HEADER,x as dispatchLogout,w as endSessionAndLogout,y as getAccountIdProvider,_ as getErrorMessage,b as getRefreshPath,d as injectStore,f as injectUserAuth,T as isRefreshInFlight,E as queueBehindRefresh,C as readSignOutReason,P as refreshAccessToken,g as resetRefreshState};
1
+ import{Logger as e}from"../logger/logger.js";import{ACCESS_TOKEN_KEY as t,REFRESH_TOKEN_KEY as n,clearSession as r,getToken as i,storeToken as a}from"../auth/authStore.js";import{withCrossTabRefreshLock as o}from"./crossTabRefreshLock.js";import s from"./publicClient.js";import c from"axios";var l,u,d=e=>{l=e},f=e=>{u=e},p=null,m=[],h=(e,t=null)=>{m.forEach(({resolve:n,reject:r})=>{e?r(e):t!==null&&n(t)}),m=[]},g=()=>{p=null,h(Error(`[@webority/web-core] Session ended`))},_=e=>e instanceof Error?e.message:typeof e==`string`?e:`Unknown error`,v=()=>{if(!l||!u)throw Error(`User package store is not initialized. Call initUserAuth first.`);return{store:l,auth:u}},y=()=>u?.accountId,b=()=>u?.refresh.path,x=async()=>{let e=v();await e.store.dispatch(e.auth.logout())},S=`X-Sign-Out-Reason`,C=e=>{let t=u?.signOutReasonHeader===void 0?S:u.signOutReasonHeader;if(!t||!e||typeof e!=`object`)return null;let n=e.get,r=typeof n==`function`?n.call(e,t):e[t.toLowerCase()];return typeof r==`string`&&r.trim()!==``?r.trim():null},w=async(e,t)=>{await r(e,{signOutReason:t}),await x()},T=()=>p!==null,E=0,D=Promise.resolve(),O=null,k=()=>{E===0&&(D=new Promise(e=>{O=e})),E+=1;let e=!1;return()=>{e||(e=!0,--E,E===0&&(O?.(),O=null))}},A=()=>E===0?Promise.resolve():D,j=async r=>{let i;try{i=(u?.refresh.readTokens??L)(r)}catch(t){e.error(`A session-replacing response carried no tokens:`,_(t));return}await a(t,i.accessToken,i.accessTokenExpiry),await a(n,i.refreshToken,i.refreshTokenExpiry)},M=async e=>{let t=(await i(`accessToken`))?.token??null;return t!==null&&t!==e?t:null},N=()=>new Promise((e,t)=>{m.push({resolve:e,reject:t})}),P=class e extends Error{constructor(t){super(t),this.name=`SessionEndedError`,Object.setPrototypeOf(this,e.prototype)}},F=e=>e instanceof P||c.isAxiosError(e)&&(e.response?.status===400||e.response?.status===401),I=(e,t)=>{if(typeof e!=`string`||e===``)throw Error(`The refresh response has no "${t}".`);return e},L=e=>{let t=e??{};return{accessToken:I(t.accessToken,`accessToken`),accessTokenExpiry:I(t.accessTokenExpiration,`accessTokenExpiration`),refreshToken:I(t.refreshToken,`refreshToken`),refreshTokenExpiry:I(t.refreshTokenExpiration,`refreshTokenExpiration`)}},R=async r=>{let o=await i(n);if(!o?.token)throw new P(`No refresh token available`);if(Date.parse(o.expiry)<=Date.now())throw new P(`Refresh token expired`);let c={accessToken:(await i(`accessToken`))?.token??null,refreshToken:o.token},l=r.body?await r.body(c):c,u=r.headers?await r.headers():void 0;e.info(`Refreshing the access token`);let{data:d}=await s.post(r.path,l,u?{headers:u}:void 0),f=(r.readTokens??L)(d);await A();let p=(await i(`refreshToken`))?.token===o.token?null:await i(t);return p?.token?(e.info(`Keeping the session that replaced this one during the refresh`),p.token):(await a(t,f.accessToken,f.accessTokenExpiry),await a(n,f.refreshToken,f.refreshTokenExpiry),e.info(`Access token refreshed`),f.accessToken)},z=30*1e3,B=async(n,r)=>{let a=await i(t);return a?.token&&a.token!==r&&Date.parse(a.expiry)>Date.now()+z?(e.info(`Adopting the access token another tab refreshed`),a.token):R(n)},V=()=>{if(p)return p;let t=(async()=>{let t=null;try{let{refresh:e}=v().auth;t=(await i(`accessToken`))?.token??null;let n=await o(()=>B(e,t));return h(null,n),n}catch(n){if(F(n)){await A();let n=await M(t);if(n)return e.info(`The session was replaced during the refresh; continuing with the new one`),h(null,n),n}let r=c.isAxiosError(n)?n.response?.data:void 0;throw e.error(`Token refresh failed:`,r??_(n)),h(n,null),F(n)&&await w(`rejected`,c.isAxiosError(n)?C(n.response?.headers):null),n}})().finally(()=>{p===t&&(p=null)});return p=t,t};export{S as SIGN_OUT_REASON_HEADER,k as beginSessionReplacement,M as currentTokenIfReplaced,x as dispatchLogout,w as endSessionAndLogout,y as getAccountIdProvider,_ as getErrorMessage,b as getRefreshPath,d as injectStore,f as injectUserAuth,T as isRefreshInFlight,N as queueBehindRefresh,C as readSignOutReason,V as refreshAccessToken,g as resetRefreshState,j as storeReplacementSession,A as waitForSessionReplacement};
@@ -1 +1 @@
1
- import{Logger as e}from"../logger/logger.js";import{ACCESS_TOKEN_KEY as t,getToken as n}from"../auth/authStore.js";import{getLastKnownNetworkStatus as r}from"../network/networkStatus.js";import{applyConfiguredBaseUrl as i}from"./baseUrl.js";import{applyAccountIdHeader as a,applyClientHeaders as o}from"./clientHeaders.js";import{readMinAppVersion as s}from"./minAppVersion.js";import{sendFormDataAsMultipart as c}from"./multipart.js";import{applyRequestDefaults as l}from"./requestDefaults.js";import{responseBodyText as u}from"./responseBodyText.js";import{attachRetry as d}from"./retry.js";import{SIGN_OUT_REASON_HEADER as f,endSessionAndLogout as p,getAccountIdProvider as m,getErrorMessage as h,getRefreshPath as g,injectStore as _,injectUserAuth as v,isRefreshInFlight as y,queueBehindRefresh as b,readSignOutReason as x,refreshAccessToken as S,resetRefreshState as C}from"./tokenRefresh.js";import w,{AxiosHeaders as T}from"axios";var E=class e extends Error{constructor(t=`Request blocked: device is offline`){super(t),this.name=`RequestOfflineError`,Object.setPrototypeOf(this,e.prototype)}},D=class e extends Error{constructor(){super(`No access token available`),this.name=`NoAccessTokenError`,Object.setPrototypeOf(this,e.prototype)}},O={TERMINATED:`SESSION_TERMINATED`,EXPIRED:`SESSION_EXPIRED`},k=new Set(Object.values(O)),A=`x-security-stamp-invalid`,j=e=>{if(!e||typeof e!=`object`)return!1;let t=e.get,n=typeof t==`function`?t.call(e,A):e[A];return String(n).toLowerCase()===`true`},M=e=>{if(!e||typeof e!=`object`)return;let t=e,n=t.error,r=[t.code,t.errorCode,n&&typeof n==`object`?n.code:void 0];for(let e of r)if(typeof e==`string`&&k.has(e))return e},N=w.create({headers:{"Content-Type":`application/json`}});s(N),d(N);var P=(e,t)=>{e.headers instanceof T?e.headers.set(`Authorization`,`Bearer ${t}`):e.headers=T.from({...e.headers,Authorization:`Bearer ${t}`})},F=e=>{let t=g();return!!t&&!!e?.includes(t)};i(N),l(N),c(N),o(N),N.interceptors.request.use(async i=>{if((await r()).isConnected===!1)return e.warn(`Request blocked: browser is offline. URL: ${i.url}`),Promise.reject(new E);try{if(F(i.url))return i;let r=await n(t);if(!r?.token)throw new D;let o=Date.now();return Date.parse(r.expiry)<=o+30*1e3?(e.info(`Access token expired or about to expire, refreshing before request`),P(i,await S()),await a(i.headers,m()),i):(P(i,r.token),await a(i.headers,m()),i)}catch(t){return t instanceof D?e.info(`Request not sent: signed out. URL: ${i.url}`):e.error(`Failed to attach token:`,h(t)),Promise.reject(t)}},t=>(e.error(`Request error:`,h(t)),Promise.reject(t))),N.interceptors.response.use(e=>e,async t=>{if(w.isCancel(t)||t instanceof E||t instanceof D)return Promise.reject(t);if(!w.isAxiosError(t))return e.error(`Non-axios error in response interceptor: ${h(t)}`),Promise.reject(t);let n=t,r=n.config;if(!n.response)return r?.skipErrorLog||e.error(`Network error or no response from server: ${n.message}, URL: ${r?.url}`),Promise.reject(t);if(n.response.status===401&&r&&!r._retry){if(F(r.url))return Promise.reject(t);let i=x(n.response.headers),a=M(n.response.data)??(j(n.response.headers)||i!==null?O.TERMINATED:void 0);if(a)return e.warn(`Session ${a}, logging out without refresh. URL: ${r.url}`),C(),await p(`terminated`,i),Promise.reject(t);if(e.warn(`Unauthorized URL: ${r.url}`),r._retry=!0,y())return b().then(t=>(e.info(`Retrying request after token refresh:`,r.url),P(r,t),N(r))).catch(t=>(e.error(`Failed to retry request after token refresh:`,h(t)),Promise.reject(t)));try{return P(r,await S()),N(r)}catch(e){return Promise.reject(e)}}return r?.skipErrorLog||e.error(`Response error: ${u(n.response?.data,n.message)}, URL: ${r?.url}, Status: ${n.response.status}`),Promise.reject(t)});export{E as RequestOfflineError,A as SESSION_INVALID_HEADER,O as SESSION_TERMINATION_CODES,f as SIGN_OUT_REASON_HEADER,N as default,_ as injectStore,v as injectUserAuth,C as resetRefreshState};
1
+ import{Logger as e}from"../logger/logger.js";import{ACCESS_TOKEN_KEY as t,getToken as n}from"../auth/authStore.js";import{getLastKnownNetworkStatus as r}from"../network/networkStatus.js";import{applyConfiguredBaseUrl as i}from"./baseUrl.js";import{applyClientHeaders as a,readAccountId as o,stampAccountId as s}from"./clientHeaders.js";import{readMinAppVersion as c}from"./minAppVersion.js";import{sendFormDataAsMultipart as l}from"./multipart.js";import{applyRequestDefaults as u}from"./requestDefaults.js";import{responseBodyText as d}from"./responseBodyText.js";import{WorkspaceChangedError as f,isWorkspaceChangedResponse as p,workspaceChangedFromResponse as m}from"./workspaceChanged.js";import{attachRetry as h}from"./retry.js";import{SIGN_OUT_REASON_HEADER as g,beginSessionReplacement as _,currentTokenIfReplaced as v,endSessionAndLogout as y,getAccountIdProvider as b,getErrorMessage as x,getRefreshPath as S,injectStore as C,injectUserAuth as w,isRefreshInFlight as T,queueBehindRefresh as E,readSignOutReason as D,refreshAccessToken as O,resetRefreshState as k,storeReplacementSession as A,waitForSessionReplacement as j}from"./tokenRefresh.js";import M,{AxiosHeaders as N}from"axios";var P=class e extends Error{constructor(t=`Request blocked: device is offline`){super(t),this.name=`RequestOfflineError`,Object.setPrototypeOf(this,e.prototype)}},F=class e extends Error{constructor(){super(`No access token available`),this.name=`NoAccessTokenError`,Object.setPrototypeOf(this,e.prototype)}},I={TERMINATED:`SESSION_TERMINATED`,EXPIRED:`SESSION_EXPIRED`},L=new Set(Object.values(I)),R=`x-security-stamp-invalid`,z=e=>{if(!e||typeof e!=`object`)return!1;let t=e.get,n=typeof t==`function`?t.call(e,R):e[R];return String(n).toLowerCase()===`true`},B=e=>{if(!e||typeof e!=`object`)return;let t=e,n=t.error,r=[t.code,t.errorCode,n&&typeof n==`object`?n.code:void 0];for(let e of r)if(typeof e==`string`&&L.has(e))return e},V=e=>{e?._endReplacement?.(),e&&(e._endReplacement=void 0)},H=e=>{let t=e.headers instanceof N?e.headers.get(`Authorization`):void 0;return typeof t==`string`&&t.startsWith(`Bearer `)?t.slice(7):null},U=M.create({headers:{"Content-Type":`application/json`}});c(U),h(U);var W=(e,t)=>{e.headers instanceof N?e.headers.set(`Authorization`,`Bearer ${t}`):e.headers=N.from({...e.headers,Authorization:`Bearer ${t}`})},G=e=>{let t=S();return!!t&&!!e?.includes(t)};i(U),u(U),l(U),a(U),U.interceptors.request.use(async i=>{if(V(i),(await r()).isConnected===!1)return e.warn(`Request blocked: browser is offline. URL: ${i.url}`),Promise.reject(new P);try{if(G(i.url))return i;await s(i,b());let r=await n(t);if(!r?.token)throw new F;let a=Date.now();return Date.parse(r.expiry)<=a+30*1e3?(e.info(`Access token expired or about to expire, refreshing before request`),W(i,await O())):W(i,r.token),i.replacesSession&&(i._endReplacement=_()),i}catch(t){return t instanceof F?e.info(`Request not sent: signed out. URL: ${i.url}`):e.error(`Failed to attach token:`,x(t)),Promise.reject(t)}},t=>(e.error(`Request error:`,x(t)),Promise.reject(t))),U.interceptors.response.use(async e=>{let t=e.config;if(t._endReplacement)try{await A(e.data)}finally{V(t)}return e},async t=>{if(V(t?.config),M.isCancel(t)||t instanceof P||t instanceof F||t instanceof f)return Promise.reject(t);if(!M.isAxiosError(t))return e.error(`Non-axios error in response interceptor: ${x(t)}`),Promise.reject(t);let n=t,r=n.config;if(!n.response)return r?.skipErrorLog||e.error(`Network error or no response from server: ${n.message}, URL: ${r?.url}`),Promise.reject(t);if(p(n))return e.info(`Request refused: the workspace changed after it was made. URL: ${r?.url}`),Promise.reject(m(n));if(n.response.status===401&&r&&!r._replayedWithCurrentToken&&!r.replacesSession&&!G(r.url)&&(await j(),await v(H(r))))return(r._accountId??null)===await o(b())?(e.info(`Session changed while the request was in flight; sending it again. URL: ${r.url}`),r._replayedWithCurrentToken=!0,U(r)):(e.info(`Session moved to another workspace while the request was in flight; not sending it again. URL: ${r.url}`),Promise.reject(new f(void 0,{config:r,cause:t})));if(n.response.status===401&&r&&!r._retry){if(G(r.url))return Promise.reject(t);let i=D(n.response.headers),a=B(n.response.data)??(z(n.response.headers)||i!==null?I.TERMINATED:void 0);if(a)return e.warn(`Session ${a}, logging out without refresh. URL: ${r.url}`),k(),await y(`terminated`,i),Promise.reject(t);if(e.warn(`Unauthorized URL: ${r.url}`),r._retry=!0,T())return E().then(t=>(e.info(`Retrying request after token refresh:`,r.url),W(r,t),U(r))).catch(t=>(t instanceof f||e.error(`Failed to retry request after token refresh:`,x(t)),Promise.reject(t)));try{return W(r,await O()),U(r)}catch(e){return Promise.reject(e)}}return r?.skipErrorLog||e.error(`Response error: ${d(n.response?.data,n.message)}, URL: ${r?.url}, Status: ${n.response.status}`),Promise.reject(t)});export{P as RequestOfflineError,R as SESSION_INVALID_HEADER,I as SESSION_TERMINATION_CODES,g as SIGN_OUT_REASON_HEADER,U as default,C as injectStore,w as injectUserAuth,k as resetRefreshState};
@@ -0,0 +1 @@
1
+ var e=`workspace_changed`,t=e=>e&&typeof e==`object`&&!Array.isArray(e)?e:null,n=class t extends Error{code=e;response;config;cause;constructor(e=`This was started in another workspace. Switch back to it and try again.`,n={}){super(e),this.name=`WorkspaceChangedError`,this.response=n.response??{status:409,data:{code:`workspace_changed`,message:e},headers:{}},this.config=n.config,this.cause=n.cause,Object.setPrototypeOf(this,t.prototype)}},r=e=>e instanceof n||typeof e==`object`&&!!e&&(e.name===`WorkspaceChangedError`||e.code===`workspace_changed`),i=e=>e.response?.status===409&&t(e.response.data)?.code===`workspace_changed`,a=e=>{let r=t(e.response?.data)?.message;return new n(typeof r==`string`&&r.trim()!==``?r:void 0,{response:e.response,config:e.config,cause:e})};export{e as WORKSPACE_CHANGED_CODE,n as WorkspaceChangedError,r as isWorkspaceChangedError,i as isWorkspaceChangedResponse,a as workspaceChangedFromResponse};
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- import{DEFAULT_CORRELATION_ID_HEADER as e,DEFAULT_LOG_LEVEL as t,DEFAULT_RETRIES as n,DEFAULT_TIMEOUT_MS as r,getConfig as i,getConfigValue as a,resetConfig as o,setConfig as s}from"./config/index.js";import{Logger as c,configureLogger as l,getDeviceContext as u,setDeviceContext as d,setRemoteLogger as f}from"./logger/logger.js";import{decodeJWT as p,getJWTExpiry as m,isJWTExpired as h}from"./auth/jwt.js";import{ACCESS_TOKEN_KEY as g,LEGACY_REFRESH_TOKEN_LIFETIME_MS as _,REFRESH_TOKEN_KEY as v,SESSION_USER_KEY as y,addSessionEndedListener as b,clearSession as x,getKeepSignedIn as S,getSessionUser as C,getToken as w,hasSession as T,hasSessionEndedInTab as E,removeSessionUser as D,removeToken as O,setKeepSignedIn as k,setLegacyTokenKeys as A,storeSessionUser as j,storeToken as M}from"./auth/authStore.js";import{addNetworkStatusListener as N,getNetworkStatus as P,setNetworkStatusImplementation as F,useNetworkStatus as I}from"./network/networkStatus.js";import{ACCOUNT_ID_HEADER as L,CLIENT_BUILD_HEADER as R,CLIENT_KIND as z,CLIENT_KIND_HEADER as B,getClientBuild as V}from"./api/clientHeaders.js";import{MIN_APP_VERSION_HEADER as H,addMinAppVersionListener as U,getMinAppVersion as W}from"./api/minAppVersion.js";import{applyRequestDefaults as G,newCorrelationId as K}from"./api/requestDefaults.js";import{IDEMPOTENCY_KEY_HEADER as q,attachRetry as J,isNeverSentError as Y,isRetryableFailure as X,isRetryableStatus as Z,retryAfterMs as Q}from"./api/retry.js";import{REFRESH_LOCK_NAME as $,withCrossTabRefreshLock as ee}from"./api/crossTabRefreshLock.js";import te from"./api/publicClient.js";import{SIGN_OUT_REASON_HEADER as ne,injectStore as re,injectUserAuth as ie,refreshAccessToken as ae,resetRefreshState as oe}from"./api/tokenRefresh.js";import se,{RequestOfflineError as ce,SESSION_TERMINATION_CODES as le}from"./api/userClient.js";import{initUserAuth as ue}from"./initUserAuth.js";import{CONTRACT_ERROR_CODE as de,NETWORK_CODE as fe,OFFLINE_MESSAGE as pe,REQUEST_CANCELED_CODE as me,SERVER_FAILURE_MESSAGE as he,TIMEOUT_CODE as ge,TIMEOUT_MESSAGE as _e,UNEXPECTED_MESSAGE as ve,apiErrorMessage as ye,extractApiError as be,isApiError as xe,isNoResponse as Se,toFieldPath as Ce}from"./errors/extractApiError.js";import{configureFormatters as we,getFormatterConfig as Te}from"./formatters/formatConfig.js";import{formatCurrency as Ee}from"./formatters/currency.js";import{addDays as De}from"./formatters/dateMath.js";import{formatDate as Oe,formatDateTime as ke,formatMinor as Ae,formatNumber as je,formatPercent as Me,formatRelative as Ne,formatTime as Pe}from"./formatters/house.js";import{initWebority as Fe}from"./initSDK.js";import{createApiBaseQuery as Ie}from"./query/apiBaseQuery.js";import{APP_API_DEFAULTS as Le,createAppApi as Re}from"./query/createAppApi.js";import{setupApiListeners as ze}from"./query/listeners.js";import{buildListQuery as Be,getNextPageParam as Ve,getPreviousPageParam as He,hasNextPage as Ue,hasPreviousPage as We}from"./query/paging.js";import{LIST_TAG_ID as Ge,itemTag as Ke,listTag as qe,providesList as Je,providesPagedList as Ye}from"./query/tags.js";import{errorToastMiddleware as Xe,isSilentRejection as Ze}from"./redux/errorToastMiddleware.js";import{createRootReducer as Qe,resetState as $e}from"./redux/rootReducer.js";import{SERIALIZABLE_IGNORED_ACTION_PATHS as et}from"./redux/serializableCheck.js";import{hasRecentlyReloaded as tt,importWithReload as nt,isChunkLoadError as rt,lazyWithReload as it,markReloaded as at}from"./router/chunkReload.js";import{hasPermission as ot}from"./router/permissions.js";import{RETURN_TO_PARAM as st,buildLoginPath as ct,getReturnTo as lt,toSafeReturnTo as ut}from"./router/returnTo.js";import{PublicOnly as dt,RequireAuth as ft,RequirePermission as pt}from"./router/guards.js";import{publicOnly as mt,requireAuth as ht,requirePermission as gt,withGuards as _t}from"./router/loaders.js";import{classifyRouteError as vt,useRouteErrorReport as yt}from"./router/routeError.js";import{Storage as bt,isStorageAvailable as xt,setStorageImplementation as St,syncStorage as Ct}from"./storage/index.js";import{isBuildBelowMinimum as wt,parseBuildNumber as Tt}from"./versionCheck/buildNumber.js";import{installReloadOnNavigate as Et,isUpdateAvailable as Dt,useVersionCheck as Ot}from"./versionCheck/index.js";export{g as ACCESS_TOKEN_KEY,L as ACCOUNT_ID_HEADER,Le as APP_API_DEFAULTS,R as CLIENT_BUILD_HEADER,z as CLIENT_KIND,B as CLIENT_KIND_HEADER,de as CONTRACT_ERROR_CODE,e as DEFAULT_CORRELATION_ID_HEADER,t as DEFAULT_LOG_LEVEL,n as DEFAULT_RETRIES,r as DEFAULT_TIMEOUT_MS,q as IDEMPOTENCY_KEY_HEADER,_ as LEGACY_REFRESH_TOKEN_LIFETIME_MS,Ge as LIST_TAG_ID,c as Logger,H as MIN_APP_VERSION_HEADER,fe as NETWORK_CODE,pe as OFFLINE_MESSAGE,dt as PublicOnly,$ as REFRESH_LOCK_NAME,v as REFRESH_TOKEN_KEY,me as REQUEST_CANCELED_CODE,st as RETURN_TO_PARAM,ce as RequestOfflineError,ft as RequireAuth,pt as RequirePermission,et as SERIALIZABLE_IGNORED_ACTION_PATHS,he as SERVER_FAILURE_MESSAGE,le as SESSION_TERMINATION_CODES,y as SESSION_USER_KEY,ne as SIGN_OUT_REASON_HEADER,bt as Storage,ge as TIMEOUT_CODE,_e as TIMEOUT_MESSAGE,ve as UNEXPECTED_MESSAGE,De as addDays,U as addMinAppVersionListener,N as addNetworkStatusListener,b as addSessionEndedListener,ye as apiErrorMessage,G as applyRequestDefaults,J as attachRetry,Be as buildListQuery,ct as buildLoginPath,vt as classifyRouteError,x as clearSession,we as configureFormatters,l as configureLogger,Ie as createApiBaseQuery,Re as createAppApi,Qe as createRootReducer,p as decodeJWT,Xe as errorToastMiddleware,be as extractApiError,Ee as formatCurrency,Oe as formatDate,ke as formatDateTime,Ae as formatMinor,je as formatNumber,Me as formatPercent,Ne as formatRelative,Pe as formatTime,V as getClientBuild,i as getConfig,a as getConfigValue,u as getDeviceContext,Te as getFormatterConfig,m as getJWTExpiry,S as getKeepSignedIn,W as getMinAppVersion,P as getNetworkStatus,Ve as getNextPageParam,He as getPreviousPageParam,lt as getReturnTo,C as getSessionUser,w as getToken,Ue as hasNextPage,ot as hasPermission,We as hasPreviousPage,tt as hasRecentlyReloaded,T as hasSession,E as hasSessionEndedInTab,nt as importWithReload,ue as initUserAuth,Fe as initWebority,re as injectStore,ie as injectUserAuth,Et as installReloadOnNavigate,xe as isApiError,wt as isBuildBelowMinimum,rt as isChunkLoadError,h as isJWTExpired,Y as isNeverSentError,Se as isNoResponse,X as isRetryableFailure,Z as isRetryableStatus,Ze as isSilentRejection,xt as isStorageAvailable,Dt as isUpdateAvailable,Ke as itemTag,it as lazyWithReload,qe as listTag,at as markReloaded,K as newCorrelationId,Tt as parseBuildNumber,Je as providesList,Ye as providesPagedList,te as publicClient,mt as publicOnly,ae as refreshAccessToken,D as removeSessionUser,O as removeToken,ht as requireAuth,gt as requirePermission,o as resetConfig,oe as resetRefreshState,$e as resetState,Q as retryAfterMs,s as setConfig,d as setDeviceContext,k as setKeepSignedIn,A as setLegacyTokenKeys,F as setNetworkStatusImplementation,f as setRemoteLogger,St as setStorageImplementation,ze as setupApiListeners,j as storeSessionUser,M as storeToken,Ct as syncStorage,Ce as toFieldPath,ut as toSafeReturnTo,I as useNetworkStatus,yt as useRouteErrorReport,Ot as useVersionCheck,se as userClient,ee as withCrossTabRefreshLock,_t as withGuards};
1
+ import{DEFAULT_CORRELATION_ID_HEADER as e,DEFAULT_LOG_LEVEL as t,DEFAULT_RETRIES as n,DEFAULT_TIMEOUT_MS as r,getConfig as i,getConfigValue as a,resetConfig as o,setConfig as s}from"./config/index.js";import{Logger as c,configureLogger as l,getDeviceContext as u,setDeviceContext as d,setRemoteLogger as f}from"./logger/logger.js";import{decodeJWT as p,getJWTExpiry as m,isJWTExpired as h}from"./auth/jwt.js";import{ACCESS_TOKEN_KEY as g,LEGACY_REFRESH_TOKEN_LIFETIME_MS as _,REFRESH_TOKEN_KEY as v,SESSION_USER_KEY as y,addSessionEndedListener as b,clearSession as x,getKeepSignedIn as S,getSessionUser as C,getToken as w,hasSession as T,hasSessionEndedInTab as E,removeSessionUser as D,removeToken as O,setKeepSignedIn as k,setLegacyTokenKeys as A,storeSessionUser as j,storeToken as M}from"./auth/authStore.js";import{addNetworkStatusListener as N,getNetworkStatus as P,setNetworkStatusImplementation as F,useNetworkStatus as I}from"./network/networkStatus.js";import{ACCOUNT_ID_HEADER as L,CLIENT_BUILD_HEADER as R,CLIENT_KIND as z,CLIENT_KIND_HEADER as B,getClientBuild as V}from"./api/clientHeaders.js";import{MIN_APP_VERSION_HEADER as H,addMinAppVersionListener as U,getMinAppVersion as W}from"./api/minAppVersion.js";import{applyRequestDefaults as G,newCorrelationId as K}from"./api/requestDefaults.js";import{WORKSPACE_CHANGED_CODE as q,WorkspaceChangedError as J,isWorkspaceChangedError as Y}from"./api/workspaceChanged.js";import{IDEMPOTENCY_KEY_HEADER as X,attachRetry as Z,isNeverSentError as Q,isRetryableFailure as $,isRetryableStatus as ee,retryAfterMs as te}from"./api/retry.js";import{REFRESH_LOCK_NAME as ne,withCrossTabRefreshLock as re}from"./api/crossTabRefreshLock.js";import ie from"./api/publicClient.js";import{SIGN_OUT_REASON_HEADER as ae,injectStore as oe,injectUserAuth as se,refreshAccessToken as ce,resetRefreshState as le}from"./api/tokenRefresh.js";import ue,{RequestOfflineError as de,SESSION_TERMINATION_CODES as fe}from"./api/userClient.js";import{initUserAuth as pe}from"./initUserAuth.js";import{CONTRACT_ERROR_CODE as me,NETWORK_CODE as he,OFFLINE_MESSAGE as ge,REQUEST_CANCELED_CODE as _e,SERVER_FAILURE_MESSAGE as ve,TIMEOUT_CODE as ye,TIMEOUT_MESSAGE as be,UNEXPECTED_MESSAGE as xe,apiErrorMessage as Se,extractApiError as Ce,isApiError as we,isNoResponse as Te,toFieldPath as Ee}from"./errors/extractApiError.js";import{configureFormatters as De,getFormatterConfig as Oe}from"./formatters/formatConfig.js";import{formatCurrency as ke}from"./formatters/currency.js";import{addDays as Ae}from"./formatters/dateMath.js";import{formatDate as je,formatDateTime as Me,formatMinor as Ne,formatNumber as Pe,formatPercent as Fe,formatRelative as Ie,formatTime as Le}from"./formatters/house.js";import{initWebority as Re}from"./initSDK.js";import{createApiBaseQuery as ze}from"./query/apiBaseQuery.js";import{APP_API_DEFAULTS as Be,createAppApi as Ve}from"./query/createAppApi.js";import{setupApiListeners as He}from"./query/listeners.js";import{buildListQuery as Ue,getNextPageParam as We,getPreviousPageParam as Ge,hasNextPage as Ke,hasPreviousPage as qe}from"./query/paging.js";import{LIST_TAG_ID as Je,itemTag as Ye,listTag as Xe,providesList as Ze,providesPagedList as Qe}from"./query/tags.js";import{errorToastMiddleware as $e,isSilentRejection as et}from"./redux/errorToastMiddleware.js";import{createRootReducer as tt,resetState as nt}from"./redux/rootReducer.js";import{SERIALIZABLE_IGNORED_ACTION_PATHS as rt}from"./redux/serializableCheck.js";import{hasRecentlyReloaded as it,importWithReload as at,isChunkLoadError as ot,lazyWithReload as st,markReloaded as ct}from"./router/chunkReload.js";import{hasPermission as lt}from"./router/permissions.js";import{RETURN_TO_PARAM as ut,buildLoginPath as dt,getReturnTo as ft,toSafeReturnTo as pt}from"./router/returnTo.js";import{PublicOnly as mt,RequireAuth as ht,RequirePermission as gt}from"./router/guards.js";import{publicOnly as _t,requireAuth as vt,requirePermission as yt,withGuards as bt}from"./router/loaders.js";import{classifyRouteError as xt,useRouteErrorReport as St}from"./router/routeError.js";import{Storage as Ct,isStorageAvailable as wt,setStorageImplementation as Tt,syncStorage as Et}from"./storage/index.js";import{isBuildBelowMinimum as Dt,parseBuildNumber as Ot}from"./versionCheck/buildNumber.js";import{installReloadOnNavigate as kt,isUpdateAvailable as At,useVersionCheck as jt}from"./versionCheck/index.js";export{g as ACCESS_TOKEN_KEY,L as ACCOUNT_ID_HEADER,Be as APP_API_DEFAULTS,R as CLIENT_BUILD_HEADER,z as CLIENT_KIND,B as CLIENT_KIND_HEADER,me as CONTRACT_ERROR_CODE,e as DEFAULT_CORRELATION_ID_HEADER,t as DEFAULT_LOG_LEVEL,n as DEFAULT_RETRIES,r as DEFAULT_TIMEOUT_MS,X as IDEMPOTENCY_KEY_HEADER,_ as LEGACY_REFRESH_TOKEN_LIFETIME_MS,Je as LIST_TAG_ID,c as Logger,H as MIN_APP_VERSION_HEADER,he as NETWORK_CODE,ge as OFFLINE_MESSAGE,mt as PublicOnly,ne as REFRESH_LOCK_NAME,v as REFRESH_TOKEN_KEY,_e as REQUEST_CANCELED_CODE,ut as RETURN_TO_PARAM,de as RequestOfflineError,ht as RequireAuth,gt as RequirePermission,rt as SERIALIZABLE_IGNORED_ACTION_PATHS,ve as SERVER_FAILURE_MESSAGE,fe as SESSION_TERMINATION_CODES,y as SESSION_USER_KEY,ae as SIGN_OUT_REASON_HEADER,Ct as Storage,ye as TIMEOUT_CODE,be as TIMEOUT_MESSAGE,xe as UNEXPECTED_MESSAGE,q as WORKSPACE_CHANGED_CODE,J as WorkspaceChangedError,Ae as addDays,U as addMinAppVersionListener,N as addNetworkStatusListener,b as addSessionEndedListener,Se as apiErrorMessage,G as applyRequestDefaults,Z as attachRetry,Ue as buildListQuery,dt as buildLoginPath,xt as classifyRouteError,x as clearSession,De as configureFormatters,l as configureLogger,ze as createApiBaseQuery,Ve as createAppApi,tt as createRootReducer,p as decodeJWT,$e as errorToastMiddleware,Ce as extractApiError,ke as formatCurrency,je as formatDate,Me as formatDateTime,Ne as formatMinor,Pe as formatNumber,Fe as formatPercent,Ie as formatRelative,Le as formatTime,V as getClientBuild,i as getConfig,a as getConfigValue,u as getDeviceContext,Oe as getFormatterConfig,m as getJWTExpiry,S as getKeepSignedIn,W as getMinAppVersion,P as getNetworkStatus,We as getNextPageParam,Ge as getPreviousPageParam,ft as getReturnTo,C as getSessionUser,w as getToken,Ke as hasNextPage,lt as hasPermission,qe as hasPreviousPage,it as hasRecentlyReloaded,T as hasSession,E as hasSessionEndedInTab,at as importWithReload,pe as initUserAuth,Re as initWebority,oe as injectStore,se as injectUserAuth,kt as installReloadOnNavigate,we as isApiError,Dt as isBuildBelowMinimum,ot as isChunkLoadError,h as isJWTExpired,Q as isNeverSentError,Te as isNoResponse,$ as isRetryableFailure,ee as isRetryableStatus,et as isSilentRejection,wt as isStorageAvailable,At as isUpdateAvailable,Y as isWorkspaceChangedError,Ye as itemTag,st as lazyWithReload,Xe as listTag,ct as markReloaded,K as newCorrelationId,Ot as parseBuildNumber,Ze as providesList,Qe as providesPagedList,ie as publicClient,_t as publicOnly,ce as refreshAccessToken,D as removeSessionUser,O as removeToken,vt as requireAuth,yt as requirePermission,o as resetConfig,le as resetRefreshState,nt as resetState,te as retryAfterMs,s as setConfig,d as setDeviceContext,k as setKeepSignedIn,A as setLegacyTokenKeys,F as setNetworkStatusImplementation,f as setRemoteLogger,Tt as setStorageImplementation,He as setupApiListeners,j as storeSessionUser,M as storeToken,Et as syncStorage,Ee as toFieldPath,pt as toSafeReturnTo,I as useNetworkStatus,St as useRouteErrorReport,jt as useVersionCheck,ue as userClient,re as withCrossTabRefreshLock,bt as withGuards};
@@ -1 +1 @@
1
- import{extractApiError as e}from"../errors/extractApiError.js";var t=e=>typeof e==`object`&&!!e&&!Array.isArray(e),n=e=>t(e)&&e.silent===!0,r=e=>n(e)||n(e.arg)||t(e.arg)&&n(e.arg.originalArgs)||n(e.baseQueryMeta);function i(n){let{showToast:i,ignoreCodes:a=[],ignoreStatuses:o=[401],dedupeMs:s=4e3,onRejected:c}=n,l=new Map,u=e=>{if(s<=0)return!0;let t=Date.now();for(let[e,n]of l)t-n>=s&&l.delete(e);return l.has(e)?!1:(l.set(e,t),!0)};return n=>s=>l=>{let d=s(l);if(!t(l)||!t(l.meta))return d;let f=l.meta;if(f.requestStatus!==`rejected`||f.aborted===!0||f.condition===!0)return d;let p=e(f.rejectedWithValue===!0?l.payload:l.error);if(p.code===`request_canceled`)return d;let m=c?.(l,p,n);return m===`silent`||r(f)||m!==`toast`&&(p.code!==null&&a.includes(p.code)||o.includes(p.status))||u(p.message)&&i(p.message,p),d}}export{i as errorToastMiddleware,r as isSilentRejection};
1
+ import"../api/workspaceChanged.js";import{extractApiError as e}from"../errors/extractApiError.js";var t=e=>typeof e==`object`&&!!e&&!Array.isArray(e),n=e=>t(e)&&e.silent===!0,r=e=>n(e)||n(e.arg)||t(e.arg)&&n(e.arg.originalArgs)||n(e.baseQueryMeta);function i(n){let{showToast:i,ignoreCodes:a=[],ignoreStatuses:o=[401],dedupeMs:s=4e3,onRejected:c}=n,l=new Map,u=e=>{if(s<=0)return!0;let t=Date.now();for(let[e,n]of l)t-n>=s&&l.delete(e);return l.has(e)?!1:(l.set(e,t),!0)};return n=>s=>l=>{let d=s(l);if(!t(l)||!t(l.meta))return d;let f=l.meta;if(f.requestStatus!==`rejected`||f.aborted===!0||f.condition===!0)return d;let p=e(f.rejectedWithValue===!0?l.payload:l.error);if(p.code===`request_canceled`)return d;let m=c?.(l,p,n);return m===`silent`||r(f)||m!==`toast`&&(p.code!==null&&(p.code===`workspace_changed`||a.includes(p.code))||o.includes(p.status))||u(p.message)&&i(p.message,p),d}}export{i as errorToastMiddleware,r as isSilentRejection};
@@ -14,11 +14,22 @@ export declare const getClientBuild: () => string | null;
14
14
  */
15
15
  export declare const applyClientHeaders: (client: AxiosInstance) => void;
16
16
  export type AccountIdProvider = () => string | null | undefined | Promise<string | null | undefined>;
17
+ /** The provider's current account id; `null` when there is no provider or it names none. */
18
+ export declare const readAccountId: (provider: AccountIdProvider | undefined) => Promise<string | null>;
19
+ /** A request config carrying the account it was made in; axios copies it onto every resend. */
20
+ export interface AccountStampedConfig {
21
+ headers: {
22
+ get: (name: string) => unknown;
23
+ set: (name: string, value: string) => unknown;
24
+ };
25
+ /** `undefined` until the first send; `null` when it was made with no account. */
26
+ _accountId?: string | null;
27
+ }
17
28
  /**
18
- * Sets X-Account-Id from the app's provider on every send, overwriting, so a
19
- * request replayed after a refresh or an account switch carries the current
20
- * account rather than the one it first went out with. `null` sends none.
29
+ * Stamps a request with the account it is made in, once: the first send reads the provider and
30
+ * records the id on the config, and every resend (a retry, a replay after a refresh or a session
31
+ * replacement) sends that same id and never reads the provider again, so a request never runs in
32
+ * a workspace other than its own. `null` sends no header. A header the caller set itself (an
33
+ * outbox item naming its own workspace) wins over the provider and is recorded as the stamp.
21
34
  */
22
- export declare const applyAccountIdHeader: (headers: {
23
- set: (name: string, value: string) => unknown;
24
- }, provider: AccountIdProvider | undefined) => Promise<void>;
35
+ export declare const stampAccountId: (config: AccountStampedConfig, provider: AccountIdProvider | undefined) => Promise<void>;
@@ -12,3 +12,4 @@ export { attachRetry, IDEMPOTENCY_KEY_HEADER, isNeverSentError, isRetryableFailu
12
12
  export { refreshAccessToken } from "./tokenRefresh";
13
13
  export type { RefreshableStore, SessionTerminationCode } from "./userClient";
14
14
  export { default as userClient, injectStore, injectUserAuth, RequestOfflineError, resetRefreshState, SESSION_TERMINATION_CODES, SIGN_OUT_REASON_HEADER, } from "./userClient";
15
+ export { isWorkspaceChangedError, WORKSPACE_CHANGED_CODE, WorkspaceChangedError } from "./workspaceChanged";
@@ -43,6 +43,22 @@ export declare const readSignOutReason: (headers: unknown) => string | null;
43
43
  */
44
44
  export declare const endSessionAndLogout: (reason: SessionEndReason, signOutReason: string | null) => Promise<void>;
45
45
  export declare const isRefreshInFlight: () => boolean;
46
+ /** Opens the replacement window for one flagged request; call the returned function once it settles. */
47
+ export declare const beginSessionReplacement: () => (() => void);
48
+ /** Resolves at once when no session replacement is open, else once every open one has settled. */
49
+ export declare const waitForSessionReplacement: () => Promise<void>;
50
+ /**
51
+ * Stores the session a flagged request's response carries, read the way refresh responses are, before
52
+ * the replacement window closes, so a request waiting on the window retries with the new token.
53
+ * A body without tokens is logged and left to the caller.
54
+ */
55
+ export declare const storeReplacementSession: (data: unknown) => Promise<void>;
56
+ /**
57
+ * The stored access token when it is no longer `sentToken`: the session was refreshed or replaced
58
+ * while a request carrying `sentToken` was in flight, so that request's refusal says nothing about
59
+ * the session now held. Null when the token is unchanged or nothing is stored.
60
+ */
61
+ export declare const currentTokenIfReplaced: (sentToken: string | null) => Promise<string | null>;
46
62
  /** Wait for the in-flight refresh without joining it: resolved or rejected by processQueue. */
47
63
  export declare const queueBehindRefresh: () => Promise<string>;
48
64
  /**
@@ -0,0 +1,37 @@
1
+ import type { AxiosError, InternalAxiosRequestConfig } from "axios";
2
+ /** `code` of a request refused because the workspace changed after it was made. */
3
+ export declare const WORKSPACE_CHANGED_CODE = "workspace_changed";
4
+ interface WorkspaceChangedResponse {
5
+ status: number;
6
+ data: unknown;
7
+ headers: unknown;
8
+ }
9
+ /**
10
+ * A request was refused because it belongs to a workspace other than the one now held: the
11
+ * server answered 409 `workspace_changed`, or the user switched workspace while it was in flight
12
+ * so sending it again would run it in the new one. Never retried and never a logout.
13
+ *
14
+ * It carries the server's 409 as `response`, or one in the same shape when the client held the
15
+ * request back, so `extractApiError` reads `code: 'workspace_changed'` either way.
16
+ */
17
+ export declare class WorkspaceChangedError extends Error {
18
+ readonly code = "workspace_changed";
19
+ readonly response: WorkspaceChangedResponse;
20
+ readonly config?: InternalAxiosRequestConfig;
21
+ readonly cause?: unknown;
22
+ constructor(message?: string, options?: {
23
+ response?: WorkspaceChangedResponse;
24
+ config?: InternalAxiosRequestConfig;
25
+ cause?: unknown;
26
+ });
27
+ }
28
+ /**
29
+ * By instanceof, by name (a second copy of the package fails instanceof), and by code: the
30
+ * `ApiError` `extractApiError` or the RTK Query base query makes of it carries the code too.
31
+ */
32
+ export declare const isWorkspaceChangedError: (error: unknown) => boolean;
33
+ /** The API refused the request because the account it names is no longer the session's. */
34
+ export declare const isWorkspaceChangedResponse: (error: AxiosError) => boolean;
35
+ /** The rejection for a 409 `workspace_changed` answer: the server's message when it sent one. */
36
+ export declare const workspaceChangedFromResponse: (error: AxiosError) => WorkspaceChangedError;
37
+ export {};
@@ -37,8 +37,12 @@ export interface UserAuthOptions {
37
37
  refresh: TokenRefreshConfig;
38
38
  /**
39
39
  * The account the signed-in user is working in, sent as `X-Account-Id` on
40
- * every userClient request. Read on every send, so a switch applies at once.
41
- * Omit it in a product whose API takes the account from the token alone.
40
+ * every userClient request. Read once, when the request is made; a retry or
41
+ * replay sends the same id, so a request never runs in another workspace. A
42
+ * request whose session moved to another workspace while it was in flight is
43
+ * rejected with `WorkspaceChangedError` instead of being sent again, and the
44
+ * session is kept. Omit it in a product whose API takes the account from the
45
+ * token alone.
42
46
  */
43
47
  accountId?: AccountIdProvider;
44
48
  /**
@@ -14,7 +14,8 @@ import { type ApiError } from "../errors/extractApiError";
14
14
  *
15
15
  * Never toasted: an aborted thunk or a cancelled request (a screen unmounting), an RTK Query
16
16
  * condition skip (the data is already cached), a 401 by default (the auth layer owns a dead
17
- * session). Identical messages are de-duplicated for `dedupeMs`, because a screen that fires
17
+ * session), a `workspace_changed` rejection (the request belonged to a workspace the user left).
18
+ * Identical messages are de-duplicated for `dedupeMs`, because a screen that fires
18
19
  * four fetches and loses the network would otherwise stack four copies of one sentence.
19
20
  *
20
21
  * `onRejected` is the product's say over one rejection, run before the default rules:
@@ -3,5 +3,13 @@ declare module "axios" {
3
3
  interface AxiosRequestConfig {
4
4
  /** When true, the library's response-error interceptors do not log this request's failure. */
5
5
  skipErrorLog?: boolean;
6
+ /**
7
+ * The request ends the current session and answers with a new one in the refresh
8
+ * response's shape (a workspace switch, a reissue after a security change). userClient
9
+ * stores those tokens before the response resolves, and a request refused meanwhile for
10
+ * the old session waits for them and is sent again instead of ending the session, unless the
11
+ * new session is in another workspace: then it rejects with `WorkspaceChangedError`.
12
+ */
13
+ replacesSession?: boolean;
6
14
  }
7
15
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webority/web-core",
3
- "version": "0.51.3",
3
+ "version": "0.52.1",
4
4
  "description": "Platform layer for Webority React web portals: HTTP clients with single-flight token refresh, auth store, API error extraction, RTK Query base query, logger, formatters, storage, version check and route guards. No UI.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",