@patchstack/connect 0.3.29 → 0.3.30

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.
Files changed (40) hide show
  1. package/AGENT-INSTALL.md +37 -21
  2. package/README.md +24 -10
  3. package/dist/{chunk-MJOTFUDE.js → chunk-LLKP5EJS.js} +1 -1
  4. package/dist/chunk-LLKP5EJS.js.map +1 -0
  5. package/dist/cli.js +1062 -189
  6. package/dist/cli.js.map +1 -1
  7. package/dist/index.cjs +419 -104
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.d.cts +28 -9
  10. package/dist/index.d.ts +28 -9
  11. package/dist/index.js +412 -97
  12. package/dist/index.js.map +1 -1
  13. package/dist/protect/templates/astro-middleware.ts +33 -13
  14. package/dist/protect/templates/demo-rules.json +2 -3
  15. package/dist/protect/templates/express-guard.cjs +26 -15
  16. package/dist/protect/templates/express-guard.js +26 -15
  17. package/dist/protect/templates/express-guard.ts +33 -16
  18. package/dist/protect/templates/fastify-plugin.cjs +33 -11
  19. package/dist/protect/templates/fastify-plugin.js +33 -11
  20. package/dist/protect/templates/fastify-plugin.ts +40 -12
  21. package/dist/protect/templates/generic-guard.cjs +28 -12
  22. package/dist/protect/templates/generic-guard.js +28 -13
  23. package/dist/protect/templates/generic-guard.ts +41 -20
  24. package/dist/protect/templates/guard.ts +47 -27
  25. package/dist/protect/templates/next-middleware.ts +29 -12
  26. package/dist/protect/templates/nuxt-middleware.ts +29 -12
  27. package/dist/protect/templates/rules.json +2 -2
  28. package/dist/protect/templates/sveltekit-hooks.ts +33 -13
  29. package/dist/protect.cjs +1039 -235
  30. package/dist/protect.cjs.map +1 -1
  31. package/dist/protect.d.ts +39 -11
  32. package/dist/protect.edge.js +663 -132
  33. package/dist/protect.edge.js.map +4 -4
  34. package/dist/protect.js +665 -134
  35. package/dist/protect.js.map +1 -1
  36. package/dist/{refresh-manifest-VRBE6RH6.js → refresh-manifest-ZSP76JWQ.js} +351 -94
  37. package/dist/refresh-manifest-ZSP76JWQ.js.map +1 -0
  38. package/package.json +5 -2
  39. package/dist/chunk-MJOTFUDE.js.map +0 -1
  40. package/dist/refresh-manifest-VRBE6RH6.js.map +0 -1
@@ -7,29 +7,49 @@ import fallbackRules from "./patchstack.rules.json";
7
7
 
8
8
  const PS_SITE_UUID = "__PATCHSTACK_SITE_UUID__";
9
9
 
10
- let _protection: Awaited<ReturnType<typeof createProtection>> | undefined;
10
+ /**
11
+ * One protection policy, memoized on the IN-FLIGHT promise rather than the resolved value.
12
+ *
13
+ * A cold start takes several concurrent requests. Caching only the finished value lets each of them see an
14
+ * empty cache and start its own build — several rule fetches, several refresh loops, and several policies
15
+ * where the app is meant to have one. Holding the promise means the first request starts it and the rest
16
+ * await the same one. A failed build is not cached, so the next request retries rather than inheriting one
17
+ * bad boot for the life of the process.
18
+ */
19
+ let _protection: Promise<Awaited<ReturnType<typeof createProtection>>> | undefined;
11
20
  async function getProtection() {
12
21
  if (!_protection) {
13
- const mode = process.env.PATCHSTACK_MODE === "dry-run" ? "dry-run" : "block";
14
- const token = process.env.PATCHSTACK_WAF_TOKEN;
15
- const siteUuid = PS_SITE_UUID.startsWith("__") ? process.env.PATCHSTACK_SITE_UUID : PS_SITE_UUID;
16
- const common = { mode, egress: true } as const;
17
- _protection = await createProtection(
18
- siteUuid
19
- ? { ...common, siteUuid, rules: fallbackRules as never, cacheDir: ".patchstack" }
20
- : token
21
- ? { ...common, token, cacheDir: ".patchstack" }
22
- : { ...common, rules: fallbackRules as never },
23
- );
22
+ _protection = buildProtection().catch((err) => {
23
+ _protection = undefined; // don't cache a failed boot
24
+ throw err;
25
+ });
24
26
  }
27
+
25
28
  return _protection;
26
29
  }
