@patchstack/connect 0.4.2 → 0.5.1
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 +24 -4
- package/README.md +27 -3
- package/dist/cli.js +319 -61
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +230 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +65 -0
- package/dist/index.d.ts +65 -0
- package/dist/index.js +227 -17
- package/dist/index.js.map +1 -1
- package/dist/protect/templates/astro-middleware.ts +22 -1
- package/dist/protect/templates/express-guard.cjs +54 -4
- package/dist/protect/templates/express-guard.js +54 -4
- package/dist/protect/templates/express-guard.ts +43 -5
- package/dist/protect/templates/fastify-plugin.cjs +41 -3
- package/dist/protect/templates/fastify-plugin.js +41 -3
- package/dist/protect/templates/fastify-plugin.ts +30 -2
- package/dist/protect/templates/generic-guard.cjs +58 -5
- package/dist/protect/templates/generic-guard.js +58 -5
- package/dist/protect/templates/generic-guard.ts +47 -4
- package/dist/protect/templates/guard.ts +42 -8
- package/dist/protect/templates/next-middleware.ts +22 -1
- package/dist/protect/templates/nuxt-middleware.ts +22 -1
- package/dist/protect/templates/sveltekit-hooks.ts +22 -1
- package/dist/protect.cjs +351 -97
- package/dist/protect.cjs.map +1 -1
- package/dist/protect.edge.js +56 -24
- package/dist/protect.edge.js.map +1 -1
- package/dist/protect.js +57 -25
- package/dist/protect.js.map +1 -1
- package/dist/{refresh-manifest-JNCTIAM5.js → refresh-manifest-2MMPQN2A.js} +243 -35
- package/dist/refresh-manifest-2MMPQN2A.js.map +1 -0
- package/package.json +3 -3
- package/dist/refresh-manifest-JNCTIAM5.js.map +0 -1
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
// Patchstack runtime protection — GENERIC guard (CommonJS). Managed by `patchstack-connect protect`.
|
|
2
2
|
// Wire whichever helper fits your server into your request path, then run `protect --check`.
|
|
3
3
|
const { createProtection } = require("@patchstack/connect/protect");
|
|
4
|
-
|
|
4
|
+
// The fallback bundle is optional at RUNTIME. This file is imported on the app's own module path, so a
|
|
5
|
+
// throw here is the app failing to boot rather than protection failing open — and a rules file can be
|
|
6
|
+
// absent for ordinary reasons: a bundler that copied no JSON, a partial deploy, a half-written edit.
|
|
7
|
+
// Without it the guard holds no local bundle, and says so. Its other two rule sources are untouched:
|
|
8
|
+
// live rules for a configured site, and the engine's own compiled response/egress policy.
|
|
9
|
+
let fallbackRules;
|
|
10
|
+
try {
|
|
11
|
+
fallbackRules = require("./rules.json");
|
|
12
|
+
} catch (err) {
|
|
13
|
+
console.warn("[patchstack] ./rules.json was not read (" + err.message + "); the guard holds no local rules");
|
|
14
|
+
}
|
|
5
15
|
|
|
6
16
|
const PS_SITE_UUID = "__PATCHSTACK_SITE_UUID__";
|
|
7
17
|
let protection;
|
|
@@ -34,9 +44,32 @@ async function buildProtection() {
|
|
|
34
44
|
);
|
|
35
45
|
}
|
|
36
46
|
|
|
47
|
+
|
|
48
|
+
// A protection that could not be built must not become an app that cannot answer. Each seam below asks
|
|
49
|
+
// for one, steps aside when it cannot have one, and leaves the app to carry on unscreened.
|
|
50
|
+
// `getProtection` clears its slot on a failed build, so the next request builds again — one bad start
|
|
51
|
+
// does not switch protection off for the life of the process.
|
|
52
|
+
//
|
|
53
|
+
// Only the FIRST failure is reported: enough to know the guard is not screening, without a line per
|
|
54
|
+
// request. A later failure is not reported, including one with a different cause.
|
|
55
|
+
let psUnavailable = false;
|
|
56
|
+
function psStepAside(err) {
|
|
57
|
+
if (!psUnavailable) {
|
|
58
|
+
psUnavailable = true;
|
|
59
|
+
console.warn(
|
|
60
|
+
"[patchstack] protection is unavailable; traffic may pass through unscreened until a later attempt succeeds. Reported once per process. Cause: " +
|
|
61
|
+
(err instanceof Error ? err.message : String(err)),
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
37
67
|
function protectFetch(handler) {
|
|
38
68
|
return async (request, ...rest) => {
|
|
39
|
-
const active = await getProtection();
|
|
69
|
+
const active = await getProtection().catch(psStepAside);
|
|
70
|
+
// The handler, and nothing around it: an exception the app throws is the app's, not a protection
|
|
71
|
+
// failure, and must not be read as one or answered twice.
|
|
72
|
+
if (!active) return handler(request, ...rest);
|
|
40
73
|
const blocked = await active.fetchGuard()(request);
|
|
41
74
|
if (blocked) return blocked;
|
|
42
75
|
// Response rules can be scoped to a route or a method, and the engine can only apply that scope if it is
|
|
@@ -49,9 +82,29 @@ function protectFetch(handler) {
|
|
|
49
82
|
// Node / Connect: app.use(patchstackMiddleware) — before any body parser. This guard reads the request
|
|
50
83
|
// stream itself and exposes what it read as req.body, so a parser is not also needed.
|
|
51
84
|
function patchstackMiddleware(req, res, next) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
85
|
+
// `next` is wrapped so it can run at most once, whatever happens. That is what makes the two
|
|
86
|
+
// handlers below safe: a rejection handler passed to `then` sees only a failed build, the trailing
|
|
87
|
+
// one sees whatever the guard or the app's own chain threw and reports it — and neither can pass a
|
|
88
|
+
// request on that was already passed on, nor leave one that never was.
|
|
89
|
+
let passedOn = false;
|
|
90
|
+
const carryOn = (err) => {
|
|
91
|
+
if (passedOn) return;
|
|
92
|
+
passedOn = true;
|
|
93
|
+
next(err);
|
|
94
|
+
};
|
|
95
|
+
getProtection().then(
|
|
96
|
+
(active) => active.node()(req, res, carryOn),
|
|
97
|
+
(err) => {
|
|
98
|
+
psStepAside(err);
|
|
99
|
+
carryOn();
|
|
100
|
+
},
|
|
101
|
+
).catch((err) => {
|
|
102
|
+
// Not a failed build: the guard, or the app's own chain, threw after this point. Reported, and the
|
|
103
|
+
// request is carried on only if it never was — an error here must not take the process down and
|
|
104
|
+
// must not answer twice.
|
|
105
|
+
psStepAside(err);
|
|
106
|
+
carryOn();
|
|
107
|
+
});
|
|
55
108
|
}
|
|
56
109
|
|
|
57
110
|
module.exports = { getProtection, protectFetch, patchstackMiddleware };
|
|
@@ -3,7 +3,17 @@
|
|
|
3
3
|
import { readFileSync } from "node:fs";
|
|
4
4
|
import { createProtection } from "@patchstack/connect/protect";
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
// The fallback bundle is optional at RUNTIME. This file is imported on the app's own module path, so a
|
|
7
|
+
// throw here is the app failing to boot rather than protection failing open — and a rules file can be
|
|
8
|
+
// absent for ordinary reasons: a bundler that copied no JSON, a partial deploy, a half-written edit.
|
|
9
|
+
// Without it the guard holds no local bundle, and says so. Its other two rule sources are untouched:
|
|
10
|
+
// live rules for a configured site, and the engine's own compiled response/egress policy.
|
|
11
|
+
let fallbackRules;
|
|
12
|
+
try {
|
|
13
|
+
fallbackRules = JSON.parse(readFileSync(new URL("./rules.json", import.meta.url), "utf8"));
|
|
14
|
+
} catch (err) {
|
|
15
|
+
console.warn("[patchstack] ./rules.json was not read (" + err.message + "); the guard holds no local rules");
|
|
16
|
+
}
|
|
7
17
|
const PS_SITE_UUID = "__PATCHSTACK_SITE_UUID__";
|
|
8
18
|
let protection;
|
|
9
19
|
|
|
@@ -35,10 +45,33 @@ async function buildProtection() {
|
|
|
35
45
|
);
|
|
36
46
|
}
|
|
37
47
|
|
|
48
|
+
|
|
49
|
+
// A protection that could not be built must not become an app that cannot answer. Each seam below asks
|
|
50
|
+
// for one, steps aside when it cannot have one, and leaves the app to carry on unscreened.
|
|
51
|
+
// `getProtection` clears its slot on a failed build, so the next request builds again — one bad start
|
|
52
|
+
// does not switch protection off for the life of the process.
|
|
53
|
+
//
|
|
54
|
+
// Only the FIRST failure is reported: enough to know the guard is not screening, without a line per
|
|
55
|
+
// request. A later failure is not reported, including one with a different cause.
|
|
56
|
+
let psUnavailable = false;
|
|
57
|
+
function psStepAside(err) {
|
|
58
|
+
if (!psUnavailable) {
|
|
59
|
+
psUnavailable = true;
|
|
60
|
+
console.warn(
|
|
61
|
+
"[patchstack] protection is unavailable; traffic may pass through unscreened until a later attempt succeeds. Reported once per process. Cause: " +
|
|
62
|
+
(err instanceof Error ? err.message : String(err)),
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
38
68
|
// Web-Fetch: export default { fetch: protectFetch(originalFetch) }
|
|
39
69
|
export function protectFetch(handler) {
|
|
40
70
|
return async (request, ...rest) => {
|
|
41
|
-
const active = await getProtection();
|
|
71
|
+
const active = await getProtection().catch(psStepAside);
|
|
72
|
+
// The handler, and nothing around it: an exception the app throws is the app's, not a protection
|
|
73
|
+
// failure, and must not be read as one or answered twice.
|
|
74
|
+
if (!active) return handler(request, ...rest);
|
|
42
75
|
const blocked = await active.fetchGuard()(request);
|
|
43
76
|
if (blocked) return blocked;
|
|
44
77
|
// Response rules can be scoped to a route or a method, and the engine can only apply that scope if it is
|
|
@@ -51,7 +84,27 @@ export function protectFetch(handler) {
|
|
|
51
84
|
// Node / Connect: app.use(patchstackMiddleware) — before any body parser. This guard reads the request
|
|
52
85
|
// stream itself and exposes what it read as req.body, so a parser is not also needed.
|
|
53
86
|
export function patchstackMiddleware(req, res, next) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
87
|
+
// `next` is wrapped so it can run at most once, whatever happens. That is what makes the two
|
|
88
|
+
// handlers below safe: a rejection handler passed to `then` sees only a failed build, the trailing
|
|
89
|
+
// one sees whatever the guard or the app's own chain threw and reports it — and neither can pass a
|
|
90
|
+
// request on that was already passed on, nor leave one that never was.
|
|
91
|
+
let passedOn = false;
|
|
92
|
+
const carryOn = (err) => {
|
|
93
|
+
if (passedOn) return;
|
|
94
|
+
passedOn = true;
|
|
95
|
+
next(err);
|
|
96
|
+
};
|
|
97
|
+
getProtection().then(
|
|
98
|
+
(active) => active.node()(req, res, carryOn),
|
|
99
|
+
(err) => {
|
|
100
|
+
psStepAside(err);
|
|
101
|
+
carryOn();
|
|
102
|
+
},
|
|
103
|
+
).catch((err) => {
|
|
104
|
+
// Not a failed build: the guard, or the app's own chain, threw after this point. Reported, and the
|
|
105
|
+
// request is carried on only if it never was — an error here must not take the process down and
|
|
106
|
+
// must not answer twice.
|
|
107
|
+
psStepAside(err);
|
|
108
|
+
carryOn();
|
|
109
|
+
});
|
|
57
110
|
}
|
|
@@ -52,11 +52,34 @@ async function buildProtection() {
|
|
|
52
52
|
);
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
|
|
56
|
+
// A protection that could not be built must not become an app that cannot answer. Each seam below asks
|
|
57
|
+
// for one, steps aside when it cannot have one, and leaves the app to carry on unscreened.
|
|
58
|
+
// `getProtection` clears its slot on a failed build, so the next request builds again — one bad start
|
|
59
|
+
// does not switch protection off for the life of the process.
|
|
60
|
+
//
|
|
61
|
+
// Only the FIRST failure is reported: enough to know the guard is not screening, without a line per
|
|
62
|
+
// request. A later failure is not reported, including one with a different cause.
|
|
63
|
+
let psUnavailable = false;
|
|
64
|
+
function psStepAside(err: unknown) {
|
|
65
|
+
if (!psUnavailable) {
|
|
66
|
+
psUnavailable = true;
|
|
67
|
+
console.warn(
|
|
68
|
+
"[patchstack] protection is unavailable; traffic may pass through unscreened until a later attempt succeeds. Reported once per process. Cause: " +
|
|
69
|
+
(err instanceof Error ? err.message : String(err)),
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
55
75
|
// --- Web Fetch (Cloudflare Workers, Bun, Deno, Hono, Next edge, TanStack server.ts) ---------
|
|
56
76
|
// Wrap your fetch handler: export default { fetch: protectFetch(originalFetch) }
|
|
57
77
|
export function protectFetch<H extends (request: Request, ...rest: unknown[]) => unknown>(handler: H): H {
|
|
58
78
|
return (async (request: Request, ...rest: unknown[]) => {
|
|
59
|
-
const protection = await getProtection();
|
|
79
|
+
const protection = await getProtection().catch(psStepAside);
|
|
80
|
+
// The handler, and nothing around it: an exception the app throws is the app's, not a protection
|
|
81
|
+
// failure, and must not be read as one or answered twice.
|
|
82
|
+
if (!protection) return handler(request, ...rest);
|
|
60
83
|
const blocked = await protection.fetchGuard()(request);
|
|
61
84
|
if (blocked) return blocked;
|
|
62
85
|
// Response rules can be scoped to a route or a method, and the engine can only apply that scope if it is
|
|
@@ -70,7 +93,27 @@ export function protectFetch<H extends (request: Request, ...rest: unknown[]) =>
|
|
|
70
93
|
// app.use(patchstackMiddleware) — register it before any body parser and before your routes. This guard
|
|
71
94
|
// reads the request stream itself and exposes what it read as req.body, so a parser is not also needed.
|
|
72
95
|
export function patchstackMiddleware(req: unknown, res: unknown, next: (err?: unknown) => void) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
96
|
+
// `next` is wrapped so it can run at most once, whatever happens. That is what makes the two
|
|
97
|
+
// handlers below safe: a rejection handler passed to `then` sees only a failed build, the trailing
|
|
98
|
+
// one sees whatever the guard or the app's own chain threw and reports it — and neither can pass a
|
|
99
|
+
// request on that was already passed on, nor leave one that never was.
|
|
100
|
+
let passedOn = false;
|
|
101
|
+
const carryOn = (err?: unknown) => {
|
|
102
|
+
if (passedOn) return;
|
|
103
|
+
passedOn = true;
|
|
104
|
+
next(err);
|
|
105
|
+
};
|
|
106
|
+
getProtection().then(
|
|
107
|
+
(protection) => (protection.node() as (a: unknown, b: unknown, c: (e?: unknown) => void) => void)(req, res, carryOn),
|
|
108
|
+
(err) => {
|
|
109
|
+
psStepAside(err);
|
|
110
|
+
carryOn();
|
|
111
|
+
},
|
|
112
|
+
).catch((err) => {
|
|
113
|
+
// Not a failed build: the guard, or the app's own chain, threw after this point. Reported, and the
|
|
114
|
+
// request is carried on only if it never was — an error here must not take the process down and
|
|
115
|
+
// must not answer twice.
|
|
116
|
+
psStepAside(err);
|
|
117
|
+
carryOn();
|
|
118
|
+
});
|
|
76
119
|
}
|
|
@@ -74,6 +74,26 @@ async function buildProtection() {
|
|
|
74
74
|
);
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
|
|
78
|
+
// A protection that could not be built must not become an app that cannot answer. Each seam below asks
|
|
79
|
+
// for one, steps aside when it cannot have one, and leaves the app to carry on unscreened.
|
|
80
|
+
// `getProtection` clears its slot on a failed build, so the next request builds again — one bad start
|
|
81
|
+
// does not switch protection off for the life of the process.
|
|
82
|
+
//
|
|
83
|
+
// Only the FIRST failure is reported: enough to know the guard is not screening, without a line per
|
|
84
|
+
// request. A later failure is not reported, including one with a different cause.
|
|
85
|
+
let psUnavailable = false;
|
|
86
|
+
function psStepAside(err: unknown) {
|
|
87
|
+
if (!psUnavailable) {
|
|
88
|
+
psUnavailable = true;
|
|
89
|
+
console.warn(
|
|
90
|
+
"[patchstack] protection is unavailable; traffic may pass through unscreened until a later attempt succeeds. Reported once per process. Cause: " +
|
|
91
|
+
(err instanceof Error ? err.message : String(err)),
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
77
97
|
// Request-middleware path: the browser tunnels its direct Supabase calls here.
|
|
78
98
|
let _handle: ((request: Request) => Promise<Response>) | undefined;
|
|
79
99
|
export async function handleGuardRequest(request: Request): Promise<Response> {
|
|
@@ -91,7 +111,10 @@ export async function handleGuardRequest(request: Request): Promise<Response> {
|
|
|
91
111
|
let _inspect: ((data: unknown) => Promise<{ rule?: string; message: string } | null>) | undefined;
|
|
92
112
|
export async function inspectServerFn(data: unknown): Promise<{ rule?: string; message: string } | null> {
|
|
93
113
|
if (!_inspect) {
|
|
94
|
-
|
|
114
|
+
const protection = await getProtection().catch(psStepAside);
|
|
115
|
+
// Null is "allow" here, which is what the two paths below already answer when they cannot screen.
|
|
116
|
+
if (!protection) return null;
|
|
117
|
+
_inspect = createServerFnGuard({ protection });
|
|
95
118
|
}
|
|
96
119
|
return _inspect(data);
|
|
97
120
|
}
|
|
@@ -105,12 +128,17 @@ export async function inspectServerFn(data: unknown): Promise<{ rule?: string; m
|
|
|
105
128
|
// route or a method, and the engine can only apply that scope if it is given the request the response
|
|
106
129
|
// belongs to. Without it a scoped response rule is delivered, counted as protection, and matches nothing.
|
|
107
130
|
export async function screenResponse<T>(response: T, request?: Request): Promise<T> {
|
|
131
|
+
if (!(response instanceof Response)) return response;
|
|
132
|
+
const protection = await getProtection().catch(psStepAside);
|
|
133
|
+
if (!protection) return response;
|
|
108
134
|
try {
|
|
109
|
-
if (!(response instanceof Response)) return response;
|
|
110
|
-
const protection = await getProtection();
|
|
111
135
|
return (protection.screenResponse ? await protection.screenResponse(response, request) : response) as T;
|
|
112
|
-
} catch {
|
|
113
|
-
|
|
136
|
+
} catch (err) {
|
|
137
|
+
// Reported, then the response goes out as it is. A response that was never screened and a response
|
|
138
|
+
// that had nothing to redact look identical from here, so silence would make them one.
|
|
139
|
+
psStepAside(err);
|
|
140
|
+
|
|
141
|
+
return response;
|
|
114
142
|
}
|
|
115
143
|
}
|
|
116
144
|
|
|
@@ -120,10 +148,16 @@ export async function screenResponse<T>(response: T, request?: Request): Promise
|
|
|
120
148
|
// WAF false-positive surface, so it is opt-in. `request.clone()` so the app can still read the body.
|
|
121
149
|
let _reqGuard: ((request: Request) => Promise<Response | null>) | undefined;
|
|
122
150
|
export async function guardRequest(request: Request): Promise<Response | null> {
|
|
151
|
+
if (!_reqGuard) {
|
|
152
|
+
const protection = await getProtection().catch(psStepAside);
|
|
153
|
+
if (!protection) return null; // null is "allow" here
|
|
154
|
+
_reqGuard = protection.fetchGuard();
|
|
155
|
+
}
|
|
123
156
|
try {
|
|
124
|
-
if (!_reqGuard) _reqGuard = (await getProtection()).fetchGuard();
|
|
125
157
|
return await _reqGuard(request.clone());
|
|
126
|
-
} catch {
|
|
127
|
-
|
|
158
|
+
} catch (err) {
|
|
159
|
+
psStepAside(err);
|
|
160
|
+
|
|
161
|
+
return null;
|
|
128
162
|
}
|
|
129
163
|
}
|
|
@@ -42,9 +42,30 @@ async function buildProtection() {
|
|
|
42
42
|
);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
|
|
46
|
+
// A protection that could not be built must not become an app that cannot answer. Each seam below asks
|
|
47
|
+
// for one, steps aside when it cannot have one, and leaves the app to carry on unscreened.
|
|
48
|
+
// `getProtection` clears its slot on a failed build, so the next request builds again — one bad start
|
|
49
|
+
// does not switch protection off for the life of the process.
|
|
50
|
+
//
|
|
51
|
+
// Only the FIRST failure is reported: enough to know the guard is not screening, without a line per
|
|
52
|
+
// request. A later failure is not reported, including one with a different cause.
|
|
53
|
+
let psUnavailable = false;
|
|
54
|
+
function psStepAside(err: unknown) {
|
|
55
|
+
if (!psUnavailable) {
|
|
56
|
+
psUnavailable = true;
|
|
57
|
+
console.warn(
|
|
58
|
+
"[patchstack] protection is unavailable; traffic may pass through unscreened until a later attempt succeeds. Reported once per process. Cause: " +
|
|
59
|
+
(err instanceof Error ? err.message : String(err)),
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
45
65
|
// #region patchstack-next (managed by patchstack-connect protect — do not edit)
|
|
46
66
|
export async function middleware(request: Request) {
|
|
47
|
-
const protection = await getProtection();
|
|
67
|
+
const protection = await getProtection().catch(psStepAside);
|
|
68
|
+
if (!protection) return; // Next continues to the route
|
|
48
69
|
const blocked = await protection.fetchGuard()(request);
|
|
49
70
|
if (blocked) return blocked; // 403 — blocked before it reaches your route
|
|
50
71
|
// otherwise fall through (Next continues to the route)
|
|
@@ -42,9 +42,30 @@ async function buildProtection() {
|
|
|
42
42
|
);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
|
|
46
|
+
// A protection that could not be built must not become an app that cannot answer. Each seam below asks
|
|
47
|
+
// for one, steps aside when it cannot have one, and leaves the app to carry on unscreened.
|
|
48
|
+
// `getProtection` clears its slot on a failed build, so the next request builds again — one bad start
|
|
49
|
+
// does not switch protection off for the life of the process.
|
|
50
|
+
//
|
|
51
|
+
// Only the FIRST failure is reported: enough to know the guard is not screening, without a line per
|
|
52
|
+
// request. A later failure is not reported, including one with a different cause.
|
|
53
|
+
let psUnavailable = false;
|
|
54
|
+
function psStepAside(err: unknown) {
|
|
55
|
+
if (!psUnavailable) {
|
|
56
|
+
psUnavailable = true;
|
|
57
|
+
console.warn(
|
|
58
|
+
"[patchstack] protection is unavailable; traffic may pass through unscreened until a later attempt succeeds. Reported once per process. Cause: " +
|
|
59
|
+
(err instanceof Error ? err.message : String(err)),
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
45
65
|
// #region patchstack-nuxt (managed by patchstack-connect protect — do not edit)
|
|
46
66
|
export default defineEventHandler(async (event) => {
|
|
47
|
-
const protection = await getProtection();
|
|
67
|
+
const protection = await getProtection().catch(psStepAside);
|
|
68
|
+
if (!protection) return; // Nitro continues to the route
|
|
48
69
|
const method = event.method ?? "GET";
|
|
49
70
|
// readRawBody caches on the event, so your route handlers can still read the body.
|
|
50
71
|
const body = method !== "GET" && method !== "HEAD" ? await readRawBody(event) : undefined;
|
|
@@ -42,9 +42,30 @@ async function buildProtection() {
|
|
|
42
42
|
);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
|
|
46
|
+
// A protection that could not be built must not become an app that cannot answer. Each seam below asks
|
|
47
|
+
// for one, steps aside when it cannot have one, and leaves the app to carry on unscreened.
|
|
48
|
+
// `getProtection` clears its slot on a failed build, so the next request builds again — one bad start
|
|
49
|
+
// does not switch protection off for the life of the process.
|
|
50
|
+
//
|
|
51
|
+
// Only the FIRST failure is reported: enough to know the guard is not screening, without a line per
|
|
52
|
+
// request. A later failure is not reported, including one with a different cause.
|
|
53
|
+
let psUnavailable = false;
|
|
54
|
+
function psStepAside(err: unknown) {
|
|
55
|
+
if (!psUnavailable) {
|
|
56
|
+
psUnavailable = true;
|
|
57
|
+
console.warn(
|
|
58
|
+
"[patchstack] protection is unavailable; traffic may pass through unscreened until a later attempt succeeds. Reported once per process. Cause: " +
|
|
59
|
+
(err instanceof Error ? err.message : String(err)),
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
45
65
|
// #region patchstack-sveltekit (managed by patchstack-connect protect — do not edit)
|
|
46
66
|
export const handle: Handle = async ({ event, resolve }) => {
|
|
47
|
-
const protection = await getProtection();
|
|
67
|
+
const protection = await getProtection().catch(psStepAside);
|
|
68
|
+
if (!protection) return resolve(event);
|
|
48
69
|
const blocked = await protection.fetchGuard()(event.request);
|
|
49
70
|
if (blocked) return blocked; // 403 — blocked before it reaches your route
|
|
50
71
|
// Response rules can be scoped to a route or a method, and the engine can only apply that scope if it is
|