@rodit/rodit-auth-be 9.16.2 → 9.17.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/CHANGELOG.md +11 -0
- package/README.md +1 -1
- package/index.js +94 -7
- package/lib/auth/tokenservice.js +110 -32
- package/lib/blockchain/blockchainservice.js +190 -25
- package/lib/middleware/authenticationmw.js +186 -0
- package/package.json +1 -1
- package/services/configsdk.js +5 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@rodit/rodit-auth-be` are documented here.
|
|
4
4
|
|
|
5
|
+
## [9.17.0] — 2026-10-06
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **`nearorg_rpc_tokenfromroditid`:** a failed NEAR RPC call no longer looks like a
|
|
10
|
+
missing token. An empty `RODiT` is returned only when the `rodit_token` view
|
|
11
|
+
completes and decodes to JSON `null`. HTTP errors (including 402/403 credit
|
|
12
|
+
exhaustion), JSON-RPC `error` objects, WASM errors, and undecodable results
|
|
13
|
+
throw `NearRpcError` with the HTTP status and provider message. The function
|
|
14
|
+
catch rethrows that error instead of returning `new RODiT()`.
|
|
15
|
+
|
|
5
16
|
## [9.16.2] — 2026-09-19
|
|
6
17
|
|
|
7
18
|
### Fixed
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Node.js / Express SDK for RODiT-based mutual authentication, authorization, sess
|
|
|
4
4
|
|
|
5
5
|
**npm:** [https://www.npmjs.com/package/@rodit/rodit-auth-be](https://www.npmjs.com/package/@rodit/rodit-auth-be)
|
|
6
6
|
**Monorepo:** [discernible-io/rodit-sdk](https://github.com/discernible-io/rodit-sdk) (this directory)
|
|
7
|
-
**Version:** 9.
|
|
7
|
+
**Version:** 9.17.0 · **License:** Proprietary · **Author:** Discernible IO
|
|
8
8
|
|
|
9
9
|
## Production deployments
|
|
10
10
|
|
package/index.js
CHANGED
|
@@ -18,6 +18,7 @@ const {
|
|
|
18
18
|
authenticate_logout,
|
|
19
19
|
login_client,
|
|
20
20
|
logout_client,
|
|
21
|
+
refresh_client,
|
|
21
22
|
login_client_withnep413,
|
|
22
23
|
login_portal,
|
|
23
24
|
login_server,
|
|
@@ -160,6 +161,17 @@ class RoditClient {
|
|
|
160
161
|
return authenticate_logout;
|
|
161
162
|
}
|
|
162
163
|
|
|
164
|
+
/**
|
|
165
|
+
* Optional refresh auth middleware (same tolerance as logout: signature-valid
|
|
166
|
+
* expired tokens allowed). Only relevant if the host mounts POST /api/refresh;
|
|
167
|
+
* default SERVER-INITIATED renewals do not require this route.
|
|
168
|
+
*
|
|
169
|
+
* @returns {Function} Refresh authentication middleware function
|
|
170
|
+
*/
|
|
171
|
+
get authenticateForRefresh() {
|
|
172
|
+
return authenticate_logout;
|
|
173
|
+
}
|
|
174
|
+
|
|
163
175
|
/**
|
|
164
176
|
* Get permissions validation middleware (clean syntax)
|
|
165
177
|
* @returns {Function} Permissions validation middleware function
|
|
@@ -1042,6 +1054,27 @@ class RoditClient {
|
|
|
1042
1054
|
return await logout_client(req, res);
|
|
1043
1055
|
}
|
|
1044
1056
|
|
|
1057
|
+
/**
|
|
1058
|
+
* Optional Express handler for client-initiated token refresh (POST /api/refresh).
|
|
1059
|
+
* Normally unnecessary: default SERVER-INITIATED mode renews credentials on
|
|
1060
|
+
* authenticate and returns New-Token. Mount only for idle-client refresh or
|
|
1061
|
+
* when SECURITY_OPTIONS.SERVERORCLIENT=CLIENT-INITIATED.
|
|
1062
|
+
*
|
|
1063
|
+
* @param {Object} req - Express request object
|
|
1064
|
+
* @param {Object} res - Express response object
|
|
1065
|
+
* @returns {Promise<*>} Express response
|
|
1066
|
+
*/
|
|
1067
|
+
async refresh_client(req, res) {
|
|
1068
|
+
logger.debug('Processing Express token refresh request', {
|
|
1069
|
+
component: 'RoditClient',
|
|
1070
|
+
method: 'refresh_client',
|
|
1071
|
+
path: req.path,
|
|
1072
|
+
ip: req.ip
|
|
1073
|
+
});
|
|
1074
|
+
|
|
1075
|
+
return await refresh_client(req, res);
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1045
1078
|
/**
|
|
1046
1079
|
* Login to a peer RODiT API using RODiT id (matches login_client / POST /api/login).
|
|
1047
1080
|
*
|
|
@@ -1838,19 +1871,71 @@ class RoditClient {
|
|
|
1838
1871
|
}
|
|
1839
1872
|
|
|
1840
1873
|
/**
|
|
1841
|
-
* Refresh the
|
|
1874
|
+
* Refresh the access credential. Prefers optional peer POST /api/refresh when
|
|
1875
|
+
* available; otherwise falls back to full `login_server`. Under default
|
|
1876
|
+
* SERVER-INITIATED deployments, routine renewal is handled by the server via
|
|
1877
|
+
* New-Token on authenticated calls — this method is for explicit refresh or
|
|
1878
|
+
* re-login after hard expiry.
|
|
1879
|
+
* @param {Object} [options]
|
|
1880
|
+
* @param {string} [options.refreshPath='/api/refresh'] - Refresh endpoint path
|
|
1881
|
+
* @param {boolean} [options.preferLogin=false] - Force re-login instead of refresh
|
|
1842
1882
|
* @returns {Promise<string>} New token
|
|
1843
1883
|
*/
|
|
1844
|
-
async refreshToken() {
|
|
1884
|
+
async refreshToken(options = {}) {
|
|
1885
|
+
const refreshPath = options.refreshPath || '/api/refresh';
|
|
1886
|
+
const preferLogin = options.preferLogin === true;
|
|
1887
|
+
|
|
1845
1888
|
logger.debug('Refreshing authentication token', {
|
|
1846
1889
|
component: 'RoditClient',
|
|
1847
|
-
method: 'refreshToken'
|
|
1890
|
+
method: 'refreshToken',
|
|
1891
|
+
refreshPath,
|
|
1892
|
+
preferLogin
|
|
1848
1893
|
});
|
|
1849
1894
|
|
|
1850
|
-
|
|
1851
|
-
|
|
1895
|
+
if (!preferLogin && this.jwt_token) {
|
|
1896
|
+
try {
|
|
1897
|
+
const base =
|
|
1898
|
+
this.apiEndpoint ||
|
|
1899
|
+
config.get('API_DEFAULT_OPTIONS.API_ENDPOINT', '') ||
|
|
1900
|
+
'';
|
|
1901
|
+
if (base) {
|
|
1902
|
+
const url = `${String(base).replace(/\/$/, '')}${
|
|
1903
|
+
refreshPath.startsWith('/') ? refreshPath : `/${refreshPath}`
|
|
1904
|
+
}`;
|
|
1905
|
+
const refreshResponse = await fetch(url, {
|
|
1906
|
+
method: 'POST',
|
|
1907
|
+
headers: {
|
|
1908
|
+
Authorization: `Bearer ${this.jwt_token}`,
|
|
1909
|
+
'Content-Type': 'application/json',
|
|
1910
|
+
},
|
|
1911
|
+
});
|
|
1912
|
+
if (refreshResponse.ok) {
|
|
1913
|
+
const body = await refreshResponse.json().catch(() => ({}));
|
|
1914
|
+
const headerToken =
|
|
1915
|
+
typeof refreshResponse.headers?.get === 'function'
|
|
1916
|
+
? refreshResponse.headers.get('New-Token')
|
|
1917
|
+
: null;
|
|
1918
|
+
const newToken = body.token || body.jwt_token || headerToken;
|
|
1919
|
+
if (newToken) {
|
|
1920
|
+
this.jwt_token = newToken;
|
|
1921
|
+
if (typeof this.setSessionToken === 'function') {
|
|
1922
|
+
await this.setSessionToken(newToken);
|
|
1923
|
+
}
|
|
1924
|
+
return newToken;
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
} catch (refreshError) {
|
|
1929
|
+
logger.warn('Client-initiated refresh failed; falling back to login_server', {
|
|
1930
|
+
component: 'RoditClient',
|
|
1931
|
+
method: 'refreshToken',
|
|
1932
|
+
error: refreshError.message
|
|
1933
|
+
});
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1852
1936
|
|
|
1853
|
-
|
|
1937
|
+
await this.login_server();
|
|
1938
|
+
return this.getSessionToken();
|
|
1854
1939
|
}
|
|
1855
1940
|
|
|
1856
1941
|
/**
|
|
@@ -1920,7 +2005,7 @@ class RoditClient {
|
|
|
1920
2005
|
}
|
|
1921
2006
|
|
|
1922
2007
|
// Import health check and retry functions
|
|
1923
|
-
const { healthCheckRPC, resolveHealthyNearRpcUrl, fetchWithRetry } = require('./lib/blockchain/blockchainservice');
|
|
2008
|
+
const { healthCheckRPC, resolveHealthyNearRpcUrl, fetchWithRetry, NearRpcError } = require('./lib/blockchain/blockchainservice');
|
|
1924
2009
|
|
|
1925
2010
|
// Export the RoditClient class and commonly used SDK components
|
|
1926
2011
|
module.exports = {
|
|
@@ -1941,6 +2026,7 @@ module.exports = {
|
|
|
1941
2026
|
authenticate_logout,
|
|
1942
2027
|
login_client,
|
|
1943
2028
|
logout_client,
|
|
2029
|
+
refresh_client,
|
|
1944
2030
|
login_client_withnep413,
|
|
1945
2031
|
login_portal,
|
|
1946
2032
|
login_server,
|
|
@@ -1966,6 +2052,7 @@ module.exports = {
|
|
|
1966
2052
|
VersionManager,
|
|
1967
2053
|
// Blockchain service functions
|
|
1968
2054
|
nearorg_rpc_timestamp: blockchainService.nearorg_rpc_timestamp,
|
|
2055
|
+
NearRpcError,
|
|
1969
2056
|
// Startup validation and health check functions
|
|
1970
2057
|
validateConfig: config.validate,
|
|
1971
2058
|
healthCheckRPC,
|
package/lib/auth/tokenservice.js
CHANGED
|
@@ -977,9 +977,15 @@ function resolveCredentialExpirationUnix(now, sessionExpiration, own_rodit) {
|
|
|
977
977
|
tokenJti: token.jti,
|
|
978
978
|
});
|
|
979
979
|
|
|
980
|
-
|
|
980
|
+
const closedError = new Error("Session inactive or closed");
|
|
981
|
+
closedError.code = "SESSION_CLOSED";
|
|
982
|
+
closedError.renewalReason = "session_closed";
|
|
983
|
+
throw closedError;
|
|
981
984
|
}
|
|
982
985
|
} catch (sessionError) {
|
|
986
|
+
if (sessionError.renewalReason === "session_closed" || sessionError.code === "SESSION_CLOSED") {
|
|
987
|
+
throw sessionError;
|
|
988
|
+
}
|
|
983
989
|
logger.error("Session check failed", {
|
|
984
990
|
component: "JwtAuth",
|
|
985
991
|
method: "generate_jwt_token_fromtoken",
|
|
@@ -988,7 +994,10 @@ function resolveCredentialExpirationUnix(now, sessionExpiration, own_rodit) {
|
|
|
988
994
|
error: sessionError.message,
|
|
989
995
|
});
|
|
990
996
|
// Fail closed for session validation errors during renewal.
|
|
991
|
-
|
|
997
|
+
const checkFailed = new Error(`Session check failed: ${sessionError.message}`);
|
|
998
|
+
checkFailed.code = "SESSION_CLOSED";
|
|
999
|
+
checkFailed.renewalReason = "session_closed";
|
|
1000
|
+
throw checkFailed;
|
|
992
1001
|
}
|
|
993
1002
|
}
|
|
994
1003
|
|
|
@@ -1795,45 +1804,92 @@ function resolveCredentialExpirationUnix(now, sessionExpiration, own_rodit) {
|
|
|
1795
1804
|
};
|
|
1796
1805
|
|
|
1797
1806
|
let newToken = null;
|
|
1798
|
-
|
|
1799
|
-
|
|
1807
|
+
const serverOrClient = String(
|
|
1808
|
+
config.get("SECURITY_OPTIONS.SERVERORCLIENT", "SERVER-INITIATED")
|
|
1809
|
+
).toUpperCase();
|
|
1810
|
+
const clientInitiatedRenewal = serverOrClient === "CLIENT-INITIATED";
|
|
1811
|
+
const forceCredentialRenewal = options.forceCredentialRenewal === true;
|
|
1812
|
+
|
|
1813
|
+
if (forceCredentialRenewal) {
|
|
1800
1814
|
const renewalResult = await checkandrenew_jwt_token(
|
|
1801
1815
|
payload,
|
|
1802
1816
|
Math.floor(Date.now() / 1000),
|
|
1803
1817
|
requestId,
|
|
1804
|
-
|
|
1818
|
+
true
|
|
1805
1819
|
);
|
|
1806
1820
|
newToken = renewalResult.newToken;
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1821
|
+
if (renewalResult.reason === "session_closed") {
|
|
1822
|
+
throw new Error("Error 012: Session is not active");
|
|
1823
|
+
}
|
|
1824
|
+
if (!newToken) {
|
|
1825
|
+
const renewFail = new Error(
|
|
1826
|
+
"Error 007: Token has expired and renewal failed"
|
|
1827
|
+
);
|
|
1828
|
+
renewFail.code = "RENEWAL_FAILED";
|
|
1829
|
+
renewFail.renewalReason = renewalResult.reason || "renewal_failed";
|
|
1830
|
+
throw renewFail;
|
|
1831
|
+
}
|
|
1832
|
+
} else if (!options.allowExpiredToken) {
|
|
1833
|
+
if (clientInitiatedRenewal && !isExpired) {
|
|
1834
|
+
// CLIENT-INITIATED: skip piggyback renewal; client must call /api/refresh.
|
|
1835
|
+
newToken = null;
|
|
1836
|
+
} else if (clientInitiatedRenewal && isExpired) {
|
|
1837
|
+
logger.error(
|
|
1838
|
+
"Token expired under CLIENT-INITIATED mode; client must refresh explicitly",
|
|
1839
|
+
{
|
|
1840
|
+
component: "JwtAuth",
|
|
1841
|
+
method: "validate_jwt_token_be",
|
|
1842
|
+
requestId,
|
|
1843
|
+
jti: payload.jti,
|
|
1844
|
+
}
|
|
1845
|
+
);
|
|
1846
|
+
throw new Error("Error 007: Token has expired and renewal failed");
|
|
1847
|
+
} else {
|
|
1848
|
+
// SERVER-INITIATED: renew on authenticate when eligible or expired
|
|
1849
|
+
const renewalResult = await checkandrenew_jwt_token(
|
|
1850
|
+
payload,
|
|
1851
|
+
Math.floor(Date.now() / 1000),
|
|
1852
|
+
requestId,
|
|
1853
|
+
isExpired
|
|
1854
|
+
);
|
|
1855
|
+
newToken = renewalResult.newToken;
|
|
1856
|
+
|
|
1857
|
+
if (renewalResult.reason === "session_closed") {
|
|
1858
|
+
throw new Error("Error 012: Session is not active");
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
if (isExpired && !newToken) {
|
|
1862
|
+
const renewalNow = Math.floor(Date.now() / 1000);
|
|
1863
|
+
const sessionExpUnix =
|
|
1864
|
+
payload.session_exp != null ? Number(payload.session_exp) : null;
|
|
1865
|
+
const sessionStillActive =
|
|
1866
|
+
Number.isFinite(sessionExpUnix) && sessionExpUnix > renewalNow;
|
|
1867
|
+
|
|
1868
|
+
if (sessionStillActive) {
|
|
1869
|
+
logger.warn(
|
|
1870
|
+
"Credential expired and renewal failed but session still active; allowing request",
|
|
1871
|
+
{
|
|
1872
|
+
component: "JwtAuth",
|
|
1873
|
+
method: "validate_jwt_token_be",
|
|
1874
|
+
requestId,
|
|
1875
|
+
jti: payload.jti,
|
|
1876
|
+
sessionExp: sessionExpUnix,
|
|
1877
|
+
now: renewalNow,
|
|
1878
|
+
renewalReason: renewalResult.reason || "renewal_failed",
|
|
1879
|
+
}
|
|
1880
|
+
);
|
|
1881
|
+
} else {
|
|
1882
|
+
logger.error("Token expired and renewal failed", {
|
|
1819
1883
|
component: "JwtAuth",
|
|
1820
1884
|
method: "validate_jwt_token_be",
|
|
1821
1885
|
requestId,
|
|
1822
1886
|
jti: payload.jti,
|
|
1823
1887
|
sessionExp: sessionExpUnix,
|
|
1824
|
-
|
|
1825
|
-
}
|
|
1826
|
-
);
|
|
1827
|
-
} else {
|
|
1828
|
-
logger.error("Token expired and renewal failed", {
|
|
1829
|
-
component: "JwtAuth",
|
|
1830
|
-
method: "validate_jwt_token_be",
|
|
1831
|
-
requestId,
|
|
1832
|
-
jti: payload.jti,
|
|
1833
|
-
sessionExp: sessionExpUnix,
|
|
1834
|
-
});
|
|
1888
|
+
renewalReason: renewalResult.reason || "renewal_failed",
|
|
1889
|
+
});
|
|
1835
1890
|
|
|
1836
|
-
|
|
1891
|
+
throw new Error("Error 007: Token has expired and renewal failed");
|
|
1892
|
+
}
|
|
1837
1893
|
}
|
|
1838
1894
|
}
|
|
1839
1895
|
} else if (isExpired) {
|
|
@@ -2472,7 +2528,7 @@ async function thorough_validate_jwt_token_be(token, requestId = ulid()) {
|
|
|
2472
2528
|
session_status: payload.session_status || "unknown",
|
|
2473
2529
|
seconds_until_eligibility: secondsUntilEligibility,
|
|
2474
2530
|
});
|
|
2475
|
-
return { newToken: null };
|
|
2531
|
+
return { newToken: null, reason: null };
|
|
2476
2532
|
}
|
|
2477
2533
|
|
|
2478
2534
|
// Token needs renewal
|
|
@@ -2602,6 +2658,7 @@ async function thorough_validate_jwt_token_be(token, requestId = ulid()) {
|
|
|
2602
2658
|
|
|
2603
2659
|
return {
|
|
2604
2660
|
newToken,
|
|
2661
|
+
reason: null,
|
|
2605
2662
|
logInfo: {
|
|
2606
2663
|
newDuration: newduration,
|
|
2607
2664
|
reason: shouldDoFullVerification
|
|
@@ -2618,15 +2675,35 @@ async function thorough_validate_jwt_token_be(token, requestId = ulid()) {
|
|
|
2618
2675
|
};
|
|
2619
2676
|
}
|
|
2620
2677
|
} catch (error) {
|
|
2678
|
+
const sessionClosed =
|
|
2679
|
+
error.renewalReason === "session_closed" ||
|
|
2680
|
+
error.code === "SESSION_CLOSED" ||
|
|
2681
|
+
/session inactive or closed/i.test(error.message || "");
|
|
2682
|
+
|
|
2621
2683
|
logger.error("Token renewal failed", {
|
|
2622
2684
|
component: "TokenRenewalService",
|
|
2623
2685
|
method: "checkandrenew_jwt_token",
|
|
2624
2686
|
requestId,
|
|
2625
2687
|
error: error.message,
|
|
2688
|
+
reason: sessionClosed ? "session_closed" : "renewal_failed",
|
|
2689
|
+
});
|
|
2690
|
+
|
|
2691
|
+
const totalDuration = Date.now() - startTime;
|
|
2692
|
+
logger.metric("token_renewal_check_duration_ms", totalDuration, {
|
|
2693
|
+
component: "TokenRenewalService",
|
|
2694
|
+
renewalNeeded: true,
|
|
2695
|
+
success: false,
|
|
2696
|
+
session_status: payload.session_status || "unknown",
|
|
2697
|
+
reason: sessionClosed ? "session_closed" : "renewal_failed",
|
|
2626
2698
|
});
|
|
2699
|
+
|
|
2700
|
+
return {
|
|
2701
|
+
newToken: null,
|
|
2702
|
+
reason: sessionClosed ? "session_closed" : "renewal_failed",
|
|
2703
|
+
};
|
|
2627
2704
|
}
|
|
2628
2705
|
|
|
2629
|
-
// If we reach here, renewal wasn't successful
|
|
2706
|
+
// If we reach here, renewal wasn't successful (verification returned invalid)
|
|
2630
2707
|
const totalDuration = Date.now() - startTime;
|
|
2631
2708
|
logger.debug("Token renewal not performed", {
|
|
2632
2709
|
component: "TokenRenewalService",
|
|
@@ -2641,9 +2718,10 @@ async function thorough_validate_jwt_token_be(token, requestId = ulid()) {
|
|
|
2641
2718
|
renewalNeeded: true,
|
|
2642
2719
|
success: false,
|
|
2643
2720
|
session_status: payload.session_status || "unknown",
|
|
2721
|
+
reason: "verification_failed",
|
|
2644
2722
|
});
|
|
2645
2723
|
|
|
2646
|
-
return { newToken: null };
|
|
2724
|
+
return { newToken: null, reason: "verification_failed" };
|
|
2647
2725
|
}
|
|
2648
2726
|
|
|
2649
2727
|
|
|
@@ -82,6 +82,96 @@ class RODiT {
|
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
/**
|
|
86
|
+
* A NEAR RPC query failed before a completed view result was available.
|
|
87
|
+
* `status` is the HTTP status. `message` includes that status and the
|
|
88
|
+
* provider text (including credit or quota notices). `creditFailure` is set
|
|
89
|
+
* for HTTP 402 and for messages that say the provider is out of credits.
|
|
90
|
+
*
|
|
91
|
+
* A completed `rodit_token` view whose decoded value is JSON null is not this
|
|
92
|
+
* error: that path returns an empty RODiT.
|
|
93
|
+
*/
|
|
94
|
+
class NearRpcError extends Error {
|
|
95
|
+
/**
|
|
96
|
+
* @param {string} message
|
|
97
|
+
* @param {{ status?: number|null, providerMessage?: string, creditFailure?: boolean }} [details]
|
|
98
|
+
*/
|
|
99
|
+
constructor(message, details = {}) {
|
|
100
|
+
super(message);
|
|
101
|
+
this.name = "NearRpcError";
|
|
102
|
+
this.status = details.status == null ? null : details.status;
|
|
103
|
+
this.providerMessage = details.providerMessage || "";
|
|
104
|
+
this.creditFailure = details.creditFailure === true;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function clipRpcText(value, max = 500) {
|
|
109
|
+
const text = String(value ?? "").replace(/\s+/g, " ").trim();
|
|
110
|
+
if (text.length <= max) return text;
|
|
111
|
+
return `${text.slice(0, max)}...`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function describeRpcError(errorField) {
|
|
115
|
+
if (errorField == null) return "";
|
|
116
|
+
if (typeof errorField === "string") return clipRpcText(errorField);
|
|
117
|
+
if (typeof errorField === "object") {
|
|
118
|
+
const message = errorField.message || errorField.name || "";
|
|
119
|
+
const code = errorField.code != null ? ` (${errorField.code})` : "";
|
|
120
|
+
let dataText = "";
|
|
121
|
+
if (errorField.data != null) {
|
|
122
|
+
dataText = typeof errorField.data === "string"
|
|
123
|
+
? errorField.data
|
|
124
|
+
: JSON.stringify(errorField.data);
|
|
125
|
+
}
|
|
126
|
+
const combined = [message ? `${message}${code}` : "", dataText].filter(Boolean).join(": ");
|
|
127
|
+
return clipRpcText(combined || JSON.stringify(errorField));
|
|
128
|
+
}
|
|
129
|
+
return clipRpcText(errorField);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function looksLikeCreditFailure(status, text) {
|
|
133
|
+
if (status === 402) return true;
|
|
134
|
+
return /credit|quota|payment required|insufficient (balance|funds)/i.test(text || "");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function nearRpcFailure(status, providerMessage) {
|
|
138
|
+
const detail = providerMessage ? clipRpcText(providerMessage) : "";
|
|
139
|
+
const message = detail
|
|
140
|
+
? `NEAR RPC call failed (HTTP ${status}): ${detail}`
|
|
141
|
+
: `NEAR RPC call failed (HTTP ${status})`;
|
|
142
|
+
return new NearRpcError(message, {
|
|
143
|
+
status,
|
|
144
|
+
providerMessage: detail,
|
|
145
|
+
creditFailure: looksLikeCreditFailure(status, detail)
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function readResponseErrorMessage(response) {
|
|
150
|
+
let bodyText = "";
|
|
151
|
+
try {
|
|
152
|
+
bodyText = await response.text();
|
|
153
|
+
} catch (_readError) {
|
|
154
|
+
bodyText = "";
|
|
155
|
+
}
|
|
156
|
+
const trimmed = bodyText.trim();
|
|
157
|
+
if (!trimmed) {
|
|
158
|
+
return response.statusText ? clipRpcText(response.statusText) : "";
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
const parsed = JSON.parse(trimmed);
|
|
162
|
+
if (parsed && parsed.error) {
|
|
163
|
+
const described = describeRpcError(parsed.error);
|
|
164
|
+
if (described) return described;
|
|
165
|
+
}
|
|
166
|
+
if (parsed && typeof parsed.message === "string" && parsed.message.trim()) {
|
|
167
|
+
return clipRpcText(parsed.message);
|
|
168
|
+
}
|
|
169
|
+
} catch (_parseError) {
|
|
170
|
+
// Credit notices are often plain text rather than JSON-RPC.
|
|
171
|
+
}
|
|
172
|
+
return clipRpcText(trimmed);
|
|
173
|
+
}
|
|
174
|
+
|
|
85
175
|
/**
|
|
86
176
|
* Payload class for NEP-413 standard
|
|
87
177
|
*/
|
|
@@ -340,10 +430,16 @@ const PayloadNEP413Schema = {
|
|
|
340
430
|
}
|
|
341
431
|
|
|
342
432
|
/**
|
|
343
|
-
* Fetches a RODiT token by ID from the NEAR blockchain
|
|
344
|
-
*
|
|
433
|
+
* Fetches a RODiT token by ID from the NEAR blockchain.
|
|
434
|
+
*
|
|
435
|
+
* An empty RODiT (no token_id) is returned only when the view call completes
|
|
436
|
+
* and the contract decodes to JSON null (token absent). HTTP failures,
|
|
437
|
+
* JSON-RPC errors, credit exhaustion, WASM errors, and undecodable results
|
|
438
|
+
* throw NearRpcError.
|
|
439
|
+
*
|
|
345
440
|
* @param {string} roditid - RODiT token ID to fetch
|
|
346
441
|
* @returns {Promise<RODiT>} RODiT token object
|
|
442
|
+
* @throws {NearRpcError} When the NEAR RPC call does not yield a completed view
|
|
347
443
|
*/
|
|
348
444
|
async function nearorg_rpc_tokenfromroditid(roditid) {
|
|
349
445
|
const requestId = ulid();
|
|
@@ -423,13 +519,16 @@ const PayloadNEP413Schema = {
|
|
|
423
519
|
|
|
424
520
|
if (!response.ok) {
|
|
425
521
|
const duration = Date.now() - startTime;
|
|
522
|
+
const providerMessage = await readResponseErrorMessage(response);
|
|
523
|
+
const httpError = nearRpcFailure(response.status, providerMessage);
|
|
426
524
|
|
|
427
525
|
// Add metric for failed RPC calls
|
|
428
526
|
logger.metric("near_rpc_calls", fetchDuration, {
|
|
429
527
|
result: "failure",
|
|
430
|
-
reason:
|
|
528
|
+
reason: httpError.message,
|
|
431
529
|
status_code: response.status,
|
|
432
530
|
method: "rodit_token",
|
|
531
|
+
credit_failure: httpError.creditFailure ? "true" : "false",
|
|
433
532
|
});
|
|
434
533
|
|
|
435
534
|
logErrorWithMetrics(
|
|
@@ -437,27 +536,37 @@ const PayloadNEP413Schema = {
|
|
|
437
536
|
{
|
|
438
537
|
...baseContext,
|
|
439
538
|
statusCode: response.status,
|
|
539
|
+
providerMessage: httpError.providerMessage,
|
|
540
|
+
creditFailure: httpError.creditFailure,
|
|
440
541
|
duration,
|
|
441
542
|
result: 'failure',
|
|
442
|
-
reason:
|
|
543
|
+
reason: httpError.message
|
|
443
544
|
},
|
|
444
|
-
|
|
545
|
+
httpError,
|
|
445
546
|
"near_rpc_http_error",
|
|
446
547
|
{
|
|
447
548
|
result: "failure",
|
|
448
|
-
reason:
|
|
549
|
+
reason: httpError.message,
|
|
449
550
|
status_code: response.status,
|
|
450
551
|
method: "rodit_token",
|
|
451
552
|
duration
|
|
452
553
|
}
|
|
453
554
|
);
|
|
454
555
|
|
|
455
|
-
|
|
556
|
+
throw httpError;
|
|
456
557
|
}
|
|
457
558
|
|
|
458
559
|
const parseStartTime = Date.now();
|
|
459
560
|
const responseText = await response.text();
|
|
460
|
-
|
|
561
|
+
let parsedJson;
|
|
562
|
+
try {
|
|
563
|
+
parsedJson = JSON.parse(responseText);
|
|
564
|
+
} catch (error) {
|
|
565
|
+
throw nearRpcFailure(
|
|
566
|
+
response.status,
|
|
567
|
+
`Failed to parse NEAR RPC response: ${error.message}`
|
|
568
|
+
);
|
|
569
|
+
}
|
|
461
570
|
const parseDuration = Date.now() - parseStartTime;
|
|
462
571
|
|
|
463
572
|
logger.debugWithContext("RPC response parsed", {
|
|
@@ -466,15 +575,60 @@ const PayloadNEP413Schema = {
|
|
|
466
575
|
hasResult: !!parsedJson.result
|
|
467
576
|
});
|
|
468
577
|
|
|
578
|
+
if (parsedJson.error) {
|
|
579
|
+
const duration = Date.now() - startTime;
|
|
580
|
+
const rpcError = nearRpcFailure(
|
|
581
|
+
response.status,
|
|
582
|
+
describeRpcError(parsedJson.error)
|
|
583
|
+
);
|
|
584
|
+
|
|
585
|
+
logger.metric("near_rpc_calls", fetchDuration, {
|
|
586
|
+
result: "failure",
|
|
587
|
+
reason: rpcError.message,
|
|
588
|
+
status_code: response.status,
|
|
589
|
+
method: "rodit_token",
|
|
590
|
+
credit_failure: rpcError.creditFailure ? "true" : "false",
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
logErrorWithMetrics(
|
|
594
|
+
"JSON-RPC error from blockchain RPC",
|
|
595
|
+
{
|
|
596
|
+
...baseContext,
|
|
597
|
+
statusCode: response.status,
|
|
598
|
+
rpcError: parsedJson.error,
|
|
599
|
+
providerMessage: rpcError.providerMessage,
|
|
600
|
+
creditFailure: rpcError.creditFailure,
|
|
601
|
+
duration,
|
|
602
|
+
result: 'failure',
|
|
603
|
+
reason: rpcError.message
|
|
604
|
+
},
|
|
605
|
+
rpcError,
|
|
606
|
+
"near_rpc_jsonrpc_error",
|
|
607
|
+
{
|
|
608
|
+
result: "failure",
|
|
609
|
+
reason: rpcError.message,
|
|
610
|
+
status_code: response.status,
|
|
611
|
+
method: "rodit_token",
|
|
612
|
+
duration
|
|
613
|
+
}
|
|
614
|
+
);
|
|
615
|
+
|
|
616
|
+
throw rpcError;
|
|
617
|
+
}
|
|
618
|
+
|
|
469
619
|
if (parsedJson.result && parsedJson.result.error) {
|
|
470
620
|
const duration = Date.now() - startTime;
|
|
621
|
+
const wasmText = typeof parsedJson.result.error === "string"
|
|
622
|
+
? parsedJson.result.error
|
|
623
|
+
: JSON.stringify(parsedJson.result.error);
|
|
624
|
+
const wasmError = nearRpcFailure(response.status, `WASM execution error: ${wasmText}`);
|
|
471
625
|
|
|
472
626
|
// Add metric for WASM errors
|
|
473
627
|
logger.metric("near_rpc_wasm_errors", 1, {
|
|
474
628
|
method: "rodit_token",
|
|
475
629
|
rodit_id: roditid,
|
|
476
630
|
result: 'failure',
|
|
477
|
-
reason:
|
|
631
|
+
reason: wasmError.message
|
|
478
632
|
});
|
|
479
633
|
|
|
480
634
|
logErrorWithMetrics(
|
|
@@ -484,25 +638,26 @@ const PayloadNEP413Schema = {
|
|
|
484
638
|
wasmError: parsedJson.result.error,
|
|
485
639
|
duration,
|
|
486
640
|
result: 'failure',
|
|
487
|
-
reason:
|
|
641
|
+
reason: wasmError.message
|
|
488
642
|
},
|
|
489
|
-
|
|
643
|
+
wasmError,
|
|
490
644
|
"near_rpc_wasm_error",
|
|
491
645
|
{
|
|
492
646
|
method: "rodit_token",
|
|
493
647
|
rodit_id: roditid,
|
|
494
648
|
duration,
|
|
495
649
|
result: 'failure',
|
|
496
|
-
reason:
|
|
650
|
+
reason: wasmError.message
|
|
497
651
|
}
|
|
498
652
|
);
|
|
499
653
|
|
|
500
|
-
|
|
654
|
+
throw wasmError;
|
|
501
655
|
}
|
|
502
656
|
|
|
503
|
-
const resultArray = parsedJson.result.result;
|
|
657
|
+
const resultArray = parsedJson.result && parsedJson.result.result;
|
|
504
658
|
if (!Array.isArray(resultArray)) {
|
|
505
659
|
const duration = Date.now() - startTime;
|
|
660
|
+
const formatError = nearRpcFailure(response.status, "Result is not an array");
|
|
506
661
|
|
|
507
662
|
// Add metric for format errors
|
|
508
663
|
logger.metric("near_rpc_format_errors", 1, {
|
|
@@ -510,7 +665,7 @@ const PayloadNEP413Schema = {
|
|
|
510
665
|
rodit_id: roditid,
|
|
511
666
|
error_type: "invalid_array",
|
|
512
667
|
result: 'failure',
|
|
513
|
-
reason:
|
|
668
|
+
reason: formatError.message
|
|
514
669
|
});
|
|
515
670
|
|
|
516
671
|
logErrorWithMetrics(
|
|
@@ -520,9 +675,9 @@ const PayloadNEP413Schema = {
|
|
|
520
675
|
resultType: typeof resultArray,
|
|
521
676
|
duration,
|
|
522
677
|
result: 'failure',
|
|
523
|
-
reason:
|
|
678
|
+
reason: formatError.message
|
|
524
679
|
},
|
|
525
|
-
|
|
680
|
+
formatError,
|
|
526
681
|
"near_rpc_format_error",
|
|
527
682
|
{
|
|
528
683
|
method: "rodit_token",
|
|
@@ -530,11 +685,11 @@ const PayloadNEP413Schema = {
|
|
|
530
685
|
error_type: "invalid_array",
|
|
531
686
|
duration,
|
|
532
687
|
result: 'failure',
|
|
533
|
-
reason:
|
|
688
|
+
reason: formatError.message
|
|
534
689
|
}
|
|
535
690
|
);
|
|
536
691
|
|
|
537
|
-
|
|
692
|
+
throw formatError;
|
|
538
693
|
}
|
|
539
694
|
|
|
540
695
|
const decodeStartTime = Date.now();
|
|
@@ -544,24 +699,28 @@ const PayloadNEP413Schema = {
|
|
|
544
699
|
try {
|
|
545
700
|
parsed = JSON.parse(resultString);
|
|
546
701
|
} catch (error) {
|
|
702
|
+
const parseError = nearRpcFailure(
|
|
703
|
+
response.status,
|
|
704
|
+
`Failed to parse RODiT data: ${error.message}`
|
|
705
|
+
);
|
|
547
706
|
logErrorWithMetrics(
|
|
548
707
|
"Failed to parse RODiT data",
|
|
549
708
|
{
|
|
550
709
|
...baseContext,
|
|
551
710
|
error: error.message,
|
|
552
711
|
result: 'failure',
|
|
553
|
-
reason:
|
|
712
|
+
reason: parseError.message
|
|
554
713
|
},
|
|
555
|
-
|
|
714
|
+
parseError,
|
|
556
715
|
"rodit_parse_error",
|
|
557
716
|
{
|
|
558
717
|
error_type: error.name || "Unknown",
|
|
559
718
|
rodit_id: roditid,
|
|
560
719
|
result: 'failure',
|
|
561
|
-
reason:
|
|
720
|
+
reason: parseError.message
|
|
562
721
|
}
|
|
563
722
|
);
|
|
564
|
-
|
|
723
|
+
throw parseError;
|
|
565
724
|
}
|
|
566
725
|
|
|
567
726
|
const decodeDuration = Date.now() - decodeStartTime;
|
|
@@ -610,13 +769,18 @@ const PayloadNEP413Schema = {
|
|
|
610
769
|
roditMetadata: rodit.metadata
|
|
611
770
|
});
|
|
612
771
|
} else {
|
|
772
|
+
const formatError = nearRpcFailure(
|
|
773
|
+
response.status,
|
|
774
|
+
"Invalid RODiT data format from blockchain"
|
|
775
|
+
);
|
|
613
776
|
logger.warnWithContext("Invalid RODiT data format", {
|
|
614
777
|
...baseContext,
|
|
615
778
|
parsedType: typeof parsed,
|
|
616
779
|
parsedValue: parsed,
|
|
617
780
|
result: 'failure',
|
|
618
|
-
reason:
|
|
781
|
+
reason: formatError.message
|
|
619
782
|
});
|
|
783
|
+
throw formatError;
|
|
620
784
|
}
|
|
621
785
|
|
|
622
786
|
const totalDuration = Date.now() - startTime;
|
|
@@ -675,7 +839,7 @@ const PayloadNEP413Schema = {
|
|
|
675
839
|
}
|
|
676
840
|
);
|
|
677
841
|
|
|
678
|
-
|
|
842
|
+
throw error;
|
|
679
843
|
}
|
|
680
844
|
}
|
|
681
845
|
|
|
@@ -1697,6 +1861,7 @@ async function fetchWithRetry(url, options, maxRetries = 3) {
|
|
|
1697
1861
|
|
|
1698
1862
|
module.exports = {
|
|
1699
1863
|
RODiT,
|
|
1864
|
+
NearRpcError,
|
|
1700
1865
|
PayloadNEP413,
|
|
1701
1866
|
PayloadNEP413Schema,
|
|
1702
1867
|
CONSTANTS,
|
|
@@ -1343,6 +1343,191 @@ async function login_client(req, res) {
|
|
|
1343
1343
|
}
|
|
1344
1344
|
}
|
|
1345
1345
|
|
|
1346
|
+
/**
|
|
1347
|
+
* Optional client-initiated access-credential refresh.
|
|
1348
|
+
* Not required for default SERVER-INITIATED deployments: the server renews
|
|
1349
|
+
* access JWTs on authenticated API calls and returns them via New-Token.
|
|
1350
|
+
* Mount POST /api/refresh only for idle-client timers or CLIENT-INITIATED mode.
|
|
1351
|
+
*
|
|
1352
|
+
* @param {Object} req - Express request object
|
|
1353
|
+
* @param {Object} res - Express response object
|
|
1354
|
+
* @returns {Object} Response object
|
|
1355
|
+
*/
|
|
1356
|
+
async function refresh_client(req, res) {
|
|
1357
|
+
const requestId = ulid();
|
|
1358
|
+
const startTime = Date.now();
|
|
1359
|
+
const baseContext = createLogContext(
|
|
1360
|
+
"AuthenticationService",
|
|
1361
|
+
"refresh_client",
|
|
1362
|
+
{
|
|
1363
|
+
requestId,
|
|
1364
|
+
path: req.path,
|
|
1365
|
+
method: req.method,
|
|
1366
|
+
ip: req.ip,
|
|
1367
|
+
}
|
|
1368
|
+
);
|
|
1369
|
+
|
|
1370
|
+
logger.infoWithContext("Token refresh request received", {
|
|
1371
|
+
...baseContext,
|
|
1372
|
+
userAgent: req.get("User-Agent"),
|
|
1373
|
+
});
|
|
1374
|
+
|
|
1375
|
+
try {
|
|
1376
|
+
verifySessionManager();
|
|
1377
|
+
|
|
1378
|
+
const jwt_token =
|
|
1379
|
+
req.headers.authorization &&
|
|
1380
|
+
req.headers.authorization.startsWith("Bearer ")
|
|
1381
|
+
? req.headers.authorization.substring(7)
|
|
1382
|
+
: null;
|
|
1383
|
+
|
|
1384
|
+
if (!jwt_token) {
|
|
1385
|
+
logger.metric &&
|
|
1386
|
+
logger.metric("token_refresh_attempts", 1, {
|
|
1387
|
+
component: "AuthenticationService",
|
|
1388
|
+
result: "no_jwt_token",
|
|
1389
|
+
});
|
|
1390
|
+
return sendError(res, {
|
|
1391
|
+
statusCode: 401,
|
|
1392
|
+
requestId,
|
|
1393
|
+
code: "MISSING_TOKEN",
|
|
1394
|
+
message: "No authentication jwt_token provided",
|
|
1395
|
+
});
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
const isTokenInvalid = await sessionManager.isTokenInvalidated(jwt_token);
|
|
1399
|
+
if (isTokenInvalid) {
|
|
1400
|
+
const invalidationInfo =
|
|
1401
|
+
await sessionManager.getTokenInvalidationInfo(jwt_token);
|
|
1402
|
+
logger.metric &&
|
|
1403
|
+
logger.metric("token_refresh_attempts", 1, {
|
|
1404
|
+
component: "AuthenticationService",
|
|
1405
|
+
result: "invalidated_token",
|
|
1406
|
+
});
|
|
1407
|
+
return sendError(res, {
|
|
1408
|
+
statusCode: 401,
|
|
1409
|
+
requestId,
|
|
1410
|
+
code: "INVALIDATED_TOKEN",
|
|
1411
|
+
message: "Token has been invalidated",
|
|
1412
|
+
details: {
|
|
1413
|
+
reason: invalidationInfo?.reason || "session_inactive",
|
|
1414
|
+
invalidatedAt: invalidationInfo?.timestamp,
|
|
1415
|
+
},
|
|
1416
|
+
});
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
const config_own_rodit = await stateManager.getConfigOwnRodit();
|
|
1420
|
+
if (!config_own_rodit || !config_own_rodit.own_rodit) {
|
|
1421
|
+
return sendError(res, {
|
|
1422
|
+
statusCode: 500,
|
|
1423
|
+
requestId,
|
|
1424
|
+
code: "SERVER_CONFIG_ERROR",
|
|
1425
|
+
message: "Server configuration not initialized",
|
|
1426
|
+
});
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
let validationResult;
|
|
1430
|
+
try {
|
|
1431
|
+
validationResult = await validate_jwt_token_be(
|
|
1432
|
+
jwt_token,
|
|
1433
|
+
config_own_rodit.own_rodit,
|
|
1434
|
+
{
|
|
1435
|
+
allowExpiredToken: true,
|
|
1436
|
+
forceCredentialRenewal: true,
|
|
1437
|
+
}
|
|
1438
|
+
);
|
|
1439
|
+
} catch (validationError) {
|
|
1440
|
+
const message = validationError.message || "Token refresh failed";
|
|
1441
|
+
const sessionClosed =
|
|
1442
|
+
/Error 012/i.test(message) ||
|
|
1443
|
+
/Session is not active/i.test(message) ||
|
|
1444
|
+
validationError.renewalReason === "session_closed";
|
|
1445
|
+
logger.metric &&
|
|
1446
|
+
logger.metric("token_refresh_attempts", 1, {
|
|
1447
|
+
component: "AuthenticationService",
|
|
1448
|
+
result: sessionClosed ? "session_closed" : "validation_failed",
|
|
1449
|
+
});
|
|
1450
|
+
return sendError(res, {
|
|
1451
|
+
statusCode: sessionClosed ? 401 : 403,
|
|
1452
|
+
requestId,
|
|
1453
|
+
code: sessionClosed
|
|
1454
|
+
? "INVALIDATED_TOKEN"
|
|
1455
|
+
: validationError.code || "REFRESH_FAILED",
|
|
1456
|
+
message,
|
|
1457
|
+
});
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
const newToken = validationResult?.newToken;
|
|
1461
|
+
if (!newToken) {
|
|
1462
|
+
logger.metric &&
|
|
1463
|
+
logger.metric("token_refresh_attempts", 1, {
|
|
1464
|
+
component: "AuthenticationService",
|
|
1465
|
+
result: "no_new_token",
|
|
1466
|
+
});
|
|
1467
|
+
return sendError(res, {
|
|
1468
|
+
statusCode: 403,
|
|
1469
|
+
requestId,
|
|
1470
|
+
code: "REFRESH_FAILED",
|
|
1471
|
+
message: "Token refresh did not produce a new credential",
|
|
1472
|
+
});
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
if (typeof res.setHeader === "function") {
|
|
1476
|
+
res.setHeader("New-Token", newToken);
|
|
1477
|
+
} else if (typeof res.set === "function") {
|
|
1478
|
+
res.set("New-Token", newToken);
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
const duration = Date.now() - startTime;
|
|
1482
|
+
logger.infoWithContext("Token refresh completed", {
|
|
1483
|
+
...baseContext,
|
|
1484
|
+
duration,
|
|
1485
|
+
success: true,
|
|
1486
|
+
});
|
|
1487
|
+
logger.metric &&
|
|
1488
|
+
logger.metric("token_refresh_attempts", 1, {
|
|
1489
|
+
component: "AuthenticationService",
|
|
1490
|
+
result: "success",
|
|
1491
|
+
});
|
|
1492
|
+
logger.metric &&
|
|
1493
|
+
logger.metric("token_refresh_duration_ms", duration, {
|
|
1494
|
+
component: "AuthenticationService",
|
|
1495
|
+
success: true,
|
|
1496
|
+
});
|
|
1497
|
+
|
|
1498
|
+
return res.json({
|
|
1499
|
+
token: newToken,
|
|
1500
|
+
jwt_token: newToken,
|
|
1501
|
+
message: "Token refreshed",
|
|
1502
|
+
requestId,
|
|
1503
|
+
});
|
|
1504
|
+
} catch (error) {
|
|
1505
|
+
const duration = Date.now() - startTime;
|
|
1506
|
+
logErrorWithMetrics(
|
|
1507
|
+
"Token refresh process failed",
|
|
1508
|
+
{
|
|
1509
|
+
...baseContext,
|
|
1510
|
+
duration,
|
|
1511
|
+
},
|
|
1512
|
+
error,
|
|
1513
|
+
"refresh_error",
|
|
1514
|
+
{ error_type: "general_refresh_error" }
|
|
1515
|
+
);
|
|
1516
|
+
logger.metric &&
|
|
1517
|
+
logger.metric("token_refresh_attempts", 1, {
|
|
1518
|
+
component: "AuthenticationService",
|
|
1519
|
+
result: "error",
|
|
1520
|
+
});
|
|
1521
|
+
return sendError(res, {
|
|
1522
|
+
statusCode: 500,
|
|
1523
|
+
requestId,
|
|
1524
|
+
code: "REFRESH_ERROR",
|
|
1525
|
+
message: "Internal server error during token refresh",
|
|
1526
|
+
details: !isStrictEnvironment() ? { error: error.message } : undefined,
|
|
1527
|
+
});
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1346
1531
|
/**
|
|
1347
1532
|
* Handle client login with NEP-413 standard
|
|
1348
1533
|
*
|
|
@@ -2474,5 +2659,6 @@ module.exports = {
|
|
|
2474
2659
|
login_client_withnep413,
|
|
2475
2660
|
logout_client,
|
|
2476
2661
|
logout_server,
|
|
2662
|
+
refresh_client,
|
|
2477
2663
|
createLoginTimestampChallenge,
|
|
2478
2664
|
};
|
package/package.json
CHANGED
package/services/configsdk.js
CHANGED
|
@@ -229,10 +229,12 @@ const FALLBACK_DEFAULTS = {
|
|
|
229
229
|
LAPSED_LIFETIME_PROPORTION_4RENEWAL_ELIGIBILITY: "0.80",
|
|
230
230
|
THRESHOLD_VALIDATION_TYPE: "0.10",
|
|
231
231
|
DURATIONRAMP: "0.85",
|
|
232
|
-
//
|
|
232
|
+
// Token renewal initiator (default: server handles renewals).
|
|
233
233
|
// Options:
|
|
234
|
-
// - "SERVER-INITIATED":
|
|
235
|
-
// -
|
|
234
|
+
// - "SERVER-INITIATED" (default): renew access JWT during authenticate and
|
|
235
|
+
// return New-Token. Clients normally do not need POST /api/refresh.
|
|
236
|
+
// - "CLIENT-INITIATED" (optional/advanced): skip piggyback renewal; client
|
|
237
|
+
// must call the optional POST /api/refresh endpoint.
|
|
236
238
|
SERVERORCLIENT: "SERVER-INITIATED",
|
|
237
239
|
// Login error behavior.
|
|
238
240
|
// Options:
|