27
30
 
31
+ async function buildProtection() {
32
+ const mode = process.env.PATCHSTACK_MODE === "dry-run" ? "dry-run" : "block";
33
+ const token = process.env.PATCHSTACK_WAF_TOKEN;
34
+ const siteUuid = PS_SITE_UUID.startsWith("__") ? process.env.PATCHSTACK_SITE_UUID : PS_SITE_UUID;
35
+ const common = { mode, egress: true } as const;
36
+ return createProtection(
37
+ siteUuid
38
+ ? { ...common, siteUuid, rules: fallbackRules as never, cacheDir: ".patchstack" }
39
+ : token
40
+ ? { ...common, token, cacheDir: ".patchstack" }
41
+ : { ...common, rules: fallbackRules as never },
42
+ );
43
+ }
44
+
28
45
  // #region patchstack-astro (managed by patchstack-connect protect — do not edit)
29
46
  export const onRequest: MiddlewareHandler = async (context, next) => {
30
47
  const protection = await getProtection();
31
48
  const blocked = await protection.fetchGuard()(context.request);
32
49
  if (blocked) return blocked; // 403 — blocked before it reaches your route
33
- return protection.screenResponse(await next());
50
+ // Response rules can be scoped to a route or a method, and the engine can only apply that scope if it is
51
+ // given the request the response belongs to. Passed through here for that reason: without it a scoped
52
+ // response rule is delivered, counted as protection, and never matches anything.
53
+ return protection.screenResponse(await next(), context.request);
34
54
  };
35
55
  // #endregion patchstack-astro
@@ -54,7 +54,7 @@
54
54
  "parameter": [
55
55
  "get.file",
56
56
  "post.file",
57
- "raw.file",
57
+
58
58
  "get.path",
59
59
  "post.path"
60
60
  ],
