@patchstack/connect 0.5.27 → 0.5.32

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 +75 -25
  2. package/README.md +28 -14
  3. package/dist/{chunk-YG5C7HYB.js → chunk-52IV7LN7.js} +1 -1
  4. package/dist/{chunk-YG5C7HYB.js.map → chunk-52IV7LN7.js.map} +1 -1
  5. package/dist/cli.js +2749 -372
  6. package/dist/cli.js.map +1 -1
  7. package/dist/index.cjs +10 -6
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.d.cts +5 -0
  10. package/dist/index.d.ts +5 -0
  11. package/dist/index.js +10 -6
  12. package/dist/index.js.map +1 -1
  13. package/dist/protect/templates/astro-middleware.ts +5 -4
  14. package/dist/protect/templates/express-guard.cjs +5 -8
  15. package/dist/protect/templates/express-guard.js +5 -8
  16. package/dist/protect/templates/express-guard.ts +5 -8
  17. package/dist/protect/templates/fastify-plugin.cjs +52 -6
  18. package/dist/protect/templates/fastify-plugin.js +52 -6
  19. package/dist/protect/templates/fastify-plugin.ts +53 -7
  20. package/dist/protect/templates/fetch-guard.ts +52 -0
  21. package/dist/protect/templates/generic-guard.cjs +6 -5
  22. package/dist/protect/templates/generic-guard.js +6 -5
  23. package/dist/protect/templates/generic-guard.ts +5 -8
  24. package/dist/protect/templates/guard.ts +4 -7
  25. package/dist/protect/templates/next-guard.ts +5 -4
  26. package/dist/protect/templates/next-middleware.ts +5 -4
  27. package/dist/protect/templates/nuxt-middleware.ts +5 -4
  28. package/dist/protect/templates/sveltekit-hooks.ts +5 -4
  29. package/dist/protect.cjs +10 -6
  30. package/dist/protect.cjs.map +1 -1
  31. package/dist/protect.d.cts +1 -1
  32. package/dist/protect.d.ts +1 -1
  33. package/dist/protect.edge.js +2 -2
  34. package/dist/protect.edge.js.map +1 -1
  35. package/dist/protect.js +4 -4
  36. package/dist/protect.js.map +1 -1
  37. package/dist/{refresh-manifest-THITZHBD.js → refresh-manifest-EBORKM75.js} +10 -6
  38. package/dist/refresh-manifest-EBORKM75.js.map +1 -0
  39. package/package.json +5 -2
  40. package/dist/refresh-manifest-THITZHBD.js.map +0 -1
@@ -1,6 +1,6 @@
1
1
  // Patchstack runtime guard for Fastify — plugin. Managed by `patchstack-connect protect`.
2
2
  // Register it once (`app.register(patchstackFastify)`); it adds a preHandler hook that runs the
3
- // request-phase WAF (+ egress SSRF) on every request. Fastify's request/reply aren't Web-Fetch
3
+ // request-phase WAF (+ egress SSRF) and filters buffered onSend output. Fastify's request/reply aren't Web-Fetch
4
4
  // shaped, so we reconstruct a Request from the parsed fastify request and run the fetch guard.
5
5
  import { createProtection, sentinelAnswer, VERIFY_HEADER } from "@patchstack/connect/protect";
6
6
  import fallbackRules from "./rules.json";
@@ -29,10 +29,11 @@ async function getProtection() {
29
29
  }
30
30
 
