@masterteam/gateway-auth 0.0.25 → 0.0.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { firstValueFrom, EMPTY, of, defer, from, isObservable, map, tap as tap$1, catchError as catchError$1, throwError, switchMap as switchMap$1 } from 'rxjs';
1
+ import { firstValueFrom, EMPTY, of, defer, from, isObservable, map, tap as tap$1, throwError, catchError as catchError$1, switchMap as switchMap$1 } from 'rxjs';
2
2
  import { HttpClient, HttpContextToken, HttpBackend, HttpResponse } from '@angular/common/http';
3
3
  import * as i0 from '@angular/core';
4
4
  import { InjectionToken, inject, Injectable, computed, input, ChangeDetectionStrategy, Component, signal, effect } from '@angular/core';
@@ -1435,25 +1435,64 @@ const normalizePath = (path) => {
1435
1435
  const withoutQuery = trimmed.split('?')[0] || '';
1436
1436
  return withoutQuery.startsWith('api/') ? withoutQuery.slice(4) : withoutQuery;
1437
1437
  };
1438
- function resolveGatewayAuthPath(url, gatewayApiBaseUrl) {
1438
+ const getBasePath = (baseUrl) => {
1439
+ if (!baseUrl) {
1440
+ return '';
1441
+ }
1442
+ try {
1443
+ return new URL(baseUrl).pathname;
1444
+ }
1445
+ catch {
1446
+ return baseUrl.startsWith('/') ? baseUrl : '';
1447
+ }
1448
+ };
1449
+ const stripBasePath = (path, baseUrl) => {
1450
+ const basePath = getBasePath(baseUrl).replace(/^\/+/, '').replace(/\/+$/, '');
1451
+ const normalizedPath = path.replace(/^\/+/, '');
1452
+ if (basePath &&
1453
+ (normalizedPath === basePath || normalizedPath.startsWith(`${basePath}/`))) {
1454
+ return normalizedPath.slice(basePath.length).replace(/^\/+/, '');
1455
+ }
1456
+ return path;
1457
+ };
1458
+ const resolveRequestPath = (url, baseUrl) => {
1459
+ let path = url;
1439
1460
  if (isAbsoluteUrl(url)) {
1440
1461
  try {
1441
- return normalizePath(new URL(url).pathname);
1462
+ path = new URL(url).pathname;
1442
1463
  }
1443
1464
  catch {
1444
- return normalizePath(url);
1465
+ path = url;
1445
1466
  }
1446
1467
  }
1447
- if (gatewayApiBaseUrl && url.startsWith(gatewayApiBaseUrl)) {
1448
- return normalizePath(url.slice(gatewayApiBaseUrl.length));
1468
+ else if (baseUrl && url.startsWith(baseUrl)) {
1469
+ path = url.slice(baseUrl.length);
1449
1470
  }
1450
- return normalizePath(url);
1471
+ return normalizePath(stripBasePath(path, baseUrl));
1472
+ };
1473
+ function resolveGatewayAuthPath(url, gatewayApiBaseUrl) {
1474
+ return resolveRequestPath(url, gatewayApiBaseUrl);
1451
1475
  }
1452
1476
  function isGatewayAuthRequestUrl(url, gatewayApiBaseUrl) {
1453
1477
  const path = resolveGatewayAuthPath(url, gatewayApiBaseUrl).toLowerCase();
1454
1478
  return (GATEWAY_AUTH_ENDPOINT_PATHS.has(path) ||
1455
1479
  GATEWAY_AUTH_ENDPOINT_PREFIXES.some((prefix) => path.startsWith(prefix)));
1456
1480
  }
1481
+ const GATEWAY_APPLICATION_LAUNCH_PATH_PATTERN = /^applications\/[^/]+\/launch$/i;
1482
+ const isGatewayEndpointRequestUrl = (url, gatewayApiBaseUrl) => {
1483
+ const path = resolveGatewayAuthPath(url, gatewayApiBaseUrl).toLowerCase();
1484
+ return (path.startsWith('auth/') ||
1485
+ GATEWAY_APPLICATION_LAUNCH_PATH_PATTERN.test(path));
1486
+ };
1487
+ const isApplicationContextRequestUrl = (url, applicationApiBaseUrl) => resolveRequestPath(url, applicationApiBaseUrl).toLowerCase() ===
1488
+ GATEWAY_AUTH_ENDPOINTS.applicationContext;
1489
+ class GatewaySessionDeadError extends Error {
1490
+ constructor() {
1491
+ super('GATEWAY_SESSION_DEAD');
1492
+ }
1493
+ }
1494
+ const isGatewaySessionDeadError = (error) => error instanceof GatewaySessionDeadError;
1495
+ const isUnauthorizedError = (error) => error?.status === 401;
1457
1496
  const getBrowserRefreshLock = () => {
1458
1497
  if (typeof navigator === 'undefined') {
1459
1498
  return null;
@@ -1637,22 +1676,31 @@ const executeRelaunch = (context, options) => {
1637
1676
  },
1638
1677
  };
1639
1678
  }
1640
- let gatewayAccessToken = context.auth.token();
1641
- const gatewayAccessTokenExpiresAt = context.auth.accessTokenExpiresAt();
1642
- const gatewayAccessExpired = !gatewayAccessToken ||
1643
- isExpired(gatewayAccessTokenExpiresAt, context.refreshSkewMs);
1644
- if (gatewayAccessExpired) {
1679
+ const ensureGatewayAccessToken = async (forceRefresh = false) => {
1680
+ const gatewayAccessToken = context.auth.token();
1681
+ const gatewayAccessTokenExpiresAt = context.auth.accessTokenExpiresAt();
1682
+ const gatewayAccessExpired = !gatewayAccessToken ||
1683
+ isExpired(gatewayAccessTokenExpiresAt, context.refreshSkewMs);
1684
+ if (!forceRefresh && !gatewayAccessExpired && gatewayAccessToken) {
1685
+ return gatewayAccessToken;
1686
+ }
1645
1687
  const gatewayRefreshToken = context.auth.refreshToken();
1646
1688
  if (!gatewayRefreshToken ||
1647
1689
  isExpired(context.auth.refreshTokenExpiresAt())) {
1648
- throw new Error('GATEWAY_SESSION_DEAD');
1690
+ throw new GatewaySessionDeadError();
1649
1691
  }
1650
- await refreshAccessToken({ ...context, scope: 'gateway', applicationCode: undefined }, gatewayRefreshToken);
1651
- gatewayAccessToken = context.auth.token();
1652
- if (!gatewayAccessToken) {
1653
- throw new Error('GATEWAY_SESSION_DEAD');
1692
+ try {
1693
+ await refreshAccessToken({ ...context, scope: 'gateway', applicationCode: undefined }, gatewayRefreshToken);
1654
1694
  }
1655
- }
1695
+ catch {
1696
+ throw new GatewaySessionDeadError();
1697
+ }
1698
+ const refreshedGatewayAccessToken = context.auth.token();
1699
+ if (!refreshedGatewayAccessToken) {
1700
+ throw new GatewaySessionDeadError();
1701
+ }
1702
+ return refreshedGatewayAccessToken;
1703
+ };
1656
1704
  const url = buildGatewayUrl(context.gatewayApiBaseUrl, GATEWAY_AUTH_ENDPOINTS.applicationLaunch(code));
1657
1705
  const params = {};
1658
1706
  const returnUrl = resolveRelaunchReturnUrl(options, code);
@@ -1662,7 +1710,7 @@ const executeRelaunch = (context, options) => {
1662
1710
  if (context.deviceToken) {
1663
1711
  params['deviceToken'] = context.deviceToken;
1664
1712
  }
1665
- return firstValueFrom(context.http
1713
+ const requestLaunch = (gatewayAccessToken) => firstValueFrom(context.http
1666
1714
  .get(url, {
1667
1715
  params,
1668
1716
  headers: {
@@ -1686,6 +1734,25 @@ const executeRelaunch = (context, options) => {
1686
1734
  context.auth.setAppSession(session);
1687
1735
  return data.tokens;
1688
1736
  })));
1737
+ const gatewayAccessToken = await ensureGatewayAccessToken();
1738
+ try {
1739
+ return await requestLaunch(gatewayAccessToken);
1740
+ }
1741
+ catch (error) {
1742
+ if (!isUnauthorizedError(error)) {
1743
+ throw error;
1744
+ }
1745
+ const refreshedGatewayAccessToken = await ensureGatewayAccessToken(true);
1746
+ try {
1747
+ return await requestLaunch(refreshedGatewayAccessToken);
1748
+ }
1749
+ catch (retryError) {
1750
+ if (isUnauthorizedError(retryError)) {
1751
+ throw new GatewaySessionDeadError();
1752
+ }
1753
+ throw retryError;
1754
+ }
1755
+ }
1689
1756
  });
1690
1757
  };
1691
1758
  const relaunchApplication = (context, options) => {
@@ -1728,13 +1795,46 @@ const prepareRequest = (req, token, markRetried, baseUrl) => {
1728
1795
  return modifiedReq;
1729
1796
  };
1730
1797
  const urlMatchesBase = (url, baseUrl) => {
1731
- if (!baseUrl || !isAbsoluteUrl(url)) {
1798
+ if (!baseUrl) {
1732
1799
  return false;
1733
1800
  }
1734
- return url.startsWith(normalizeBase(baseUrl));
1801
+ const normalizedBase = normalizeBase(baseUrl);
1802
+ if (isAbsoluteUrl(url) && isAbsoluteUrl(normalizedBase)) {
1803
+ return url.startsWith(normalizedBase);
1804
+ }
1805
+ const basePath = getBasePath(baseUrl).replace(/^\/+/, '').replace(/\/+$/, '');
1806
+ if (!basePath) {
1807
+ return !isAbsoluteUrl(url) && url.startsWith(normalizedBase);
1808
+ }
1809
+ let path = url;
1810
+ if (isAbsoluteUrl(url)) {
1811
+ try {
1812
+ path = new URL(url).pathname;
1813
+ }
1814
+ catch {
1815
+ path = url;
1816
+ }
1817
+ }
1818
+ path = path.split('?')[0].replace(/^\/+/, '');
1819
+ return path === basePath || path.startsWith(`${basePath}/`);
1735
1820
  };
1736
- const resolveRequestScope = (req, options, auth, gatewayApiBaseUrl) => {
1737
- // 1. Per-request override wins.
1821
+ const resolveRequestScope = (req, options, auth, gatewayApiBaseUrl, applicationApiBaseUrl) => {
1822
+ // 1. Public app context is deliberately unauthenticated app scope.
1823
+ if (isApplicationContextRequestUrl(req.url, applicationApiBaseUrl)) {
1824
+ return { scope: 'application', useGatewayBaseUrl: false };
1825
+ }
1826
+ // 2. Known Gateway endpoints must always use GatewaySession.
1827
+ if (isGatewayEndpointRequestUrl(req.url, gatewayApiBaseUrl)) {
1828
+ return {
1829
+ scope: 'gateway',
1830
+ useGatewayBaseUrl: !urlMatchesBase(req.url, gatewayApiBaseUrl),
1831
+ };
1832
+ }
1833
+ // 3. Caller hint via request context.
1834
+ if (options.shouldUseGatewayApiBaseUrl?.(req)) {
1835
+ return { scope: 'gateway', useGatewayBaseUrl: true };
1836
+ }
1837
+ // 4. Per-request app override.
1738
1838
  const explicitCode = options.resolveApplicationCodeForRequest?.(req);
1739
1839
  if (explicitCode) {
1740
1840
  const session = auth.getAppSession(explicitCode);
@@ -1745,30 +1845,34 @@ const resolveRequestScope = (req, options, auth, gatewayApiBaseUrl) => {
1745
1845
  useGatewayBaseUrl: false,
1746
1846
  };
1747
1847
  }
1748
- // 2. Caller hint via request context.
1749
- if (options.shouldUseGatewayApiBaseUrl?.(req)) {
1750
- return { scope: 'gateway', useGatewayBaseUrl: true };
1848
+ const defaultCode = resolveApplicationCodeOption(options.applicationCode) ??
1849
+ auth.activeApplicationCode();
1850
+ // 5. Requests targeting the configured app API use ApplicationAccess.
1851
+ if (urlMatchesBase(req.url, applicationApiBaseUrl) && defaultCode) {
1852
+ return {
1853
+ scope: 'application',
1854
+ applicationCode: defaultCode,
1855
+ session: auth.getAppSession(defaultCode),
1856
+ useGatewayBaseUrl: false,
1857
+ };
1751
1858
  }
1752
- // 3. Absolute URL pointing at the Gateway base must use GatewaySession.
1753
- // Covers /api/auth/*, /api/auth/me/applications, /api/applications/{code}/launch.
1859
+ // 6. Absolute URL pointing at the Gateway base must use GatewaySession.
1754
1860
  if (urlMatchesBase(req.url, gatewayApiBaseUrl)) {
1755
1861
  return { scope: 'gateway', useGatewayBaseUrl: false };
1756
1862
  }
1757
- // 4. Otherwise treat as app scope when a session exists.
1758
- const defaultCode = resolveApplicationCodeOption(options.applicationCode) ??
1759
- auth.activeApplicationCode();
1863
+ // 7. App shells with a configured/active app code should not send the
1864
+ // GatewaySession to direct app APIs. The interceptor will launch the app
1865
+ // if the ApplicationAccess pair is not available yet.
1760
1866
  if (defaultCode) {
1761
1867
  const session = auth.getAppSession(defaultCode);
1762
- if (session) {
1763
- return {
1764
- scope: 'application',
1765
- applicationCode: defaultCode,
1766
- session,
1767
- useGatewayBaseUrl: false,
1768
- };
1769
- }
1868
+ return {
1869
+ scope: 'application',
1870
+ applicationCode: defaultCode,
1871
+ session,
1872
+ useGatewayBaseUrl: false,
1873
+ };
1770
1874
  }
1771
- // 5. No app session known yet fall back to GatewaySession.
1875
+ // 8. No app context known yet - fall back to GatewaySession.
1772
1876
  return { scope: 'gateway', useGatewayBaseUrl: false };
1773
1877
  };
1774
1878
  const gatewayAuthInterceptor = (req, next) => {
@@ -1784,21 +1888,18 @@ const gatewayAuthInterceptor = (req, next) => {
1784
1888
  const refreshSkewMs = resolveAccessTokenRefreshSkewMs(options.accessTokenRefreshSkewMs);
1785
1889
  const isAuthRequest = isGatewayAuthRequestUrl(req.url, gatewayApiBaseUrl);
1786
1890
  const alreadyRetried = req.context.get(GATEWAY_AUTH_RETRY_CONTEXT);
1787
- const resolved = resolveRequestScope(req, options, auth, gatewayApiBaseUrl);
1891
+ const resolved = resolveRequestScope(req, options, auth, gatewayApiBaseUrl, appApiBaseUrl);
1788
1892
  const baseUrl = resolved.useGatewayBaseUrl
1789
1893
  ? gatewayApiBaseUrl
1790
1894
  : appApiBaseUrl;
1791
1895
  const gatewayAccessToken = auth.token();
1792
1896
  const session = resolved.session ?? null;
1793
- const accessToken = resolved.scope === 'application' && session
1794
- ? session.accessToken
1897
+ const accessToken = resolved.scope === 'application'
1898
+ ? (session?.accessToken ?? null)
1795
1899
  : gatewayAccessToken;
1796
- // Reactive-only refresh: we never call /auth/refresh proactively. If the
1797
- // access token is genuinely expired the request goes out with the existing
1798
- // one, the backend will 401, and the catchError below refreshes once and
1799
- // retries. This avoids the post-login refresh cascade caused by short
1800
- // access-token TTLs versus any skew.
1801
- const tokenToAttach = !isAuthRequest && accessToken ? accessToken : null;
1900
+ const isApplicationContextRequest = isApplicationContextRequestUrl(req.url, appApiBaseUrl);
1901
+ const shouldAttachAuth = !isAuthRequest && !isApplicationContextRequest;
1902
+ const tokenToAttach = shouldAttachAuth && accessToken ? accessToken : null;
1802
1903
  const buildRefreshContext = (scopeOverride, appCodeOverride) => ({
1803
1904
  http,
1804
1905
  gatewayApiBaseUrl,
@@ -1808,18 +1909,74 @@ const gatewayAuthInterceptor = (req, next) => {
1808
1909
  scope: scopeOverride ?? resolved.scope,
1809
1910
  applicationCode: appCodeOverride ?? resolved.applicationCode,
1810
1911
  });
1811
- const initialAccessToken = accessToken;
1812
- return next(prepareRequest(req, tokenToAttach, false, baseUrl)).pipe(catchError$1((error) => {
1813
- if (error?.status !== 401 || isAuthRequest || alreadyRetried) {
1912
+ const handleRelaunchError = (relaunchError) => {
1913
+ if (isGatewaySessionDeadError(relaunchError)) {
1914
+ auth.logout();
1915
+ }
1916
+ else if (resolved.applicationCode) {
1917
+ auth.clearAppSession(resolved.applicationCode);
1918
+ }
1919
+ return throwError(() => relaunchError);
1920
+ };
1921
+ const relaunch$ = (clearAppSessionBeforeLaunch = true) => {
1922
+ if (clearAppSessionBeforeLaunch && resolved.applicationCode) {
1923
+ auth.clearAppSession(resolved.applicationCode);
1924
+ }
1925
+ return from(relaunchApplication(buildRefreshContext('application', resolved.applicationCode), options)).pipe(catchError$1(handleRelaunchError));
1926
+ };
1927
+ const resolveGatewayTokenBeforeRequest$ = () => {
1928
+ const currentAccessToken = auth.token();
1929
+ const currentAccessTokenExpiresAt = auth.accessTokenExpiresAt();
1930
+ if (currentAccessToken &&
1931
+ !isExpired(currentAccessTokenExpiresAt, refreshSkewMs)) {
1932
+ return of(currentAccessToken);
1933
+ }
1934
+ const gatewayRefreshToken = auth.refreshToken();
1935
+ if (!currentAccessToken && !gatewayRefreshToken) {
1936
+ return of(null);
1937
+ }
1938
+ if (!gatewayRefreshToken || isExpired(auth.refreshTokenExpiresAt())) {
1939
+ auth.logout();
1940
+ return throwError(() => new GatewaySessionDeadError());
1941
+ }
1942
+ return refreshTokens$(buildRefreshContext('gateway', undefined), gatewayRefreshToken).pipe(map((tokens) => tokens.accessToken), catchError$1((refreshError) => {
1943
+ auth.logout();
1944
+ return throwError(() => refreshError);
1945
+ }));
1946
+ };
1947
+ const resolveAppTokenBeforeRequest$ = () => {
1948
+ const code = resolved.applicationCode;
1949
+ if (!code) {
1950
+ return of(tokenToAttach);
1951
+ }
1952
+ const latestSession = auth.getAppSession(code);
1953
+ if (latestSession?.accessToken &&
1954
+ !isExpired(latestSession.accessTokenExpiresAt, refreshSkewMs)) {
1955
+ return of(latestSession.accessToken);
1956
+ }
1957
+ if (!latestSession?.refreshToken ||
1958
+ isExpired(latestSession.refreshTokenExpiresAt)) {
1959
+ return relaunch$().pipe(map((tokens) => tokens.accessToken));
1960
+ }
1961
+ return refreshTokens$(buildRefreshContext('application', code), latestSession.refreshToken).pipe(map((tokens) => tokens.accessToken), catchError$1(() => relaunch$().pipe(map((tokens) => tokens.accessToken))));
1962
+ };
1963
+ const initialToken$ = !shouldAttachAuth
1964
+ ? of(null)
1965
+ : resolved.scope === 'application'
1966
+ ? resolveAppTokenBeforeRequest$()
1967
+ : resolveGatewayTokenBeforeRequest$();
1968
+ return initialToken$.pipe(switchMap$1((requestAccessToken) => next(prepareRequest(req, requestAccessToken, false, baseUrl)).pipe(catchError$1((error) => {
1969
+ if (error?.status !== 401 || !shouldAttachAuth || alreadyRetried) {
1814
1970
  return throwError(() => error);
1815
1971
  }
1816
1972
  // If a concurrent flow already rotated the token while this request was
1817
- // in flight, just retry with the freshest token do NOT trigger another
1973
+ // in flight, just retry with the freshest token - do NOT trigger another
1818
1974
  // refresh against the backend.
1819
1975
  const currentAccessToken = resolved.scope === 'application' && resolved.applicationCode
1820
- ? (auth.getAppSession(resolved.applicationCode)?.accessToken ?? null)
1976
+ ? (auth.getAppSession(resolved.applicationCode)?.accessToken ??
1977
+ null)
1821
1978
  : auth.token();
1822
- if (currentAccessToken && currentAccessToken !== initialAccessToken) {
1979
+ if (currentAccessToken && currentAccessToken !== requestAccessToken) {
1823
1980
  return next(prepareRequest(req, currentAccessToken, true, baseUrl));
1824
1981
  }
1825
1982
  const isApp = resolved.scope === 'application' && !!resolved.applicationCode;
@@ -1833,21 +1990,6 @@ const gatewayAuthInterceptor = (req, next) => {
1833
1990
  ? (latestAppSession?.refreshTokenExpiresAt ?? null)
1834
1991
  : auth.refreshTokenExpiresAt();
1835
1992
  const canRefreshNow = !!latestRefreshToken && !isExpired(latestRefreshTokenExpiresAt);
1836
- // App-scope recovery: prefer app-refresh, fall back to silent re-launch
1837
- // (so direct Pplus calls keep working when only the app's refresh
1838
- // token expired but the Gateway session is still alive).
1839
- const relaunch$ = () => from(relaunchApplication(buildRefreshContext(), options)).pipe(catchError$1((relaunchError) => {
1840
- const gatewayDead = !auth.token() &&
1841
- (!auth.refreshToken() ||
1842
- isExpired(auth.refreshTokenExpiresAt()));
1843
- if (gatewayDead) {
1844
- auth.logout();
1845
- }
1846
- else if (resolved.applicationCode) {
1847
- auth.clearAppSession(resolved.applicationCode);
1848
- }
1849
- return throwError(() => relaunchError);
1850
- }));
1851
1993
  const tokens$ = !canRefreshNow || !latestRefreshToken
1852
1994
  ? isApp
1853
1995
  ? relaunch$()
@@ -1860,7 +2002,7 @@ const gatewayAuthInterceptor = (req, next) => {
1860
2002
  return throwError(() => refreshError);
1861
2003
  }));
1862
2004
  return tokens$.pipe(switchMap$1((tokens) => next(prepareRequest(req, tokens.accessToken, true, baseUrl))));
1863
- }));
2005
+ }))));
1864
2006
  };
1865
2007
 
1866
2008
  const CORRELATION_ID_HEADER = 'X-Correlation-ID';
@@ -2014,7 +2156,11 @@ function createMessageInterceptor(options) {
2014
2156
  break;
2015
2157
  case 400: {
2016
2158
  const validationErrors = error?.error?.errors;
2017
- if (validationErrors && typeof validationErrors === 'object') {
2159
+ if (typeof validationErrors?.code === 'string') {
2160
+ showError(validationErrors.code);
2161
+ }
2162
+ else if (validationErrors &&
2163
+ typeof validationErrors === 'object') {
2018
2164
  const messages = Object.values(validationErrors)
2019
2165
  .flat()
2020
2166
  .filter((m) => typeof m === 'string');