@patchstack/connect 0.5.2 → 0.5.3
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 +51 -7
- package/README.md +6 -3
- package/dist/{chunk-3G2I6QL6.js → chunk-IOODP4ZB.js} +19 -2
- package/dist/chunk-IOODP4ZB.js.map +1 -0
- package/dist/cli.js +315 -59
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/protect.cjs +182 -21
- 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-3QGLYXV5.js} +2 -2
- package/dist/{refresh-manifest-2MMPQN2A.js.map → refresh-manifest-3QGLYXV5.js.map} +1 -1
- package/package.json +1 -1
- package/dist/chunk-3G2I6QL6.js.map +0 -1
package/dist/protect.d.cts
CHANGED
|
@@ -290,6 +290,32 @@ export interface CreateProtectionOptions {
|
|
|
290
290
|
responseRules?: unknown[];
|
|
291
291
|
/** Override the default egress-phase (SSRF) rule set. */
|
|
292
292
|
egressRules?: unknown[];
|
|
293
|
+
/**
|
|
294
|
+
* The mapped coordinate identity this guard carries, as a complete 64-hex SHA-256 value. It is
|
|
295
|
+
* trimmed and compared in lowercase.
|
|
296
|
+
*
|
|
297
|
+
* Only needed when wiring `createProtection` by hand. A scaffolded guard carries this in the rules
|
|
298
|
+
* file its build stamped, and passing it here overrides that.
|
|
299
|
+
*
|
|
300
|
+
* What it authorises is narrow: a rule carrying `build_scope` may block only when the platform
|
|
301
|
+
* confirms the coordinates it served belong to THIS mapped document. Presenting an id is not that
|
|
302
|
+
* confirmation — the server answers it through response metadata, including on a `304` — so a wrong
|
|
303
|
+
* or invented value withholds enforcement rather than granting it. Any other format is ignored, with
|
|
304
|
+
* the same effect as passing nothing.
|
|
305
|
+
*/
|
|
306
|
+
buildId?: string;
|
|
307
|
+
/**
|
|
308
|
+
* Take responsibility for `rules` you supply yourself, including any `build_scope` they carry.
|
|
309
|
+
*
|
|
310
|
+
* Off by default, and the default is the safe one. A rule scoped to a build addresses a route and a
|
|
311
|
+
* field name read from one particular source; supplying it locally establishes that you intend it,
|
|
312
|
+
* not that it still describes the code now running. Left alone, such a rule detects without blocking
|
|
313
|
+
* unless it names the map identity this guard reports.
|
|
314
|
+
*
|
|
315
|
+
* Set this only where the caller genuinely knows the rules match the running source — a test that
|
|
316
|
+
* vendors a rule against a fixed fixture, or a build that generates both together.
|
|
317
|
+
*/
|
|
318
|
+
trustLocalRuleScope?: boolean;
|
|
293
319
|
/** Opt in to wrapping global fetch to screen the app's outbound calls (SSRF). */
|
|
294
320
|
egress?: boolean;
|
|
295
321
|
/** Hosts exempt from egress screening. */
|
package/dist/protect.d.ts
CHANGED
|
@@ -290,6 +290,32 @@ export interface CreateProtectionOptions {
|
|
|
290
290
|
responseRules?: unknown[];
|
|
291
291
|
/** Override the default egress-phase (SSRF) rule set. */
|
|
292
292
|
egressRules?: unknown[];
|
|
293
|
+
/**
|
|
294
|
+
* The mapped coordinate identity this guard carries, as a complete 64-hex SHA-256 value. It is
|
|
295
|
+
* trimmed and compared in lowercase.
|
|
296
|
+
*
|
|
297
|
+
* Only needed when wiring `createProtection` by hand. A scaffolded guard carries this in the rules
|
|
298
|
+
* file its build stamped, and passing it here overrides that.
|
|
299
|
+
*
|
|
300
|
+
* What it authorises is narrow: a rule carrying `build_scope` may block only when the platform
|
|
301
|
+
* confirms the coordinates it served belong to THIS mapped document. Presenting an id is not that
|
|
302
|
+
* confirmation — the server answers it through response metadata, including on a `304` — so a wrong
|
|
303
|
+
* or invented value withholds enforcement rather than granting it. Any other format is ignored, with
|
|
304
|
+
* the same effect as passing nothing.
|
|
305
|
+
*/
|
|
306
|
+
buildId?: string;
|
|
307
|
+
/**
|
|
308
|
+
* Take responsibility for `rules` you supply yourself, including any `build_scope` they carry.
|
|
309
|
+
*
|
|
310
|
+
* Off by default, and the default is the safe one. A rule scoped to a build addresses a route and a
|
|
311
|
+
* field name read from one particular source; supplying it locally establishes that you intend it,
|
|
312
|
+
* not that it still describes the code now running. Left alone, such a rule detects without blocking
|
|
313
|
+
* unless it names the map identity this guard reports.
|
|
314
|
+
*
|
|
315
|
+
* Set this only where the caller genuinely knows the rules match the running source — a test that
|
|
316
|
+
* vendors a rule against a fixed fixture, or a build that generates both together.
|
|
317
|
+
*/
|
|
318
|
+
trustLocalRuleScope?: boolean;
|
|
293
319
|
/** Opt in to wrapping global fetch to screen the app's outbound calls (SSRF). */
|
|
294
320
|
egress?: boolean;
|
|
295
321
|
/** Hosts exempt from egress screening. */
|
package/dist/protect.edge.js
CHANGED
|
@@ -1867,6 +1867,18 @@ var RULE_PROPERTY_SHAPES = Object.freeze({
|
|
|
1867
1867
|
});
|
|
1868
1868
|
var ENFORCEMENT_VALUES = Object.freeze(["dry-run"]);
|
|
1869
1869
|
var WHEN_KEYS = Object.freeze(["path", "method"]);
|
|
1870
|
+
var BUILD_SCOPE_PROPERTY = "build_scope";
|
|
1871
|
+
var BUILD_SCOPE = Object.freeze({
|
|
1872
|
+
applies_to: Object.freeze(["firewall"]),
|
|
1873
|
+
usable: Object.freeze({
|
|
1874
|
+
type: "string",
|
|
1875
|
+
trim: true,
|
|
1876
|
+
pattern: "^[0-9a-fA-F]{64}$",
|
|
1877
|
+
canonical: "lowercase"
|
|
1878
|
+
}),
|
|
1879
|
+
unreadable: "the rule remains accepted and detects only by default",
|
|
1880
|
+
local_override: "trustLocalRuleScope may explicitly enforce a caller-supplied rule"
|
|
1881
|
+
});
|
|
1870
1882
|
var RULE_PROPERTIES = Object.freeze([
|
|
1871
1883
|
"id",
|
|
1872
1884
|
"rule_id",
|
|
@@ -1886,12 +1898,13 @@ var RULE_PROPERTIES = Object.freeze([
|
|
|
1886
1898
|
"remove_headers",
|
|
1887
1899
|
"cookie_flags",
|
|
1888
1900
|
"ensure",
|
|
1889
|
-
"capture"
|
|
1901
|
+
"capture",
|
|
1902
|
+
BUILD_SCOPE_PROPERTY
|
|
1890
1903
|
]);
|
|
1891
1904
|
var CAPTURE_VERSION = 1;
|
|
1892
1905
|
var CAPTURE_KEYS = Object.freeze(["version", "raw_chars"]);
|
|
1893
1906
|
var CAPTURE_RAW_CHARS_MAX = 512;
|
|
1894
|
-
var NULL_EXEMPT_PROPERTIES = Object.freeze(["capture"]);
|
|
1907
|
+
var NULL_EXEMPT_PROPERTIES = Object.freeze(["capture", BUILD_SCOPE_PROPERTY]);
|
|
1895
1908
|
function captureProblem(capture) {
|
|
1896
1909
|
if (capture === void 0 || capture === null) return null;
|
|
1897
1910
|
if (typeof capture !== "object" || Array.isArray(capture)) return "capture must be an object";
|
|
@@ -2132,6 +2145,10 @@ function validateBundle(bundle, opts = {}) {
|
|
|
2132
2145
|
rejected.push({ id: idOf(wl), reason: "whitelist has no rule_id (would suppress every rule); set allowGlobalWhitelists to permit" });
|
|
2133
2146
|
continue;
|
|
2134
2147
|
}
|
|
2148
|
+
if (wl && typeof wl === "object" && BUILD_SCOPE_PROPERTY in wl) {
|
|
2149
|
+
rejected.push({ id: idOf(wl), reason: `whitelist may not carry ${BUILD_SCOPE_PROPERTY}` });
|
|
2150
|
+
continue;
|
|
2151
|
+
}
|
|
2135
2152
|
const reason = conditionsProblem(wl?.rule_v2);
|
|
2136
2153
|
if (reason) rejected.push({ id: idOf(wl), reason: `whitelist: ${reason}` });
|
|
2137
2154
|
else whitelists.push(wl);
|
|
@@ -2482,9 +2499,26 @@ async function pulseFetch(config, url, init, fetchImpl = fetch) {
|
|
|
2482
2499
|
return first.response;
|
|
2483
2500
|
}
|
|
2484
2501
|
|
|
2502
|
+
// src/build-id.ts
|
|
2503
|
+
var BUILD_ID = /^[0-9a-f]{64}$/i;
|
|
2504
|
+
function canonicalBuildId(value) {
|
|
2505
|
+
if (typeof value !== "string") return null;
|
|
2506
|
+
const trimmed = value.trim();
|
|
2507
|
+
return BUILD_ID.test(trimmed) ? trimmed.toLowerCase() : null;
|
|
2508
|
+
}
|
|
2509
|
+
var BUILD_STAMP_KEY = "_patchstack";
|
|
2510
|
+
function readBuildStamp(bundle) {
|
|
2511
|
+
if (bundle === null || typeof bundle !== "object") return null;
|
|
2512
|
+
const namespace = bundle[BUILD_STAMP_KEY];
|
|
2513
|
+
if (namespace === null || typeof namespace !== "object") return null;
|
|
2514
|
+
return canonicalBuildId(namespace.build_id);
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2485
2517
|
// src/protect/engine/pulse-client.js
|
|
2486
2518
|
var DEFAULT_BASE_URL2 = "https://api.patchstack.com/monitor/pulse";
|
|
2487
2519
|
var DEFAULT_CACHE_TTL2 = 3e5;
|
|
2520
|
+
var BUILD_VERDICT_HEADER = "X-Patchstack-Build-Match";
|
|
2521
|
+
var BUILD_IDENTITY_HEADER = "X-Patchstack-Build-ID";
|
|
2488
2522
|
var JITTER_FRACTION2 = 0.1;
|
|
2489
2523
|
var PulseRuleClient = class {
|
|
2490
2524
|
#siteUuid;
|
|
@@ -2497,7 +2531,8 @@ var PulseRuleClient = class {
|
|
|
2497
2531
|
#etag;
|
|
2498
2532
|
#pulseAuth;
|
|
2499
2533
|
#detectionState;
|
|
2500
|
-
|
|
2534
|
+
#buildId;
|
|
2535
|
+
constructor({ siteUuid, baseUrl, cacheTtl, etag, timeoutMs, pulseAuth, detectionState, buildId } = {}) {
|
|
2501
2536
|
this.#timeoutMs = Number(timeoutMs) > 0 ? Number(timeoutMs) : 3e4;
|
|
2502
2537
|
this.#siteUuid = siteUuid ?? process.env.PATCHSTACK_SITE_UUID;
|
|
2503
2538
|
this.#baseUrl = safeBaseUrl(baseUrl ?? process.env.PATCHSTACK_PULSE_RULES_URL, DEFAULT_BASE_URL2, "rule endpoint");
|
|
@@ -2505,6 +2540,7 @@ var PulseRuleClient = class {
|
|
|
2505
2540
|
this.#etag = etag ?? null;
|
|
2506
2541
|
this.#pulseAuth = pulseAuth ?? null;
|
|
2507
2542
|
this.#detectionState = typeof detectionState === "string" ? detectionState : null;
|
|
2543
|
+
this.#buildId = canonicalBuildId(buildId);
|
|
2508
2544
|
if (!this.#siteUuid) {
|
|
2509
2545
|
throw new Error("Patchstack site UUID is required. Pass { siteUuid } or set PATCHSTACK_SITE_UUID.");
|
|
2510
2546
|
}
|
|
@@ -2524,11 +2560,23 @@ var PulseRuleClient = class {
|
|
|
2524
2560
|
if (this.#detectionState !== null && typeof auth.Authorization === "string") {
|
|
2525
2561
|
headers["X-Patchstack-Detections"] = this.#detectionState;
|
|
2526
2562
|
}
|
|
2563
|
+
if (this.#buildId !== null && typeof auth.Authorization === "string") {
|
|
2564
|
+
headers["X-Patchstack-Build"] = this.#buildId;
|
|
2565
|
+
}
|
|
2527
2566
|
if (this.#etag) headers["If-None-Match"] = this.#etag;
|
|
2528
2567
|
const response = await fetch(url, { method: "GET", headers, signal: AbortSignal.timeout(this.#timeoutMs) });
|
|
2529
2568
|
if (response.status === 304) {
|
|
2530
2569
|
this.#touch(now);
|
|
2531
|
-
|
|
2570
|
+
const result2 = {
|
|
2571
|
+
...this.#cache ?? { firewall: [], whitelists: [], whitelist_keys: {} },
|
|
2572
|
+
success: true,
|
|
2573
|
+
notModified: true,
|
|
2574
|
+
etag: response.headers?.get?.("etag") ?? this.#etag,
|
|
2575
|
+
build: buildVerdictOf(response.headers)
|
|
2576
|
+
};
|
|
2577
|
+
this.#cache = result2;
|
|
2578
|
+
this.#etag = result2.etag;
|
|
2579
|
+
return result2;
|
|
2532
2580
|
}
|
|
2533
2581
|
if (!response.ok) {
|
|
2534
2582
|
return { success: false, error: `API returned ${response.status}`, firewall: [], whitelists: [], whitelist_keys: {} };
|
|
@@ -2544,7 +2592,10 @@ var PulseRuleClient = class {
|
|
|
2544
2592
|
firewall: data.firewall,
|
|
2545
2593
|
whitelists: Array.isArray(data.whitelists) ? data.whitelists : [],
|
|
2546
2594
|
whitelist_keys: data.whitelist_keys ?? {},
|
|
2547
|
-
...enforcementField(data)
|
|
2595
|
+
...enforcementField(data),
|
|
2596
|
+
// Carried through so the caller can decide what a build-scoped rule may do. Response metadata,
|
|
2597
|
+
// rather than bundle content, because it varies with the build asking and is also present on 304.
|
|
2598
|
+
build: buildVerdictOf(response.headers)
|
|
2548
2599
|
};
|
|
2549
2600
|
this.#cache = result;
|
|
2550
2601
|
this.#etag = result.etag;
|
|
@@ -2570,6 +2621,13 @@ var PulseRuleClient = class {
|
|
|
2570
2621
|
this.#etag = null;
|
|
2571
2622
|
}
|
|
2572
2623
|
};
|
|
2624
|
+
function buildVerdictOf(headers) {
|
|
2625
|
+
const get = headers !== null && typeof headers === "object" && typeof headers.get === "function" ? (name) => headers.get(name) : () => null;
|
|
2626
|
+
const stated = get(BUILD_VERDICT_HEADER);
|
|
2627
|
+
const verdict = stated === "match" || stated === "missing" || stated === "mismatch" ? stated : "unknown";
|
|
2628
|
+
const matched = get(BUILD_IDENTITY_HEADER);
|
|
2629
|
+
return { verdict, matchedBuildId: typeof matched === "string" ? matched : null };
|
|
2630
|
+
}
|
|
2573
2631
|
function enforcementField(data) {
|
|
2574
2632
|
if (!data || typeof data !== "object") return {};
|
|
2575
2633
|
const v = data.enforcement ?? data.mode;
|
|
@@ -3412,10 +3470,19 @@ async function cacheRead(dir) {
|
|
|
3412
3470
|
function toEnvelope(value) {
|
|
3413
3471
|
if (!value || typeof value !== "object") return null;
|
|
3414
3472
|
if (value.bundle && typeof value.bundle === "object") {
|
|
3415
|
-
|
|
3473
|
+
const buildId = canonicalBuildId(value.buildId);
|
|
3474
|
+
const matched = canonicalBuildId(value.matchedBuildId);
|
|
3475
|
+
return {
|
|
3476
|
+
bundle: value.bundle,
|
|
3477
|
+
etag: value.etag ?? null,
|
|
3478
|
+
buildId,
|
|
3479
|
+
// A confirmation belongs to the presentation stored beside it. A crossed or partially written
|
|
3480
|
+
// envelope confirms nothing, even when one of its fields happens to name the current map.
|
|
3481
|
+
matchedBuildId: buildId !== null && matched === buildId ? matched : null
|
|
3482
|
+
};
|
|
3416
3483
|
}
|
|
3417
3484
|
if (Array.isArray(value.firewall) || Array.isArray(value.whitelists)) {
|
|
3418
|
-
return { bundle: value, etag: null };
|
|
3485
|
+
return { bundle: value, etag: null, buildId: null, matchedBuildId: null };
|
|
3419
3486
|
}
|
|
3420
3487
|
return null;
|
|
3421
3488
|
}
|
|
@@ -3447,62 +3514,146 @@ function fromSource(bundle, origin, reason) {
|
|
|
3447
3514
|
source: reason === void 0 ? { ok: true, origin } : { ok: false, origin, reason }
|
|
3448
3515
|
};
|
|
3449
3516
|
}
|
|
3517
|
+
function buildIdentity(options) {
|
|
3518
|
+
if (options.buildId !== void 0 && options.buildId !== null) {
|
|
3519
|
+
const explicit = canonicalBuildId(options.buildId);
|
|
3520
|
+
return explicit === null ? { id: null, reason: "the buildId option is not a complete 64-hex SHA-256 identity" } : { id: explicit };
|
|
3521
|
+
}
|
|
3522
|
+
const stamped = readBuildStamp(options.rules);
|
|
3523
|
+
if (stamped !== null) return { id: stamped };
|
|
3524
|
+
return {
|
|
3525
|
+
id: null,
|
|
3526
|
+
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"
|
|
3527
|
+
};
|
|
3528
|
+
}
|
|
3529
|
+
function withScopedDryRun(bundle, confirmed, reason, options = {}) {
|
|
3530
|
+
let held = 0;
|
|
3531
|
+
let unusable = 0;
|
|
3532
|
+
const firewall = bundle.firewall.map((rule) => {
|
|
3533
|
+
if (rule === null || typeof rule !== "object") return rule;
|
|
3534
|
+
if (!(BUILD_SCOPE_PROPERTY in rule)) return rule;
|
|
3535
|
+
const scope = rule[BUILD_SCOPE_PROPERTY];
|
|
3536
|
+
const target = canonicalBuildId(scope);
|
|
3537
|
+
if (target !== null && confirmed !== null && target === confirmed) return rule;
|
|
3538
|
+
if (rule.enforcement === "dry-run") return rule;
|
|
3539
|
+
held += 1;
|
|
3540
|
+
if (target === null) unusable += 1;
|
|
3541
|
+
return { ...rule, enforcement: "dry-run" };
|
|
3542
|
+
});
|
|
3543
|
+
if (held === 0) return bundle;
|
|
3544
|
+
const explanations = [
|
|
3545
|
+
...unusable > 0 ? [`${unusable} rule(s) carry an unusable ${BUILD_SCOPE_PROPERTY}`] : [],
|
|
3546
|
+
...held > unusable ? [reason] : []
|
|
3547
|
+
];
|
|
3548
|
+
notify(
|
|
3549
|
+
options.onError,
|
|
3550
|
+
new Error(`${held} build-scoped rule(s) are detecting only, not blocking: ${explanations.join("; ")}`),
|
|
3551
|
+
"onError"
|
|
3552
|
+
);
|
|
3553
|
+
return { ...bundle, firewall };
|
|
3554
|
+
}
|
|
3555
|
+
function confirmedBy(res, presented) {
|
|
3556
|
+
if (presented === null) return null;
|
|
3557
|
+
const verdict = res?.build;
|
|
3558
|
+
if (verdict?.verdict !== "match") return null;
|
|
3559
|
+
return canonicalBuildId(verdict.matchedBuildId) === presented ? presented : null;
|
|
3560
|
+
}
|
|
3450
3561
|
async function resolveRules(options, store, ctx = {}) {
|
|
3451
3562
|
const timeoutMs = ctx.timeoutMs;
|
|
3452
3563
|
if (options.siteUuid) {
|
|
3453
3564
|
const prior = await store.read();
|
|
3454
|
-
const
|
|
3565
|
+
const identity = buildIdentity(options);
|
|
3566
|
+
const sameBuild = prior?.buildId === identity.id;
|
|
3567
|
+
const cacheConfirmed = identity.id !== null && sameBuild && prior?.matchedBuildId === identity.id ? identity.id : null;
|
|
3568
|
+
const unconfirmed = identity.reason ?? "the platform did not confirm that these coordinates belong to this map";
|
|
3569
|
+
const served = (bundle, confirmed) => withScopedDryRun(bundle, confirmed, unconfirmed, options);
|
|
3570
|
+
const local = (bundle) => options.trustLocalRuleScope === true ? bundle : withScopedDryRun(bundle, identity.id, identity.reason ?? "these rules name a different map", options);
|
|
3571
|
+
const client = new PulseRuleClient({
|
|
3572
|
+
siteUuid: options.siteUuid,
|
|
3573
|
+
baseUrl: options.pulseRulesUrl,
|
|
3574
|
+
etag: sameBuild ? prior?.etag : null,
|
|
3575
|
+
timeoutMs,
|
|
3576
|
+
pulseAuth: ctx.pulseAuth,
|
|
3577
|
+
detectionState: ctx.detectionState,
|
|
3578
|
+
buildId: identity.id
|
|
3579
|
+
});
|
|
3455
3580
|
const res = await client.getRules();
|
|
3456
|
-
if (res.success && res.notModified && prior?.bundle)
|
|
3581
|
+
if (res.success && res.notModified && prior?.bundle) {
|
|
3582
|
+
const confirmed = confirmedBy(res, identity.id);
|
|
3583
|
+
await store.write({
|
|
3584
|
+
bundle: prior.bundle,
|
|
3585
|
+
etag: res.etag ?? prior.etag ?? null,
|
|
3586
|
+
buildId: identity.id,
|
|
3587
|
+
matchedBuildId: confirmed
|
|
3588
|
+
});
|
|
3589
|
+
return fromSource(served(normalizeBundle(prior.bundle, options), confirmed), "cache");
|
|
3590
|
+
}
|
|
3457
3591
|
if (res.success && !res.notModified) {
|
|
3592
|
+
const confirmed = confirmedBy(res, identity.id);
|
|
3458
3593
|
const rejected = liveUpdateRejections(res, options);
|
|
3459
3594
|
if (rejected.length > 0) {
|
|
3460
3595
|
reportRejections(rejected, options, "rule update rejected");
|
|
3461
|
-
if (prior?.bundle) return fromSource(normalizeBundle(prior.bundle, options), "cache", "update rejected");
|
|
3462
|
-
if (options.rules) return fromSource(normalizeBundle(options.rules, options), "bundled", "update rejected");
|
|
3596
|
+
if (prior?.bundle) return fromSource(served(normalizeBundle(prior.bundle, options), cacheConfirmed), "cache", "update rejected");
|
|
3597
|
+
if (options.rules) return fromSource(local(normalizeBundle(options.rules, options)), "bundled", "update rejected");
|
|
3463
3598
|
return fromSource(emptyBundle(), "empty", "update rejected");
|
|
3464
3599
|
}
|
|
3465
3600
|
const bundle = normalizeBundle(res, options);
|
|
3466
|
-
await store.write({ bundle, etag: res.etag ?? null });
|
|
3467
|
-
return fromSource(bundle, "api");
|
|
3601
|
+
await store.write({ bundle, etag: res.etag ?? null, buildId: identity.id, matchedBuildId: confirmed });
|
|
3602
|
+
return fromSource(served(bundle, confirmed), "api");
|
|
3468
3603
|
}
|
|
3469
3604
|
if (prior?.bundle) {
|
|
3470
3605
|
notify(options.onError, new Error(`pulse rule fetch failed (${res.error ?? "no usable response"}); using cached bundle`), "onError");
|
|
3471
|
-
return fromSource(normalizeBundle(prior.bundle, options), "cache", res.error ?? "no usable response");
|
|
3606
|
+
return fromSource(served(normalizeBundle(prior.bundle, options), cacheConfirmed), "cache", res.error ?? "no usable response");
|
|
3472
3607
|
}
|
|
3473
3608
|
if (options.rules) {
|
|
3474
3609
|
notify(options.onError, new Error(`pulse rule fetch failed (${res.error ?? "no usable response"}); using bundled fallback`), "onError");
|
|
3475
|
-
return fromSource(normalizeBundle(options.rules, options), "bundled", res.error ?? "no usable response");
|
|
3610
|
+
return fromSource(local(normalizeBundle(options.rules, options)), "bundled", res.error ?? "no usable response");
|
|
3476
3611
|
}
|
|
3477
3612
|
notify(options.onError, new Error(`pulse rule fetch failed (${res.error ?? "no usable response"}); no cache \u2014 running with no rules`), "onError");
|
|
3478
3613
|
return fromSource(emptyBundle(), "empty", res.error ?? "no usable response");
|
|
3479
3614
|
}
|
|
3480
3615
|
if (options.token) {
|
|
3616
|
+
const identity = buildIdentity(options);
|
|
3617
|
+
const remote = (bundle) => withScopedDryRun(
|
|
3618
|
+
bundle,
|
|
3619
|
+
null,
|
|
3620
|
+
"the token-authenticated rules service did not corroborate this map",
|
|
3621
|
+
options
|
|
3622
|
+
);
|
|
3623
|
+
const local = (bundle) => options.trustLocalRuleScope === true ? bundle : withScopedDryRun(bundle, identity.id, identity.reason ?? "these rules name a different map", options);
|
|
3481
3624
|
const prior = await store.read();
|
|
3482
3625
|
const client = new PatchstackRuleClient({ token: options.token, baseUrl: options.baseUrl, etag: prior?.etag, timeoutMs });
|
|
3483
3626
|
const res = await client.getRules();
|
|
3484
|
-
if (res.success && res.notModified && prior?.bundle)
|
|
3627
|
+
if (res.success && res.notModified && prior?.bundle) {
|
|
3628
|
+
return fromSource(remote(normalizeBundle(prior.bundle, options)), "cache");
|
|
3629
|
+
}
|
|
3485
3630
|
if (res.success && !res.notModified) {
|
|
3486
3631
|
const rejected = liveUpdateRejections(res, options);
|
|
3487
3632
|
if (rejected.length > 0) {
|
|
3488
3633
|
reportRejections(rejected, options, "rule update rejected");
|
|
3489
|
-
if (prior?.bundle) return fromSource(normalizeBundle(prior.bundle, options), "cache", "update rejected");
|
|
3490
|
-
if (options.rules) return fromSource(normalizeBundle(options.rules, options), "bundled", "update rejected");
|
|
3634
|
+
if (prior?.bundle) return fromSource(remote(normalizeBundle(prior.bundle, options)), "cache", "update rejected");
|
|
3635
|
+
if (options.rules) return fromSource(local(normalizeBundle(options.rules, options)), "bundled", "update rejected");
|
|
3491
3636
|
return fromSource(emptyBundle(), "empty", "update rejected");
|
|
3492
3637
|
}
|
|
3493
3638
|
const bundle = normalizeBundle(res, options);
|
|
3494
3639
|
await store.write({ bundle, etag: res.etag ?? null });
|
|
3495
|
-
return fromSource(bundle, "api");
|
|
3640
|
+
return fromSource(remote(bundle), "api");
|
|
3496
3641
|
}
|
|
3497
3642
|
if (prior?.bundle) {
|
|
3498
3643
|
notify(options.onError, new Error(`rule fetch failed (${res.error ?? "no usable response"}); using cached bundle`), "onError");
|
|
3499
|
-
return fromSource(normalizeBundle(prior.bundle, options), "cache", res.error ?? "no usable response");
|
|
3644
|
+
return fromSource(remote(normalizeBundle(prior.bundle, options)), "cache", res.error ?? "no usable response");
|
|
3500
3645
|
}
|
|
3501
3646
|
notify(options.onError, new Error(`rule fetch failed (${res.error ?? "no usable response"}); no cache \u2014 running with no rules`), "onError");
|
|
3502
3647
|
return fromSource(emptyBundle(), "empty", res.error ?? "no usable response");
|
|
3503
3648
|
}
|
|
3504
3649
|
if (options.rules) {
|
|
3505
|
-
|
|
3650
|
+
const identity = buildIdentity(options);
|
|
3651
|
+
const bundle = normalizeBundle(options.rules, options);
|
|
3652
|
+
if (options.trustLocalRuleScope === true) return fromSource(bundle, "bundled");
|
|
3653
|
+
return fromSource(
|
|
3654
|
+
withScopedDryRun(bundle, identity.id, identity.reason ?? "these rules name a different map", options),
|
|
3655
|
+
"bundled"
|
|
3656
|
+
);
|
|
3506
3657
|
}
|
|
3507
3658
|
return fromSource(emptyBundle(), "empty");
|
|
3508
3659
|
}
|