aip-master-node-sumit 1.0.9 → 1.0.12
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/index.js +120 -122
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -10,10 +10,9 @@ const getSafeMasterUrl = (url) => {
|
|
|
10
10
|
return clean;
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
-
// 🛡️
|
|
13
|
+
// 🛡️ Bulletproof Native Cookie Parser (Handles JWT padding '=')
|
|
14
14
|
const parseRawCookies = (cookieHeader) => {
|
|
15
15
|
if (!cookieHeader) return {};
|
|
16
|
-
|
|
17
16
|
return cookieHeader.split(';').reduce((acc, cookie) => {
|
|
18
17
|
const index = cookie.indexOf('=');
|
|
19
18
|
if (index < 0) return acc;
|
|
@@ -44,37 +43,38 @@ if (API_KEY) {
|
|
|
44
43
|
setInterval(sendHeartbeat, 5 * 60 * 1000);
|
|
45
44
|
}
|
|
46
45
|
|
|
47
|
-
|
|
46
|
+
// 🛡️ THE FIX: Added manageCors and enableWaf options for total developer control
|
|
47
|
+
const aipGuard = (options = { requireLogin: true, manageCors: false, enableWaf: true }) => {
|
|
48
48
|
return async (req, res, next) => {
|
|
49
49
|
|
|
50
50
|
// ==========================================
|
|
51
|
-
// 🛡️ LAYER 0: DYNAMIC CORS
|
|
51
|
+
// 🛡️ LAYER 0: DYNAMIC CORS (Opt-in only!)
|
|
52
52
|
// ==========================================
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
53
|
+
if (options.manageCors) {
|
|
54
|
+
const origin = req.headers.origin || req.headers.Origin;
|
|
55
|
+
|
|
56
|
+
if (origin) {
|
|
57
|
+
res.setHeader('Access-Control-Allow-Origin', origin);
|
|
58
|
+
res.setHeader('Access-Control-Allow-Credentials', 'true');
|
|
59
|
+
} else {
|
|
60
|
+
res.setHeader('Access-Control-Allow-Origin', process.env.AIP_ALLOWED_ORIGIN || '*');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
res.setHeader('X-Frame-Options', 'DENY');
|
|
64
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
65
|
+
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
|
|
66
|
+
|
|
67
|
+
if (req.method === 'OPTIONS') {
|
|
68
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS');
|
|
69
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-AIP-Token, X-Requested-With, Accept');
|
|
70
|
+
return res.status(200).end();
|
|
71
|
+
}
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
// ==========================================
|
|
74
75
|
// 🛡️ LAYER 1: INFRASTRUCTURE SHIELD
|
|
75
76
|
// ==========================================
|
|
76
77
|
const MAX_PAYLOAD_BYTES = 2 * 1024 * 1024;
|
|
77
|
-
|
|
78
78
|
if (req.headers['transfer-encoding'] && req.headers['transfer-encoding'].includes('chunked')) {
|
|
79
79
|
return res.status(411).json({ error: "AIP Infrastructure Shield: Chunked encoding rejected." });
|
|
80
80
|
}
|
|
@@ -82,92 +82,47 @@ const aipGuard = (options = { requireLogin: true }) => {
|
|
|
82
82
|
return res.status(413).json({ error: "AIP Infrastructure Shield: Payload Too Large." });
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if (
|
|
85
|
+
// ==========================================
|
|
86
|
+
// 🛡️ TOKEN EXTRACTION
|
|
87
|
+
// ==========================================
|
|
88
|
+
let token = null;
|
|
89
|
+
if (req.headers.authorization && req.headers.authorization.startsWith('Bearer ')) {
|
|
90
90
|
token = req.headers.authorization.split(' ')[1];
|
|
91
|
-
} else if (
|
|
91
|
+
} else if (req.headers['x-aip-token']) {
|
|
92
92
|
token = req.headers['x-aip-token'];
|
|
93
|
+
} else {
|
|
94
|
+
const cookies = req.cookies || parseRawCookies(req.headers.cookie);
|
|
95
|
+
token = cookies.aip_session;
|
|
93
96
|
}
|
|
94
97
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
// ==========================================
|
|
98
|
-
if (options.requireLogin) {
|
|
99
|
-
if (!token) {
|
|
100
|
-
return res.status(401).json({ error: "Unauthorized: Missing AIP Session Token" });
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
try {
|
|
104
|
-
const currentAction = req.method + " " + (req.baseUrl + req.path);
|
|
105
|
-
|
|
106
|
-
const authRes = await axios.post(`${AIP_MASTER_API}/iam/verify-session`,
|
|
107
|
-
{ session_token: token, action: currentAction },
|
|
108
|
-
{ headers: { 'Authorization': `Bearer ${API_KEY}` } }
|
|
109
|
-
);
|
|
110
|
-
|
|
111
|
-
req.user = authRes.data.user;
|
|
112
|
-
|
|
113
|
-
// 🛡️ THE FIX: Native Node.js Fallback for Set-Cookie (Framework Agnostic)
|
|
114
|
-
const isProd = process.env.NODE_ENV === 'production';
|
|
115
|
-
|
|
116
|
-
if (typeof res.cookie === 'function') {
|
|
117
|
-
// Express.js environment
|
|
118
|
-
res.cookie('aip_session', token, {
|
|
119
|
-
httpOnly: true,
|
|
120
|
-
secure: isProd,
|
|
121
|
-
sameSite: isProd ? 'none' : 'lax',
|
|
122
|
-
path: '/',
|
|
123
|
-
maxAge: 12 * 60 * 60 * 1000
|
|
124
|
-
});
|
|
125
|
-
} else {
|
|
126
|
-
// Raw Node.js or alternate framework environment
|
|
127
|
-
const cookieStr = `aip_session=${token}; HttpOnly; ${isProd ? 'Secure; SameSite=None' : 'SameSite=Lax'}; Path=/; Max-Age=43200`;
|
|
128
|
-
res.setHeader('Set-Cookie', cookieStr);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
} catch (error) {
|
|
132
|
-
const status = error.response?.status || 500;
|
|
133
|
-
const errMsg = error.response?.data?.error || "AIP Identity Blocked: Invalid Token.";
|
|
134
|
-
const isProd = process.env.NODE_ENV === 'production';
|
|
135
|
-
|
|
136
|
-
if (status === 401) {
|
|
137
|
-
if (typeof res.clearCookie === 'function') {
|
|
138
|
-
res.clearCookie('aip_session', {
|
|
139
|
-
path: '/',
|
|
140
|
-
sameSite: isProd ? 'none' : 'lax',
|
|
141
|
-
secure: isProd
|
|
142
|
-
});
|
|
143
|
-
} else {
|
|
144
|
-
res.setHeader('Set-Cookie', `aip_session=; HttpOnly; ${isProd ? 'Secure; SameSite=None' : 'SameSite=Lax'}; Path=/; Max-Age=0`);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
return res.status(status === 401 ? 401 : 403).json({ error: errMsg });
|
|
149
|
-
}
|
|
98
|
+
if (options.requireLogin && !token) {
|
|
99
|
+
return res.status(401).json({ error: "Unauthorized: Missing AIP Session Token" });
|
|
150
100
|
}
|
|
151
101
|
|
|
152
102
|
// ==========================================
|
|
153
|
-
// 🛡️ LAYER 3
|
|
103
|
+
// 🛡️ THE FIX: PARALLEL LAYER 2 (IAM) & LAYER 3 (WAF)
|
|
104
|
+
// Cuts latency entirely in half!
|
|
154
105
|
// ==========================================
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
106
|
+
let authPromise = null;
|
|
107
|
+
let wafPromise = null;
|
|
108
|
+
|
|
109
|
+
if (options.requireLogin && token) {
|
|
110
|
+
const currentAction = req.method + " " + (req.baseUrl + req.path);
|
|
111
|
+
authPromise = axios.post(`${AIP_MASTER_API}/iam/verify-session`,
|
|
112
|
+
{ session_token: token, action: currentAction },
|
|
113
|
+
{ headers: { 'Authorization': `Bearer ${API_KEY}` } }
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (options.enableWaf !== false) {
|
|
118
|
+
const payloadData = JSON.stringify({ body: req.body || {}, query: req.query || {} });
|
|
161
119
|
const forwardedFor = req.headers['x-forwarded-for'];
|
|
162
120
|
let clientIp = req.ip || (req.connection && req.connection.remoteAddress) || "0.0.0.0";
|
|
163
|
-
|
|
164
121
|
if (forwardedFor) {
|
|
165
|
-
clientIp = Array.isArray(forwardedFor)
|
|
166
|
-
? forwardedFor[0].trim()
|
|
167
|
-
: forwardedFor.split(',')[0].trim();
|
|
122
|
+
clientIp = Array.isArray(forwardedFor) ? forwardedFor[0].trim() : forwardedFor.split(',')[0].trim();
|
|
168
123
|
}
|
|
169
124
|
|
|
170
|
-
|
|
125
|
+
wafPromise = axios.post(`${AIP_MASTER_API}/verify`, {
|
|
171
126
|
api_key: API_KEY,
|
|
172
127
|
ip: clientIp,
|
|
173
128
|
method: req.method,
|
|
@@ -177,13 +132,58 @@ const aipGuard = (options = { requireLogin: true }) => {
|
|
|
177
132
|
user_agent: req.headers['user-agent'] || "",
|
|
178
133
|
session_token: token || ""
|
|
179
134
|
});
|
|
135
|
+
}
|
|
180
136
|
|
|
181
|
-
|
|
182
|
-
|
|
137
|
+
try {
|
|
138
|
+
// Execute both promises at the exact same time
|
|
139
|
+
const [authOutcome, wafOutcome] = await Promise.allSettled([
|
|
140
|
+
authPromise || Promise.resolve(null),
|
|
141
|
+
wafPromise || Promise.resolve(null)
|
|
142
|
+
]);
|
|
143
|
+
|
|
144
|
+
// Handle Identity Rejections (401s)
|
|
145
|
+
if (options.requireLogin && authOutcome.status === 'rejected') {
|
|
146
|
+
const status = authOutcome.reason.response?.status || 500;
|
|
147
|
+
const errMsg = authOutcome.reason.response?.data?.error || "AIP Identity Blocked: Invalid Token.";
|
|
148
|
+
const isProd = process.env.NODE_ENV === 'production';
|
|
149
|
+
|
|
150
|
+
if (status === 401) {
|
|
151
|
+
if (typeof res.clearCookie === 'function') {
|
|
152
|
+
res.clearCookie('aip_session', { path: '/', sameSite: isProd ? 'none' : 'lax', secure: isProd });
|
|
153
|
+
} else {
|
|
154
|
+
res.setHeader('Set-Cookie', `aip_session=; HttpOnly; ${isProd ? 'Secure; SameSite=None' : 'SameSite=Lax'}; Path=/; Max-Age=0`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return res.status(status === 401 ? 401 : 403).json({ error: errMsg });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Bind User Data to Request if successful
|
|
161
|
+
if (authOutcome.status === 'fulfilled' && authOutcome.value) {
|
|
162
|
+
req.user = authOutcome.value.data.user;
|
|
163
|
+
|
|
164
|
+
// Refresh Cookie
|
|
165
|
+
const isProd = process.env.NODE_ENV === 'production';
|
|
166
|
+
if (typeof res.cookie === 'function') {
|
|
167
|
+
res.cookie('aip_session', token, {
|
|
168
|
+
httpOnly: true, secure: isProd, sameSite: isProd ? 'none' : 'lax', path: '/', maxAge: 12 * 60 * 60 * 1000
|
|
169
|
+
});
|
|
170
|
+
} else {
|
|
171
|
+
res.setHeader('Set-Cookie', `aip_session=${token}; HttpOnly; ${isProd ? 'Secure; SameSite=None' : 'SameSite=Lax'}; Path=/; Max-Age=43200`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Handle WAF Blocks (400, 403, 500s)
|
|
176
|
+
if (options.enableWaf !== false) {
|
|
177
|
+
if (wafOutcome.status === 'rejected') {
|
|
178
|
+
return res.status(500).json({ error: "Security verification failed to connect to AIP Master." });
|
|
179
|
+
}
|
|
180
|
+
if (wafOutcome.status === 'fulfilled' && wafOutcome.value && wafOutcome.value.data.action === "block") {
|
|
181
|
+
return res.status(403).json({ error: `AIP Firewall Blocked Request: ${wafOutcome.value.data.reason}` });
|
|
182
|
+
}
|
|
183
183
|
}
|
|
184
184
|
|
|
185
185
|
} catch (error) {
|
|
186
|
-
return res.status(500).json({ error: "
|
|
186
|
+
return res.status(500).json({ error: "Critical internal security failure." });
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
next();
|
|
@@ -205,37 +205,37 @@ class AipAdminClient {
|
|
|
205
205
|
}
|
|
206
206
|
|
|
207
207
|
async getUsers() {
|
|
208
|
-
const res = await this.client.get('/sdk/admin/users');
|
|
208
|
+
const res = await this.client.get('/sdk/admin/users');
|
|
209
209
|
return res.data;
|
|
210
210
|
}
|
|
211
211
|
|
|
212
212
|
async createUser(data) {
|
|
213
|
-
const res = await this.client.post('/sdk/admin/users', data);
|
|
213
|
+
const res = await this.client.post('/sdk/admin/users', data);
|
|
214
214
|
return res.data;
|
|
215
215
|
}
|
|
216
216
|
|
|
217
217
|
async updateUser(userId, data) {
|
|
218
|
-
const res = await this.client.put(`/sdk/admin/users/${userId}`, data);
|
|
218
|
+
const res = await this.client.put(`/sdk/admin/users/${userId}`, data);
|
|
219
219
|
return res.data;
|
|
220
220
|
}
|
|
221
221
|
|
|
222
222
|
async updateRBAC(userId, permissions) {
|
|
223
|
-
const res = await this.client.put(`/sdk/admin/users/${userId}/rbac`, { permissions });
|
|
223
|
+
const res = await this.client.put(`/sdk/admin/users/${userId}/rbac`, { permissions });
|
|
224
224
|
return res.data;
|
|
225
225
|
}
|
|
226
226
|
|
|
227
227
|
async deleteUser(userId) {
|
|
228
|
-
const res = await this.client.delete(`/sdk/admin/users/${userId}`);
|
|
228
|
+
const res = await this.client.delete(`/sdk/admin/users/${userId}`);
|
|
229
229
|
return res.data;
|
|
230
230
|
}
|
|
231
231
|
|
|
232
232
|
async getRoles() {
|
|
233
|
-
const res = await this.client.get('/sdk/admin/roles');
|
|
233
|
+
const res = await this.client.get('/sdk/admin/roles');
|
|
234
234
|
return res.data;
|
|
235
235
|
}
|
|
236
236
|
|
|
237
237
|
async createRole(data) {
|
|
238
|
-
const res = await this.client.post('/sdk/admin/roles', data);
|
|
238
|
+
const res = await this.client.post('/sdk/admin/roles', data);
|
|
239
239
|
return res.data;
|
|
240
240
|
}
|
|
241
241
|
}
|
|
@@ -256,9 +256,7 @@ class AipIAMClient {
|
|
|
256
256
|
const forwardedFor = req.headers['x-forwarded-for'];
|
|
257
257
|
|
|
258
258
|
if (forwardedFor) {
|
|
259
|
-
clientIp = Array.isArray(forwardedFor)
|
|
260
|
-
? forwardedFor[0].trim()
|
|
261
|
-
: forwardedFor.split(',')[0].trim();
|
|
259
|
+
clientIp = Array.isArray(forwardedFor) ? forwardedFor[0].trim() : forwardedFor.split(',')[0].trim();
|
|
262
260
|
}
|
|
263
261
|
|
|
264
262
|
const authRes = await this.client.post('/iam/auth', {
|
|
@@ -266,17 +264,12 @@ class AipIAMClient {
|
|
|
266
264
|
ip: clientIp,
|
|
267
265
|
...credentials
|
|
268
266
|
});
|
|
269
|
-
|
|
267
|
+
|
|
270
268
|
if (authRes.data.session_token) {
|
|
271
269
|
const isProd = process.env.NODE_ENV === 'production';
|
|
272
|
-
|
|
273
270
|
if (typeof res.cookie === 'function') {
|
|
274
271
|
res.cookie('aip_session', authRes.data.session_token, {
|
|
275
|
-
httpOnly: true,
|
|
276
|
-
secure: isProd,
|
|
277
|
-
sameSite: isProd ? 'none' : 'lax',
|
|
278
|
-
path: '/',
|
|
279
|
-
maxAge: 12 * 60 * 60 * 1000
|
|
272
|
+
httpOnly: true, secure: isProd, sameSite: isProd ? 'none' : 'lax', path: '/', maxAge: 12 * 60 * 60 * 1000
|
|
280
273
|
});
|
|
281
274
|
} else {
|
|
282
275
|
res.setHeader('Set-Cookie', `aip_session=${authRes.data.session_token}; HttpOnly; ${isProd ? 'Secure; SameSite=None' : 'SameSite=Lax'}; Path=/; Max-Age=43200`);
|
|
@@ -285,15 +278,20 @@ class AipIAMClient {
|
|
|
285
278
|
return authRes.data;
|
|
286
279
|
}
|
|
287
280
|
|
|
281
|
+
// 🛡️ THE FIX: Solves the 404 error! Developers can now cleanly verify a token manually.
|
|
282
|
+
async verifyToken(token) {
|
|
283
|
+
if (!token) throw new Error("Token is required");
|
|
284
|
+
const res = await this.client.post('/iam/verify-session',
|
|
285
|
+
{ session_token: token },
|
|
286
|
+
{ headers: { 'Authorization': `Bearer ${this.apiKey}` } }
|
|
287
|
+
);
|
|
288
|
+
return res.data;
|
|
289
|
+
}
|
|
290
|
+
|
|
288
291
|
logoutUser(res) {
|
|
289
292
|
const isProd = process.env.NODE_ENV === 'production';
|
|
290
|
-
|
|
291
293
|
if (typeof res.clearCookie === 'function') {
|
|
292
|
-
res.clearCookie('aip_session', {
|
|
293
|
-
path: '/',
|
|
294
|
-
sameSite: isProd ? 'none' : 'lax',
|
|
295
|
-
secure: isProd
|
|
296
|
-
});
|
|
294
|
+
res.clearCookie('aip_session', { path: '/', sameSite: isProd ? 'none' : 'lax', secure: isProd });
|
|
297
295
|
} else {
|
|
298
296
|
res.setHeader('Set-Cookie', `aip_session=; HttpOnly; ${isProd ? 'Secure; SameSite=None' : 'SameSite=Lax'}; Path=/; Max-Age=0`);
|
|
299
297
|
}
|