@patchstack/connect 0.3.10 → 0.3.11
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/AGENT-INSTALL.md +14 -1
- package/README.md +1 -1
- package/dist/cli.js +0 -1
- package/dist/cli.js.map +1 -1
- package/dist/protect.cjs +109 -15
- package/dist/protect.cjs.map +1 -1
- package/dist/protect.d.ts +4 -2
- package/dist/protect.js +109 -15
- package/dist/protect.js.map +1 -1
- package/package.json +1 -1
package/dist/protect.cjs
CHANGED
|
@@ -1199,7 +1199,8 @@ function createSupabaseGuard({ protection, supabaseUrl, fetchImpl = fetch }) {
|
|
|
1199
1199
|
if (!HOP_BY_HOP.has(key.toLowerCase())) outHeaders.set(key, value);
|
|
1200
1200
|
});
|
|
1201
1201
|
const buf = await upstream.arrayBuffer();
|
|
1202
|
-
|
|
1202
|
+
const forwarded = new Response(buf, { status: upstream.status, statusText: upstream.statusText, headers: outHeaders });
|
|
1203
|
+
return protection.screenResponse ? protection.screenResponse(forwarded) : forwarded;
|
|
1203
1204
|
};
|
|
1204
1205
|
}
|
|
1205
1206
|
|
|
@@ -1254,10 +1255,11 @@ async function createProtection(options = {}) {
|
|
|
1254
1255
|
onDetect({ phase, mode, category: result.rule?.category, rule: result.rule, message: result.message });
|
|
1255
1256
|
return mode === "block" ? block() : allow2();
|
|
1256
1257
|
};
|
|
1257
|
-
const
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1258
|
+
const isTextCT = (ct) => {
|
|
1259
|
+
ct = (ct || "").toLowerCase();
|
|
1260
|
+
return ct === "" || /(json|text|xml|html|javascript|csv|yaml|x-www-form-urlencoded)/.test(ct);
|
|
1261
|
+
};
|
|
1262
|
+
const screenText = (text, meta) => {
|
|
1261
1263
|
let blockRule = null;
|
|
1262
1264
|
const redactions = [];
|
|
1263
1265
|
for (const { rule, engine: re, redactors } of responseRuleSet) {
|
|
@@ -1274,15 +1276,92 @@ async function createProtection(options = {}) {
|
|
|
1274
1276
|
if (redactors && redactors.length) redactions.push({ rule, redactors });
|
|
1275
1277
|
else if (!blockRule) blockRule = rule;
|
|
1276
1278
|
}
|
|
1277
|
-
if (mode !== "block") return
|
|
1278
|
-
if (blockRule) return
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1279
|
+
if (mode !== "block" || !blockRule && !redactions.length) return { verdict: "pass" };
|
|
1280
|
+
if (blockRule) return { verdict: "block" };
|
|
1281
|
+
let body = text;
|
|
1282
|
+
for (const { rule, redactors } of redactions) body = applyRedactors(body, redactors, maskFn(rule.category));
|
|
1283
|
+
return { verdict: "redact", body };
|
|
1284
|
+
};
|
|
1285
|
+
const screenResp = async (response) => {
|
|
1286
|
+
const text = await readTextResponse(response);
|
|
1287
|
+
if (text == null) return response;
|
|
1288
|
+
const r = screenText(text, { status: response.status, headers: headerObject(response.headers) });
|
|
1289
|
+
if (r.verdict === "block") return leakResponse();
|
|
1290
|
+
if (r.verdict === "redact") return rebuildResponse(response, r.body);
|
|
1284
1291
|
return response;
|
|
1285
1292
|
};
|
|
1293
|
+
const wrapNodeResponse = (res) => {
|
|
1294
|
+
const origWrite = res.write.bind(res);
|
|
1295
|
+
const origEnd = res.end.bind(res);
|
|
1296
|
+
const chunks = [];
|
|
1297
|
+
let size = 0;
|
|
1298
|
+
let overflow = false;
|
|
1299
|
+
const MAX = 512 * 1024;
|
|
1300
|
+
const collect = (chunk, enc) => {
|
|
1301
|
+
if (chunk == null) return;
|
|
1302
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, typeof enc === "string" ? enc : "utf8");
|
|
1303
|
+
size += buf.length;
|
|
1304
|
+
if (size > MAX) {
|
|
1305
|
+
overflow = true;
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1308
|
+
chunks.push(buf);
|
|
1309
|
+
};
|
|
1310
|
+
res.write = function(chunk, enc, cb) {
|
|
1311
|
+
if (overflow) return origWrite(chunk, enc, cb);
|
|
1312
|
+
collect(chunk, enc);
|
|
1313
|
+
if (typeof enc === "function") enc();
|
|
1314
|
+
else if (typeof cb === "function") cb();
|
|
1315
|
+
return true;
|
|
1316
|
+
};
|
|
1317
|
+
res.end = function(chunk, enc, cb) {
|
|
1318
|
+
if (typeof chunk === "function") {
|
|
1319
|
+
cb = chunk;
|
|
1320
|
+
chunk = void 0;
|
|
1321
|
+
enc = void 0;
|
|
1322
|
+
} else if (typeof enc === "function") {
|
|
1323
|
+
cb = enc;
|
|
1324
|
+
enc = void 0;
|
|
1325
|
+
}
|
|
1326
|
+
if (overflow) {
|
|
1327
|
+
if (chunk != null) origWrite(chunk, enc);
|
|
1328
|
+
return origEnd(cb);
|
|
1329
|
+
}
|
|
1330
|
+
collect(chunk, enc);
|
|
1331
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
1332
|
+
let ct = res.getHeader ? res.getHeader("content-type") : void 0;
|
|
1333
|
+
if (Array.isArray(ct)) ct = ct[0];
|
|
1334
|
+
if (!isTextCT(ct)) {
|
|
1335
|
+
for (const c of chunks) origWrite(c);
|
|
1336
|
+
return origEnd(cb);
|
|
1337
|
+
}
|
|
1338
|
+
let r;
|
|
1339
|
+
try {
|
|
1340
|
+
r = screenText(text, { status: res.statusCode, headers: {} });
|
|
1341
|
+
} catch (err) {
|
|
1342
|
+
onError?.(err);
|
|
1343
|
+
for (const c of chunks) origWrite(c);
|
|
1344
|
+
return origEnd(cb);
|
|
1345
|
+
}
|
|
1346
|
+
if (r.verdict === "block") {
|
|
1347
|
+
res.statusCode = 500;
|
|
1348
|
+
try {
|
|
1349
|
+
res.setHeader("content-type", "application/json");
|
|
1350
|
+
} catch {
|
|
1351
|
+
}
|
|
1352
|
+
return origEnd(JSON.stringify({ error: "Response withheld by Patchstack (sensitive data detected)" }), cb);
|
|
1353
|
+
}
|
|
1354
|
+
if (r.verdict === "redact") {
|
|
1355
|
+
try {
|
|
1356
|
+
res.removeHeader && res.removeHeader("content-length");
|
|
1357
|
+
} catch {
|
|
1358
|
+
}
|
|
1359
|
+
return origEnd(r.body, cb);
|
|
1360
|
+
}
|
|
1361
|
+
for (const c of chunks) origWrite(c);
|
|
1362
|
+
return origEnd(cb);
|
|
1363
|
+
};
|
|
1364
|
+
};
|
|
1286
1365
|
const allow = new Set((options.allowHosts ?? []).map((h) => String(h).toLowerCase()));
|
|
1287
1366
|
const egressShouldBlock = (url, host, method) => {
|
|
1288
1367
|
if (host && allow.has(host.toLowerCase())) return false;
|
|
@@ -1300,6 +1379,9 @@ async function createProtection(options = {}) {
|
|
|
1300
1379
|
const protection = {
|
|
1301
1380
|
mode,
|
|
1302
1381
|
rules: { request: requestRules, response: responseRules, egress: egressRules },
|
|
1382
|
+
// Screen a fetch Response through the response-phase rules (redact/block). Used by
|
|
1383
|
+
// .fetch(), and by the Supabase guard on its forwarded upstream response.
|
|
1384
|
+
screenResponse: (response) => screenResp(response),
|
|
1303
1385
|
// (request) => Response | null (null = allow, caller proceeds). Request phase only.
|
|
1304
1386
|
fetchGuard() {
|
|
1305
1387
|
return async (request) => {
|
|
@@ -1320,23 +1402,34 @@ async function createProtection(options = {}) {
|
|
|
1320
1402
|
const blocked = await guard(request);
|
|
1321
1403
|
if (blocked) return blocked;
|
|
1322
1404
|
const response = await handler(request, ...rest);
|
|
1323
|
-
return
|
|
1405
|
+
return screenResp(response);
|
|
1324
1406
|
};
|
|
1325
1407
|
},
|
|
1326
1408
|
// Express middleware (request phase; expects express-parsed req.query/req.body).
|
|
1327
|
-
|
|
1409
|
+
// Pass { screenResponses: true } to also screen the outgoing response (buffers it).
|
|
1410
|
+
express(exprOptions = {}) {
|
|
1328
1411
|
return (req, res, next) => {
|
|
1329
1412
|
let result;
|
|
1330
1413
|
try {
|
|
1331
1414
|
result = engine.evaluate(req);
|
|
1332
1415
|
} catch (err) {
|
|
1333
1416
|
onError?.(err);
|
|
1417
|
+
if (exprOptions.screenResponses) wrapNodeResponse(res);
|
|
1334
1418
|
return next();
|
|
1335
1419
|
}
|
|
1336
|
-
decide(
|
|
1420
|
+
decide(
|
|
1421
|
+
"request",
|
|
1422
|
+
result,
|
|
1423
|
+
() => res.status(403).json(blockBody(result)),
|
|
1424
|
+
() => {
|
|
1425
|
+
if (exprOptions.screenResponses) wrapNodeResponse(res);
|
|
1426
|
+
next();
|
|
1427
|
+
}
|
|
1428
|
+
);
|
|
1337
1429
|
};
|
|
1338
1430
|
},
|
|
1339
1431
|
// Node / Connect middleware — buffers the body itself (request phase).
|
|
1432
|
+
// Pass { screenResponses: true } to also screen the outgoing response (buffers it).
|
|
1340
1433
|
node(nodeOptions = {}) {
|
|
1341
1434
|
const maxBytes = nodeOptions.maxBodyBytes ?? 1024 * 1024;
|
|
1342
1435
|
return (req, res, next) => {
|
|
@@ -1376,6 +1469,7 @@ async function createProtection(options = {}) {
|
|
|
1376
1469
|
},
|
|
1377
1470
|
() => {
|
|
1378
1471
|
if (req.body === void 0) req.body = shaped.body;
|
|
1472
|
+
if (nodeOptions.screenResponses) wrapNodeResponse(res);
|
|
1379
1473
|
next();
|
|
1380
1474
|
}
|
|
1381
1475
|
);
|