joye-backend-utility 7.3.6 → 8.0.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/dist/jwt.js
CHANGED
|
@@ -7,6 +7,7 @@ const path = require('path');
|
|
|
7
7
|
const folderPath = path.resolve(`${__dirname}`);
|
|
8
8
|
const JWT_PRIVATE_KEY = fs.readFileSync(`${folderPath}/jwtKeys/jwtRS256.key`, 'utf8');
|
|
9
9
|
const JWT_PUBLIC_KEY = fs.readFileSync(`${folderPath}/jwtKeys/jwtRS256.key.pub`, 'utf8');
|
|
10
|
+
const ALLOWED_JWT_ALGORITHMS = ['RS256'];
|
|
10
11
|
const createToken = async (payload, expiresIn) => {
|
|
11
12
|
const token = jwt.sign(payload, { key: JWT_PRIVATE_KEY.replace(/\\n/gm, '\n'), passphrase: process.env.JWT_SECRET }, {
|
|
12
13
|
expiresIn,
|
|
@@ -16,7 +17,12 @@ const createToken = async (payload, expiresIn) => {
|
|
|
16
17
|
};
|
|
17
18
|
exports.createToken = createToken;
|
|
18
19
|
const jwtVerify = async (token) => new Promise(resolve => {
|
|
19
|
-
|
|
20
|
+
var _a;
|
|
21
|
+
const decodedHeader = jwt.decode(token, { complete: true });
|
|
22
|
+
if (!((_a = decodedHeader === null || decodedHeader === void 0 ? void 0 : decodedHeader.header) === null || _a === void 0 ? void 0 : _a.alg) || decodedHeader.header.alg === 'none') {
|
|
23
|
+
return resolve(new Error('Invalid token algorithm'));
|
|
24
|
+
}
|
|
25
|
+
jwt.verify(token, JWT_PUBLIC_KEY.replace(/\\n/gm, '\n'), { algorithms: ALLOWED_JWT_ALGORITHMS }, async (err, decoded) => {
|
|
20
26
|
if (err) {
|
|
21
27
|
return resolve(err);
|
|
22
28
|
}
|
package/dist/redisClient.d.ts
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
|
-
import { createClient } from 'redis';
|
|
1
|
+
import { createClient, createCluster } from 'redis';
|
|
2
|
+
/**
|
|
3
|
+
* Standalone or `node-redis` cluster client. Cluster mode uses `createCluster` + `nodeAddressMap` so slot
|
|
4
|
+
* topology (internal IPs from MOVED / CLUSTER SLOTS) maps to the Azure TLS hostname — avoids ioredis
|
|
5
|
+
* MOVED + `natMap` slot-key mismatch that caused “Too many Cluster redirections”.
|
|
6
|
+
*/
|
|
7
|
+
export declare type RedisConnection = Awaited<ReturnType<ReturnType<typeof createClient>['connect']>> | ReturnType<typeof createCluster>;
|
|
2
8
|
declare class RedisClient {
|
|
3
9
|
private static instance;
|
|
4
|
-
|
|
10
|
+
/** Ensures concurrent `getInstance()` share one in-flight connect (avoids duplicate clients / flaky MOVED). */
|
|
11
|
+
private static initPromise;
|
|
12
|
+
private static createConnection;
|
|
13
|
+
static getInstance(): Promise<RedisConnection>;
|
|
5
14
|
}
|
|
6
15
|
export { RedisClient };
|
package/dist/redisClient.js
CHANGED
|
@@ -2,17 +2,72 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.RedisClient = void 0;
|
|
4
4
|
const redis_1 = require("redis");
|
|
5
|
+
function trimEnv(value) {
|
|
6
|
+
if (value === undefined)
|
|
7
|
+
return undefined;
|
|
8
|
+
let t = value.trim();
|
|
9
|
+
if ((t.startsWith('"') && t.endsWith('"')) || (t.startsWith("'") && t.endsWith("'"))) {
|
|
10
|
+
t = t.slice(1, -1).trim();
|
|
11
|
+
}
|
|
12
|
+
return t;
|
|
13
|
+
}
|
|
14
|
+
function envBool(value, defaultValue) {
|
|
15
|
+
var _a;
|
|
16
|
+
const v = (_a = trimEnv(value)) === null || _a === void 0 ? void 0 : _a.toLowerCase();
|
|
17
|
+
if (v === undefined || v === '')
|
|
18
|
+
return defaultValue;
|
|
19
|
+
if (v === 'true' || v === '1' || v === 'yes')
|
|
20
|
+
return true;
|
|
21
|
+
if (v === 'false' || v === '0' || v === 'no')
|
|
22
|
+
return false;
|
|
23
|
+
return defaultValue;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Azure Managed Redis (ACL): default user is `default`.
|
|
27
|
+
* Set `REDIS_CACHE_USERNAME=none` (or empty after trim) to omit username for legacy password-only caches.
|
|
28
|
+
*/
|
|
29
|
+
function redisUsername() {
|
|
30
|
+
const raw = process.env.REDIS_CACHE_USERNAME;
|
|
31
|
+
if (raw === undefined)
|
|
32
|
+
return 'default';
|
|
33
|
+
const t = trimEnv(raw);
|
|
34
|
+
if (t === '' || t.toLowerCase() === 'none')
|
|
35
|
+
return undefined;
|
|
36
|
+
return t;
|
|
37
|
+
}
|
|
38
|
+
function mapClusterAddressToPublicEndpoint(address, publicHost, fallbackPort) {
|
|
39
|
+
const idx = address.lastIndexOf(':');
|
|
40
|
+
if (idx === -1)
|
|
41
|
+
return { host: publicHost, port: fallbackPort };
|
|
42
|
+
const p = Number.parseInt(address.slice(idx + 1), 10);
|
|
43
|
+
return Number.isNaN(p) ? { host: publicHost, port: fallbackPort } : { host: publicHost, port: p };
|
|
44
|
+
}
|
|
5
45
|
class RedisClient {
|
|
6
|
-
static async
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
46
|
+
static async createConnection() {
|
|
47
|
+
const cachePortRaw = trimEnv(process.env.REDIS_CACHE_PORT);
|
|
48
|
+
const cacheHostName = trimEnv(process.env.REDIS_CACHE_HOST);
|
|
49
|
+
const cachePassword = trimEnv(process.env.REDIS_CACHE_PASSWORD);
|
|
50
|
+
const cachePort = cachePortRaw ? Number.parseInt(cachePortRaw, 10) : Number.NaN;
|
|
51
|
+
const username = redisUsername();
|
|
52
|
+
const useCluster = envBool(process.env.REDIS_CACHE_CLUSTER, false);
|
|
53
|
+
const unifiedClusterEndpoint = useCluster && envBool(process.env.REDIS_CACHE_CLUSTER_UNIFIED_ENDPOINT, true);
|
|
54
|
+
if (!cacheHostName || Number.isNaN(cachePort)) {
|
|
55
|
+
throw new Error('RedisClient: REDIS_CACHE_HOST and a numeric REDIS_CACHE_PORT are required');
|
|
56
|
+
}
|
|
57
|
+
const tlsSocket = {
|
|
58
|
+
tls: true,
|
|
59
|
+
servername: cacheHostName,
|
|
60
|
+
};
|
|
61
|
+
const standaloneSocket = Object.assign({ host: cacheHostName, port: cachePort }, tlsSocket);
|
|
62
|
+
const authOptions = Object.assign(Object.assign({}, (username !== undefined ? { username } : {})), (cachePassword !== undefined ? { password: cachePassword } : {}));
|
|
63
|
+
if (useCluster) {
|
|
64
|
+
console.log('RedisClient: CLUSTER mode (node-redis createCluster), unifiedEndpoint=%s, maxCommandRedirections=64', String(unifiedClusterEndpoint));
|
|
65
|
+
const clusterRoot = Object.assign({ socket: standaloneSocket }, authOptions);
|
|
66
|
+
const cluster = (0, redis_1.createCluster)(Object.assign({ rootNodes: [clusterRoot], defaults: Object.assign(Object.assign({}, authOptions), { socket: tlsSocket }), maxCommandRedirections: 64 }, (unifiedClusterEndpoint
|
|
67
|
+
? {
|
|
68
|
+
nodeAddressMap: (address) => mapClusterAddressToPublicEndpoint(address, cacheHostName, cachePort),
|
|
69
|
+
}
|
|
70
|
+
: {})))
|
|
16
71
|
.on('error', err => {
|
|
17
72
|
console.log('Redis Client Error', err);
|
|
18
73
|
})
|
|
@@ -21,14 +76,42 @@ class RedisClient {
|
|
|
21
76
|
})
|
|
22
77
|
.on('end', () => {
|
|
23
78
|
console.log('Connection to Redis end');
|
|
24
|
-
})
|
|
25
|
-
|
|
79
|
+
});
|
|
80
|
+
await cluster.connect();
|
|
81
|
+
return cluster;
|
|
82
|
+
}
|
|
83
|
+
console.log('RedisClient: STANDALONE mode (node-redis createClient) — not for OSS cluster endpoints (MOVED).');
|
|
84
|
+
const client = (0, redis_1.createClient)(Object.assign({ socket: standaloneSocket }, authOptions))
|
|
85
|
+
.on('error', err => {
|
|
86
|
+
console.log('Redis Client Error', err);
|
|
87
|
+
})
|
|
88
|
+
.on('connect', () => {
|
|
89
|
+
console.log('Connected to Redis');
|
|
90
|
+
})
|
|
91
|
+
.on('end', () => {
|
|
92
|
+
console.log('Connection to Redis end');
|
|
93
|
+
});
|
|
94
|
+
return await client.connect();
|
|
95
|
+
}
|
|
96
|
+
static async getInstance() {
|
|
97
|
+
if (!RedisClient.instance) {
|
|
98
|
+
if (!RedisClient.initPromise) {
|
|
99
|
+
RedisClient.initPromise = RedisClient.createConnection();
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
RedisClient.instance = await RedisClient.initPromise;
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
RedisClient.initPromise = null;
|
|
106
|
+
}
|
|
26
107
|
}
|
|
27
108
|
if (!RedisClient.instance.isOpen) {
|
|
28
|
-
|
|
109
|
+
await RedisClient.instance.connect();
|
|
29
110
|
}
|
|
30
111
|
return RedisClient.instance;
|
|
31
112
|
}
|
|
32
113
|
}
|
|
33
114
|
exports.RedisClient = RedisClient;
|
|
34
115
|
RedisClient.instance = null;
|
|
116
|
+
/** Ensures concurrent `getInstance()` share one in-flight connect (avoids duplicate clients / flaky MOVED). */
|
|
117
|
+
RedisClient.initPromise = null;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const assert = require("assert");
|
|
4
|
+
const jwt = require('jsonwebtoken');
|
|
5
|
+
const jwt_1 = require("../jwt");
|
|
6
|
+
const run = async () => {
|
|
7
|
+
const payload = {
|
|
8
|
+
userId: 'security-test-user',
|
|
9
|
+
email: 'security-test@example.com',
|
|
10
|
+
};
|
|
11
|
+
const noneToken = jwt.sign(payload, null, { algorithm: 'none' });
|
|
12
|
+
const noneResult = await (0, jwt_1.jwtVerify)(noneToken);
|
|
13
|
+
assert.ok(noneResult instanceof Error, 'Expected alg=none token to be rejected');
|
|
14
|
+
const hsToken = jwt.sign(payload, 'not-rs256-key', { algorithm: 'HS256' });
|
|
15
|
+
const hsResult = await (0, jwt_1.jwtVerify)(hsToken);
|
|
16
|
+
assert.ok(hsResult instanceof Error, 'Expected HS256 token to be rejected');
|
|
17
|
+
console.log('JWT algorithm validation tests passed.');
|
|
18
|
+
};
|
|
19
|
+
run().catch(error => {
|
|
20
|
+
console.error('JWT algorithm validation tests failed:', error);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "joye-backend-utility",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "8.0.0",
|
|
4
4
|
"description": "Joye backend utility for db functions and common functions",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"scripts": {
|
|
14
14
|
"build": "tsc && yarn copy-files",
|
|
15
15
|
"test": "echo \"Error: no test specified\"",
|
|
16
|
+
"test:jwt-algorithm": "ts-node lib/tests/jwtAlgorithmValidation.test.ts",
|
|
16
17
|
"postinstall": "npm run build",
|
|
17
18
|
"lint": "eslint --ignore-path .gitignore --ext .ts .",
|
|
18
19
|
"lint:fix": "npm run lint -- --fix",
|