@patchstack/connect 0.5.2 → 0.5.4
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 +53 -9
- package/README.md +9 -6
- package/dist/{chunk-3G2I6QL6.js → chunk-IOODP4ZB.js} +19 -2
- package/dist/chunk-IOODP4ZB.js.map +1 -0
- package/dist/cli.js +4014 -3351
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +102 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +68 -55
- package/dist/index.d.ts +68 -55
- package/dist/index.js +102 -11
- package/dist/index.js.map +1 -1
- package/dist/protect.cjs +298 -35
- package/dist/protect.cjs.map +1 -1
- package/dist/protect.d.cts +26 -0
- package/dist/protect.d.ts +26 -0
- package/dist/protect.edge.js +172 -21
- package/dist/protect.edge.js.map +3 -3
- package/dist/protect.js +162 -24
- package/dist/protect.js.map +1 -1
- package/dist/{refresh-manifest-2MMPQN2A.js → refresh-manifest-K3RT4XNH.js} +99 -11
- package/dist/refresh-manifest-K3RT4XNH.js.map +1 -0
- package/package.json +1 -1
- package/dist/chunk-3G2I6QL6.js.map +0 -1
- package/dist/refresh-manifest-2MMPQN2A.js.map +0 -1
package/dist/protect.cjs
CHANGED
|
@@ -123,6 +123,27 @@ var init_pulse_token = __esm({
|
|
|
123
123
|
}
|
|
124
124
|
});
|
|
125
125
|
|
|
126
|
+
// src/build-id.ts
|
|
127
|
+
function canonicalBuildId(value) {
|
|
128
|
+
if (typeof value !== "string") return null;
|
|
129
|
+
const trimmed = value.trim();
|
|
130
|
+
return BUILD_ID.test(trimmed) ? trimmed.toLowerCase() : null;
|
|
131
|
+
}
|
|
132
|
+
function readBuildStamp(bundle) {
|
|
133
|
+
if (bundle === null || typeof bundle !== "object") return null;
|
|
134
|
+
const namespace = bundle[BUILD_STAMP_KEY];
|
|
135
|
+
if (namespace === null || typeof namespace !== "object") return null;
|
|
136
|
+
return canonicalBuildId(namespace.build_id);
|
|
137
|
+
}
|
|
138
|
+
var BUILD_ID, BUILD_STAMP_KEY;
|
|
139
|
+
var init_build_id = __esm({
|
|
140
|
+
"src/build-id.ts"() {
|
|
141
|
+
"use strict";
|
|
142
|
+
BUILD_ID = /^[0-9a-f]{64}$/i;
|
|
143
|
+
BUILD_STAMP_KEY = "_patchstack";
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
126
147
|
// src/types.ts
|
|
127
148
|
var PatchstackError;
|
|
128
149
|
var init_types = __esm({
|
|
@@ -137,6 +158,46 @@ var init_types = __esm({
|
|
|
137
158
|
}
|
|
138
159
|
code;
|
|
139
160
|
cause;
|
|
161
|
+
/**
|
|
162
|
+
* For a `VALIDATION_ERROR`, the request fields the server refused, as it named them. Read from the
|
|
163
|
+
* structured `errors` object a validation response carries, so a caller deciding what to do about a
|
|
164
|
+
* refusal keys on the field rather than on the wording of a sentence.
|
|
165
|
+
*/
|
|
166
|
+
fields = [];
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// src/hosting.ts
|
|
172
|
+
function detectHostingPlatform(env = process.env) {
|
|
173
|
+
for (const rule of RULES) {
|
|
174
|
+
const evidence = rule.any.filter((name) => present(env, name));
|
|
175
|
+
if (evidence.length > 0) {
|
|
176
|
+
return { platform: rule.platform, evidence };
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return { platform: null, evidence: [] };
|
|
180
|
+
}
|
|
181
|
+
var RULES, present;
|
|
182
|
+
var init_hosting = __esm({
|
|
183
|
+
"src/hosting.ts"() {
|
|
184
|
+
"use strict";
|
|
185
|
+
RULES = [
|
|
186
|
+
{ platform: "netlify", any: ["NETLIFY", "NETLIFY_BUILD_BASE", "DEPLOY_PRIME_URL"] },
|
|
187
|
+
{ platform: "vercel", any: ["VERCEL", "VERCEL_ENV", "VERCEL_URL"] },
|
|
188
|
+
{ platform: "cloudflare", any: ["CF_PAGES", "CF_PAGES_URL", "CLOUDFLARE_ACCOUNT_ID"] },
|
|
189
|
+
{ platform: "aws", any: ["AWS_APP_ID", "AWS_LAMBDA_FUNCTION_NAME", "AWS_EXECUTION_ENV"] },
|
|
190
|
+
{ platform: "render", any: ["RENDER", "RENDER_SERVICE_ID", "RENDER_EXTERNAL_URL"] },
|
|
191
|
+
{ platform: "railway", any: ["RAILWAY_ENVIRONMENT", "RAILWAY_ENVIRONMENT_NAME", "RAILWAY_PROJECT_ID"] },
|
|
192
|
+
{ platform: "fly", any: ["FLY_APP_NAME", "FLY_REGION", "FLY_ALLOC_ID"] },
|
|
193
|
+
{ platform: "heroku", any: ["DYNO", "HEROKU_APP_NAME"] },
|
|
194
|
+
{ platform: "google-cloud", any: ["K_SERVICE", "GAE_APPLICATION", "GAE_SERVICE"] },
|
|
195
|
+
{ platform: "deno-deploy", any: ["DENO_DEPLOYMENT_ID"] },
|
|
196
|
+
{ platform: "azure", any: ["WEBSITE_SITE_NAME", "WEBSITE_INSTANCE_ID"] }
|
|
197
|
+
];
|
|
198
|
+
present = (env, name) => {
|
|
199
|
+
const value = env[name];
|
|
200
|
+
return value !== void 0 && value !== "";
|
|
140
201
|
};
|
|
141
202
|
}
|
|
142
203
|
});
|
|
@@ -162,14 +223,22 @@ function authFailureMessage(status, config) {
|
|
|
162
223
|
}
|
|
163
224
|
return null;
|
|
164
225
|
}
|
|
165
|
-
function buildManifestBody(config, payload) {
|
|
226
|
+
function buildManifestBody(config, payload, env = process.env) {
|
|
227
|
+
const hosting = config.environment === "local" ? null : detectHostingPlatform(env);
|
|
166
228
|
return {
|
|
167
229
|
...payload,
|
|
168
230
|
environment: config.environment,
|
|
169
231
|
...typeof config.siteUrl === "string" && config.siteUrl !== "" ? { url: config.siteUrl } : {},
|
|
170
|
-
...typeof config.siteName === "string" && config.siteName !== "" ? { name: config.siteName } : {}
|
|
232
|
+
...typeof config.siteName === "string" && config.siteName !== "" ? { name: config.siteName } : {},
|
|
233
|
+
...hosting !== null && hosting.platform !== null ? { hosting } : {}
|
|
171
234
|
};
|
|
172
235
|
}
|
|
236
|
+
function validationFields(body) {
|
|
237
|
+
if (typeof body !== "object" || body === null) return [];
|
|
238
|
+
const errors = body.errors;
|
|
239
|
+
if (typeof errors !== "object" || errors === null || Array.isArray(errors)) return [];
|
|
240
|
+
return Object.keys(errors);
|
|
241
|
+
}
|
|
173
242
|
async function postManifest(config, payload) {
|
|
174
243
|
const url = buildEndpointUrl(config.endpoint, config.siteUuid);
|
|
175
244
|
const timeoutMs = config.timeoutMs;
|
|
@@ -214,10 +283,12 @@ async function postManifest(config, payload) {
|
|
|
214
283
|
);
|
|
215
284
|
}
|
|
216
285
|
if (response.status === 422) {
|
|
217
|
-
|
|
286
|
+
const refused2 = new PatchstackError(
|
|
218
287
|
body?.message ?? "Patchstack rejected the manifest payload (validation failed).",
|
|
219
288
|
"VALIDATION_ERROR"
|
|
220
289
|
);
|
|
290
|
+
refused2.fields = validationFields(body);
|
|
291
|
+
throw refused2;
|
|
221
292
|
}
|
|
222
293
|
const refused = authFailureMessage(response.status, config);
|
|
223
294
|
if (refused !== null) {
|
|
@@ -245,13 +316,63 @@ var init_client = __esm({
|
|
|
245
316
|
"src/client.ts"() {
|
|
246
317
|
"use strict";
|
|
247
318
|
init_types();
|
|
319
|
+
init_hosting();
|
|
248
320
|
init_pulse_token();
|
|
321
|
+
init_build_id();
|
|
249
322
|
DEFAULT_ENDPOINT = "https://api.patchstack.com/monitor/pulse/manifest";
|
|
250
323
|
DEFAULT_TIMEOUT_MS = 3e4;
|
|
251
324
|
CLAIM_TOKEN_HEADER = "X-Patchstack-Claim-Token";
|
|
252
325
|
}
|
|
253
326
|
});
|
|
254
327
|
|
|
328
|
+
// src/environment.ts
|
|
329
|
+
function inferEnvironment(env = process.env) {
|
|
330
|
+
for (const discriminator of DISCRIMINATORS) {
|
|
331
|
+
const verdict = discriminator.read(env);
|
|
332
|
+
if (verdict !== null) {
|
|
333
|
+
return { environment: verdict.environment, evidence: [`${discriminator.platform}: ${verdict.evidence}`] };
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return { environment: "local", evidence: [] };
|
|
337
|
+
}
|
|
338
|
+
var set, DISCRIMINATORS;
|
|
339
|
+
var init_environment = __esm({
|
|
340
|
+
"src/environment.ts"() {
|
|
341
|
+
"use strict";
|
|
342
|
+
set = (value) => value !== void 0 && value !== "";
|
|
343
|
+
DISCRIMINATORS = [
|
|
344
|
+
{
|
|
345
|
+
platform: "vercel",
|
|
346
|
+
read: (env) => {
|
|
347
|
+
if (!set(env.VERCEL) || !set(env.VERCEL_ENV)) return null;
|
|
348
|
+
return env.VERCEL_ENV === "production" ? { environment: "production", evidence: "VERCEL_ENV=production" } : { environment: "sandbox", evidence: `VERCEL_ENV=${env.VERCEL_ENV}` };
|
|
349
|
+
}
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
platform: "netlify",
|
|
353
|
+
read: (env) => {
|
|
354
|
+
if (env.NETLIFY !== "true" || !set(env.CONTEXT)) return null;
|
|
355
|
+
return env.CONTEXT === "production" ? { environment: "production", evidence: "CONTEXT=production" } : { environment: "sandbox", evidence: `CONTEXT=${env.CONTEXT}` };
|
|
356
|
+
}
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
platform: "render",
|
|
360
|
+
read: (env) => {
|
|
361
|
+
if (env.RENDER !== "true") return null;
|
|
362
|
+
return env.IS_PULL_REQUEST === "true" ? { environment: "sandbox", evidence: "IS_PULL_REQUEST=true" } : { environment: "production", evidence: "RENDER=true, not a pull request" };
|
|
363
|
+
}
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
platform: "railway",
|
|
367
|
+
read: (env) => {
|
|
368
|
+
if (!set(env.RAILWAY_ENVIRONMENT_NAME)) return null;
|
|
369
|
+
return env.RAILWAY_ENVIRONMENT_NAME === "production" ? { environment: "production", evidence: "RAILWAY_ENVIRONMENT_NAME=production" } : { environment: "sandbox", evidence: `RAILWAY_ENVIRONMENT_NAME=${env.RAILWAY_ENVIRONMENT_NAME}` };
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
];
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
|
|
255
376
|
// src/site-url.ts
|
|
256
377
|
function isUnpublishableHost(hostname) {
|
|
257
378
|
const host = hostname.toLowerCase().replace(/\.+$/, "");
|
|
@@ -463,11 +584,13 @@ async function resolveConfig(options) {
|
|
|
463
584
|
const environmentRaw = fromEnv.environment ?? fromFile.environment;
|
|
464
585
|
if (environmentRaw !== void 0 && !isEnvironment(environmentRaw)) {
|
|
465
586
|
throw new PatchstackError(
|
|
466
|
-
`Environment must be "production" or "
|
|
587
|
+
`Environment must be "production", "sandbox" or "local"; got "${environmentRaw}".`,
|
|
467
588
|
"CONFIG_INVALID"
|
|
468
589
|
);
|
|
469
590
|
}
|
|
470
|
-
const
|
|
591
|
+
const inferred = environmentRaw === void 0 ? inferEnvironment(process.env) : null;
|
|
592
|
+
const environment = environmentRaw ?? inferred.environment;
|
|
593
|
+
const environmentEvidence = inferred?.evidence ?? [];
|
|
471
594
|
if (siteUuid !== null && siteUuid.length > 0 && !isUuid(siteUuid)) {
|
|
472
595
|
throw new PatchstackError(
|
|
473
596
|
`Site UUID "${siteUuid}" does not look like a valid UUID.`,
|
|
@@ -493,6 +616,7 @@ async function resolveConfig(options) {
|
|
|
493
616
|
endpoint,
|
|
494
617
|
timeoutMs,
|
|
495
618
|
environment,
|
|
619
|
+
environmentEvidence,
|
|
496
620
|
widget: fromFile.widget !== false,
|
|
497
621
|
claimToken
|
|
498
622
|
};
|
|
@@ -558,9 +682,9 @@ function isUuid(value) {
|
|
|
558
682
|
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
|
|
559
683
|
}
|
|
560
684
|
function isEnvironment(value) {
|
|
561
|
-
return value === "production" || value === "sandbox";
|
|
685
|
+
return value === "production" || value === "sandbox" || value === "local";
|
|
562
686
|
}
|
|
563
|
-
var import_promises2, import_node_path2, CONFIG_FILENAME, SECRET_FILENAME
|
|
687
|
+
var import_promises2, import_node_path2, CONFIG_FILENAME, SECRET_FILENAME;
|
|
564
688
|
var init_config = __esm({
|
|
565
689
|
"src/config.ts"() {
|
|
566
690
|
"use strict";
|
|
@@ -568,11 +692,11 @@ var init_config = __esm({
|
|
|
568
692
|
import_node_path2 = __toESM(require("path"), 1);
|
|
569
693
|
init_types();
|
|
570
694
|
init_client();
|
|
695
|
+
init_environment();
|
|
571
696
|
init_site_url();
|
|
572
697
|
init_site_name();
|
|
573
698
|
CONFIG_FILENAME = ".patchstackrc.json";
|
|
574
699
|
SECRET_FILENAME = ".patchstackrc.local.json";
|
|
575
|
-
DEFAULT_ENVIRONMENT = "production";
|
|
576
700
|
}
|
|
577
701
|
});
|
|
578
702
|
|
|
@@ -609,8 +733,8 @@ async function readDeclaredDependencyNames(cwd) {
|
|
|
609
733
|
return [...names];
|
|
610
734
|
}
|
|
611
735
|
function missingDependencies(declared, packages) {
|
|
612
|
-
const
|
|
613
|
-
return declared.filter((name) => !
|
|
736
|
+
const present2 = new Set(packages.map((entry) => entry.name));
|
|
737
|
+
return declared.filter((name) => !present2.has(name));
|
|
614
738
|
}
|
|
615
739
|
function disagreements(packages, other) {
|
|
616
740
|
const otherVersions = /* @__PURE__ */ new Map();
|
|
@@ -3685,6 +3809,18 @@ var RULE_PROPERTY_SHAPES = Object.freeze({
|
|
|
3685
3809
|
});
|
|
3686
3810
|
var ENFORCEMENT_VALUES = Object.freeze(["dry-run"]);
|
|
3687
3811
|
var WHEN_KEYS = Object.freeze(["path", "method"]);
|
|
3812
|
+
var BUILD_SCOPE_PROPERTY = "build_scope";
|
|
3813
|
+
var BUILD_SCOPE = Object.freeze({
|
|
3814
|
+
applies_to: Object.freeze(["firewall"]),
|
|
3815
|
+
usable: Object.freeze({
|
|
3816
|
+
type: "string",
|
|
3817
|
+
trim: true,
|
|
3818
|
+
pattern: "^[0-9a-fA-F]{64}$",
|
|
3819
|
+
canonical: "lowercase"
|
|
3820
|
+
}),
|
|
3821
|
+
unreadable: "the rule remains accepted and detects only by default",
|
|
3822
|
+
local_override: "trustLocalRuleScope may explicitly enforce a caller-supplied rule"
|
|
3823
|
+
});
|
|
3688
3824
|
var RULE_PROPERTIES = Object.freeze([
|
|
3689
3825
|
"id",
|
|
3690
3826
|
"rule_id",
|
|
@@ -3704,12 +3840,13 @@ var RULE_PROPERTIES = Object.freeze([
|
|
|
3704
3840
|
"remove_headers",
|
|
3705
3841
|
"cookie_flags",
|
|
3706
3842
|
"ensure",
|
|
3707
|
-
"capture"
|
|
3843
|
+
"capture",
|
|
3844
|
+
BUILD_SCOPE_PROPERTY
|
|
3708
3845
|
]);
|
|
3709
3846
|
var CAPTURE_VERSION = 1;
|
|
3710
3847
|
var CAPTURE_KEYS = Object.freeze(["version", "raw_chars"]);
|
|
3711
3848
|
var CAPTURE_RAW_CHARS_MAX = 512;
|
|
3712
|
-
var NULL_EXEMPT_PROPERTIES = Object.freeze(["capture"]);
|
|
3849
|
+
var NULL_EXEMPT_PROPERTIES = Object.freeze(["capture", BUILD_SCOPE_PROPERTY]);
|
|
3713
3850
|
function captureProblem(capture) {
|
|
3714
3851
|
if (capture === void 0 || capture === null) return null;
|
|
3715
3852
|
if (typeof capture !== "object" || Array.isArray(capture)) return "capture must be an object";
|
|
@@ -3950,6 +4087,10 @@ function validateBundle(bundle, opts = {}) {
|
|
|
3950
4087
|
rejected.push({ id: idOf(wl), reason: "whitelist has no rule_id (would suppress every rule); set allowGlobalWhitelists to permit" });
|
|
3951
4088
|
continue;
|
|
3952
4089
|
}
|
|
4090
|
+
if (wl && typeof wl === "object" && BUILD_SCOPE_PROPERTY in wl) {
|
|
4091
|
+
rejected.push({ id: idOf(wl), reason: `whitelist may not carry ${BUILD_SCOPE_PROPERTY}` });
|
|
4092
|
+
continue;
|
|
4093
|
+
}
|
|
3953
4094
|
const reason = conditionsProblem(wl?.rule_v2);
|
|
3954
4095
|
if (reason) rejected.push({ id: idOf(wl), reason: `whitelist: ${reason}` });
|
|
3955
4096
|
else whitelists.push(wl);
|
|
@@ -4220,8 +4361,11 @@ function asText(value) {
|
|
|
4220
4361
|
|
|
4221
4362
|
// src/protect/engine/pulse-client.js
|
|
4222
4363
|
init_pulse_token();
|
|
4364
|
+
init_build_id();
|
|
4223
4365
|
var DEFAULT_BASE_URL2 = "https://api.patchstack.com/monitor/pulse";
|
|
4224
4366
|
var DEFAULT_CACHE_TTL2 = 3e5;
|
|
4367
|
+
var BUILD_VERDICT_HEADER = "X-Patchstack-Build-Match";
|
|
4368
|
+
var BUILD_IDENTITY_HEADER = "X-Patchstack-Build-ID";
|
|
4225
4369
|
var JITTER_FRACTION2 = 0.1;
|
|
4226
4370
|
var PulseRuleClient = class {
|
|
4227
4371
|
#siteUuid;
|
|
@@ -4234,7 +4378,8 @@ var PulseRuleClient = class {
|
|
|
4234
4378
|
#etag;
|
|
4235
4379
|
#pulseAuth;
|
|
4236
4380
|
#detectionState;
|
|
4237
|
-
|
|
4381
|
+
#buildId;
|
|
4382
|
+
constructor({ siteUuid, baseUrl, cacheTtl, etag, timeoutMs, pulseAuth, detectionState, buildId } = {}) {
|
|
4238
4383
|
this.#timeoutMs = Number(timeoutMs) > 0 ? Number(timeoutMs) : 3e4;
|
|
4239
4384
|
this.#siteUuid = siteUuid ?? process.env.PATCHSTACK_SITE_UUID;
|
|
4240
4385
|
this.#baseUrl = safeBaseUrl(baseUrl ?? process.env.PATCHSTACK_PULSE_RULES_URL, DEFAULT_BASE_URL2, "rule endpoint");
|
|
@@ -4242,6 +4387,7 @@ var PulseRuleClient = class {
|
|
|
4242
4387
|
this.#etag = etag ?? null;
|
|
4243
4388
|
this.#pulseAuth = pulseAuth ?? null;
|
|
4244
4389
|
this.#detectionState = typeof detectionState === "string" ? detectionState : null;
|
|
4390
|
+
this.#buildId = canonicalBuildId(buildId);
|
|
4245
4391
|
if (!this.#siteUuid) {
|
|
4246
4392
|
throw new Error("Patchstack site UUID is required. Pass { siteUuid } or set PATCHSTACK_SITE_UUID.");
|
|
4247
4393
|
}
|
|
@@ -4261,11 +4407,23 @@ var PulseRuleClient = class {
|
|
|
4261
4407
|
if (this.#detectionState !== null && typeof auth.Authorization === "string") {
|
|
4262
4408
|
headers["X-Patchstack-Detections"] = this.#detectionState;
|
|
4263
4409
|
}
|
|
4410
|
+
if (this.#buildId !== null && typeof auth.Authorization === "string") {
|
|
4411
|
+
headers["X-Patchstack-Build"] = this.#buildId;
|
|
4412
|
+
}
|
|
4264
4413
|
if (this.#etag) headers["If-None-Match"] = this.#etag;
|
|
4265
4414
|
const response = await fetch(url, { method: "GET", headers, signal: AbortSignal.timeout(this.#timeoutMs) });
|
|
4266
4415
|
if (response.status === 304) {
|
|
4267
4416
|
this.#touch(now);
|
|
4268
|
-
|
|
4417
|
+
const result2 = {
|
|
4418
|
+
...this.#cache ?? { firewall: [], whitelists: [], whitelist_keys: {} },
|
|
4419
|
+
success: true,
|
|
4420
|
+
notModified: true,
|
|
4421
|
+
etag: response.headers?.get?.("etag") ?? this.#etag,
|
|
4422
|
+
build: buildVerdictOf(response.headers)
|
|
4423
|
+
};
|
|
4424
|
+
this.#cache = result2;
|
|
4425
|
+
this.#etag = result2.etag;
|
|
4426
|
+
return result2;
|
|
4269
4427
|
}
|
|
4270
4428
|
if (!response.ok) {
|
|
4271
4429
|
return { success: false, error: `API returned ${response.status}`, firewall: [], whitelists: [], whitelist_keys: {} };
|
|
@@ -4281,7 +4439,10 @@ var PulseRuleClient = class {
|
|
|
4281
4439
|
firewall: data.firewall,
|
|
4282
4440
|
whitelists: Array.isArray(data.whitelists) ? data.whitelists : [],
|
|
4283
4441
|
whitelist_keys: data.whitelist_keys ?? {},
|
|
4284
|
-
...enforcementField(data)
|
|
4442
|
+
...enforcementField(data),
|
|
4443
|
+
// Carried through so the caller can decide what a build-scoped rule may do. Response metadata,
|
|
4444
|
+
// rather than bundle content, because it varies with the build asking and is also present on 304.
|
|
4445
|
+
build: buildVerdictOf(response.headers)
|
|
4285
4446
|
};
|
|
4286
4447
|
this.#cache = result;
|
|
4287
4448
|
this.#etag = result.etag;
|
|
@@ -4307,6 +4468,13 @@ var PulseRuleClient = class {
|
|
|
4307
4468
|
this.#etag = null;
|
|
4308
4469
|
}
|
|
4309
4470
|
};
|
|
4471
|
+
function buildVerdictOf(headers) {
|
|
4472
|
+
const get = headers !== null && typeof headers === "object" && typeof headers.get === "function" ? (name) => headers.get(name) : () => null;
|
|
4473
|
+
const stated2 = get(BUILD_VERDICT_HEADER);
|
|
4474
|
+
const verdict = stated2 === "match" || stated2 === "missing" || stated2 === "mismatch" ? stated2 : "unknown";
|
|
4475
|
+
const matched = get(BUILD_IDENTITY_HEADER);
|
|
4476
|
+
return { verdict, matchedBuildId: typeof matched === "string" ? matched : null };
|
|
4477
|
+
}
|
|
4310
4478
|
function enforcementField(data) {
|
|
4311
4479
|
if (!data || typeof data !== "object") return {};
|
|
4312
4480
|
const v = data.enforcement ?? data.mode;
|
|
@@ -5069,6 +5237,7 @@ function renderBlockPage({ url = "", code = "patchstack" } = {}) {
|
|
|
5069
5237
|
}
|
|
5070
5238
|
|
|
5071
5239
|
// src/protect/rules/store.js
|
|
5240
|
+
init_build_id();
|
|
5072
5241
|
var fsMod;
|
|
5073
5242
|
async function loadFs() {
|
|
5074
5243
|
if (fsMod !== void 0) return fsMod;
|
|
@@ -5149,15 +5318,25 @@ async function cacheRead(dir) {
|
|
|
5149
5318
|
function toEnvelope(value) {
|
|
5150
5319
|
if (!value || typeof value !== "object") return null;
|
|
5151
5320
|
if (value.bundle && typeof value.bundle === "object") {
|
|
5152
|
-
|
|
5321
|
+
const buildId = canonicalBuildId(value.buildId);
|
|
5322
|
+
const matched = canonicalBuildId(value.matchedBuildId);
|
|
5323
|
+
return {
|
|
5324
|
+
bundle: value.bundle,
|
|
5325
|
+
etag: value.etag ?? null,
|
|
5326
|
+
buildId,
|
|
5327
|
+
// A confirmation belongs to the presentation stored beside it. A crossed or partially written
|
|
5328
|
+
// envelope confirms nothing, even when one of its fields happens to name the current map.
|
|
5329
|
+
matchedBuildId: buildId !== null && matched === buildId ? matched : null
|
|
5330
|
+
};
|
|
5153
5331
|
}
|
|
5154
5332
|
if (Array.isArray(value.firewall) || Array.isArray(value.whitelists)) {
|
|
5155
|
-
return { bundle: value, etag: null };
|
|
5333
|
+
return { bundle: value, etag: null, buildId: null, matchedBuildId: null };
|
|
5156
5334
|
}
|
|
5157
5335
|
return null;
|
|
5158
5336
|
}
|
|
5159
5337
|
|
|
5160
5338
|
// src/protect/rules/source.js
|
|
5339
|
+
init_build_id();
|
|
5161
5340
|
function liveUpdateRejections(res, options) {
|
|
5162
5341
|
if (options.acceptPartialBundle) return [];
|
|
5163
5342
|
const { rejected } = validateBundle(
|
|
@@ -5184,62 +5363,146 @@ function fromSource(bundle, origin, reason) {
|
|
|
5184
5363
|
source: reason === void 0 ? { ok: true, origin } : { ok: false, origin, reason }
|
|
5185
5364
|
};
|
|
5186
5365
|
}
|
|
5366
|
+
function buildIdentity(options) {
|
|
5367
|
+
if (options.buildId !== void 0 && options.buildId !== null) {
|
|
5368
|
+
const explicit = canonicalBuildId(options.buildId);
|
|
5369
|
+
return explicit === null ? { id: null, reason: "the buildId option is not a complete 64-hex SHA-256 identity" } : { id: explicit };
|
|
5370
|
+
}
|
|
5371
|
+
const stamped = readBuildStamp(options.rules);
|
|
5372
|
+
if (stamped !== null) return { id: stamped };
|
|
5373
|
+
return {
|
|
5374
|
+
id: null,
|
|
5375
|
+
reason: options.rules ? "the guard rules file carries no build identity \u2014 the build did not stamp one" : "this guard was configured without bundled rules, so it carries no build identity"
|
|
5376
|
+
};
|
|
5377
|
+
}
|
|
5378
|
+
function withScopedDryRun(bundle, confirmed, reason, options = {}) {
|
|
5379
|
+
let held = 0;
|
|
5380
|
+
let unusable = 0;
|
|
5381
|
+
const firewall = bundle.firewall.map((rule) => {
|
|
5382
|
+
if (rule === null || typeof rule !== "object") return rule;
|
|
5383
|
+
if (!(BUILD_SCOPE_PROPERTY in rule)) return rule;
|
|
5384
|
+
const scope = rule[BUILD_SCOPE_PROPERTY];
|
|
5385
|
+
const target = canonicalBuildId(scope);
|
|
5386
|
+
if (target !== null && confirmed !== null && target === confirmed) return rule;
|
|
5387
|
+
if (rule.enforcement === "dry-run") return rule;
|
|
5388
|
+
held += 1;
|
|
5389
|
+
if (target === null) unusable += 1;
|
|
5390
|
+
return { ...rule, enforcement: "dry-run" };
|
|
5391
|
+
});
|
|
5392
|
+
if (held === 0) return bundle;
|
|
5393
|
+
const explanations = [
|
|
5394
|
+
...unusable > 0 ? [`${unusable} rule(s) carry an unusable ${BUILD_SCOPE_PROPERTY}`] : [],
|
|
5395
|
+
...held > unusable ? [reason] : []
|
|
5396
|
+
];
|
|
5397
|
+
notify(
|
|
5398
|
+
options.onError,
|
|
5399
|
+
new Error(`${held} build-scoped rule(s) are detecting only, not blocking: ${explanations.join("; ")}`),
|
|
5400
|
+
"onError"
|
|
5401
|
+
);
|
|
5402
|
+
return { ...bundle, firewall };
|
|
5403
|
+
}
|
|
5404
|
+
function confirmedBy(res, presented) {
|
|
5405
|
+
if (presented === null) return null;
|
|
5406
|
+
const verdict = res?.build;
|
|
5407
|
+
if (verdict?.verdict !== "match") return null;
|
|
5408
|
+
return canonicalBuildId(verdict.matchedBuildId) === presented ? presented : null;
|
|
5409
|
+
}
|
|
5187
5410
|
async function resolveRules(options, store, ctx = {}) {
|
|
5188
5411
|
const timeoutMs = ctx.timeoutMs;
|
|
5189
5412
|
if (options.siteUuid) {
|
|
5190
5413
|
const prior = await store.read();
|
|
5191
|
-
const
|
|
5414
|
+
const identity = buildIdentity(options);
|
|
5415
|
+
const sameBuild = prior?.buildId === identity.id;
|
|
5416
|
+
const cacheConfirmed = identity.id !== null && sameBuild && prior?.matchedBuildId === identity.id ? identity.id : null;
|
|
5417
|
+
const unconfirmed = identity.reason ?? "the platform did not confirm that these coordinates belong to this map";
|
|
5418
|
+
const served = (bundle, confirmed) => withScopedDryRun(bundle, confirmed, unconfirmed, options);
|
|
5419
|
+
const local = (bundle) => options.trustLocalRuleScope === true ? bundle : withScopedDryRun(bundle, identity.id, identity.reason ?? "these rules name a different map", options);
|
|
5420
|
+
const client = new PulseRuleClient({
|
|
5421
|
+
siteUuid: options.siteUuid,
|
|
5422
|
+
baseUrl: options.pulseRulesUrl,
|
|
5423
|
+
etag: sameBuild ? prior?.etag : null,
|
|
5424
|
+
timeoutMs,
|
|
5425
|
+
pulseAuth: ctx.pulseAuth,
|
|
5426
|
+
detectionState: ctx.detectionState,
|
|
5427
|
+
buildId: identity.id
|
|
5428
|
+
});
|
|
5192
5429
|
const res = await client.getRules();
|
|
5193
|
-
if (res.success && res.notModified && prior?.bundle)
|
|
5430
|
+
if (res.success && res.notModified && prior?.bundle) {
|
|
5431
|
+
const confirmed = confirmedBy(res, identity.id);
|
|
5432
|
+
await store.write({
|
|
5433
|
+
bundle: prior.bundle,
|
|
5434
|
+
etag: res.etag ?? prior.etag ?? null,
|
|
5435
|
+
buildId: identity.id,
|
|
5436
|
+
matchedBuildId: confirmed
|
|
5437
|
+
});
|
|
5438
|
+
return fromSource(served(normalizeBundle(prior.bundle, options), confirmed), "cache");
|
|
5439
|
+
}
|
|
5194
5440
|
if (res.success && !res.notModified) {
|
|
5441
|
+
const confirmed = confirmedBy(res, identity.id);
|
|
5195
5442
|
const rejected = liveUpdateRejections(res, options);
|
|
5196
5443
|
if (rejected.length > 0) {
|
|
5197
5444
|
reportRejections(rejected, options, "rule update rejected");
|
|
5198
|
-
if (prior?.bundle) return fromSource(normalizeBundle(prior.bundle, options), "cache", "update rejected");
|
|
5199
|
-
if (options.rules) return fromSource(normalizeBundle(options.rules, options), "bundled", "update rejected");
|
|
5445
|
+
if (prior?.bundle) return fromSource(served(normalizeBundle(prior.bundle, options), cacheConfirmed), "cache", "update rejected");
|
|
5446
|
+
if (options.rules) return fromSource(local(normalizeBundle(options.rules, options)), "bundled", "update rejected");
|
|
5200
5447
|
return fromSource(emptyBundle(), "empty", "update rejected");
|
|
5201
5448
|
}
|
|
5202
5449
|
const bundle = normalizeBundle(res, options);
|
|
5203
|
-
await store.write({ bundle, etag: res.etag ?? null });
|
|
5204
|
-
return fromSource(bundle, "api");
|
|
5450
|
+
await store.write({ bundle, etag: res.etag ?? null, buildId: identity.id, matchedBuildId: confirmed });
|
|
5451
|
+
return fromSource(served(bundle, confirmed), "api");
|
|
5205
5452
|
}
|
|
5206
5453
|
if (prior?.bundle) {
|
|
5207
5454
|
notify(options.onError, new Error(`pulse rule fetch failed (${res.error ?? "no usable response"}); using cached bundle`), "onError");
|
|
5208
|
-
return fromSource(normalizeBundle(prior.bundle, options), "cache", res.error ?? "no usable response");
|
|
5455
|
+
return fromSource(served(normalizeBundle(prior.bundle, options), cacheConfirmed), "cache", res.error ?? "no usable response");
|
|
5209
5456
|
}
|
|
5210
5457
|
if (options.rules) {
|
|
5211
5458
|
notify(options.onError, new Error(`pulse rule fetch failed (${res.error ?? "no usable response"}); using bundled fallback`), "onError");
|
|
5212
|
-
return fromSource(normalizeBundle(options.rules, options), "bundled", res.error ?? "no usable response");
|
|
5459
|
+
return fromSource(local(normalizeBundle(options.rules, options)), "bundled", res.error ?? "no usable response");
|
|
5213
5460
|
}
|
|
5214
5461
|
notify(options.onError, new Error(`pulse rule fetch failed (${res.error ?? "no usable response"}); no cache \u2014 running with no rules`), "onError");
|
|
5215
5462
|
return fromSource(emptyBundle(), "empty", res.error ?? "no usable response");
|
|
5216
5463
|
}
|
|
5217
5464
|
if (options.token) {
|
|
5465
|
+
const identity = buildIdentity(options);
|
|
5466
|
+
const remote = (bundle) => withScopedDryRun(
|
|
5467
|
+
bundle,
|
|
5468
|
+
null,
|
|
5469
|
+
"the token-authenticated rules service did not corroborate this map",
|
|
5470
|
+
options
|
|
5471
|
+
);
|
|
5472
|
+
const local = (bundle) => options.trustLocalRuleScope === true ? bundle : withScopedDryRun(bundle, identity.id, identity.reason ?? "these rules name a different map", options);
|
|
5218
5473
|
const prior = await store.read();
|
|
5219
5474
|
const client = new PatchstackRuleClient({ token: options.token, baseUrl: options.baseUrl, etag: prior?.etag, timeoutMs });
|
|
5220
5475
|
const res = await client.getRules();
|
|
5221
|
-
if (res.success && res.notModified && prior?.bundle)
|
|
5476
|
+
if (res.success && res.notModified && prior?.bundle) {
|
|
5477
|
+
return fromSource(remote(normalizeBundle(prior.bundle, options)), "cache");
|
|
5478
|
+
}
|
|
5222
5479
|
if (res.success && !res.notModified) {
|
|
5223
5480
|
const rejected = liveUpdateRejections(res, options);
|
|
5224
5481
|
if (rejected.length > 0) {
|
|
5225
5482
|
reportRejections(rejected, options, "rule update rejected");
|
|
5226
|
-
if (prior?.bundle) return fromSource(normalizeBundle(prior.bundle, options), "cache", "update rejected");
|
|
5227
|
-
if (options.rules) return fromSource(normalizeBundle(options.rules, options), "bundled", "update rejected");
|
|
5483
|
+
if (prior?.bundle) return fromSource(remote(normalizeBundle(prior.bundle, options)), "cache", "update rejected");
|
|
5484
|
+
if (options.rules) return fromSource(local(normalizeBundle(options.rules, options)), "bundled", "update rejected");
|
|
5228
5485
|
return fromSource(emptyBundle(), "empty", "update rejected");
|
|
5229
5486
|
}
|
|
5230
5487
|
const bundle = normalizeBundle(res, options);
|
|
5231
5488
|
await store.write({ bundle, etag: res.etag ?? null });
|
|
5232
|
-
return fromSource(bundle, "api");
|
|
5489
|
+
return fromSource(remote(bundle), "api");
|
|
5233
5490
|
}
|
|
5234
5491
|
if (prior?.bundle) {
|
|
5235
5492
|
notify(options.onError, new Error(`rule fetch failed (${res.error ?? "no usable response"}); using cached bundle`), "onError");
|
|
5236
|
-
return fromSource(normalizeBundle(prior.bundle, options), "cache", res.error ?? "no usable response");
|
|
5493
|
+
return fromSource(remote(normalizeBundle(prior.bundle, options)), "cache", res.error ?? "no usable response");
|
|
5237
5494
|
}
|
|
5238
5495
|
notify(options.onError, new Error(`rule fetch failed (${res.error ?? "no usable response"}); no cache \u2014 running with no rules`), "onError");
|
|
5239
5496
|
return fromSource(emptyBundle(), "empty", res.error ?? "no usable response");
|
|
5240
5497
|
}
|
|
5241
5498
|
if (options.rules) {
|
|
5242
|
-
|
|
5499
|
+
const identity = buildIdentity(options);
|
|
5500
|
+
const bundle = normalizeBundle(options.rules, options);
|
|
5501
|
+
if (options.trustLocalRuleScope === true) return fromSource(bundle, "bundled");
|
|
5502
|
+
return fromSource(
|
|
5503
|
+
withScopedDryRun(bundle, identity.id, identity.reason ?? "these rules name a different map", options),
|
|
5504
|
+
"bundled"
|
|
5505
|
+
);
|
|
5243
5506
|
}
|
|
5244
5507
|
return fromSource(emptyBundle(), "empty");
|
|
5245
5508
|
}
|
|
@@ -6824,8 +7087,8 @@ async function createProtection(options = {}) {
|
|
|
6824
7087
|
hardened = true;
|
|
6825
7088
|
try {
|
|
6826
7089
|
if (res.headersSent || typeof res.setHeader !== "function") return null;
|
|
6827
|
-
const
|
|
6828
|
-
const headers = effective && effective.headers ? { ...
|
|
7090
|
+
const set2 = typeof res.getHeaders === "function" ? res.getHeaders() : {};
|
|
7091
|
+
const headers = effective && effective.headers ? { ...set2, ...effective.headers } : set2;
|
|
6829
7092
|
const status = effective && effective.status !== void 0 ? effective.status : res.statusCode;
|
|
6830
7093
|
const r = screenText("", { status, headers }, reqCtx, hardensWithoutBody);
|
|
6831
7094
|
answeredWithoutBody = true;
|
|
@@ -7537,8 +7800,8 @@ function applyHeaderMutation(headers, rule) {
|
|
|
7537
7800
|
const ensure = rule.ensure === true;
|
|
7538
7801
|
for (const [name, value] of Object.entries(rule.set_headers ?? {})) {
|
|
7539
7802
|
const key = String(name).toLowerCase();
|
|
7540
|
-
const
|
|
7541
|
-
if (ensure &&
|
|
7803
|
+
const present2 = headers[key] != null && headers[key] !== "";
|
|
7804
|
+
if (ensure && present2) continue;
|
|
7542
7805
|
headers[key] = String(value);
|
|
7543
7806
|
}
|
|
7544
7807
|
return;
|