@patchstack/connect 0.3.10 → 0.3.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/AGENT-INSTALL.md +14 -1
- package/README.md +2 -2
- package/dist/cli.js +0 -1
- package/dist/cli.js.map +1 -1
- package/dist/protect/templates/guard.ts +14 -3
- package/dist/protect/templates/rules.json +34 -7
- 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.js
CHANGED
|
@@ -1170,7 +1170,8 @@ function createSupabaseGuard({ protection, supabaseUrl, fetchImpl = fetch }) {
|
|
|
1170
1170
|
if (!HOP_BY_HOP.has(key.toLowerCase())) outHeaders.set(key, value);
|
|
1171
1171
|
});
|
|
1172
1172
|
const buf = await upstream.arrayBuffer();
|
|
1173
|
-
|
|
1173
|
+
const forwarded = new Response(buf, { status: upstream.status, statusText: upstream.statusText, headers: outHeaders });
|
|
1174
|
+
return protection.screenResponse ? protection.screenResponse(forwarded) : forwarded;
|
|
1174
1175
|
};
|
|
1175
1176
|
}
|
|
1176
1177
|
|
|
@@ -1225,10 +1226,11 @@ async function createProtection(options = {}) {
|
|
|
1225
1226
|
onDetect({ phase, mode, category: result.rule?.category, rule: result.rule, message: result.message });
|
|
1226
1227
|
return mode === "block" ? block() : allow2();
|
|
1227
1228
|
};
|
|
1228
|
-
const
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1229
|
+
const isTextCT = (ct) => {
|
|
1230
|
+
ct = (ct || "").toLowerCase();
|
|
1231
|
+
return ct === "" || /(json|text|xml|html|javascript|csv|yaml|x-www-form-urlencoded)/.test(ct);
|
|
1232
|
+
};
|
|
1233
|
+
const screenText = (text, meta) => {
|
|
1232
1234
|
let blockRule = null;
|
|
1233
1235
|
const redactions = [];
|
|
1234
1236
|
for (const { rule, engine: re, redactors } of responseRuleSet) {
|
|
@@ -1245,15 +1247,92 @@ async function createProtection(options = {}) {
|
|
|
1245
1247
|
if (redactors && redactors.length) redactions.push({ rule, redactors });
|
|
1246
1248
|
else if (!blockRule) blockRule = rule;
|
|
1247
1249
|
}
|
|
1248
|
-
if (mode !== "block") return
|
|
1249
|
-
if (blockRule) return
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1250
|
+
if (mode !== "block" || !blockRule && !redactions.length) return { verdict: "pass" };
|
|
1251
|
+
if (blockRule) return { verdict: "block" };
|
|
1252
|
+
let body = text;
|
|
1253
|
+
for (const { rule, redactors } of redactions) body = applyRedactors(body, redactors, maskFn(rule.category));
|
|
1254
|
+
return { verdict: "redact", body };
|
|
1255
|
+
};
|
|
1256
|
+
const screenResp = async (response) => {
|
|
1257
|
+
const text = await readTextResponse(response);
|
|
1258
|
+
if (text == null) return response;
|
|
1259
|
+
const r = screenText(text, { status: response.status, headers: headerObject(response.headers) });
|
|
1260
|
+
if (r.verdict === "block") return leakResponse();
|
|
1261
|
+
if (r.verdict === "redact") return rebuildResponse(response, r.body);
|
|
1255
1262
|
return response;
|
|
1256
1263
|
};
|
|
1264
|
+
const wrapNodeResponse = (res) => {
|
|
1265
|
+
const origWrite = res.write.bind(res);
|
|
1266
|
+
const origEnd = res.end.bind(res);
|
|
1267
|
+
const chunks = [];
|
|
1268
|
+
let size = 0;
|
|
1269
|
+
let overflow = false;
|
|
1270
|
+
const MAX = 512 * 1024;
|
|
1271
|
+
const collect = (chunk, enc) => {
|
|
1272
|
+
if (chunk == null) return;
|
|
1273
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, typeof enc === "string" ? enc : "utf8");
|
|
1274
|
+
size += buf.length;
|
|
1275
|
+
if (size > MAX) {
|
|
1276
|
+
overflow = true;
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
chunks.push(buf);
|
|
1280
|
+
};
|
|
1281
|
+
res.write = function(chunk, enc, cb) {
|
|
1282
|
+
if (overflow) return origWrite(chunk, enc, cb);
|
|
1283
|
+
collect(chunk, enc);
|
|
1284
|
+
if (typeof enc === "function") enc();
|
|
1285
|
+
else if (typeof cb === "function") cb();
|
|
1286
|
+
return true;
|
|
1287
|
+
};
|
|
1288
|
+
res.end = function(chunk, enc, cb) {
|
|
1289
|
+
if (typeof chunk === "function") {
|
|
1290
|
+
cb = chunk;
|
|
1291
|
+
chunk = void 0;
|
|
1292
|
+
enc = void 0;
|
|
1293
|
+
} else if (typeof enc === "function") {
|
|
1294
|
+
cb = enc;
|
|
1295
|
+
enc = void 0;
|
|
1296
|
+
}
|
|
1297
|
+
if (overflow) {
|
|
1298
|
+
if (chunk != null) origWrite(chunk, enc);
|
|
1299
|
+
return origEnd(cb);
|
|
1300
|
+
}
|
|
1301
|
+
collect(chunk, enc);
|
|
1302
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
1303
|
+
let ct = res.getHeader ? res.getHeader("content-type") : void 0;
|
|
1304
|
+
if (Array.isArray(ct)) ct = ct[0];
|
|
1305
|
+
if (!isTextCT(ct)) {
|
|
1306
|
+
for (const c of chunks) origWrite(c);
|
|
1307
|
+
return origEnd(cb);
|
|
1308
|
+
}
|
|
1309
|
+
let r;
|
|
1310
|
+
try {
|
|
1311
|
+
r = screenText(text, { status: res.statusCode, headers: {} });
|
|
1312
|
+
} catch (err) {
|
|
1313
|
+
onError?.(err);
|
|
1314
|
+
for (const c of chunks) origWrite(c);
|
|
1315
|
+
return origEnd(cb);
|
|
1316
|
+
}
|
|
1317
|
+
if (r.verdict === "block") {
|
|
1318
|
+
res.statusCode = 500;
|
|
1319
|
+
try {
|
|
1320
|
+
res.setHeader("content-type", "application/json");
|
|
1321
|
+
} catch {
|
|
1322
|
+
}
|
|
1323
|
+
return origEnd(JSON.stringify({ error: "Response withheld by Patchstack (sensitive data detected)" }), cb);
|
|
1324
|
+
}
|
|
1325
|
+
if (r.verdict === "redact") {
|
|
1326
|
+
try {
|
|
1327
|
+
res.removeHeader && res.removeHeader("content-length");
|
|
1328
|
+
} catch {
|
|
1329
|
+
}
|
|
1330
|
+
return origEnd(r.body, cb);
|
|
1331
|
+
}
|
|
1332
|
+
for (const c of chunks) origWrite(c);
|
|
1333
|
+
return origEnd(cb);
|
|
1334
|
+
};
|
|
1335
|
+
};
|
|
1257
1336
|
const allow = new Set((options.allowHosts ?? []).map((h) => String(h).toLowerCase()));
|
|
1258
1337
|
const egressShouldBlock = (url, host, method) => {
|
|
1259
1338
|
if (host && allow.has(host.toLowerCase())) return false;
|
|
@@ -1271,6 +1350,9 @@ async function createProtection(options = {}) {
|
|
|
1271
1350
|
const protection = {
|
|
1272
1351
|
mode,
|
|
1273
1352
|
rules: { request: requestRules, response: responseRules, egress: egressRules },
|
|
1353
|
+
// Screen a fetch Response through the response-phase rules (redact/block). Used by
|
|
1354
|
+
// .fetch(), and by the Supabase guard on its forwarded upstream response.
|
|
1355
|
+
screenResponse: (response) => screenResp(response),
|
|
1274
1356
|
// (request) => Response | null (null = allow, caller proceeds). Request phase only.
|
|
1275
1357
|
fetchGuard() {
|
|
1276
1358
|
return async (request) => {
|
|
@@ -1291,23 +1373,34 @@ async function createProtection(options = {}) {
|
|
|
1291
1373
|
const blocked = await guard(request);
|
|
1292
1374
|
if (blocked) return blocked;
|
|
1293
1375
|
const response = await handler(request, ...rest);
|
|
1294
|
-
return
|
|
1376
|
+
return screenResp(response);
|
|
1295
1377
|
};
|
|
1296
1378
|
},
|
|
1297
1379
|
// Express middleware (request phase; expects express-parsed req.query/req.body).
|
|
1298
|
-
|
|
1380
|
+
// Pass { screenResponses: true } to also screen the outgoing response (buffers it).
|
|
1381
|
+
express(exprOptions = {}) {
|
|
1299
1382
|
return (req, res, next) => {
|
|
1300
1383
|
let result;
|
|
1301
1384
|
try {
|
|
1302
1385
|
result = engine.evaluate(req);
|
|
1303
1386
|
} catch (err) {
|
|
1304
1387
|
onError?.(err);
|
|
1388
|
+
if (exprOptions.screenResponses) wrapNodeResponse(res);
|
|
1305
1389
|
return next();
|
|
1306
1390
|
}
|
|
1307
|
-
decide(
|
|
1391
|
+
decide(
|
|
1392
|
+
"request",
|
|
1393
|
+
result,
|
|
1394
|
+
() => res.status(403).json(blockBody(result)),
|
|
1395
|
+
() => {
|
|
1396
|
+
if (exprOptions.screenResponses) wrapNodeResponse(res);
|
|
1397
|
+
next();
|
|
1398
|
+
}
|
|
1399
|
+
);
|
|
1308
1400
|
};
|
|
1309
1401
|
},
|
|
1310
1402
|
// Node / Connect middleware — buffers the body itself (request phase).
|
|
1403
|
+
// Pass { screenResponses: true } to also screen the outgoing response (buffers it).
|
|
1311
1404
|
node(nodeOptions = {}) {
|
|
1312
1405
|
const maxBytes = nodeOptions.maxBodyBytes ?? 1024 * 1024;
|
|
1313
1406
|
return (req, res, next) => {
|
|
@@ -1347,6 +1440,7 @@ async function createProtection(options = {}) {
|
|
|
1347
1440
|
},
|
|
1348
1441
|
() => {
|
|
1349
1442
|
if (req.body === void 0) req.body = shaped.body;
|
|
1443
|
+
if (nodeOptions.screenResponses) wrapNodeResponse(res);
|
|
1350
1444
|
next();
|
|
1351
1445
|
}
|
|
1352
1446
|
);
|