nucleus-core-ts 0.9.708 → 0.9.709
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/dist/src/Client/Proxy/httpProxy.d.ts +11 -1
- package/dist/src/Client/Proxy/httpProxy.js +2 -2
- package/dist/src/Client/Proxy/server.js +1 -1
- package/dist/src/Client/Proxy/types.d.ts +10 -0
- package/dist/src/Client/Proxy/wsProxy.d.ts +1 -1
- package/dist/src/Client/Proxy/wsProxy.js +41 -5
- package/package.json +1 -1
|
@@ -1,4 +1,14 @@
|
|
|
1
|
-
import type { HttpProxyConfig } from './types';
|
|
1
|
+
import type { HttpProxyConfig, TokenRefreshConfig } from './types';
|
|
2
|
+
interface RefreshResult {
|
|
3
|
+
accessToken: string;
|
|
4
|
+
setCookieHeaders: string[];
|
|
5
|
+
}
|
|
6
|
+
export declare function deduplicatedRefresh(refreshConfig: TokenRefreshConfig, refreshToken: string, logger: {
|
|
7
|
+
info: (...args: unknown[]) => void;
|
|
8
|
+
warn: (...args: unknown[]) => void;
|
|
9
|
+
error: (...args: unknown[]) => void;
|
|
10
|
+
}, originalCookieHeader?: string | null): Promise<RefreshResult | null>;
|
|
2
11
|
export declare function createHttpProxyHandler(config: HttpProxyConfig): {
|
|
3
12
|
handle(req: Request, clientIp?: string): Promise<Response | null>;
|
|
4
13
|
};
|
|
14
|
+
export {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { evaluateAccess, ManifestCache,
|
|
1
|
+
import { evaluateAccess, ManifestCache, RoleClaimsCache, resolveClaimsFromRoles, verifyJwtHS256 } from './authz';
|
|
2
2
|
import { createProxyLogger, matchPath, parseCookies, rewritePath } from './utils';
|
|
3
3
|
const pendingRefreshes = new Map();
|
|
4
4
|
function extractAccessTokenFromSetCookie(setCookieHeaders, accessCookieName) {
|
|
@@ -89,7 +89,7 @@ async function performTokenRefresh(refreshConfig, refreshToken, logger, original
|
|
|
89
89
|
return null;
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
|
-
function deduplicatedRefresh(refreshConfig, refreshToken, logger, originalCookieHeader) {
|
|
92
|
+
export function deduplicatedRefresh(refreshConfig, refreshToken, logger, originalCookieHeader) {
|
|
93
93
|
const dedupeKey = refreshToken.slice(-16);
|
|
94
94
|
const existing = pendingRefreshes.get(dedupeKey);
|
|
95
95
|
if (existing) {
|
|
@@ -39,7 +39,7 @@ export async function createProxyServer(config) {
|
|
|
39
39
|
async fetch (req, server) {
|
|
40
40
|
if (req.headers.get('upgrade')?.toLowerCase() === 'websocket') {
|
|
41
41
|
if (wsHandler) {
|
|
42
|
-
const result = wsHandler.upgrade(req, server);
|
|
42
|
+
const result = await wsHandler.upgrade(req, server);
|
|
43
43
|
if (result === true) {
|
|
44
44
|
return undefined;
|
|
45
45
|
}
|
|
@@ -8,6 +8,16 @@ export interface WsProxyTarget {
|
|
|
8
8
|
queryParam?: string;
|
|
9
9
|
headerName?: string;
|
|
10
10
|
};
|
|
11
|
+
/**
|
|
12
|
+
* Proactively refresh the access token before injecting it into the handshake.
|
|
13
|
+
* A WS handshake bearer MUST be the short-lived access token (the backend verifies
|
|
14
|
+
* it with the access-token secret) — a `refresh_token`/`session_token` fallback is
|
|
15
|
+
* signed with a DIFFERENT secret and always fails verification, producing a 4001
|
|
16
|
+
* reconnect storm once the access-token cookie ages out (e.g. after 30m idle on a
|
|
17
|
+
* sibling subdomain). When set, if the access cookie is missing/expiring and a
|
|
18
|
+
* refresh cookie is present, exchange it for a fresh access token and inject THAT.
|
|
19
|
+
*/
|
|
20
|
+
tokenRefresh?: TokenRefreshConfig;
|
|
11
21
|
headers?: Record<string, string>;
|
|
12
22
|
changeOrigin?: boolean;
|
|
13
23
|
secure?: boolean;
|
|
@@ -5,7 +5,7 @@ export declare function createWsProxyHandler(config: WsProxyConfig): {
|
|
|
5
5
|
upgrade: (req: Request, options: {
|
|
6
6
|
data: WsProxyState;
|
|
7
7
|
}) => boolean;
|
|
8
|
-
}): boolean | Response
|
|
8
|
+
}): Promise<boolean | Response>;
|
|
9
9
|
websocket: {
|
|
10
10
|
open(ws: ServerWebSocket<WsProxyState>): void;
|
|
11
11
|
message(ws: ServerWebSocket<WsProxyState>, message: string | Buffer): void;
|
|
@@ -1,4 +1,21 @@
|
|
|
1
|
+
import { deduplicatedRefresh } from './httpProxy';
|
|
1
2
|
import { addQueryParam, createProxyLogger, httpToWs, matchPath, parseCookies, rewritePath } from './utils';
|
|
3
|
+
/**
|
|
4
|
+
* True when an access token is absent or within `skewMs` of expiring. Decodes the JWT
|
|
5
|
+
* exp WITHOUT verifying (verification happens at the backend) — this only decides whether
|
|
6
|
+
* to pre-emptively refresh before the WS handshake.
|
|
7
|
+
*/ function accessTokenExpiringSoon(token, skewMs = 60000) {
|
|
8
|
+
if (!token) return true;
|
|
9
|
+
try {
|
|
10
|
+
const part = token.split('.')[1];
|
|
11
|
+
if (!part) return true;
|
|
12
|
+
const json = JSON.parse(Buffer.from(part.replace(/-/g, '+').replace(/_/g, '/'), 'base64').toString('utf8'));
|
|
13
|
+
if (typeof json.exp !== 'number') return false;
|
|
14
|
+
return json.exp * 1000 - Date.now() < skewMs;
|
|
15
|
+
} catch {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
2
19
|
export function createWsProxyHandler(config) {
|
|
3
20
|
const logger = createProxyLogger('WS Proxy', config.debug ?? false);
|
|
4
21
|
const pingIntervalMs = config.pingIntervalMs ?? 0;
|
|
@@ -10,7 +27,7 @@ export function createWsProxyHandler(config) {
|
|
|
10
27
|
}
|
|
11
28
|
return null;
|
|
12
29
|
}
|
|
13
|
-
function buildBackendUrl(path, target, cookies, query) {
|
|
30
|
+
async function buildBackendUrl(path, target, cookies, query) {
|
|
14
31
|
let baseUrl = target.url.replace(/\/$/, '');
|
|
15
32
|
if (baseUrl.startsWith('http')) {
|
|
16
33
|
baseUrl = httpToWs(baseUrl);
|
|
@@ -40,6 +57,25 @@ export function createWsProxyHandler(config) {
|
|
|
40
57
|
} else {
|
|
41
58
|
let token = cookies[target.injectTokenFromCookie.cookieName];
|
|
42
59
|
let tokenSource = target.injectTokenFromCookie.cookieName;
|
|
60
|
+
// A WS handshake bearer MUST be the short-lived access token. If it's missing or
|
|
61
|
+
// about to expire and a refresh is configured, exchange the refresh cookie for a
|
|
62
|
+
// FRESH access token and inject that — NEVER fall through to a refresh/session
|
|
63
|
+
// cookie, which is signed with a different secret and guarantees a 4001 storm.
|
|
64
|
+
const refreshConfig = target.tokenRefresh;
|
|
65
|
+
const refreshToken = cookies[refreshConfig?.refreshCookieName ?? 'refresh_token'];
|
|
66
|
+
if (refreshConfig?.enabled && refreshToken && accessTokenExpiringSoon(token)) {
|
|
67
|
+
const cookieHeader = Object.entries(cookies).map(([k, v])=>`${k}=${v}`).join('; ');
|
|
68
|
+
const refreshed = await deduplicatedRefresh(refreshConfig, refreshToken, logger, cookieHeader);
|
|
69
|
+
if (refreshed?.accessToken) {
|
|
70
|
+
token = refreshed.accessToken;
|
|
71
|
+
tokenSource = 'refreshed access_token';
|
|
72
|
+
refreshConfig.onRefreshSuccess?.(path);
|
|
73
|
+
} else {
|
|
74
|
+
refreshConfig.onRefreshFailure?.(path, 'WS pre-handshake refresh failed');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Legacy fallback — reached only when refresh is NOT configured. Prefer configuring
|
|
78
|
+
// tokenRefresh: a refresh/session cookie here is not a valid handshake bearer.
|
|
43
79
|
if (!token && target.injectTokenFromCookie.fallbackCookieNames) {
|
|
44
80
|
for (const fallback of target.injectTokenFromCookie.fallbackCookieNames){
|
|
45
81
|
if (cookies[fallback]) {
|
|
@@ -60,7 +96,7 @@ export function createWsProxyHandler(config) {
|
|
|
60
96
|
return url;
|
|
61
97
|
}
|
|
62
98
|
return {
|
|
63
|
-
upgrade (req, server) {
|
|
99
|
+
async upgrade (req, server) {
|
|
64
100
|
const url = new URL(req.url);
|
|
65
101
|
const path = url.pathname;
|
|
66
102
|
if (config.allowHmr !== false && path.startsWith('/_next/webpack-hmr')) {
|
|
@@ -78,7 +114,7 @@ export function createWsProxyHandler(config) {
|
|
|
78
114
|
return false;
|
|
79
115
|
}
|
|
80
116
|
const cookies = parseCookies(req.headers.get('cookie'));
|
|
81
|
-
const backendUrl = buildBackendUrl(path, target, cookies, url.searchParams);
|
|
117
|
+
const backendUrl = await buildBackendUrl(path, target, cookies, url.searchParams);
|
|
82
118
|
logger.info(`Upgrading: ${path} -> ${backendUrl}`);
|
|
83
119
|
// Extract original request headers to forward to backend
|
|
84
120
|
const originalHeaders = {};
|
|
@@ -224,8 +260,8 @@ export function startWsProxyServer(config) {
|
|
|
224
260
|
const server = Bun.serve({
|
|
225
261
|
port,
|
|
226
262
|
hostname,
|
|
227
|
-
fetch (req, server) {
|
|
228
|
-
const result = handler.upgrade(req, server);
|
|
263
|
+
async fetch (req, server) {
|
|
264
|
+
const result = await handler.upgrade(req, server);
|
|
229
265
|
if (result === true) {
|
|
230
266
|
return undefined;
|
|
231
267
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nucleus-core-ts",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.709",
|
|
4
4
|
"description": "Production-ready, enterprise-grade TypeScript framework for building multi-tenant APIs",
|
|
5
5
|
"author": "Hidayet Can Özcan <hidayetcan@gmail.com>",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|