@webority/web-core 0.51.3 → 0.52.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 +13 -0
- package/dist/api/clientHeaders.js +1 -1
- package/dist/api/index.js +1 -1
- package/dist/api/retry.js +1 -1
- package/dist/api/tokenRefresh.js +1 -1
- package/dist/api/userClient.js +1 -1
- package/dist/api/workspaceChanged.js +1 -0
- package/dist/index.js +1 -1
- package/dist/redux/errorToastMiddleware.js +1 -1
- package/dist/types/api/clientHeaders.d.ts +17 -6
- package/dist/types/api/index.d.ts +1 -0
- package/dist/types/api/tokenRefresh.d.ts +16 -0
- package/dist/types/api/workspaceChanged.d.ts +37 -0
- package/dist/types/initUserAuth.d.ts +6 -2
- package/dist/types/redux/errorToastMiddleware.d.ts +2 -1
- package/dist/types/types/axios.d.ts +8 -0
- package/package.json +1 -1
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=
|
|
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{
|
|
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
|
|
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};
|
package/dist/api/tokenRefresh.js
CHANGED
|
@@ -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})}),
|
|
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};
|
package/dist/api/userClient.js
CHANGED
|
@@ -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{
|
|
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{
|
|
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
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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
|
|
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
|
|
41
|
-
*
|
|
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)
|
|
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.
|
|
3
|
+
"version": "0.52.0",
|
|
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",
|