31
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;
32
+ const mode = (typeof process === "undefined" ? undefined : process.env.PATCHSTACK_MODE) === "dry-run" ? "dry-run" : "block";
33
+ const token = (typeof process === "undefined" ? undefined : process.env.PATCHSTACK_WAF_TOKEN);
34
+ const siteUuid = PS_SITE_UUID.startsWith("__") ? (typeof process === "undefined" ? undefined : process.env.PATCHSTACK_SITE_UUID) : PS_SITE_UUID;
35
+ const refreshMs = (typeof process === "undefined" ? undefined : process.env.PATCHSTACK_ENVIRONMENT) === "sandbox" ? 15000 : 300000;
36
+ const common = { mode, egress: true, refreshMs } as const;
36
37
  return createProtection(
37
38
  siteUuid
38
39
  ? { ...common, siteUuid, rules: fallbackRules as never, cacheDir: ".patchstack" }
@@ -92,8 +93,23 @@ export async function patchstackFastify(fastify: any) {
92
93
  const host = request.headers?.host ?? "localhost";
93
94
  const url = `http://${host}${request.url ?? "/"}`;
94
95
  const hasBody = method !== "GET" && method !== "HEAD" && request.body != null;
95
- const body = hasBody ? (typeof request.body === "string" ? request.body : JSON.stringify(request.body)) : undefined;
96
- const blocked = await guard(new Request(url, { method, headers: request.headers as HeadersInit, body }));
96
+ let blocked;
97
+ try {
98
+ const headers = new Headers();
99
+ for (const [name, value] of Object.entries(request.headers)) {
100
+ if (value !== undefined) headers.set(name, Array.isArray(value) ? value.join(", ") : String(value));
101
+ }
102
+ const parsed = hasBody && typeof request.body === "object" && !Buffer.isBuffer(request.body);
103
+ const body = hasBody ? (parsed ? JSON.stringify(request.body) : request.body) : undefined;
104
+ // Fastify already parsed form fields. Screen that object as JSON, not JSON mislabeled as a form.
105
+ // Only the screening copy changes; the route keeps its original body, headers and raw stream.
106
+ if (parsed) headers.set("content-type", "application/json");
107
+ headers.delete("content-length");
108
+ blocked = await guard(new Request(url, { method, headers, body }));
109
+ } catch (err) {
110
+ psStepAside(err);
111
+ return;
112
+ }
97
113
  if (blocked) {
98
114
  const contentType = blocked.headers.get("content-type");
99
115
  reply.code(blocked.status);
@@ -102,6 +118,36 @@ export async function patchstackFastify(fastify: any) {
102
118
  return reply; // stop the request here
103
119
  }
104
120
  });
121
+
122
+ fastify.addHook("onSend", async (request: any, reply: any, payload: any) => {
123
+ // Do not consume streams, hijacked responses, or bodyless status codes.
124
+ if ((typeof payload !== "string" && !Buffer.isBuffer(payload))
125
+ || request.method === "HEAD" || reply.statusCode < 200 || [204, 205, 304].includes(reply.statusCode)) return payload;
126
+ const protection = await getProtection().catch(psStepAside);
127
+ if (!protection) return payload;
128
+ try {
129
+ const headers = new Headers();
130
+ for (const [name, value] of Object.entries(reply.getHeaders())) {
131
+ for (const item of Array.isArray(value) ? value : [value]) {
132
+ if (item !== undefined) headers.append(name, String(item));
133
+ }
134
+ }
135
+ const original = new Response(typeof payload === "string" ? payload : new Uint8Array(payload).buffer, { status: reply.statusCode, headers });
136
+ const context = new Request(`http://${request.headers?.host ?? "localhost"}${request.url ?? "/"}`, { method: request.method, headers: request.headers });
137
+ const screened = await protection.screenResponse(original, context);
138
+ if (screened === original) return payload;
139
+ const body = Buffer.from(await screened.arrayBuffer());
140
+ reply.code(screened.status);
141
+ for (const name of Object.keys(reply.getHeaders())) reply.removeHeader(name);
142
+ screened.headers.forEach((value, name) => { if (name !== "set-cookie") reply.header(name, value); });
143
+ const cookies = screened.headers.getSetCookie();
144
+ if (cookies.length) reply.header("set-cookie", cookies);
145
+ return body;
146
+ } catch (err) {
147
+ psStepAside(err);
148
+ return payload;
149
+ }
150
+ });
105
151
  }
106
152
  // #endregion patchstack-fastify
107
153
 
