aip-master-node-sumit 1.0.11 → 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 +112 -121
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -13,7 +13,6 @@ const getSafeMasterUrl = (url) => {
|
|
|
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
|
}
|
|
@@ -83,98 +83,46 @@ const aipGuard = (options = { requireLogin: true }) => {
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
// ==========================================
|
|
86
|
-
// 🛡️
|
|
86
|
+
// 🛡️ TOKEN EXTRACTION
|
|
87
87
|
// ==========================================
|
|
88
88
|
let token = null;
|
|
89
|
-
|
|
90
|
-
// 1. Check Explicit Headers FIRST
|
|
91
89
|
if (req.headers.authorization && req.headers.authorization.startsWith('Bearer ')) {
|
|
92
90
|
token = req.headers.authorization.split(' ')[1];
|
|
93
91
|
} else if (req.headers['x-aip-token']) {
|
|
94
92
|
token = req.headers['x-aip-token'];
|
|
95
|
-
}
|
|
96
|
-
// 2. Fallback to browser cookies ONLY if no explicit headers exist
|
|
97
|
-
else {
|
|
93
|
+
} else {
|
|
98
94
|
const cookies = req.cookies || parseRawCookies(req.headers.cookie);
|
|
99
95
|
token = cookies.aip_session;
|
|
100
96
|
}
|
|
101
97
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
// ==========================================
|
|
105
|
-
if (options.requireLogin) {
|
|
106
|
-
if (!token) {
|
|
107
|
-
return res.status(401).json({ error: "Unauthorized: Missing AIP Session Token" });
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
try {
|
|
111
|
-
const currentAction = req.method + " " + (req.baseUrl + req.path);
|
|
112
|
-
|
|
113
|
-
const authRes = await axios.post(`${AIP_MASTER_API}/iam/verify-session`,
|
|
114
|
-
{ session_token: token, action: currentAction },
|
|
115
|
-
{ headers: { 'Authorization': `Bearer ${API_KEY}` } }
|
|
116
|
-
);
|
|
117
|
-
|
|
118
|
-
req.user = authRes.data.user;
|
|
119
|
-
|
|
120
|
-
// 🛡️ Native Node.js Fallback for Set-Cookie (Framework Agnostic)
|
|
121
|
-
const isProd = process.env.NODE_ENV === 'production';
|
|
122
|
-
|
|
123
|
-
if (typeof res.cookie === 'function') {
|
|
124
|
-
// Express.js environment
|
|
125
|
-
res.cookie('aip_session', token, {
|
|
126
|
-
httpOnly: true,
|
|
127
|
-
secure: isProd,
|
|
128
|
-
sameSite: isProd ? 'none' : 'lax',
|
|
129
|
-
path: '/',
|
|
130
|
-
maxAge: 12 * 60 * 60 * 1000
|
|
131
|
-
});
|
|
132
|
-
} else {
|
|
133
|
-
// Raw Node.js or alternate framework environment
|
|
134
|
-
const cookieStr = `aip_session=${token}; HttpOnly; ${isProd ? 'Secure; SameSite=None' : 'SameSite=Lax'}; Path=/; Max-Age=43200`;
|
|
135
|
-
res.setHeader('Set-Cookie', cookieStr);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
} catch (error) {
|
|
139
|
-
const status = error.response?.status || 500;
|
|
140
|
-
const errMsg = error.response?.data?.error || "AIP Identity Blocked: Invalid Token.";
|
|
141
|
-
const isProd = process.env.NODE_ENV === 'production';
|
|
142
|
-
|
|
143
|
-
if (status === 401) {
|
|
144
|
-
if (typeof res.clearCookie === 'function') {
|
|
145
|
-
res.clearCookie('aip_session', {
|
|
146
|
-
path: '/',
|
|
147
|
-
sameSite: isProd ? 'none' : 'lax',
|
|
148
|
-
secure: isProd
|
|
149
|
-
});
|
|
150
|
-
} else {
|
|
151
|
-
res.setHeader('Set-Cookie', `aip_session=; HttpOnly; ${isProd ? 'Secure; SameSite=None' : 'SameSite=Lax'}; Path=/; Max-Age=0`);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
return res.status(status === 401 ? 401 : 403).json({ error: errMsg });
|
|
156
|
-
}
|
|
98
|
+
if (options.requireLogin && !token) {
|
|
99
|
+
return res.status(401).json({ error: "Unauthorized: Missing AIP Session Token" });
|
|
157
100
|
}
|
|
158
101
|
|
|
159
102
|
// ==========================================
|
|
160
|
-
// 🛡️ LAYER 3
|
|
103
|
+
// 🛡️ THE FIX: PARALLEL LAYER 2 (IAM) & LAYER 3 (WAF)
|
|
104
|
+
// Cuts latency entirely in half!
|
|
161
105
|
// ==========================================
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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 || {} });
|
|
168
119
|
const forwardedFor = req.headers['x-forwarded-for'];
|
|
169
120
|
let clientIp = req.ip || (req.connection && req.connection.remoteAddress) || "0.0.0.0";
|
|
170
|
-
|
|
171
121
|
if (forwardedFor) {
|
|
172
|
-
clientIp = Array.isArray(forwardedFor)
|
|
173
|
-
? forwardedFor[0].trim()
|
|
174
|
-
: forwardedFor.split(',')[0].trim();
|
|
122
|
+
clientIp = Array.isArray(forwardedFor) ? forwardedFor[0].trim() : forwardedFor.split(',')[0].trim();
|
|
175
123
|
}
|
|
176
124
|
|
|
177
|
-
|
|
125
|
+
wafPromise = axios.post(`${AIP_MASTER_API}/verify`, {
|
|
178
126
|
api_key: API_KEY,
|
|
179
127
|
ip: clientIp,
|
|
180
128
|
method: req.method,
|
|
@@ -184,13 +132,58 @@ const aipGuard = (options = { requireLogin: true }) => {
|
|
|
184
132
|
user_agent: req.headers['user-agent'] || "",
|
|
185
133
|
session_token: token || ""
|
|
186
134
|
});
|
|
135
|
+
}
|
|
136
|
+
|
|
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
|
+
}
|
|
187
159
|
|
|
188
|
-
|
|
189
|
-
|
|
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
|
+
}
|
|
190
183
|
}
|
|
191
184
|
|
|
192
185
|
} catch (error) {
|
|
193
|
-
return res.status(500).json({ error: "
|
|
186
|
+
return res.status(500).json({ error: "Critical internal security failure." });
|
|
194
187
|
}
|
|
195
188
|
|
|
196
189
|
next();
|
|
@@ -212,37 +205,37 @@ class AipAdminClient {
|
|
|
212
205
|
}
|
|
213
206
|
|
|
214
207
|
async getUsers() {
|
|
215
|
-
const res = await this.client.get('/sdk/admin/users');
|
|
208
|
+
const res = await this.client.get('/sdk/admin/users');
|
|
216
209
|
return res.data;
|
|
217
210
|
}
|
|
218
211
|
|
|
219
212
|
async createUser(data) {
|
|
220
|
-
const res = await this.client.post('/sdk/admin/users', data);
|
|
213
|
+
const res = await this.client.post('/sdk/admin/users', data);
|
|
221
214
|
return res.data;
|
|
222
215
|
}
|
|
223
216
|
|
|
224
217
|
async updateUser(userId, data) {
|
|
225
|
-
const res = await this.client.put(`/sdk/admin/users/${userId}`, data);
|
|
218
|
+
const res = await this.client.put(`/sdk/admin/users/${userId}`, data);
|
|
226
219
|
return res.data;
|
|
227
220
|
}
|
|
228
221
|
|
|
229
222
|
async updateRBAC(userId, permissions) {
|
|
230
|
-
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 });
|
|
231
224
|
return res.data;
|
|
232
225
|
}
|
|
233
226
|
|
|
234
227
|
async deleteUser(userId) {
|
|
235
|
-
const res = await this.client.delete(`/sdk/admin/users/${userId}`);
|
|
228
|
+
const res = await this.client.delete(`/sdk/admin/users/${userId}`);
|
|
236
229
|
return res.data;
|
|
237
230
|
}
|
|
238
231
|
|
|
239
232
|
async getRoles() {
|
|
240
|
-
const res = await this.client.get('/sdk/admin/roles');
|
|
233
|
+
const res = await this.client.get('/sdk/admin/roles');
|
|
241
234
|
return res.data;
|
|
242
235
|
}
|
|
243
236
|
|
|
244
237
|
async createRole(data) {
|
|
245
|
-
const res = await this.client.post('/sdk/admin/roles', data);
|
|
238
|
+
const res = await this.client.post('/sdk/admin/roles', data);
|
|
246
239
|
return res.data;
|
|
247
240
|
}
|
|
248
241
|
}
|
|
@@ -263,9 +256,7 @@ class AipIAMClient {
|
|
|
263
256
|
const forwardedFor = req.headers['x-forwarded-for'];
|
|
264
257
|
|
|
265
258
|
if (forwardedFor) {
|
|
266
|
-
clientIp = Array.isArray(forwardedFor)
|
|
267
|
-
? forwardedFor[0].trim()
|
|
268
|
-
: forwardedFor.split(',')[0].trim();
|
|
259
|
+
clientIp = Array.isArray(forwardedFor) ? forwardedFor[0].trim() : forwardedFor.split(',')[0].trim();
|
|
269
260
|
}
|
|
270
261
|
|
|
271
262
|
const authRes = await this.client.post('/iam/auth', {
|
|
@@ -273,17 +264,12 @@ class AipIAMClient {
|
|
|
273
264
|
ip: clientIp,
|
|
274
265
|
...credentials
|
|
275
266
|
});
|
|
276
|
-
|
|
267
|
+
|
|
277
268
|
if (authRes.data.session_token) {
|
|
278
269
|
const isProd = process.env.NODE_ENV === 'production';
|
|
279
|
-
|
|
280
270
|
if (typeof res.cookie === 'function') {
|
|
281
271
|
res.cookie('aip_session', authRes.data.session_token, {
|
|
282
|
-
httpOnly: true,
|
|
283
|
-
secure: isProd,
|
|
284
|
-
sameSite: isProd ? 'none' : 'lax',
|
|
285
|
-
path: '/',
|
|
286
|
-
maxAge: 12 * 60 * 60 * 1000
|
|
272
|
+
httpOnly: true, secure: isProd, sameSite: isProd ? 'none' : 'lax', path: '/', maxAge: 12 * 60 * 60 * 1000
|
|
287
273
|
});
|
|
288
274
|
} else {
|
|
289
275
|
res.setHeader('Set-Cookie', `aip_session=${authRes.data.session_token}; HttpOnly; ${isProd ? 'Secure; SameSite=None' : 'SameSite=Lax'}; Path=/; Max-Age=43200`);
|
|
@@ -292,15 +278,20 @@ class AipIAMClient {
|
|
|
292
278
|
return authRes.data;
|
|
293
279
|
}
|
|
294
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
|
+
|
|
295
291
|
logoutUser(res) {
|
|
296
292
|
const isProd = process.env.NODE_ENV === 'production';
|
|
297
|
-
|
|
298
293
|
if (typeof res.clearCookie === 'function') {
|
|
299
|
-
res.clearCookie('aip_session', {
|
|
300
|
-
path: '/',
|
|
301
|
-
sameSite: isProd ? 'none' : 'lax',
|
|
302
|
-
secure: isProd
|
|
303
|
-
});
|
|
294
|
+
res.clearCookie('aip_session', { path: '/', sameSite: isProd ? 'none' : 'lax', secure: isProd });
|
|
304
295
|
} else {
|
|
305
296
|
res.setHeader('Set-Cookie', `aip_session=; HttpOnly; ${isProd ? 'Secure; SameSite=None' : 'SameSite=Lax'}; Path=/; Max-Age=0`);
|
|
306
297
|
}
|