@patchstack/connect 0.4.1 → 0.5.0

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 (34) hide show
  1. package/AGENT-INSTALL.md +21 -4
  2. package/README.md +10 -2
  3. package/dist/cli.js +277 -56
  4. package/dist/cli.js.map +1 -1
  5. package/dist/index.cjs +221 -18
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.d.cts +37 -0
  8. package/dist/index.d.ts +37 -0
  9. package/dist/index.js +218 -15
  10. package/dist/index.js.map +1 -1
  11. package/dist/protect/templates/astro-middleware.ts +22 -1
  12. package/dist/protect/templates/express-guard.cjs +54 -4
  13. package/dist/protect/templates/express-guard.js +54 -4
  14. package/dist/protect/templates/express-guard.ts +43 -5
  15. package/dist/protect/templates/fastify-plugin.cjs +41 -3
  16. package/dist/protect/templates/fastify-plugin.js +41 -3
  17. package/dist/protect/templates/fastify-plugin.ts +30 -2
  18. package/dist/protect/templates/generic-guard.cjs +58 -5
  19. package/dist/protect/templates/generic-guard.js +58 -5
  20. package/dist/protect/templates/generic-guard.ts +47 -4
  21. package/dist/protect/templates/guard.ts +42 -8
  22. package/dist/protect/templates/next-middleware.ts +22 -1
  23. package/dist/protect/templates/nuxt-middleware.ts +22 -1
  24. package/dist/protect/templates/sveltekit-hooks.ts +22 -1
  25. package/dist/protect.cjs +341 -94
  26. package/dist/protect.cjs.map +1 -1
  27. package/dist/protect.edge.js +56 -24
  28. package/dist/protect.edge.js.map +1 -1
  29. package/dist/protect.js +57 -25
  30. package/dist/protect.js.map +1 -1
  31. package/dist/{refresh-manifest-SWCPQ52Z.js → refresh-manifest-LDS35UKV.js} +234 -33
  32. package/dist/refresh-manifest-LDS35UKV.js.map +1 -0
  33. package/package.json +4 -4
  34. package/dist/refresh-manifest-SWCPQ52Z.js.map +0 -1
@@ -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
- _inspect = createServerFnGuard({ protection: await getProtection() });
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
- return response; // fail open
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
- return null; // fail open
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