@@ -172,8 +172,7 @@
172
172
  {
173
173
  "parameter": [
174
174
  "get.url",
175
- "post.url",
176
- "raw.url"
175
+ "post.url"
177
176
  ],
178
177
  "mutations": [
179
178
  "urldecode"
@@ -7,25 +7,36 @@ let protection;
7
7
 
8
8
  async function getProtection() {
9
9
  if (!protection) {
10
- const mode = process.env.PATCHSTACK_MODE === "dry-run" ? "dry-run" : "block";
11
- const token = process.env.PATCHSTACK_WAF_TOKEN;
12
- const siteUuid = PS_SITE_UUID.startsWith("__") ? process.env.PATCHSTACK_SITE_UUID : PS_SITE_UUID;
13
- // The sandbox dev server is long-lived and isn't restarted on change, so refresh the live
14
- // rules periodically — a dependency flagged after boot is then enforced without a restart.
15
- // Production relies on a redeploy (which re-fetches at boot), so refresh stays off there.
16
- const refreshMs = process.env.PATCHSTACK_ENVIRONMENT === "sandbox" ? 15000 : 0;
17
- const common = { mode, egress: true, refreshMs };
18
- protection = await createProtection(
19
- siteUuid
20
- ? { ...common, siteUuid, rules: fallbackRules, cacheDir: ".patchstack" }
21
- : token
22
- ? { ...common, token, cacheDir: ".patchstack" }
23
- : { ...common, rules: fallbackRules },
24
- );
10
+ // Memoized on the in-flight promise, not the resolved value: a cold start takes several
11
+ // concurrent requests, and caching only the finished value lets each of them build its own
12
+ // policy — several rule fetches and several refresh loops where the app should have one.
13
+ protection = buildProtection().catch((err) => {
14
+ protection = undefined; // don't cache a failed boot
15
+ throw err;
16
+ });
25
17
  }
18
+
26
19
  return protection;
27
20
  }
28
21
 
22
+ async function buildProtection() {
23
+ const mode = process.env.PATCHSTACK_MODE === "dry-run" ? "dry-run" : "block";
24
+ const token = process.env.PATCHSTACK_WAF_TOKEN;
25
+ const siteUuid = PS_SITE_UUID.startsWith("__") ? process.env.PATCHSTACK_SITE_UUID : PS_SITE_UUID;
26
+ // The sandbox dev server is long-lived and isn't restarted on change, so refresh the live
27
+ // rules periodically — a dependency flagged after boot is then enforced without a restart.
28
+ // Production relies on a redeploy (which re-fetches at boot), so refresh stays off there.
29
+ const refreshMs = process.env.PATCHSTACK_ENVIRONMENT === "sandbox" ? 15000 : 0;
30
+ const common = { mode, egress: true, refreshMs };
31
+ return createProtection(
32
+ siteUuid
33
+ ? { ...common, siteUuid, rules: fallbackRules, cacheDir: ".patchstack" }
34
+ : token
35
+ ? { ...common, token, cacheDir: ".patchstack" }
36
+ : { ...common, rules: fallbackRules },
37
+ );
38
+ }
39
+
29
40
  function patchstackMiddleware(req, res, next) {
30
41
  getProtection()
31
42
  .then((active) => active.express()(req, res, next))
@@ -8,25 +8,36 @@ let protection;
8
8
 
9
9
  async function getProtection() {
10
10
  if (!protection) {
11
- const mode = process.env.PATCHSTACK_MODE === "dry-run" ? "dry-run" : "block";
12
- const token = process.env.PATCHSTACK_WAF_TOKEN;
13
- const siteUuid = PS_SITE_UUID.startsWith("__") ? process.env.PATCHSTACK_SITE_UUID : PS_SITE_UUID;
14
- // The sandbox dev server is long-lived and isn't restarted on change, so refresh the live
15
- // rules periodically — a dependency flagged after boot is then enforced without a restart.
16
- // Production relies on a redeploy (which re-fetches at boot), so refresh stays off there.
17
- const refreshMs = process.env.PATCHSTACK_ENVIRONMENT === "sandbox" ? 15000 : 0;
18
- const common = { mode, egress: true, refreshMs };
19
- protection = await createProtection(
20
- siteUuid
21
- ? { ...common, siteUuid, rules: fallbackRules, cacheDir: ".patchstack" }
22
- : token
23
- ? { ...common, token, cacheDir: ".patchstack" }
24
- : { ...common, rules: fallbackRules },
25
- );
11
+ // Memoized on the in-flight promise, not the resolved value: a cold start takes several
12
+ // concurrent requests, and caching only the finished value lets each of them build its own
13
+ // policy — several rule fetches and several refresh loops where the app should have one.
14
+ protection = buildProtection().catch((err) => {
15
+ protection = undefined; // don't cache a failed boot
16
+ throw err;
17
+ });
26
18
  }
19
+
27
20
  return protection;
28
21
  }
29
22
 
23
+ async function buildProtection() {
24
+ const mode = process.env.PATCHSTACK_MODE === "dry-run" ? "dry-run" : "block";
25
+ const token = process.env.PATCHSTACK_WAF_TOKEN;
26
+ const siteUuid = PS_SITE_UUID.startsWith("__") ? process.env.PATCHSTACK_SITE_UUID : PS_SITE_UUID;
27
+ // The sandbox dev server is long-lived and isn't restarted on change, so refresh the live
28
+ // rules periodically — a dependency flagged after boot is then enforced without a restart.
29
+ // Production relies on a redeploy (which re-fetches at boot), so refresh stays off there.
30
+ const refreshMs = process.env.PATCHSTACK_ENVIRONMENT === "sandbox" ? 15000 : 0;
31
+ const common = { mode, egress: true, refreshMs };
32
+ return createProtection(
33
+ siteUuid
34
+ ? { ...common, siteUuid, rules: fallbackRules, cacheDir: ".patchstack" }
35
+ : token
36
+ ? { ...common, token, cacheDir: ".patchstack" }
37
+ : { ...common, rules: fallbackRules },
38
+ );
39
+ }
40
+
30
41
  export function patchstackMiddleware(req, res, next) {
31
42
  getProtection()
32
43
  .then((active) => active.express()(req, res, next))
@@ -4,29 +4,46 @@ import { createProtection } from "@patchstack/connect/protect";
4
4
  import fallbackRules from "./rules.json";
5
5
 
6
6
  const PS_SITE_UUID = "__PATCHSTACK_SITE_UUID__";
7
- let _protection: Awaited<ReturnType<typeof createProtection>> | undefined;
7
+ /**
8
+ * One protection policy, memoized on the IN-FLIGHT promise rather than the resolved value.
9
+ *
10
+ * A cold start takes several concurrent requests. Caching only the finished value lets each of them see an
11
+ * empty cache and start its own build — several rule fetches, several refresh loops, and several policies
12
+ * where the app is meant to have one. Holding the promise means the first request starts it and the rest
13
+ * await the same one. A failed build is not cached, so the next request retries rather than inheriting one
14
+ * bad boot for the life of the process.
15
+ */
16
+ let _protection: Promise<Awaited<ReturnType<typeof createProtection>>> | undefined;
8
17
 
9
18
  async function getProtection() {
10
19
  if (!_protection) {
11
- const mode = process.env.PATCHSTACK_MODE === "dry-run" ? "dry-run" : "block";
12
- const token = process.env.PATCHSTACK_WAF_TOKEN;
13
- const siteUuid = PS_SITE_UUID.startsWith("__") ? process.env.PATCHSTACK_SITE_UUID : PS_SITE_UUID;
14
- // The sandbox dev server is long-lived and isn't restarted on change, so refresh the live
15
- // rules periodically — a dependency flagged after boot is then enforced without a restart.
16
- // Production relies on a redeploy (which re-fetches at boot), so refresh stays off there.
17
- const refreshMs = process.env.PATCHSTACK_ENVIRONMENT === "sandbox" ? 15000 : 0;
18
- const common = { mode, egress: true, refreshMs } as const;
19
- _protection = await createProtection(
20
- siteUuid
21
- ? { ...common, siteUuid, rules: fallbackRules as never, cacheDir: ".patchstack" }
22
- : token
23
- ? { ...common, token, cacheDir: ".patchstack" }
24
- : { ...common, rules: fallbackRules as never },
25
- );
20
+ _protection = buildProtection().catch((err) => {
21
+ _protection = undefined; // don't cache a failed boot
22
+ throw err;
23
+ });
26
24
  }
25
+
27
26
  return _protection;
28
27
  }
29
28
 
29
+ async function buildProtection() {
30
+ const mode = process.env.PATCHSTACK_MODE === "dry-run" ? "dry-run" : "block";
31
+ const token = process.env.PATCHSTACK_WAF_TOKEN;
32
+ const siteUuid = PS_SITE_UUID.startsWith("__") ? process.env.PATCHSTACK_SITE_UUID : PS_SITE_UUID;
33
+ // The sandbox dev server is long-lived and isn't restarted on change, so refresh the live
34
+ // rules periodically — a dependency flagged after boot is then enforced without a restart.
35
+ // Production relies on a redeploy (which re-fetches at boot), so refresh stays off there.
36
+ const refreshMs = process.env.PATCHSTACK_ENVIRONMENT === "sandbox" ? 15000 : 0;
37
+ const common = { mode, egress: true, refreshMs } as const;
38
+ return createProtection(
39
+ siteUuid
40
+ ? { ...common, siteUuid, rules: fallbackRules as never, cacheDir: ".patchstack" }
41
+ : token
42
+ ? { ...common, token, cacheDir: ".patchstack" }
43
+ : { ...common, rules: fallbackRules as never },
44
+ );
45
+ }
46
+
30
47
  export function patchstackMiddleware(req: unknown, res: unknown, next: (err?: unknown) => void) {
31
48
  getProtection()
32
49
  .then((protection) =>
@@ -8,21 +8,32 @@ let protection;
8
8
 
9
9
  async function getProtection() {
10
10
  if (!protection) {
11
- const mode = process.env.PATCHSTACK_MODE === "dry-run" ? "dry-run" : "block";
12
- const token = process.env.PATCHSTACK_WAF_TOKEN;
13
- const siteUuid = PS_SITE_UUID.startsWith("__") ? process.env.PATCHSTACK_SITE_UUID : PS_SITE_UUID;
14
- const common = { mode, egress: true };
15
- protection = await createProtection(
16
- siteUuid
17
- ? { ...common, siteUuid, rules: fallbackRules, cacheDir: ".patchstack" }
18
- : token
19
- ? { ...common, token, cacheDir: ".patchstack" }
20
- : { ...common, rules: fallbackRules },
21
- );
11
+ // Memoized on the in-flight promise, not the resolved value: a cold start takes several
12
+ // concurrent requests, and caching only the finished value lets each of them build its own
13
+ // policy — several rule fetches and several refresh loops where the app should have one.
14
+ protection = buildProtection().catch((err) => {
15
+ protection = undefined; // don't cache a failed boot
16
+ throw err;
17
+ });
22
18
  }
19
+
23
20
  return protection;
24
21
  }
25
22
 
23
+ async function buildProtection() {
24
+ const mode = process.env.PATCHSTACK_MODE === "dry-run" ? "dry-run" : "block";
25
+ const token = process.env.PATCHSTACK_WAF_TOKEN;
26
+ const siteUuid = PS_SITE_UUID.startsWith("__") ? process.env.PATCHSTACK_SITE_UUID : PS_SITE_UUID;
27
+ const common = { mode, egress: true };
28
+ return createProtection(
29
+ siteUuid
30
+ ? { ...common, siteUuid, rules: fallbackRules, cacheDir: ".patchstack" }
31
+ : token
32
+ ? { ...common, token, cacheDir: ".patchstack" }
33
+ : { ...common, rules: fallbackRules },
34
+ );
35
+ }
36
+
26
37
  async function patchstackFastify(fastify) {
27
38
  const protection = await getProtection();
28
39
  const guard = protection.fetchGuard();
@@ -43,4 +54,15 @@ async function patchstackFastify(fastify) {
43
54
  });
44
55
  }
45
56
 
57
+ // Fastify ENCAPSULATES a registered plugin: hooks added inside one apply to that plugin's context and
58
+ // its children, and to nothing else. Registered as an ordinary plugin, this guard would screen nothing
59
+ // on the root instance and nothing in sibling route plugins — which is most of an application, and it
60
+ // would look installed the whole time.
61
+ //
62
+ // This is the marker `fastify-plugin` sets, and the mechanism Fastify documents for opting out: with it,
63
+ // `register` runs the function against the ROOT instance instead of a child context, so the hook applies
64
+ // to every route. Set here rather than pulling in `fastify-plugin` so an installed app gains no
65
+ // dependency it did not already have.
66
+ patchstackFastify[Symbol.for("skip-override")] = true;
67
+
46
68
  module.exports = { patchstackFastify };
@@ -9,21 +9,32 @@ let protection;
9
9
 
10
10
  async function getProtection() {
11
11
  if (!protection) {
12
- const mode = process.env.PATCHSTACK_MODE === "dry-run" ? "dry-run" : "block";
13
- const token = process.env.PATCHSTACK_WAF_TOKEN;
14
- const siteUuid = PS_SITE_UUID.startsWith("__") ? process.env.PATCHSTACK_SITE_UUID : PS_SITE_UUID;
15
- const common = { mode, egress: true };
16
- protection = await createProtection(
17
- siteUuid
18
- ? { ...common, siteUuid, rules: fallbackRules, cacheDir: ".patchstack" }
19
- : token
20
- ? { ...common, token, cacheDir: ".patchstack" }
21
- : { ...common, rules: fallbackRules },
22
- );
12
+ // Memoized on the in-flight promise, not the resolved value: a cold start takes several
13
+ // concurrent requests, and caching only the finished value lets each of them build its own
14
+ // policy — several rule fetches and several refresh loops where the app should have one.
15
+ protection = buildProtection().catch((err) => {
16
+ protection = undefined; // don't cache a failed boot
17
+ throw err;
18
+ });
23
19
  }
20
+
24
21
  return protection;
25
22
  }
26
23
 
24
+ async function buildProtection() {
25
+ const mode = process.env.PATCHSTACK_MODE === "dry-run" ? "dry-run" : "block";
26
+ const token = process.env.PATCHSTACK_WAF_TOKEN;
27
+ const siteUuid = PS_SITE_UUID.startsWith("__") ? process.env.PATCHSTACK_SITE_UUID : PS_SITE_UUID;
28
+ const common = { mode, egress: true };
29
+ return createProtection(
30
+ siteUuid
31
+ ? { ...common, siteUuid, rules: fallbackRules, cacheDir: ".patchstack" }
32
+ : token
33
+ ? { ...common, token, cacheDir: ".patchstack" }
34
+ : { ...common, rules: fallbackRules },
35
+ );
36
+ }
37
+
27
38
  export async function patchstackFastify(fastify) {
28
39
  const protection = await getProtection();
29
40
  const guard = protection.fetchGuard();
@@ -43,3 +54,14 @@ export async function patchstackFastify(fastify) {
43
54
  }
44
55
  });
45
56
  }
57
+
58
+ // Fastify ENCAPSULATES a registered plugin: hooks added inside one apply to that plugin's context and
59
+ // its children, and to nothing else. Registered as an ordinary plugin, this guard would screen nothing
60
+ // on the root instance and nothing in sibling route plugins — which is most of an application, and it
61
+ // would look installed the whole time.
62
+ //
63
+ // This is the marker `fastify-plugin` sets, and the mechanism Fastify documents for opting out: with it,
64
+ // `register` runs the function against the ROOT instance instead of a child context, so the hook applies
65
+ // to every route. Set here rather than pulling in `fastify-plugin` so an installed app gains no
66
+ // dependency it did not already have.
67
+ patchstackFastify[Symbol.for("skip-override")] = true;
@@ -7,24 +7,41 @@ import fallbackRules from "./rules.json";
7
7
 
8
8
  const PS_SITE_UUID = "__PATCHSTACK_SITE_UUID__";
9
9
 
10
- let _protection: Awaited<ReturnType<typeof createProtection>> | undefined;
10
+ /**
11
+ * One protection policy, memoized on the IN-FLIGHT promise rather than the resolved value.
12
+ *
13
+ * A cold start takes several concurrent requests. Caching only the finished value lets each of them see an
14
+ * empty cache and start its own build — several rule fetches, several refresh loops, and several policies
15
+ * where the app is meant to have one. Holding the promise means the first request starts it and the rest
16
+ * await the same one. A failed build is not cached, so the next request retries rather than inheriting one
17
+ * bad boot for the life of the process.
18
+ */
19
+ let _protection: Promise<Awaited<ReturnType<typeof createProtection>>> | undefined;
11
20
  async function getProtection() {
12
21
  if (!_protection) {
13
- const mode = process.env.PATCHSTACK_MODE === "dry-run" ? "dry-run" : "block";
14
- const token = process.env.PATCHSTACK_WAF_TOKEN;
15
- const siteUuid = PS_SITE_UUID.startsWith("__") ? process.env.PATCHSTACK_SITE_UUID : PS_SITE_UUID;
16
- const common = { mode, egress: true } as const;
17
- _protection = await createProtection(
18
- siteUuid
19
- ? { ...common, siteUuid, rules: fallbackRules as never, cacheDir: ".patchstack" }
20
- : token
21
- ? { ...common, token, cacheDir: ".patchstack" }
22
- : { ...common, rules: fallbackRules as never },
23
- );
22
+ _protection = buildProtection().catch((err) => {
23
+ _protection = undefined; // don't cache a failed boot
24
+ throw err;
25
+ });
24
26
  }
27
+
25
28
  return _protection;
26
29
  }
27
30
 
31
+ async function buildProtection() {
32
+ const mode = process.env.PATCHSTACK_MODE === "dry-run" ? "dry-run" : "block";
33
+ const token = process.env.PATCHSTACK_WAF_TOKEN;
34
+ const siteUuid = PS_SITE_UUID.startsWith("__") ? process.env.PATCHSTACK_SITE_UUID : PS_SITE_UUID;
35
+ const common = { mode, egress: true } as const;
36
+ return createProtection(
37
+ siteUuid
38
+ ? { ...common, siteUuid, rules: fallbackRules as never, cacheDir: ".patchstack" }
39
+ : token
40
+ ? { ...common, token, cacheDir: ".patchstack" }
41
+ : { ...common, rules: fallbackRules as never },
42
+ );
43
+ }
44
+
28
45
  // #region patchstack-fastify (managed by patchstack-connect protect — do not edit)
29
46
  export async function patchstackFastify(fastify: any) {
30
47
  const protection = await getProtection();
@@ -46,3 +63,14 @@ export async function patchstackFastify(fastify: any) {
46
63
  });
47
64
  }
48
65
  // #endregion patchstack-fastify
66
+
67
+ // Fastify ENCAPSULATES a registered plugin: hooks added inside one apply to that plugin's context and
68
+ // its children, and to nothing else. Registered as an ordinary plugin, this guard would screen nothing
69
+ // on the root instance and nothing in sibling route plugins — which is most of an application, and it
70
+ // would look installed the whole time.
71
+ //
72
+ // This is the marker `fastify-plugin` sets, and the mechanism Fastify documents for opting out: with it,
73
+ // `register` runs the function against the ROOT instance instead of a child context, so the hook applies
74
+ // to every route. Set here rather than pulling in `fastify-plugin` so an installed app gains no
75
+ // dependency it did not already have.
76
+ (patchstackFastify as any)[Symbol.for("skip-override")] = true;
@@ -8,30 +8,46 @@ let protection;
8
8
 
9
9
  async function getProtection() {
10
10
  if (!protection) {
11
- const mode = process.env.PATCHSTACK_MODE === "dry-run" ? "dry-run" : "block";
12
- const token = process.env.PATCHSTACK_WAF_TOKEN;
13
- const siteUuid = PS_SITE_UUID.startsWith("__") ? process.env.PATCHSTACK_SITE_UUID : PS_SITE_UUID;
14
- const common = { mode, egress: true };
15
- protection = await createProtection(
16
- siteUuid
17
- ? { ...common, siteUuid, rules: fallbackRules, cacheDir: ".patchstack" }
18
- : token
19
- ? { ...common, token, cacheDir: ".patchstack" }
20
- : { ...common, rules: fallbackRules },
21
- );
11
+ // Memoized on the in-flight promise, not the resolved value: a cold start takes several
12
+ // concurrent requests, and caching only the finished value lets each of them build its own
13
+ // policy — several rule fetches and several refresh loops where the app should have one.
14
+ protection = buildProtection().catch((err) => {
15
+ protection = undefined; // don't cache a failed boot
16
+ throw err;
17
+ });
22
18
  }
19
+
23
20
  return protection;
24
21
  }
25
22
 
23
+ async function buildProtection() {
24
+ const mode = process.env.PATCHSTACK_MODE === "dry-run" ? "dry-run" : "block";
25
+ const token = process.env.PATCHSTACK_WAF_TOKEN;
26
+ const siteUuid = PS_SITE_UUID.startsWith("__") ? process.env.PATCHSTACK_SITE_UUID : PS_SITE_UUID;
27
+ const common = { mode, egress: true };
28
+ return createProtection(
29
+ siteUuid
30
+ ? { ...common, siteUuid, rules: fallbackRules, cacheDir: ".patchstack" }
31
+ : token
32
+ ? { ...common, token, cacheDir: ".patchstack" }
33
+ : { ...common, rules: fallbackRules },
34
+ );
35
+ }
36
+
26
37
  function protectFetch(handler) {
27
38
  return async (request, ...rest) => {
28
39
  const active = await getProtection();
29
40
  const blocked = await active.fetchGuard()(request);
30
41
  if (blocked) return blocked;
31
- return active.screenResponse(await handler(request, ...rest));
42
+ // Response rules can be scoped to a route or a method, and the engine can only apply that scope if it is
43
+ // given the request the response belongs to. Passed through here for that reason: without it a scoped
44
+ // response rule is delivered, counted as protection, and never matches anything.
45
+ return active.screenResponse(await handler(request, ...rest), request);
32
46
  };
33
47
  }
34
48
 
49
+ // Node / Connect: app.use(patchstackMiddleware) — before any body parser. This guard reads the request
50
+ // stream itself and exposes what it read as req.body, so a parser is not also needed.
35
51
  function patchstackMiddleware(req, res, next) {
36
52
  getProtection()
37
53
  .then((active) => active.node()(req, res, next))
@@ -9,32 +9,47 @@ let protection;
9
9
 
10
10
  export async function getProtection() {
11
11
  if (!protection) {
12
- const mode = process.env.PATCHSTACK_MODE === "dry-run" ? "dry-run" : "block";
13
- const token = process.env.PATCHSTACK_WAF_TOKEN;
14
- const siteUuid = PS_SITE_UUID.startsWith("__") ? process.env.PATCHSTACK_SITE_UUID : PS_SITE_UUID;
15
- const common = { mode, egress: true };
16
- protection = await createProtection(
17
- siteUuid
18
- ? { ...common, siteUuid, rules: fallbackRules, cacheDir: ".patchstack" }
19
- : token
20
- ? { ...common, token, cacheDir: ".patchstack" }
21
- : { ...common, rules: fallbackRules },
22
- );
12
+ // Memoized on the in-flight promise, not the resolved value: a cold start takes several
13
+ // concurrent requests, and caching only the finished value lets each of them build its own
14
+ // policy — several rule fetches and several refresh loops where the app should have one.
15
+ protection = buildProtection().catch((err) => {
16
+ protection = undefined; // don't cache a failed boot
17
+ throw err;
18
+ });
23
19
  }
20
+
24
21
  return protection;
25
22
  }
26
23
 
24
+ async function buildProtection() {
25
+ const mode = process.env.PATCHSTACK_MODE === "dry-run" ? "dry-run" : "block";
26
+ const token = process.env.PATCHSTACK_WAF_TOKEN;
27
+ const siteUuid = PS_SITE_UUID.startsWith("__") ? process.env.PATCHSTACK_SITE_UUID : PS_SITE_UUID;
28
+ const common = { mode, egress: true };
29
+ return createProtection(
30
+ siteUuid
31
+ ? { ...common, siteUuid, rules: fallbackRules, cacheDir: ".patchstack" }
32
+ : token
33
+ ? { ...common, token, cacheDir: ".patchstack" }
34
+ : { ...common, rules: fallbackRules },
35
+ );
36
+ }
37
+
27
38
  // Web-Fetch: export default { fetch: protectFetch(originalFetch) }
28
39
  export function protectFetch(handler) {
29
40
  return async (request, ...rest) => {
30
41
  const active = await getProtection();
31
42
  const blocked = await active.fetchGuard()(request);
32
43
  if (blocked) return blocked;
33
- return active.screenResponse(await handler(request, ...rest));
44
+ // Response rules can be scoped to a route or a method, and the engine can only apply that scope if it is
45
+ // given the request the response belongs to. Passed through here for that reason: without it a scoped
46
+ // response rule is delivered, counted as protection, and never matches anything.
47
+ return active.screenResponse(await handler(request, ...rest), request);
34
48
  };
35
49
  }
36
50
 
37
- // Node / Express: app.use(patchstackMiddleware)
51
+ // Node / Connect: app.use(patchstackMiddleware) — before any body parser. This guard reads the request
52
+ // stream itself and exposes what it read as req.body, so a parser is not also needed.
38
53
  export function patchstackMiddleware(req, res, next) {
39
54
  getProtection()
40
55
  .then((active) => active.node()(req, res, next))