@@ -0,0 +1,52 @@
1
+ // Server-only Fetch guard for bundled applications. Never import this module into client code.
2
+ import { createProtection, sentinelAnswer, VERIFY_HEADER } from "@patchstack/connect/protect";
3
+ import fallbackRules from "./rules.json";
4
+
5
+ const PS_SITE_UUID = "__PATCHSTACK_SITE_UUID__";
6
+ type Protection = Awaited<ReturnType<typeof createProtection>>;
7
+ const policies = new WeakMap<object, Promise<Protection>>();
8
+ const emptyEnvironment = {};
9
+ let warned = false;
10
+
11
+ function environment(value: unknown): Record<string, unknown> {
12
+ if (value && typeof value === "object" && ["PATCHSTACK_API_KEY", "PATCHSTACK_SITE_UUID", "PATCHSTACK_PULSE_AUTH"].some(key => key in value)) {
13
+ return value as Record<string, unknown>;
14
+ }
15
+ return typeof process === "undefined" ? emptyEnvironment : process.env;
16
+ }
17
+
18
+ async function getProtection(bindings: unknown) {
19
+ const env = environment(bindings);
20
+ let pending = policies.get(env);
21
+ if (!pending) {
22
+ const value = (key: string) => typeof env[key] === "string" ? env[key] as string : undefined;
23
+ pending = createProtection({
24
+ siteUuid: PS_SITE_UUID.startsWith("__") ? value("PATCHSTACK_SITE_UUID") : PS_SITE_UUID,
25
+ apiKey: value("PATCHSTACK_API_KEY"),
26
+ pulseAuth: value("PATCHSTACK_PULSE_AUTH") ?? value("PATCHSTACK_API_KEY"),
27
+ mode: value("PATCHSTACK_MODE") === "dry-run" ? "dry-run" : "block",
28
+ rules: fallbackRules as never,
29
+ cacheDir: ".patchstack",
30
+ refreshMs: value("PATCHSTACK_ENVIRONMENT") === "sandbox" ? 15000 : 300000,
31
+ egress: true,
32
+ }).catch(error => { policies.delete(env); throw error; });
33
+ policies.set(env, pending);
34
+ }
35
+ return pending.catch(error => {
36
+ if (!warned) {
37
+ warned = true;
38
+ console.warn("[patchstack] protection is unavailable; traffic may pass through unscreened until a later attempt succeeds.", error instanceof Error ? error.message : String(error));
39
+ }
40
+ return null;
41
+ });
42
+ }
43
+
44
+ export function protectFetch<Args extends unknown[]>(handler: (request: Request, ...args: Args) => Response | Promise<Response>) {
45
+ return async function(this: unknown, request: Request, ...args: Args): Promise<Response> {
46
+ const answered = await sentinelAnswer(request.headers.get(VERIFY_HEADER));
47
+ if (answered) return new Response(answered, { headers: { "content-type": "text/plain" } });
48
+ const protection = await getProtection(args[0]);
49
+ // Only initialization fails open here. Application failures propagate without repeating the handler.
50
+ return protection ? protection.fetch(handler).call(this, request, ...args) : handler.call(this, request, ...args);
51
+ };
52
+ }
@@ -31,10 +31,11 @@ async function getProtection() {
31
31
  }
32
32
 
