@patchstack/connect 0.3.22 → 0.3.23
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 +2 -2
- package/dist/cli.js +14 -0
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +12 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +20 -2
- package/dist/index.d.ts +20 -2
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -1
- package/dist/protect.cjs +252 -7
- package/dist/protect.cjs.map +1 -1
- package/dist/protect.d.ts +20 -1
- package/dist/protect.js +250 -8
- package/dist/protect.js.map +1 -1
- package/dist/{refresh-manifest-OW7WT5A6.js → refresh-manifest-ZIFRZF25.js} +4 -1
- package/dist/refresh-manifest-ZIFRZF25.js.map +1 -0
- package/package.json +1 -1
- package/dist/refresh-manifest-OW7WT5A6.js.map +0 -1
package/dist/protect.cjs
CHANGED
|
@@ -155,8 +155,10 @@ async function resolveConfig(options) {
|
|
|
155
155
|
"CONFIG_MISSING"
|
|
156
156
|
);
|
|
157
157
|
}
|
|
158
|
+
const apiKeyRaw = fromEnv.apiKey ?? fromFile.apiKey ?? null;
|
|
158
159
|
return {
|
|
159
160
|
siteUuid: siteUuid === null || siteUuid.length === 0 ? null : siteUuid,
|
|
161
|
+
apiKey: apiKeyRaw === null || apiKeyRaw.length === 0 ? null : apiKeyRaw,
|
|
160
162
|
endpoint,
|
|
161
163
|
timeoutMs,
|
|
162
164
|
environment,
|
|
@@ -204,6 +206,7 @@ function readEnv() {
|
|
|
204
206
|
const environmentRaw = process.env.PATCHSTACK_ENVIRONMENT;
|
|
205
207
|
return {
|
|
206
208
|
siteUuid: process.env.PATCHSTACK_SITE_UUID ?? void 0,
|
|
209
|
+
apiKey: process.env.PATCHSTACK_API_KEY ?? void 0,
|
|
207
210
|
endpoint: process.env.PATCHSTACK_ENDPOINT ?? void 0,
|
|
208
211
|
timeoutMs,
|
|
209
212
|
environment: environmentRaw !== void 0 && environmentRaw.length > 0 ? environmentRaw : void 0
|
|
@@ -3068,6 +3071,169 @@ function makeRefreshHandler(tick, secret) {
|
|
|
3068
3071
|
};
|
|
3069
3072
|
}
|
|
3070
3073
|
|
|
3074
|
+
// src/protect/firewall-log.js
|
|
3075
|
+
var DEFAULT_API_BASE = "https://api.patchstack.com";
|
|
3076
|
+
var DEFAULT_FLUSH_MS = 1e3;
|
|
3077
|
+
var MAX_BATCH = 50;
|
|
3078
|
+
var TOKEN_SKEW_MS = 6e4;
|
|
3079
|
+
function parseApiKey(apiKey) {
|
|
3080
|
+
if (typeof apiKey !== "string" || apiKey.length === 0) return null;
|
|
3081
|
+
const idx = apiKey.lastIndexOf("-");
|
|
3082
|
+
if (idx <= 0 || idx === apiKey.length - 1) return null;
|
|
3083
|
+
const clientSecret = apiKey.slice(0, idx);
|
|
3084
|
+
const clientId = apiKey.slice(idx + 1);
|
|
3085
|
+
if (!/^\d+$/.test(clientId) || clientSecret.length === 0) return null;
|
|
3086
|
+
return { clientId, clientSecret };
|
|
3087
|
+
}
|
|
3088
|
+
function resolveApiBase(pulseOrManifestUrl) {
|
|
3089
|
+
const fromEnv = typeof process !== "undefined" ? process.env?.PATCHSTACK_API_BASE : void 0;
|
|
3090
|
+
if (typeof fromEnv === "string" && fromEnv.length > 0) {
|
|
3091
|
+
return fromEnv.replace(/\/$/, "");
|
|
3092
|
+
}
|
|
3093
|
+
if (typeof pulseOrManifestUrl === "string" && pulseOrManifestUrl.length > 0) {
|
|
3094
|
+
try {
|
|
3095
|
+
return new URL(pulseOrManifestUrl).origin;
|
|
3096
|
+
} catch {
|
|
3097
|
+
}
|
|
3098
|
+
}
|
|
3099
|
+
const endpoint = typeof process !== "undefined" ? process.env?.PATCHSTACK_ENDPOINT : void 0;
|
|
3100
|
+
if (typeof endpoint === "string" && endpoint.length > 0) {
|
|
3101
|
+
try {
|
|
3102
|
+
return new URL(endpoint).origin;
|
|
3103
|
+
} catch {
|
|
3104
|
+
}
|
|
3105
|
+
}
|
|
3106
|
+
return DEFAULT_API_BASE;
|
|
3107
|
+
}
|
|
3108
|
+
function createFirewallLogReporter(opts) {
|
|
3109
|
+
const creds = parseApiKey(opts.apiKey);
|
|
3110
|
+
if (!creds) {
|
|
3111
|
+
return { record() {
|
|
3112
|
+
}, flush() {
|
|
3113
|
+
}, stop() {
|
|
3114
|
+
} };
|
|
3115
|
+
}
|
|
3116
|
+
const apiBase = (opts.apiBase ?? DEFAULT_API_BASE).replace(/\/$/, "");
|
|
3117
|
+
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
3118
|
+
const flushMs = Number.isFinite(opts.flushMs) ? opts.flushMs : DEFAULT_FLUSH_MS;
|
|
3119
|
+
const sourceHost = typeof opts.sourceHost === "string" ? opts.sourceHost : "";
|
|
3120
|
+
let queue = [];
|
|
3121
|
+
let timer = null;
|
|
3122
|
+
let stopped = false;
|
|
3123
|
+
let cachedToken = null;
|
|
3124
|
+
let tokenInflight = null;
|
|
3125
|
+
const fetchAccessToken = async () => {
|
|
3126
|
+
if (cachedToken && Date.now() < cachedToken.expiresAt - TOKEN_SKEW_MS) {
|
|
3127
|
+
return cachedToken.token;
|
|
3128
|
+
}
|
|
3129
|
+
if (tokenInflight) return tokenInflight;
|
|
3130
|
+
tokenInflight = (async () => {
|
|
3131
|
+
try {
|
|
3132
|
+
const res = await fetchImpl(`${apiBase}/oauth/token`, {
|
|
3133
|
+
method: "POST",
|
|
3134
|
+
headers: {
|
|
3135
|
+
"Content-Type": "application/json",
|
|
3136
|
+
Accept: "application/json",
|
|
3137
|
+
"User-Agent": "@patchstack/connect"
|
|
3138
|
+
},
|
|
3139
|
+
body: JSON.stringify({
|
|
3140
|
+
grant_type: "client_credentials",
|
|
3141
|
+
client_id: creds.clientId,
|
|
3142
|
+
client_secret: creds.clientSecret
|
|
3143
|
+
})
|
|
3144
|
+
});
|
|
3145
|
+
if (!res || !res.ok) return null;
|
|
3146
|
+
const body = await res.json();
|
|
3147
|
+
const token = body?.access_token;
|
|
3148
|
+
if (typeof token !== "string" || token.length === 0) return null;
|
|
3149
|
+
const expiresIn = Number(body?.expires_in);
|
|
3150
|
+
const ttlMs = Number.isFinite(expiresIn) && expiresIn > 0 ? expiresIn * 1e3 : 36e5;
|
|
3151
|
+
cachedToken = { token, expiresAt: Date.now() + ttlMs };
|
|
3152
|
+
return token;
|
|
3153
|
+
} catch {
|
|
3154
|
+
return null;
|
|
3155
|
+
} finally {
|
|
3156
|
+
tokenInflight = null;
|
|
3157
|
+
}
|
|
3158
|
+
})();
|
|
3159
|
+
return tokenInflight;
|
|
3160
|
+
};
|
|
3161
|
+
const flush = () => {
|
|
3162
|
+
if (timer) {
|
|
3163
|
+
clearTimeout(timer);
|
|
3164
|
+
timer = null;
|
|
3165
|
+
}
|
|
3166
|
+
if (queue.length === 0 || typeof fetchImpl !== "function") return;
|
|
3167
|
+
const batch = queue.splice(0, MAX_BATCH);
|
|
3168
|
+
void (async () => {
|
|
3169
|
+
const token = await fetchAccessToken();
|
|
3170
|
+
if (!token) return;
|
|
3171
|
+
const body = new URLSearchParams();
|
|
3172
|
+
body.set("type", "firewall");
|
|
3173
|
+
body.set("logs", JSON.stringify(batch));
|
|
3174
|
+
try {
|
|
3175
|
+
const p = fetchImpl(`${apiBase}/api/logs/log`, {
|
|
3176
|
+
method: "POST",
|
|
3177
|
+
headers: {
|
|
3178
|
+
Authorization: `Bearer ${token}`,
|
|
3179
|
+
Accept: "application/json",
|
|
3180
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
3181
|
+
"User-Agent": "@patchstack/connect",
|
|
3182
|
+
...sourceHost ? { "Source-Host": sourceHost } : {}
|
|
3183
|
+
},
|
|
3184
|
+
body
|
|
3185
|
+
});
|
|
3186
|
+
if (p && typeof p.then === "function") p.catch(() => {
|
|
3187
|
+
});
|
|
3188
|
+
} catch {
|
|
3189
|
+
}
|
|
3190
|
+
})();
|
|
3191
|
+
};
|
|
3192
|
+
return {
|
|
3193
|
+
/**
|
|
3194
|
+
* @param {{
|
|
3195
|
+
* rule?: { id?: string | number },
|
|
3196
|
+
* method?: string | null,
|
|
3197
|
+
* path?: string | null,
|
|
3198
|
+
* ip?: string | null,
|
|
3199
|
+
* userAgent?: string | null,
|
|
3200
|
+
* }} event
|
|
3201
|
+
*/
|
|
3202
|
+
record(event) {
|
|
3203
|
+
if (stopped) return;
|
|
3204
|
+
const fid = event?.rule?.id;
|
|
3205
|
+
if (fid === void 0 || fid === null || fid === "") return;
|
|
3206
|
+
queue.push({
|
|
3207
|
+
fid,
|
|
3208
|
+
method: event.method ?? null,
|
|
3209
|
+
request_uri: event.path ?? null,
|
|
3210
|
+
ip: event.ip ?? null,
|
|
3211
|
+
user_agent: event.userAgent ?? null,
|
|
3212
|
+
log_date: (/* @__PURE__ */ new Date()).toISOString()
|
|
3213
|
+
});
|
|
3214
|
+
if (queue.length >= MAX_BATCH) {
|
|
3215
|
+
flush();
|
|
3216
|
+
return;
|
|
3217
|
+
}
|
|
3218
|
+
if (!timer) timer = setTimeout(flush, flushMs);
|
|
3219
|
+
},
|
|
3220
|
+
flush,
|
|
3221
|
+
stop() {
|
|
3222
|
+
stopped = true;
|
|
3223
|
+
flush();
|
|
3224
|
+
}
|
|
3225
|
+
};
|
|
3226
|
+
}
|
|
3227
|
+
function telemetryEnabled() {
|
|
3228
|
+
const v = typeof process !== "undefined" ? process.env?.PATCHSTACK_TELEMETRY : void 0;
|
|
3229
|
+
if (v === void 0 || v === "") return true;
|
|
3230
|
+
return !/^(0|false|off|no)$/i.test(String(v));
|
|
3231
|
+
}
|
|
3232
|
+
|
|
3233
|
+
// src/protect/runtime.js
|
|
3234
|
+
var import_node_fs2 = require("fs");
|
|
3235
|
+
var import_node_path8 = require("path");
|
|
3236
|
+
|
|
3071
3237
|
// src/protect/supabase-guard.js
|
|
3072
3238
|
var GUARD_PATH = "/_patchstack/guard";
|
|
3073
3239
|
var HOP_BY_HOP = /* @__PURE__ */ new Set(["content-encoding", "content-length", "transfer-encoding", "connection"]);
|
|
@@ -3140,7 +3306,26 @@ function createServerFnGuard({ protection }) {
|
|
|
3140
3306
|
}
|
|
3141
3307
|
async function createProtection(options = {}) {
|
|
3142
3308
|
const onError = options.onError;
|
|
3143
|
-
const
|
|
3309
|
+
const userOnDetect = options.onDetect ?? defaultOnDetect;
|
|
3310
|
+
const apiKey = resolveApiKey(options);
|
|
3311
|
+
const firewallLog = apiKey && telemetryEnabled() && options.reportFirewallLog !== false ? createFirewallLogReporter({
|
|
3312
|
+
apiKey,
|
|
3313
|
+
apiBase: resolveApiBase(options.pulseRulesUrl ?? options.baseUrl),
|
|
3314
|
+
sourceHost: options.sourceHost,
|
|
3315
|
+
fetchImpl: options.fetchImpl
|
|
3316
|
+
}) : null;
|
|
3317
|
+
const onDetect = (detection) => {
|
|
3318
|
+
userOnDetect(detection);
|
|
3319
|
+
if (firewallLog && detection?.mode === "block") {
|
|
3320
|
+
firewallLog.record({
|
|
3321
|
+
rule: detection.rule,
|
|
3322
|
+
method: detection.method,
|
|
3323
|
+
path: detection.path,
|
|
3324
|
+
ip: detection.ip,
|
|
3325
|
+
userAgent: detection.userAgent
|
|
3326
|
+
});
|
|
3327
|
+
}
|
|
3328
|
+
};
|
|
3144
3329
|
const store = makeStore(options);
|
|
3145
3330
|
const bundle = await resolveRules(options, store);
|
|
3146
3331
|
let mode = resolveMode(options, bundle);
|
|
@@ -3172,9 +3357,19 @@ async function createProtection(options = {}) {
|
|
|
3172
3357
|
};
|
|
3173
3358
|
applyBundle(bundle);
|
|
3174
3359
|
const maskFn = typeof options.maskWith === "function" ? options.maskWith : () => typeof options.maskWith === "string" ? options.maskWith : "[REDACTED]";
|
|
3175
|
-
const decide = (phase, result, block, allow2) => {
|
|
3360
|
+
const decide = (phase, result, block, allow2, ctx = {}) => {
|
|
3176
3361
|
if (!result || !result.blocked) return allow2();
|
|
3177
|
-
onDetect({
|
|
3362
|
+
onDetect({
|
|
3363
|
+
phase,
|
|
3364
|
+
mode,
|
|
3365
|
+
category: result.rule?.category,
|
|
3366
|
+
rule: result.rule,
|
|
3367
|
+
message: result.message,
|
|
3368
|
+
method: ctx.method,
|
|
3369
|
+
path: ctx.path,
|
|
3370
|
+
ip: ctx.ip,
|
|
3371
|
+
userAgent: ctx.userAgent
|
|
3372
|
+
});
|
|
3178
3373
|
return mode === "block" ? block() : allow2();
|
|
3179
3374
|
};
|
|
3180
3375
|
const isTextCT = (ct) => {
|
|
@@ -3357,7 +3552,7 @@ async function createProtection(options = {}) {
|
|
|
3357
3552
|
onError?.(err);
|
|
3358
3553
|
return null;
|
|
3359
3554
|
}
|
|
3360
|
-
return decide("request", result, () => blockResponse(result, request), () => null);
|
|
3555
|
+
return decide("request", result, () => blockResponse(result, request), () => null, fetchRequestMeta(request));
|
|
3361
3556
|
};
|
|
3362
3557
|
},
|
|
3363
3558
|
// Wrap a fetch handler: screens the request, then the response (redact/block).
|
|
@@ -3395,7 +3590,8 @@ async function createProtection(options = {}) {
|
|
|
3395
3590
|
() => {
|
|
3396
3591
|
if (exprOptions.screenResponses) wrapNodeResponse(res);
|
|
3397
3592
|
next();
|
|
3398
|
-
}
|
|
3593
|
+
},
|
|
3594
|
+
nodeRequestMeta(req)
|
|
3399
3595
|
);
|
|
3400
3596
|
};
|
|
3401
3597
|
},
|
|
@@ -3447,7 +3643,8 @@ async function createProtection(options = {}) {
|
|
|
3447
3643
|
if (req.body === void 0) req.body = shaped.body;
|
|
3448
3644
|
if (nodeOptions.screenResponses) wrapNodeResponse(res);
|
|
3449
3645
|
next();
|
|
3450
|
-
}
|
|
3646
|
+
},
|
|
3647
|
+
nodeRequestMeta(req)
|
|
3451
3648
|
);
|
|
3452
3649
|
});
|
|
3453
3650
|
};
|
|
@@ -3491,7 +3688,12 @@ async function createProtection(options = {}) {
|
|
|
3491
3688
|
}
|
|
3492
3689
|
if (options.refreshMs > 0 && live) {
|
|
3493
3690
|
const loop = startRefresh(runRefreshTick, { refreshMs: options.refreshMs, onError });
|
|
3494
|
-
protection.stopRefresh =
|
|
3691
|
+
protection.stopRefresh = () => {
|
|
3692
|
+
loop.stop();
|
|
3693
|
+
firewallLog?.stop();
|
|
3694
|
+
};
|
|
3695
|
+
} else if (firewallLog) {
|
|
3696
|
+
protection.stopRefresh = () => firewallLog.stop();
|
|
3495
3697
|
}
|
|
3496
3698
|
return protection;
|
|
3497
3699
|
}
|
|
@@ -3503,6 +3705,22 @@ function resolveMode(options, bundle) {
|
|
|
3503
3705
|
if (options?.mode === "dry-run") return "dry-run";
|
|
3504
3706
|
return "dry-run";
|
|
3505
3707
|
}
|
|
3708
|
+
function resolveApiKey(options) {
|
|
3709
|
+
if (typeof options?.apiKey === "string" && options.apiKey.length > 0) return options.apiKey;
|
|
3710
|
+
if (typeof process !== "undefined") {
|
|
3711
|
+
const fromEnv = process.env?.PATCHSTACK_API_KEY;
|
|
3712
|
+
if (typeof fromEnv === "string" && fromEnv.length > 0) return fromEnv;
|
|
3713
|
+
}
|
|
3714
|
+
try {
|
|
3715
|
+
if (typeof process === "undefined" || typeof process.cwd !== "function") return void 0;
|
|
3716
|
+
const cwd = options?.cwd ?? process.cwd();
|
|
3717
|
+
const raw = (0, import_node_fs2.readFileSync)((0, import_node_path8.join)(cwd, ".patchstackrc.json"), "utf8");
|
|
3718
|
+
const key = JSON.parse(raw)?.apiKey;
|
|
3719
|
+
if (typeof key === "string" && key.length > 0) return key;
|
|
3720
|
+
} catch {
|
|
3721
|
+
}
|
|
3722
|
+
return void 0;
|
|
3723
|
+
}
|
|
3506
3724
|
function byPhase(rules, phase) {
|
|
3507
3725
|
return (rules ?? []).filter((r) => (r.phase ?? "request") === phase);
|
|
3508
3726
|
}
|
|
@@ -3766,6 +3984,33 @@ function defaultOnDetect({ phase, mode, category, rule, message }) {
|
|
|
3766
3984
|
const tag = mode === "block" ? "BLOCK" : "DETECT (dry-run)";
|
|
3767
3985
|
console.warn(`[patchstack] ${tag} phase=${phase ?? "request"} category=${category ?? "?"} rule=${rule?.id ?? "?"} ${message ?? ""}`.trim());
|
|
3768
3986
|
}
|
|
3987
|
+
function fetchRequestMeta(request) {
|
|
3988
|
+
if (!request) return {};
|
|
3989
|
+
let path7 = null;
|
|
3990
|
+
try {
|
|
3991
|
+
path7 = new URL(request.url).pathname;
|
|
3992
|
+
} catch {
|
|
3993
|
+
path7 = typeof request.url === "string" ? request.url : null;
|
|
3994
|
+
}
|
|
3995
|
+
return {
|
|
3996
|
+
method: request.method ?? null,
|
|
3997
|
+
path: path7,
|
|
3998
|
+
ip: request.headers?.get?.("x-forwarded-for") ?? null,
|
|
3999
|
+
userAgent: request.headers?.get?.("user-agent") ?? null
|
|
4000
|
+
};
|
|
4001
|
+
}
|
|
4002
|
+
function nodeRequestMeta(req) {
|
|
4003
|
+
if (!req) return {};
|
|
4004
|
+
const headers = req.headers ?? {};
|
|
4005
|
+
const ua = headers["user-agent"] ?? headers["User-Agent"];
|
|
4006
|
+
const fwd = headers["x-forwarded-for"] ?? headers["X-Forwarded-For"];
|
|
4007
|
+
return {
|
|
4008
|
+
method: req.method ?? null,
|
|
4009
|
+
path: req.originalUrl || req.url || null,
|
|
4010
|
+
ip: req.ip ?? (typeof fwd === "string" ? fwd : null),
|
|
4011
|
+
userAgent: typeof ua === "string" ? ua : Array.isArray(ua) ? ua[0] : null
|
|
4012
|
+
};
|
|
4013
|
+
}
|
|
3769
4014
|
// Annotate the CommonJS export names for ESM import in node:
|
|
3770
4015
|
0 && (module.exports = {
|
|
3771
4016
|
GUARD_PATH,
|