rulesync 16.22.0 → 16.22.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/dist/cli/index.cjs +2 -2
- package/dist/cli/index.js +2 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/{import-CUyeYGxZ.cjs → import-B7o8So93.cjs} +128 -59
- package/dist/{import-BKqbq4Ut.js → import-DUE1P1zV.js} +129 -60
- package/dist/import-DUE1P1zV.js.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/import-BKqbq4Ut.js.map +0 -1
|
@@ -3385,6 +3385,47 @@ var RulesyncFile = class extends AiFile {
|
|
|
3385
3385
|
}
|
|
3386
3386
|
};
|
|
3387
3387
|
//#endregion
|
|
3388
|
+
//#region src/utils/bounded-walk.ts
|
|
3389
|
+
const ALIAS_HINT = "(a chain of YAML aliases may be amplifying the document)";
|
|
3390
|
+
/**
|
|
3391
|
+
* Create the bookkeeping for one walk. `subject` names the document kind in
|
|
3392
|
+
* every error ("Frontmatter", "Shared config"); `root`, when given, is entered
|
|
3393
|
+
* up front so the root object counts as the first nesting level, matching the
|
|
3394
|
+
* +1 that `enter` applies to every container nested inside it.
|
|
3395
|
+
*/
|
|
3396
|
+
function createBoundedWalk({ subject, limits, root }) {
|
|
3397
|
+
const ancestors = /* @__PURE__ */ new WeakSet();
|
|
3398
|
+
let valuesRemaining = limits.maxValues;
|
|
3399
|
+
let stringCharsRemaining = limits.maxStringChars;
|
|
3400
|
+
let depth = 0;
|
|
3401
|
+
const chargeChars = (chars) => {
|
|
3402
|
+
stringCharsRemaining -= chars;
|
|
3403
|
+
if (stringCharsRemaining < 0) throw new Error(`${subject}'s string values expand to more than ${limits.maxStringChars} characters; refusing to process it ${ALIAS_HINT}`);
|
|
3404
|
+
};
|
|
3405
|
+
const chargeValue = (stringChars = 0) => {
|
|
3406
|
+
valuesRemaining -= 1;
|
|
3407
|
+
if (valuesRemaining < 0) throw new Error(`${subject} expands to more than ${limits.maxValues} values; refusing to process it ${ALIAS_HINT}`);
|
|
3408
|
+
chargeChars(stringChars);
|
|
3409
|
+
};
|
|
3410
|
+
const enter = (container) => {
|
|
3411
|
+
depth += 1;
|
|
3412
|
+
if (depth > limits.maxDepth) throw new Error(`${subject} nests more than ${limits.maxDepth} levels deep; refusing to process it ${ALIAS_HINT}`);
|
|
3413
|
+
ancestors.add(container);
|
|
3414
|
+
};
|
|
3415
|
+
const leave = (container) => {
|
|
3416
|
+
ancestors.delete(container);
|
|
3417
|
+
depth -= 1;
|
|
3418
|
+
};
|
|
3419
|
+
if (root !== void 0) enter(root);
|
|
3420
|
+
return {
|
|
3421
|
+
chargeValue,
|
|
3422
|
+
chargeChars,
|
|
3423
|
+
isAncestor: (container) => ancestors.has(container),
|
|
3424
|
+
enter,
|
|
3425
|
+
leave
|
|
3426
|
+
};
|
|
3427
|
+
}
|
|
3428
|
+
//#endregion
|
|
3388
3429
|
//#region src/utils/prototype-pollution.ts
|
|
3389
3430
|
/**
|
|
3390
3431
|
* Keys that, if walked into when constructing or merging objects from
|
|
@@ -3524,30 +3565,6 @@ const MAX_FRONTMATTER_STRING_CHARS = 4e6;
|
|
|
3524
3565
|
* manifest stays well under this.
|
|
3525
3566
|
*/
|
|
3526
3567
|
const MAX_FRONTMATTER_RAW_CHARS = 65536;
|
|
3527
|
-
/** Charge string content (a string leaf or an object key) against the character budget. */
|
|
3528
|
-
function chargeStringChars({ options, chars }) {
|
|
3529
|
-
options.budget.stringCharsRemaining -= chars;
|
|
3530
|
-
if (options.budget.stringCharsRemaining < 0) throw new Error(`Frontmatter's string values expand to more than ${MAX_FRONTMATTER_STRING_CHARS} characters; refusing to process it (a chain of YAML aliases may be amplifying the document)`);
|
|
3531
|
-
}
|
|
3532
|
-
function consumeBudget({ options, stringChars = 0 }) {
|
|
3533
|
-
options.budget.remaining -= 1;
|
|
3534
|
-
if (options.budget.remaining < 0) throw new Error(`Frontmatter expands to more than ${MAX_FRONTMATTER_VALUES} values; refusing to process it (a chain of YAML aliases may be amplifying the document)`);
|
|
3535
|
-
chargeStringChars({
|
|
3536
|
-
options,
|
|
3537
|
-
chars: stringChars
|
|
3538
|
-
});
|
|
3539
|
-
}
|
|
3540
|
-
/** Enter one more container level, throwing if the depth cap is exceeded. */
|
|
3541
|
-
function enterContainer({ options, container }) {
|
|
3542
|
-
options.depth += 1;
|
|
3543
|
-
if (options.depth > 64) throw new Error(`Frontmatter nests more than 64 levels deep; refusing to process it (a chain of YAML aliases may be amplifying the document)`);
|
|
3544
|
-
options.ancestors.add(container);
|
|
3545
|
-
}
|
|
3546
|
-
/** Leave a container level entered via {@link enterContainer}. */
|
|
3547
|
-
function leaveContainer({ options, container }) {
|
|
3548
|
-
options.ancestors.delete(container);
|
|
3549
|
-
options.depth -= 1;
|
|
3550
|
-
}
|
|
3551
3568
|
/**
|
|
3552
3569
|
* Estimate the serialized character cost of a leaf that is not a string (a
|
|
3553
3570
|
* string leaf is charged by its own length instead).
|
|
@@ -3570,40 +3587,26 @@ function estimateLeafChars(value) {
|
|
|
3570
3587
|
* bounded by {@link MAX_FRONTMATTER_VALUES} instead.
|
|
3571
3588
|
*/
|
|
3572
3589
|
function deepCleanValue(value, options) {
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
stringChars: typeof value === "string" ? value.length : estimateLeafChars(value)
|
|
3576
|
-
});
|
|
3590
|
+
const leafChars = typeof value === "string" ? value.length : estimateLeafChars(value);
|
|
3591
|
+
options.walk.chargeValue(leafChars);
|
|
3577
3592
|
if (value === null || value === void 0) return;
|
|
3578
3593
|
if (typeof value === "string") return options.transformString ? options.transformString(value) : value;
|
|
3579
3594
|
if (Array.isArray(value)) {
|
|
3580
|
-
if (options.
|
|
3581
|
-
|
|
3582
|
-
options,
|
|
3583
|
-
container: value
|
|
3584
|
-
});
|
|
3595
|
+
if (options.walk.isAncestor(value)) return;
|
|
3596
|
+
options.walk.enter(value);
|
|
3585
3597
|
const cleanedArray = [];
|
|
3586
3598
|
for (const item of value) {
|
|
3587
3599
|
const cleaned = deepCleanValue(item, options);
|
|
3588
3600
|
if (cleaned !== void 0) cleanedArray.push(cleaned);
|
|
3589
3601
|
}
|
|
3590
|
-
|
|
3591
|
-
options,
|
|
3592
|
-
container: value
|
|
3593
|
-
});
|
|
3602
|
+
options.walk.leave(value);
|
|
3594
3603
|
return cleanedArray;
|
|
3595
3604
|
}
|
|
3596
3605
|
if (isPlainObject$1(value)) {
|
|
3597
|
-
if (options.
|
|
3598
|
-
|
|
3599
|
-
options,
|
|
3600
|
-
container: value
|
|
3601
|
-
});
|
|
3606
|
+
if (options.walk.isAncestor(value)) return;
|
|
3607
|
+
options.walk.enter(value);
|
|
3602
3608
|
const result = cleanOwnEntries(value, options);
|
|
3603
|
-
|
|
3604
|
-
options,
|
|
3605
|
-
container: value
|
|
3606
|
-
});
|
|
3609
|
+
options.walk.leave(value);
|
|
3607
3610
|
return result;
|
|
3608
3611
|
}
|
|
3609
3612
|
return value;
|
|
@@ -3622,10 +3625,7 @@ function deepCleanValue(value, options) {
|
|
|
3622
3625
|
function cleanOwnEntries(obj, options) {
|
|
3623
3626
|
const result = {};
|
|
3624
3627
|
for (const [key, val] of Object.entries(obj)) {
|
|
3625
|
-
|
|
3626
|
-
options,
|
|
3627
|
-
chars: key.length
|
|
3628
|
-
});
|
|
3628
|
+
options.walk.chargeChars(key.length);
|
|
3629
3629
|
const cleaned = deepCleanValue(val, options);
|
|
3630
3630
|
if (isPrototypePollutionKey(key)) continue;
|
|
3631
3631
|
if (cleaned !== void 0) result[key] = cleaned;
|
|
@@ -3636,12 +3636,15 @@ function deepCleanObject(obj, options) {
|
|
|
3636
3636
|
if (!obj || typeof obj !== "object") return {};
|
|
3637
3637
|
return cleanOwnEntries(obj, {
|
|
3638
3638
|
...options,
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3639
|
+
walk: createBoundedWalk({
|
|
3640
|
+
subject: "Frontmatter",
|
|
3641
|
+
limits: {
|
|
3642
|
+
maxValues: MAX_FRONTMATTER_VALUES,
|
|
3643
|
+
maxStringChars: MAX_FRONTMATTER_STRING_CHARS,
|
|
3644
|
+
maxDepth: 64
|
|
3645
|
+
},
|
|
3646
|
+
root: obj
|
|
3647
|
+
})
|
|
3645
3648
|
});
|
|
3646
3649
|
}
|
|
3647
3650
|
/** Drop null and undefined values, recursively. */
|
|
@@ -11483,6 +11486,21 @@ function stripStrings(_key, value) {
|
|
|
11483
11486
|
//#endregion
|
|
11484
11487
|
//#region src/features/shared/shared-config-gateway.ts
|
|
11485
11488
|
/**
|
|
11489
|
+
* Upper bound on the number of values a shared config document may expand
|
|
11490
|
+
* to once every YAML alias is written out. Real config files hold a few
|
|
11491
|
+
* hundred values at most; even a large MCP server catalog stays orders of
|
|
11492
|
+
* magnitude below the limit.
|
|
11493
|
+
*/
|
|
11494
|
+
const MAX_SHARED_CONFIG_VALUES = 1e5;
|
|
11495
|
+
/**
|
|
11496
|
+
* Upper bound on the total character count of the string leaves and keys a
|
|
11497
|
+
* shared config document may expand to. The value budget bounds how many
|
|
11498
|
+
* values are visited, but one long string aliased thousands of times fits
|
|
11499
|
+
* that budget while the duplicated output balloons; charging every visited
|
|
11500
|
+
* string's length separately bounds the output regardless of alias count.
|
|
11501
|
+
*/
|
|
11502
|
+
const MAX_SHARED_CONFIG_STRING_CHARS = 4e6;
|
|
11503
|
+
/**
|
|
11486
11504
|
* Rebuild a parsed document without its prototype-pollution keys.
|
|
11487
11505
|
*
|
|
11488
11506
|
* Every object is rebuilt, not just the ones that are already plain: a literal
|
|
@@ -11497,15 +11515,61 @@ function stripStrings(_key, value) {
|
|
|
11497
11515
|
*
|
|
11498
11516
|
* Dates are the one object the YAML and TOML parsers produce that is not a
|
|
11499
11517
|
* mapping, so they are passed through rather than flattened into `{}`.
|
|
11518
|
+
*
|
|
11519
|
+
* The rebuild is bounded, because a YAML alias makes one parsed container
|
|
11520
|
+
* reachable from many keys and every alias is copied out independently (the
|
|
11521
|
+
* writers dump with `noRefs: true`, so memoizing here would only move the
|
|
11522
|
+
* blowup into serialization). A small "alias bomb" of nested anchors would
|
|
11523
|
+
* otherwise cost exponential time and memory, and a self-referencing anchor
|
|
11524
|
+
* would recurse until the stack overflowed — both reachable from a config
|
|
11525
|
+
* file committed to a cloned repository. The walk therefore charges every
|
|
11526
|
+
* value against {@link MAX_SHARED_CONFIG_VALUES}, every string and key
|
|
11527
|
+
* against {@link MAX_SHARED_CONFIG_STRING_CHARS}, caps nesting at
|
|
11528
|
+
* {@link MAX_SHARED_CONFIG_DEPTH}, and refuses a reference back to an
|
|
11529
|
+
* ancestor outright, each with a clear error instead of a hang or a crash.
|
|
11500
11530
|
*/
|
|
11501
11531
|
function sanitizeSharedConfigValue(value) {
|
|
11502
|
-
|
|
11532
|
+
return sanitizeSharedConfigValueBounded(value, createBoundedWalk({
|
|
11533
|
+
subject: "Shared config",
|
|
11534
|
+
limits: {
|
|
11535
|
+
maxValues: MAX_SHARED_CONFIG_VALUES,
|
|
11536
|
+
maxStringChars: MAX_SHARED_CONFIG_STRING_CHARS,
|
|
11537
|
+
maxDepth: 64
|
|
11538
|
+
}
|
|
11539
|
+
}));
|
|
11540
|
+
}
|
|
11541
|
+
/**
|
|
11542
|
+
* Refuse a container that is already on the descent path. Unlike the
|
|
11543
|
+
* frontmatter cleaner, which drops such a cycle and keeps the rest of the
|
|
11544
|
+
* document, a shared config file is refused outright: silently dropping part
|
|
11545
|
+
* of a user's settings file would let a later write-back persist the loss.
|
|
11546
|
+
*/
|
|
11547
|
+
function enterSharedConfigContainer(walk, container) {
|
|
11548
|
+
if (walk.isAncestor(container)) throw new Error("Shared config contains a value that refers back to itself (a circular YAML alias); refusing to process it");
|
|
11549
|
+
walk.enter(container);
|
|
11550
|
+
}
|
|
11551
|
+
function sanitizeSharedConfigValueBounded(value, walk) {
|
|
11552
|
+
if (typeof value === "string") {
|
|
11553
|
+
walk.chargeValue(value.length);
|
|
11554
|
+
return value;
|
|
11555
|
+
}
|
|
11556
|
+
walk.chargeValue();
|
|
11557
|
+
if (Array.isArray(value)) {
|
|
11558
|
+
enterSharedConfigContainer(walk, value);
|
|
11559
|
+
const items = value.map((item) => sanitizeSharedConfigValueBounded(item, walk));
|
|
11560
|
+
walk.leave(value);
|
|
11561
|
+
return items;
|
|
11562
|
+
}
|
|
11503
11563
|
if (value === null || typeof value !== "object" || value instanceof Date) return value;
|
|
11564
|
+
enterSharedConfigContainer(walk, value);
|
|
11504
11565
|
const result = {};
|
|
11505
11566
|
for (const [key, nested] of Object.entries(value)) {
|
|
11567
|
+
walk.chargeChars(key.length);
|
|
11568
|
+
const sanitized = sanitizeSharedConfigValueBounded(nested, walk);
|
|
11506
11569
|
if (isPrototypePollutionKey(key)) continue;
|
|
11507
|
-
result[key] =
|
|
11570
|
+
result[key] = sanitized;
|
|
11508
11571
|
}
|
|
11572
|
+
walk.leave(value);
|
|
11509
11573
|
return result;
|
|
11510
11574
|
}
|
|
11511
11575
|
/**
|
|
@@ -11534,7 +11598,12 @@ function parseSharedConfig({ format, fileContent, filePath, invalidRootPolicy =
|
|
|
11534
11598
|
throw new Error(`Failed to parse shared config${at}: ${formatError(error)}`, { cause: error });
|
|
11535
11599
|
}
|
|
11536
11600
|
if (parsed === void 0 || parsed === null) return {};
|
|
11537
|
-
|
|
11601
|
+
let sanitized;
|
|
11602
|
+
try {
|
|
11603
|
+
sanitized = sanitizeSharedConfigValue(parsed);
|
|
11604
|
+
} catch (error) {
|
|
11605
|
+
throw new Error(`Failed to parse shared config${at}: ${formatError(error)}`, { cause: error });
|
|
11606
|
+
}
|
|
11538
11607
|
if (!isPlainObject$1(sanitized)) {
|
|
11539
11608
|
if (invalidRootPolicy === "error") throw new Error(`Failed to parse shared config${at}: expected a mapping at the root`);
|
|
11540
11609
|
return {};
|
|
@@ -3360,6 +3360,47 @@ var RulesyncFile = class extends AiFile {
|
|
|
3360
3360
|
}
|
|
3361
3361
|
};
|
|
3362
3362
|
//#endregion
|
|
3363
|
+
//#region src/utils/bounded-walk.ts
|
|
3364
|
+
const ALIAS_HINT = "(a chain of YAML aliases may be amplifying the document)";
|
|
3365
|
+
/**
|
|
3366
|
+
* Create the bookkeeping for one walk. `subject` names the document kind in
|
|
3367
|
+
* every error ("Frontmatter", "Shared config"); `root`, when given, is entered
|
|
3368
|
+
* up front so the root object counts as the first nesting level, matching the
|
|
3369
|
+
* +1 that `enter` applies to every container nested inside it.
|
|
3370
|
+
*/
|
|
3371
|
+
function createBoundedWalk({ subject, limits, root }) {
|
|
3372
|
+
const ancestors = /* @__PURE__ */ new WeakSet();
|
|
3373
|
+
let valuesRemaining = limits.maxValues;
|
|
3374
|
+
let stringCharsRemaining = limits.maxStringChars;
|
|
3375
|
+
let depth = 0;
|
|
3376
|
+
const chargeChars = (chars) => {
|
|
3377
|
+
stringCharsRemaining -= chars;
|
|
3378
|
+
if (stringCharsRemaining < 0) throw new Error(`${subject}'s string values expand to more than ${limits.maxStringChars} characters; refusing to process it ${ALIAS_HINT}`);
|
|
3379
|
+
};
|
|
3380
|
+
const chargeValue = (stringChars = 0) => {
|
|
3381
|
+
valuesRemaining -= 1;
|
|
3382
|
+
if (valuesRemaining < 0) throw new Error(`${subject} expands to more than ${limits.maxValues} values; refusing to process it ${ALIAS_HINT}`);
|
|
3383
|
+
chargeChars(stringChars);
|
|
3384
|
+
};
|
|
3385
|
+
const enter = (container) => {
|
|
3386
|
+
depth += 1;
|
|
3387
|
+
if (depth > limits.maxDepth) throw new Error(`${subject} nests more than ${limits.maxDepth} levels deep; refusing to process it ${ALIAS_HINT}`);
|
|
3388
|
+
ancestors.add(container);
|
|
3389
|
+
};
|
|
3390
|
+
const leave = (container) => {
|
|
3391
|
+
ancestors.delete(container);
|
|
3392
|
+
depth -= 1;
|
|
3393
|
+
};
|
|
3394
|
+
if (root !== void 0) enter(root);
|
|
3395
|
+
return {
|
|
3396
|
+
chargeValue,
|
|
3397
|
+
chargeChars,
|
|
3398
|
+
isAncestor: (container) => ancestors.has(container),
|
|
3399
|
+
enter,
|
|
3400
|
+
leave
|
|
3401
|
+
};
|
|
3402
|
+
}
|
|
3403
|
+
//#endregion
|
|
3363
3404
|
//#region src/utils/prototype-pollution.ts
|
|
3364
3405
|
/**
|
|
3365
3406
|
* Keys that, if walked into when constructing or merging objects from
|
|
@@ -3499,30 +3540,6 @@ const MAX_FRONTMATTER_STRING_CHARS = 4e6;
|
|
|
3499
3540
|
* manifest stays well under this.
|
|
3500
3541
|
*/
|
|
3501
3542
|
const MAX_FRONTMATTER_RAW_CHARS = 65536;
|
|
3502
|
-
/** Charge string content (a string leaf or an object key) against the character budget. */
|
|
3503
|
-
function chargeStringChars({ options, chars }) {
|
|
3504
|
-
options.budget.stringCharsRemaining -= chars;
|
|
3505
|
-
if (options.budget.stringCharsRemaining < 0) throw new Error(`Frontmatter's string values expand to more than ${MAX_FRONTMATTER_STRING_CHARS} characters; refusing to process it (a chain of YAML aliases may be amplifying the document)`);
|
|
3506
|
-
}
|
|
3507
|
-
function consumeBudget({ options, stringChars = 0 }) {
|
|
3508
|
-
options.budget.remaining -= 1;
|
|
3509
|
-
if (options.budget.remaining < 0) throw new Error(`Frontmatter expands to more than ${MAX_FRONTMATTER_VALUES} values; refusing to process it (a chain of YAML aliases may be amplifying the document)`);
|
|
3510
|
-
chargeStringChars({
|
|
3511
|
-
options,
|
|
3512
|
-
chars: stringChars
|
|
3513
|
-
});
|
|
3514
|
-
}
|
|
3515
|
-
/** Enter one more container level, throwing if the depth cap is exceeded. */
|
|
3516
|
-
function enterContainer({ options, container }) {
|
|
3517
|
-
options.depth += 1;
|
|
3518
|
-
if (options.depth > 64) throw new Error(`Frontmatter nests more than 64 levels deep; refusing to process it (a chain of YAML aliases may be amplifying the document)`);
|
|
3519
|
-
options.ancestors.add(container);
|
|
3520
|
-
}
|
|
3521
|
-
/** Leave a container level entered via {@link enterContainer}. */
|
|
3522
|
-
function leaveContainer({ options, container }) {
|
|
3523
|
-
options.ancestors.delete(container);
|
|
3524
|
-
options.depth -= 1;
|
|
3525
|
-
}
|
|
3526
3543
|
/**
|
|
3527
3544
|
* Estimate the serialized character cost of a leaf that is not a string (a
|
|
3528
3545
|
* string leaf is charged by its own length instead).
|
|
@@ -3545,40 +3562,26 @@ function estimateLeafChars(value) {
|
|
|
3545
3562
|
* bounded by {@link MAX_FRONTMATTER_VALUES} instead.
|
|
3546
3563
|
*/
|
|
3547
3564
|
function deepCleanValue(value, options) {
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
stringChars: typeof value === "string" ? value.length : estimateLeafChars(value)
|
|
3551
|
-
});
|
|
3565
|
+
const leafChars = typeof value === "string" ? value.length : estimateLeafChars(value);
|
|
3566
|
+
options.walk.chargeValue(leafChars);
|
|
3552
3567
|
if (value === null || value === void 0) return;
|
|
3553
3568
|
if (typeof value === "string") return options.transformString ? options.transformString(value) : value;
|
|
3554
3569
|
if (Array.isArray(value)) {
|
|
3555
|
-
if (options.
|
|
3556
|
-
|
|
3557
|
-
options,
|
|
3558
|
-
container: value
|
|
3559
|
-
});
|
|
3570
|
+
if (options.walk.isAncestor(value)) return;
|
|
3571
|
+
options.walk.enter(value);
|
|
3560
3572
|
const cleanedArray = [];
|
|
3561
3573
|
for (const item of value) {
|
|
3562
3574
|
const cleaned = deepCleanValue(item, options);
|
|
3563
3575
|
if (cleaned !== void 0) cleanedArray.push(cleaned);
|
|
3564
3576
|
}
|
|
3565
|
-
|
|
3566
|
-
options,
|
|
3567
|
-
container: value
|
|
3568
|
-
});
|
|
3577
|
+
options.walk.leave(value);
|
|
3569
3578
|
return cleanedArray;
|
|
3570
3579
|
}
|
|
3571
3580
|
if (isPlainObject$1(value)) {
|
|
3572
|
-
if (options.
|
|
3573
|
-
|
|
3574
|
-
options,
|
|
3575
|
-
container: value
|
|
3576
|
-
});
|
|
3581
|
+
if (options.walk.isAncestor(value)) return;
|
|
3582
|
+
options.walk.enter(value);
|
|
3577
3583
|
const result = cleanOwnEntries(value, options);
|
|
3578
|
-
|
|
3579
|
-
options,
|
|
3580
|
-
container: value
|
|
3581
|
-
});
|
|
3584
|
+
options.walk.leave(value);
|
|
3582
3585
|
return result;
|
|
3583
3586
|
}
|
|
3584
3587
|
return value;
|
|
@@ -3597,10 +3600,7 @@ function deepCleanValue(value, options) {
|
|
|
3597
3600
|
function cleanOwnEntries(obj, options) {
|
|
3598
3601
|
const result = {};
|
|
3599
3602
|
for (const [key, val] of Object.entries(obj)) {
|
|
3600
|
-
|
|
3601
|
-
options,
|
|
3602
|
-
chars: key.length
|
|
3603
|
-
});
|
|
3603
|
+
options.walk.chargeChars(key.length);
|
|
3604
3604
|
const cleaned = deepCleanValue(val, options);
|
|
3605
3605
|
if (isPrototypePollutionKey(key)) continue;
|
|
3606
3606
|
if (cleaned !== void 0) result[key] = cleaned;
|
|
@@ -3611,12 +3611,15 @@ function deepCleanObject(obj, options) {
|
|
|
3611
3611
|
if (!obj || typeof obj !== "object") return {};
|
|
3612
3612
|
return cleanOwnEntries(obj, {
|
|
3613
3613
|
...options,
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3614
|
+
walk: createBoundedWalk({
|
|
3615
|
+
subject: "Frontmatter",
|
|
3616
|
+
limits: {
|
|
3617
|
+
maxValues: MAX_FRONTMATTER_VALUES,
|
|
3618
|
+
maxStringChars: MAX_FRONTMATTER_STRING_CHARS,
|
|
3619
|
+
maxDepth: 64
|
|
3620
|
+
},
|
|
3621
|
+
root: obj
|
|
3622
|
+
})
|
|
3620
3623
|
});
|
|
3621
3624
|
}
|
|
3622
3625
|
/** Drop null and undefined values, recursively. */
|
|
@@ -11458,6 +11461,21 @@ function stripStrings(_key, value) {
|
|
|
11458
11461
|
//#endregion
|
|
11459
11462
|
//#region src/features/shared/shared-config-gateway.ts
|
|
11460
11463
|
/**
|
|
11464
|
+
* Upper bound on the number of values a shared config document may expand
|
|
11465
|
+
* to once every YAML alias is written out. Real config files hold a few
|
|
11466
|
+
* hundred values at most; even a large MCP server catalog stays orders of
|
|
11467
|
+
* magnitude below the limit.
|
|
11468
|
+
*/
|
|
11469
|
+
const MAX_SHARED_CONFIG_VALUES = 1e5;
|
|
11470
|
+
/**
|
|
11471
|
+
* Upper bound on the total character count of the string leaves and keys a
|
|
11472
|
+
* shared config document may expand to. The value budget bounds how many
|
|
11473
|
+
* values are visited, but one long string aliased thousands of times fits
|
|
11474
|
+
* that budget while the duplicated output balloons; charging every visited
|
|
11475
|
+
* string's length separately bounds the output regardless of alias count.
|
|
11476
|
+
*/
|
|
11477
|
+
const MAX_SHARED_CONFIG_STRING_CHARS = 4e6;
|
|
11478
|
+
/**
|
|
11461
11479
|
* Rebuild a parsed document without its prototype-pollution keys.
|
|
11462
11480
|
*
|
|
11463
11481
|
* Every object is rebuilt, not just the ones that are already plain: a literal
|
|
@@ -11472,15 +11490,61 @@ function stripStrings(_key, value) {
|
|
|
11472
11490
|
*
|
|
11473
11491
|
* Dates are the one object the YAML and TOML parsers produce that is not a
|
|
11474
11492
|
* mapping, so they are passed through rather than flattened into `{}`.
|
|
11493
|
+
*
|
|
11494
|
+
* The rebuild is bounded, because a YAML alias makes one parsed container
|
|
11495
|
+
* reachable from many keys and every alias is copied out independently (the
|
|
11496
|
+
* writers dump with `noRefs: true`, so memoizing here would only move the
|
|
11497
|
+
* blowup into serialization). A small "alias bomb" of nested anchors would
|
|
11498
|
+
* otherwise cost exponential time and memory, and a self-referencing anchor
|
|
11499
|
+
* would recurse until the stack overflowed — both reachable from a config
|
|
11500
|
+
* file committed to a cloned repository. The walk therefore charges every
|
|
11501
|
+
* value against {@link MAX_SHARED_CONFIG_VALUES}, every string and key
|
|
11502
|
+
* against {@link MAX_SHARED_CONFIG_STRING_CHARS}, caps nesting at
|
|
11503
|
+
* {@link MAX_SHARED_CONFIG_DEPTH}, and refuses a reference back to an
|
|
11504
|
+
* ancestor outright, each with a clear error instead of a hang or a crash.
|
|
11475
11505
|
*/
|
|
11476
11506
|
function sanitizeSharedConfigValue(value) {
|
|
11477
|
-
|
|
11507
|
+
return sanitizeSharedConfigValueBounded(value, createBoundedWalk({
|
|
11508
|
+
subject: "Shared config",
|
|
11509
|
+
limits: {
|
|
11510
|
+
maxValues: MAX_SHARED_CONFIG_VALUES,
|
|
11511
|
+
maxStringChars: MAX_SHARED_CONFIG_STRING_CHARS,
|
|
11512
|
+
maxDepth: 64
|
|
11513
|
+
}
|
|
11514
|
+
}));
|
|
11515
|
+
}
|
|
11516
|
+
/**
|
|
11517
|
+
* Refuse a container that is already on the descent path. Unlike the
|
|
11518
|
+
* frontmatter cleaner, which drops such a cycle and keeps the rest of the
|
|
11519
|
+
* document, a shared config file is refused outright: silently dropping part
|
|
11520
|
+
* of a user's settings file would let a later write-back persist the loss.
|
|
11521
|
+
*/
|
|
11522
|
+
function enterSharedConfigContainer(walk, container) {
|
|
11523
|
+
if (walk.isAncestor(container)) throw new Error("Shared config contains a value that refers back to itself (a circular YAML alias); refusing to process it");
|
|
11524
|
+
walk.enter(container);
|
|
11525
|
+
}
|
|
11526
|
+
function sanitizeSharedConfigValueBounded(value, walk) {
|
|
11527
|
+
if (typeof value === "string") {
|
|
11528
|
+
walk.chargeValue(value.length);
|
|
11529
|
+
return value;
|
|
11530
|
+
}
|
|
11531
|
+
walk.chargeValue();
|
|
11532
|
+
if (Array.isArray(value)) {
|
|
11533
|
+
enterSharedConfigContainer(walk, value);
|
|
11534
|
+
const items = value.map((item) => sanitizeSharedConfigValueBounded(item, walk));
|
|
11535
|
+
walk.leave(value);
|
|
11536
|
+
return items;
|
|
11537
|
+
}
|
|
11478
11538
|
if (value === null || typeof value !== "object" || value instanceof Date) return value;
|
|
11539
|
+
enterSharedConfigContainer(walk, value);
|
|
11479
11540
|
const result = {};
|
|
11480
11541
|
for (const [key, nested] of Object.entries(value)) {
|
|
11542
|
+
walk.chargeChars(key.length);
|
|
11543
|
+
const sanitized = sanitizeSharedConfigValueBounded(nested, walk);
|
|
11481
11544
|
if (isPrototypePollutionKey(key)) continue;
|
|
11482
|
-
result[key] =
|
|
11545
|
+
result[key] = sanitized;
|
|
11483
11546
|
}
|
|
11547
|
+
walk.leave(value);
|
|
11484
11548
|
return result;
|
|
11485
11549
|
}
|
|
11486
11550
|
/**
|
|
@@ -11509,7 +11573,12 @@ function parseSharedConfig({ format, fileContent, filePath, invalidRootPolicy =
|
|
|
11509
11573
|
throw new Error(`Failed to parse shared config${at}: ${formatError(error)}`, { cause: error });
|
|
11510
11574
|
}
|
|
11511
11575
|
if (parsed === void 0 || parsed === null) return {};
|
|
11512
|
-
|
|
11576
|
+
let sanitized;
|
|
11577
|
+
try {
|
|
11578
|
+
sanitized = sanitizeSharedConfigValue(parsed);
|
|
11579
|
+
} catch (error) {
|
|
11580
|
+
throw new Error(`Failed to parse shared config${at}: ${formatError(error)}`, { cause: error });
|
|
11581
|
+
}
|
|
11513
11582
|
if (!isPlainObject$1(sanitized)) {
|
|
11514
11583
|
if (invalidRootPolicy === "error") throw new Error(`Failed to parse shared config${at}: expected a mapping at the root`);
|
|
11515
11584
|
return {};
|
|
@@ -68635,4 +68704,4 @@ async function importChecksCore(params) {
|
|
|
68635
68704
|
//#endregion
|
|
68636
68705
|
export { RulesyncCheckFrontmatterSchema as $, ALL_TOOL_TARGETS_WITH_WILDCARD as $t, FACTORYDROID_DIR as A, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as An, fileExists as At, RulesyncSkillFrontmatterSchema as B, stripControlCharacters as Bn, readFileContent as Bt, CODEXCLI_BASH_RULES_FILE_NAME as C, RULESYNC_PERMISSIONS_LEGACY_FILE_NAME as Cn, assertDirectoryIfExists as Ct, CLAUDECODE_MEMORIES_DIR_NAME as D, RULESYNC_RULES_RELATIVE_DIR_PATH as Dn, createTempDirectory as Dt, CLAUDECODE_LOCAL_RULE_FILE_NAME as E, RULESYNC_RELATIVE_DIR_PATH as En, checkPathTraversal as Et, AUGMENTCODE_SETTINGS_LOCAL_FILE_NAME as F, formatError as Fn, isSymlink as Ft, RulesyncIgnore as G, removeFileStrict as Gt, RulesyncRuleFrontmatterSchema as H, stripHiddenCharacters as Hn, removeDirectory as Ht, getLocalSkillDirNames as I, truncateText as In, listDirectoryEntryNames as It, resolveRulesyncSourceWritePath as J, runWithDirectoryRollback as Jt, RulesyncHooks as K, removeTempDirectory as Kt, RulesyncSubagent as L, hasDeceptiveHiddenCharacters as Ln, listFilePathsRecursively as Lt, caseFoldIdentity as M, ALL_FEATURES as Mn, getHomeDirectory as Mt, groupSpellingsByCaseFoldedIdentity as N, ALL_FEATURES_WITH_WILDCARD as Nn, isFileNotFoundError as Nt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as O, RULESYNC_SKILLS_RELATIVE_DIR_PATH as On, directoryExists as Ot, AUGMENTCODE_DIR as P, DEPRECATED_FEATURE_REPLACEMENTS as Pn, isFileSystemError as Pt, RulesyncCheck as Q, ALL_TOOL_TARGETS as Qt, RulesyncSubagentFrontmatterSchema as R, hasEnclosingMarkOutsideKeycap as Rn, listSubdirectoryNames as Rt, ChecksProcessor as S, RULESYNC_PERMISSIONS_FILE_NAME as Sn, applyFileMode as St, CLAUDECODE_DIR as T, RULESYNC_PERMISSIONS_SCHEMA_URL as Tn, assertWritablePathInsideRoot as Tt, RulesyncPermissions as U, removeDirectoryStrict as Ut, RulesyncRule as V, stripControlCharactersKeepingLineFeeds as Vn, readFileContentOrNull as Vt, RulesyncMcp as W, removeFile as Wt, RulesyncCommand as X, writeFileBuffer as Xt, parseJsonc as Y, toPosixPath as Yt, RulesyncCommandFrontmatterSchema as Z, writeFileContent as Zt, IgnoreProcessor as _, RULESYNC_MCP_FILE_NAME as _n, withFallbackLoggerTarget as _t, getProcessorRegistryEntry as a, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as an, mergeInputRootConfigs as at, QWENCODE_DIR as b, RULESYNC_MCP_SCHEMA_URL as bn, CLIError as bt, RulesProcessor as c, RULESYNC_CONFIG_RELATIVE_FILE_PATH as cn, ConfigFileSchema as ct, CODEBUDDY_DIR as d, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as dn, findControlCharacter as dt, PACKAGING_TOOL_TARGETS as en, stringifyFrontmatter as et, CODEBUDDY_LOCAL_RULE_FILE_NAME as f, RULESYNC_HOOKS_FILE_NAME as fn, ConsoleLogger as ft, McpProcessor as g, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as gn, warnOnConflictingFlags as gt, shortenToWidth as h, RULESYNC_IGNORE_RELATIVE_FILE_PATH as hn, fallbackLogger as ht, inspectInputRoots as i, RULESYNC_AIIGNORE_FILE_NAME as in, ConfigResolver as it, FACTORYDROID_SETTINGS_LOCAL_FILE_NAME as j, parseCommaSeparatedList as jn, getFileSize as jt, CLAUDECODE_SKILLS_DIR_PATH as k, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as kn, ensureDir as kt, SubagentsProcessor as l, RULESYNC_CONFIG_SCHEMA_URL as ln, GITIGNORE_DESTINATION_KEY as lt, displayWidthOf as m, RULESYNC_HOOKS_RELATIVE_FILE_PATH as mn, WarningCollectingLogger as mt, formatSourceLoadFailure as n, CURATED_RULES_FEATURE_SUBDIR as nn, SHARED_USER_MANAGED_CONFIG_PATHS as nt, convertFromTool as o, RULESYNC_CHECKS_RELATIVE_DIR_PATH as on, resolveEffectiveInputRoots as ot, ELLIPSIS_WIDTH as p, RULESYNC_HOOKS_LEGACY_FILE_NAME as pn, JsonLogger as pt, getRulesyncSourceCandidates as q, resolvePath as qt, generate as r, MAX_FILE_SIZE as rn, SKILL_FILE_NAME as rt, isPackagingToolTarget as s, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as sn, CONFLICTING_TARGET_PAIRS as st, importFromTool as t, ToolTargetSchema as tn, loadYaml as tt, SkillsProcessor as u, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as un, SourceEntrySchema as ut, HooksProcessor as v, RULESYNC_MCP_LEGACY_FILE_NAME as vn, resetRunWarningState as vt, CODEXCLI_DIR as w, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as wn, assertTreeContainsNoSymlinks as wt, QWENCODE_LOCAL_RULE_FILE_NAME as x, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as xn, ErrorCodes as xt, CommandsProcessor as y, RULESYNC_MCP_RELATIVE_FILE_PATH as yn, withWarnOnceScope as yt, RulesyncSkill as z, quoteForLog as zn, pathEscapesRoot as zt };
|
|
68637
68706
|
|
|
68638
|
-
//# sourceMappingURL=import-
|
|
68707
|
+
//# sourceMappingURL=import-DUE1P1zV.js.map
|