33
33
  async function buildProtection() {
34
- const mode = process.env.PATCHSTACK_MODE === "dry-run" ? "dry-run" : "block";
35
- const token = process.env.PATCHSTACK_WAF_TOKEN;
36
- const siteUuid = PS_SITE_UUID.startsWith("__") ? process.env.PATCHSTACK_SITE_UUID : PS_SITE_UUID;
37
- const common = { mode, egress: true };
34
+ const mode = (typeof process === "undefined" ? undefined : process.env.PATCHSTACK_MODE) === "dry-run" ? "dry-run" : "block";
35
+ const token = (typeof process === "undefined" ? undefined : process.env.PATCHSTACK_WAF_TOKEN);
36
+ const siteUuid = PS_SITE_UUID.startsWith("__") ? (typeof process === "undefined" ? undefined : process.env.PATCHSTACK_SITE_UUID) : PS_SITE_UUID;
37
+ const refreshMs = (typeof process === "undefined" ? undefined : process.env.PATCHSTACK_ENVIRONMENT) === "sandbox" ? 15000 : 300000;
38
+ const common = { mode, egress: true, refreshMs };
38
39
  return createProtection(
39
40
  siteUuid
40
41
  ? { ...common, siteUuid, rules: fallbackRules, cacheDir: ".patchstack" }
@@ -105,7 +106,7 @@ function patchstackMiddleware(req, res, next) {
105
106
  // there is nothing to answer and the request is screened as normal.
106
107
  const screen = () => {
107
108
  getProtection().then(
108
- (active) => active.node()(req, res, carryOn),
109
+ (active) => active.node({ screenResponses: true })(req, res, carryOn),
109
110
  (err) => {
110
111
  psStepAside(err);
111
112
  carryOn();
@@ -32,10 +32,11 @@ export async function getProtection() {
32
32
  }
33
33
 
34
34
  async function buildProtection() {
35
- const mode = process.env.PATCHSTACK_MODE === "dry-run" ? "dry-run" : "block";
36
- const token = process.env.PATCHSTACK_WAF_TOKEN;
37
- const siteUuid = PS_SITE_UUID.startsWith("__") ? process.env.PATCHSTACK_SITE_UUID : PS_SITE_UUID;
38
- const common = { mode, egress: true };
35
+ const mode = (typeof process === "undefined" ? undefined : process.env.PATCHSTACK_MODE) === "dry-run" ? "dry-run" : "block";
36
+ const token = (typeof process === "undefined" ? undefined : process.env.PATCHSTACK_WAF_TOKEN);
37
+ const siteUuid = PS_SITE_UUID.startsWith("__") ? (typeof process === "undefined" ? undefined : process.env.PATCHSTACK_SITE_UUID) : PS_SITE_UUID;
38
+ const refreshMs = (typeof process === "undefined" ? undefined : process.env.PATCHSTACK_ENVIRONMENT) === "sandbox" ? 15000 : 300000;
39
+ const common = { mode, egress: true, refreshMs };
39
40
  return createProtection(
40
41
  siteUuid
41
42
  ? { ...common, siteUuid, rules: fallbackRules, cacheDir: ".patchstack" }
@@ -107,7 +108,7 @@ export function patchstackMiddleware(req, res, next) {
107
108
  // there is nothing to answer and the request is screened as normal.
108
109
  const screen = () => {
109
110
  getProtection().then(
110
- (active) => active.node()(req, res, carryOn),
111
+ (active) => active.node({ screenResponses: true })(req, res, carryOn),
111
112
  (err) => {
112
113
  psStepAside(err);
113
114
  carryOn();
@@ -35,13 +35,10 @@ export async function getProtection() {
35
35
  }
36
36
 
37
37
  async function buildProtection() {
38
- const mode = process.env.PATCHSTACK_MODE === "dry-run" ? "dry-run" : "block";
39
- const token = process.env.PATCHSTACK_WAF_TOKEN;
40
- const siteUuid = PS_SITE_UUID.startsWith("__") ? process.env.PATCHSTACK_SITE_UUID : PS_SITE_UUID;
41
- // The sandbox dev server is long-lived and isn't restarted on change, so refresh the live
42
- // rules periodically — a dependency flagged after boot is then enforced without a restart.
43
- // Production relies on a redeploy (which re-fetches at boot), so refresh stays off there.
44
- const refreshMs = process.env.PATCHSTACK_ENVIRONMENT === "sandbox" ? 15000 : 0;
38
+ const mode = (typeof process === "undefined" ? undefined : process.env.PATCHSTACK_MODE) === "dry-run" ? "dry-run" : "block";
39
+ const token = (typeof process === "undefined" ? undefined : process.env.PATCHSTACK_WAF_TOKEN);
40
+ const siteUuid = PS_SITE_UUID.startsWith("__") ? (typeof process === "undefined" ? undefined : process.env.PATCHSTACK_SITE_UUID) : PS_SITE_UUID;
41
+ const refreshMs = (typeof process === "undefined" ? undefined : process.env.PATCHSTACK_ENVIRONMENT) === "sandbox" ? 15000 : 300000;
45
42
  const common = { mode, egress: true, refreshMs } as const;
46
43
  return createProtection(
47
44
  siteUuid
@@ -116,7 +113,7 @@ export function patchstackMiddleware(req: unknown, res: unknown, next: (err?: un
116
113
  // there is nothing to answer and the request is screened as normal.
117
114
  const screen = () => {
118
115
  getProtection().then(
119
- (protection) => (protection.node() as (a: unknown, b: unknown, c: (e?: unknown) => void) => void)(req, res, carryOn),
116
+ (protection) => (protection.node({ screenResponses: true }) as (a: unknown, b: unknown, c: (e?: unknown) => void) => void)(req, res, carryOn),
120
117
  (err) => {
121
118
  psStepAside(err);
122
119
  carryOn();
@@ -49,9 +49,9 @@ async function getProtection() {
49
49
 
50
50
  async function buildProtection() {
51
51
  // Always-on: block by default. An explicit PATCHSTACK_MODE=dry-run downgrades to log-only.
52
- const mode: "block" | "dry-run" = process.env.PATCHSTACK_MODE === "dry-run" ? "dry-run" : "block";
53
- const token = process.env.PATCHSTACK_WAF_TOKEN;
54
- const siteUuid = PS_SITE_UUID.startsWith("__") ? process.env.PATCHSTACK_SITE_UUID : PS_SITE_UUID;
52
+ const mode: "block" | "dry-run" = (typeof process === "undefined" ? undefined : process.env.PATCHSTACK_MODE) === "dry-run" ? "dry-run" : "block";
53
+ const token = (typeof process === "undefined" ? undefined : process.env.PATCHSTACK_WAF_TOKEN);
54
+ const siteUuid = PS_SITE_UUID.startsWith("__") ? (typeof process === "undefined" ? undefined : process.env.PATCHSTACK_SITE_UUID) : PS_SITE_UUID;
55
55
  // Egress SSRF screening: block the app's outbound calls to internal / metadata addresses,
56
56
  // but never its own Supabase project.
57
57
  let allowHosts: string[] = [];
@@ -60,10 +60,7 @@ async function buildProtection() {
60
60
  } catch {
61
61
  /* ignore a malformed SUPABASE_URL — just don't add an allow entry */
62
62
  }
63
- // The sandbox dev server is long-lived and isn't restarted on change, so refresh the live
64
- // rules periodically — a dependency flagged after boot is then enforced without a restart.
65
- // Production relies on a redeploy (which re-fetches at boot), so refresh stays off there.
66
- const refreshMs = process.env.PATCHSTACK_ENVIRONMENT === "sandbox" ? 15000 : 0;
63
+ const refreshMs = (typeof process === "undefined" ? undefined : process.env.PATCHSTACK_ENVIRONMENT) === "sandbox" ? 15000 : 300000;
67
64
  const common = { mode, egress: true, allowHosts, refreshMs };
68
65
  return createProtection(
69
66
  siteUuid
@@ -18,10 +18,11 @@ async function getProtection() {
18
18
  }
19
19
 
20
20
  async function buildProtection() {
21
- const mode = process.env.PATCHSTACK_MODE === "dry-run" ? "dry-run" : "block";
22
- const siteUuid = PS_SITE_UUID.startsWith("__") ? process.env.PATCHSTACK_SITE_UUID : PS_SITE_UUID;
23
- const token = process.env.PATCHSTACK_WAF_TOKEN;
24
- const common = { mode, egress: true } as const;
21
+ const mode = (typeof process === "undefined" ? undefined : process.env.PATCHSTACK_MODE) === "dry-run" ? "dry-run" : "block";
22
+ const siteUuid = PS_SITE_UUID.startsWith("__") ? (typeof process === "undefined" ? undefined : process.env.PATCHSTACK_SITE_UUID) : PS_SITE_UUID;
23
+ const token = (typeof process === "undefined" ? undefined : process.env.PATCHSTACK_WAF_TOKEN);
24
+ const refreshMs = (typeof process === "undefined" ? undefined : process.env.PATCHSTACK_ENVIRONMENT) === "sandbox" ? 15000 : 300000;
25
+ const common = { mode, egress: true, refreshMs } as const;
25
26
  return createProtection(
26
27
  siteUuid
27
28
  ? { ...common, siteUuid, rules: fallbackRules as never, cacheDir: ".patchstack" }
@@ -29,10 +29,11 @@ async function getProtection() {
29
29
  }
30
30
 
31
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;
32
+ const mode = (typeof process === "undefined" ? undefined : process.env.PATCHSTACK_MODE) === "dry-run" ? "dry-run" : "block";
33
+ const token = (typeof process === "undefined" ? undefined : process.env.PATCHSTACK_WAF_TOKEN);
34
+ const siteUuid = PS_SITE_UUID.startsWith("__") ? (typeof process === "undefined" ? undefined : process.env.PATCHSTACK_SITE_UUID) : PS_SITE_UUID;
35
+ const refreshMs = (typeof process === "undefined" ? undefined : process.env.PATCHSTACK_ENVIRONMENT) === "sandbox" ? 15000 : 300000;
36
+ const common = { mode, egress: true, refreshMs } as const;
36
37
  return createProtection(
37
38
  siteUuid
38
39
  ? { ...common, siteUuid, rules: fallbackRules as never, cacheDir: ".patchstack" }
@@ -29,10 +29,11 @@ async function getProtection() {
29
29
  }
30
30
 
31
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;
32
+ const mode = (typeof process === "undefined" ? undefined : process.env.PATCHSTACK_MODE) === "dry-run" ? "dry-run" : "block";
33
+ const token = (typeof process === "undefined" ? undefined : process.env.PATCHSTACK_WAF_TOKEN);
34
+ const siteUuid = PS_SITE_UUID.startsWith("__") ? (typeof process === "undefined" ? undefined : process.env.PATCHSTACK_SITE_UUID) : PS_SITE_UUID;
35
+ const refreshMs = (typeof process === "undefined" ? undefined : process.env.PATCHSTACK_ENVIRONMENT) === "sandbox" ? 15000 : 300000;
36
+ const common = { mode, egress: true, refreshMs } as const;
36
37
  return createProtection(
37
38
  siteUuid
38
39
  ? { ...common, siteUuid, rules: fallbackRules as never, cacheDir: ".patchstack" }
@@ -29,10 +29,11 @@ async function getProtection() {
29
29
  }
30
30
 
31
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;
32
+ const mode = (typeof process === "undefined" ? undefined : process.env.PATCHSTACK_MODE) === "dry-run" ? "dry-run" : "block";
33
+ const token = (typeof process === "undefined" ? undefined : process.env.PATCHSTACK_WAF_TOKEN);
34
+ const siteUuid = PS_SITE_UUID.startsWith("__") ? (typeof process === "undefined" ? undefined : process.env.PATCHSTACK_SITE_UUID) : PS_SITE_UUID;
35
+ const refreshMs = (typeof process === "undefined" ? undefined : process.env.PATCHSTACK_ENVIRONMENT) === "sandbox" ? 15000 : 300000;
36
+ const common = { mode, egress: true, refreshMs } as const;
36
37
  return createProtection(
37
38
  siteUuid
38
39
  ? { ...common, siteUuid, rules: fallbackRules as never, cacheDir: ".patchstack" }
package/dist/protect.cjs CHANGED
@@ -976,10 +976,13 @@ async function resolveConfig(options) {
976
976
  "CONFIG_INVALID"
977
977
  );
978
978
  }
979
- const inferred = environmentRaw === void 0 ? inferEnvironment(process.env, detectHostedBuilder(options.cwd)) : null;
980
- const environment = environmentRaw ?? inferred.environment;
981
- const environmentEvidence = inferred?.evidence ?? [];
982
- const environmentSource = environmentRaw !== void 0 ? "override" : inferred.source;
979
+ const inferred = fromEnv.environment === void 0 ? inferEnvironment(process.env, detectHostedBuilder(options.cwd)) : null;
980
+ const fileLabelOverruled = fromEnv.environment === void 0 && environmentRaw !== void 0 && environmentRaw !== "production" && inferred.environment === "production";
981
+ const labelled = fileLabelOverruled ? void 0 : environmentRaw;
982
+ const environment = labelled ?? inferred.environment;
983
+ const environmentEvidence = labelled === void 0 ? inferred.evidence : [];
984
+ const environmentSource = labelled !== void 0 ? "override" : inferred.source;
985
+ const ignoredFileEnvironment = fileLabelOverruled ? environmentRaw : null;
983
986
  if (siteUuid !== null && siteUuid.length > 0 && !isCanonicalUuid(siteUuid)) {
984
987
  throw new PatchstackError(
985
988
  `Site UUID "${siteUuid}" does not look like a valid UUID.`,
@@ -1008,6 +1011,7 @@ async function resolveConfig(options) {
1008
1011
  environment,
1009
1012
  environmentEvidence,
1010
1013
  environmentSource,
1014
+ ignoredFileEnvironment,
1011
1015
  widget: fromFile.widget !== false,
1012
1016
  claimToken
1013
1017
  };
@@ -9360,10 +9364,10 @@ async function createProtection(options = {}) {
9360
9364
  },
9361
9365
  // Wrap a fetch handler: screens the request, then the response (redact/block).
9362
9366
  fetch(handler) {
9363
- return async (request, ...rest) => {
9367
+ return async function(request, ...rest) {
9364
9368
  const { blocked, client } = await screenFetchRequest(request, rest);
9365
9369
  if (blocked) return blocked;
9366
- const response = await handler(request, ...rest);
9370
+ const response = await handler.call(this, request, ...rest);
9367
9371
  return screenResp(response, reqContextFromFetch(request, client));
9368
9372
  };
9369
9373
  },