@patchstack/connect 0.3.3 → 0.3.5
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 +9 -5
- package/README.md +16 -3
- package/dist/cli.js +427 -105
- package/dist/cli.js.map +1 -1
- package/dist/protect/templates/guard.ts +52 -95
- package/dist/protect/templates/rules.json +4 -7
- package/dist/protect.cjs +1240 -0
- package/dist/protect.cjs.map +1 -0
- package/dist/protect.d.ts +51 -0
- package/dist/protect.js +1210 -0
- package/dist/protect.js.map +1 -0
- package/package.json +6 -1
- package/dist/protect/templates/engine.d.ts +0 -57
- package/dist/protect/templates/engine.js +0 -137
- package/dist/protect/templates/manifest.d.ts +0 -1
|
@@ -1,102 +1,59 @@
|
|
|
1
|
-
// Patchstack
|
|
2
|
-
// touches this. It runs in the app's own server (the Cloudflare Worker)
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
//
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
1
|
+
// Patchstack runtime protection — installed by `patchstack-connect protect`; the vibe coder
|
|
2
|
+
// never touches this. It runs in the app's own server (e.g. the Cloudflare Worker) and covers
|
|
3
|
+
// both ways a Lovable app talks to its database:
|
|
4
|
+
// - browser → Supabase directly: the generated Supabase client is patched to tunnel every call
|
|
5
|
+
// through handleGuardRequest (registered as request middleware in src/start.ts).
|
|
6
|
+
// - browser → TanStack server function → Supabase: inspectServerFn checks the server-fn args
|
|
7
|
+
// (registered as function middleware in src/start.ts) before anything is written.
|
|
8
|
+
// Either way Patchstack sees the traffic and runs the same policy.
|
|
9
|
+
//
|
|
10
|
+
// Always-on by default (blocks). Rules come from the Patchstack API per-site (cached); the
|
|
11
|
+
// bundled rules.json is only a fallback for before a token is configured. The engine ships
|
|
12
|
+
// inside @patchstack/connect — nothing else to install.
|
|
13
|
+
import {
|
|
14
|
+
createProtection,
|
|
15
|
+
createSupabaseGuard,
|
|
16
|
+
createServerFnGuard,
|
|
17
|
+
GUARD_PATH,
|
|
18
|
+
} from "@patchstack/connect/protect";
|
|
19
|
+
import fallbackRules from "./rules.json";
|
|
20
|
+
|
|
21
|
+
export { GUARD_PATH };
|
|
22
|
+
|
|
23
|
+
// One shared protection policy for both guards (rules load once).
|
|
24
|
+
let _protection: Awaited<ReturnType<typeof createProtection>> | undefined;
|
|
25
|
+
async function getProtection() {
|
|
26
|
+
if (!_protection) {
|
|
27
|
+
// Always-on: block by default. An explicit PATCHSTACK_MODE=dry-run downgrades to log-only.
|
|
28
|
+
const mode = process.env.PATCHSTACK_MODE === "dry-run" ? "dry-run" : "block";
|
|
29
|
+
const token = process.env.PATCHSTACK_WAF_TOKEN;
|
|
30
|
+
_protection = await createProtection(
|
|
31
|
+
token
|
|
32
|
+
? { token, mode, cacheDir: ".patchstack" } // live per-site rules from the Patchstack API (cached)
|
|
33
|
+
: { rules: fallbackRules as never, mode }, // demo fallback until a token is set
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
return _protection;
|
|
18
37
|
}
|
|
19
38
|
|
|
39
|
+
// Request-middleware path: the browser tunnels its direct Supabase calls here.
|
|
40
|
+
let _handle: ((request: Request) => Promise<Response>) | undefined;
|
|
20
41
|
export async function handleGuardRequest(request: Request): Promise<Response> {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
// Supabase project (origin known server-side). Anything else — internal hosts, cloud
|
|
26
|
-
// metadata, a different scheme — is rejected before we make any outbound request.
|
|
27
|
-
const supabaseUrl = process.env.SUPABASE_URL;
|
|
28
|
-
let targetUrl: URL;
|
|
29
|
-
try {
|
|
30
|
-
targetUrl = new URL(target);
|
|
31
|
-
} catch {
|
|
32
|
-
return new Response("patchstack: invalid target", { status: 400 });
|
|
33
|
-
}
|
|
34
|
-
if (!supabaseUrl || targetUrl.protocol !== "https:" || targetUrl.origin !== new URL(supabaseUrl).origin) {
|
|
35
|
-
return new Response("patchstack: target not allowed", { status: 403 });
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const hasBody = request.method !== "GET" && request.method !== "HEAD";
|
|
39
|
-
const bodyText = hasBody ? await request.text() : "";
|
|
40
|
-
|
|
41
|
-
let record: Record<string, unknown> = {};
|
|
42
|
-
if (bodyText) {
|
|
43
|
-
try {
|
|
44
|
-
const parsed = JSON.parse(bodyText);
|
|
45
|
-
record = Array.isArray(parsed) ? (parsed[0] ?? {}) : parsed;
|
|
46
|
-
} catch {
|
|
47
|
-
// not JSON — leave record empty; the rule simply won't match
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const ctx = {
|
|
52
|
-
method: request.method,
|
|
53
|
-
url: target,
|
|
54
|
-
headers: Object.fromEntries(request.headers),
|
|
55
|
-
// expose the record under a few shapes so rule parameters like "insert.title" resolve
|
|
56
|
-
body: { ...record, insert: record, update: record },
|
|
57
|
-
ip: request.headers.get("x-forwarded-for") ?? "",
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
const t0 = performance.now();
|
|
61
|
-
const verdict = evaluate(ctx, (rulesData as { firewall: Rule[] }).firewall, manifest);
|
|
62
|
-
const latencyMs = Math.round((performance.now() - t0) * 100) / 100;
|
|
63
|
-
|
|
64
|
-
// Always log the flag (this is the "dry-run sees it too" behavior).
|
|
65
|
-
const decision = verdict.matched ? `MATCH ${verdict.rule_id}` : "clean";
|
|
66
|
-
console.log(`[patchstack] ${request.method} ${new URL(target).pathname} -> ${decision} · mode=${mode()}`);
|
|
67
|
-
|
|
68
|
-
if (verdict.matched && mode() === "block") {
|
|
69
|
-
const receipt = {
|
|
70
|
-
blocked: true,
|
|
71
|
-
rule_id: verdict.rule_id,
|
|
72
|
-
vulnerability_id: verdict.vulnerability_id,
|
|
73
|
-
package: verdict.package,
|
|
74
|
-
version: verdict.version,
|
|
75
|
-
matched_conditions: verdict.explain,
|
|
76
|
-
latency_ms: latencyMs,
|
|
77
|
-
};
|
|
78
|
-
console.log("[patchstack] BLOCKED", JSON.stringify(receipt));
|
|
79
|
-
return new Response(JSON.stringify(receipt), {
|
|
80
|
-
status: 403,
|
|
81
|
-
headers: { "content-type": "application/json", "x-patchstack": "blocked" },
|
|
42
|
+
if (!_handle) {
|
|
43
|
+
_handle = createSupabaseGuard({
|
|
44
|
+
protection: await getProtection(),
|
|
45
|
+
supabaseUrl: process.env.SUPABASE_URL,
|
|
82
46
|
});
|
|
83
47
|
}
|
|
48
|
+
return _handle(request);
|
|
49
|
+
}
|
|
84
50
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
redirect: "manual",
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
const outHeaders = new Headers();
|
|
97
|
-
upstream.headers.forEach((value, key) => {
|
|
98
|
-
if (!HOP_BY_HOP.has(key.toLowerCase())) outHeaders.set(key, value);
|
|
99
|
-
});
|
|
100
|
-
const buf = await upstream.arrayBuffer();
|
|
101
|
-
return new Response(buf, { status: upstream.status, statusText: upstream.statusText, headers: outHeaders });
|
|
51
|
+
// Function-middleware path: inspect a server function's decoded args before the handler runs.
|
|
52
|
+
// Returns a block receipt (throw on it to abort the call) or null to allow.
|
|
53
|
+
let _inspect: ((data: unknown) => Promise<{ rule?: string; message: string } | null>) | undefined;
|
|
54
|
+
export async function inspectServerFn(data: unknown): Promise<{ rule?: string; message: string } | null> {
|
|
55
|
+
if (!_inspect) {
|
|
56
|
+
_inspect = createServerFnGuard({ protection: await getProtection() });
|
|
57
|
+
}
|
|
58
|
+
return _inspect(data);
|
|
102
59
|
}
|
|
@@ -1,22 +1,19 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_comment": "
|
|
2
|
+
"_comment": "Demo fallback rules — used only when PATCHSTACK_WAF_TOKEN is unset. In production the guard fetches per-site rules from the Patchstack API (api.patchstack.com/api/get-rules/3, cached to disk). CVE id is representative until aligned with the real corpus.",
|
|
3
3
|
"firewall": [
|
|
4
4
|
{
|
|
5
5
|
"id": "rm-npm-0001",
|
|
6
6
|
"title": "Block stored XSS via vulnerable markdown renderer (marked)",
|
|
7
7
|
"vulnerability_id": "CVE-2022-21681",
|
|
8
|
-
"package_cond": {
|
|
9
|
-
"package": "marked",
|
|
10
|
-
"vulnerable_versions": ["4.0.0"]
|
|
11
|
-
},
|
|
12
8
|
"rule_v2": [
|
|
13
9
|
{
|
|
14
|
-
"parameter":
|
|
10
|
+
"parameter": "post.title",
|
|
15
11
|
"mutations": ["urldecode"],
|
|
16
12
|
"match": { "type": "inline_xss" }
|
|
17
13
|
}
|
|
18
14
|
]
|
|
19
15
|
}
|
|
20
16
|
],
|
|
21
|
-
"whitelists": []
|
|
17
|
+
"whitelists": [],
|
|
18
|
+
"whitelist_keys": {}
|
|
22
19
|
}
|