@normed/bundle 4.8.3 → 4.8.6

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.
@@ -1,4 +1,15 @@
1
1
  import type * as esbuild from "esbuild";
2
+ export declare function getLinkingAttrs(elementName: string): string[] | undefined;
3
+ export declare function findPackageRoot(filePath: string): string | null;
4
+ /**
5
+ * Resolve an asset path from a pug file, treating "/" as srcWebRoot (not filesystem root).
6
+ */
7
+ export declare function resolveAssetPath(assetPath: string, pugDir: string, srcWebRoot: string): string;
8
+ /**
9
+ * Assert that an absolute path is within one of the allowed source roots.
10
+ * Throws if the path escapes all allowed roots.
11
+ */
12
+ export declare function assertWithinAllowedRoots(absolutePath: string, allowedRoots: string[], originalValue: string, pugFilePath: string): void;
2
13
  /**
3
14
  * Discovered pug file reference from HTML.
4
15
  */
package/bundles/index.js CHANGED
@@ -68797,19 +68797,17 @@ function computeContentHash(content) {
68797
68797
  const num = hash.readUInt32BE(0);
68798
68798
  return num.toString(36).toUpperCase().padStart(8, "0").slice(0, 8);
68799
68799
  }
68800
- var ASSET_ATTRIBUTES = {
68801
- img: ["src", "srcset"],
68802
- video: ["src", "poster"],
68803
- audio: ["src"],
68804
- source: ["src", "srcset"],
68805
- link: ["href"],
68806
- script: ["src"],
68800
+ var UNIVERSAL_LINKING_ATTRS = ["src", "srcset", "href"];
68801
+ var ELEMENT_SPECIFIC_LINKING_ATTRS = {
68802
+ form: ["action"],
68807
68803
  object: ["data"],
68808
- embed: ["src"],
68809
- track: ["src"],
68810
- input: ["src"]
68811
- // for type="image"
68804
+ video: ["poster"]
68812
68805
  };
68806
+ function getLinkingAttrs(elementName) {
68807
+ if (!elementName) return void 0;
68808
+ const extra = ELEMENT_SPECIFIC_LINKING_ATTRS[elementName];
68809
+ return extra ? [...UNIVERSAL_LINKING_ATTRS, ...extra] : UNIVERSAL_LINKING_ATTRS;
68810
+ }
68813
68811
  function isRelativeAssetPath(assetPath) {
68814
68812
  if (!assetPath) return false;
68815
68813
  if (assetPath.startsWith("#")) return false;
@@ -68878,6 +68876,42 @@ function resolvePackagePath(packageSpecifier, fromFile) {
68878
68876
  return null;
68879
68877
  }
68880
68878
  }
68879
+ var packageRootCache = /* @__PURE__ */ new Map();
68880
+ function findPackageRoot(filePath) {
68881
+ let dir = path5.dirname(path5.resolve(filePath));
68882
+ const cacheKey = dir;
68883
+ if (packageRootCache.has(cacheKey)) return packageRootCache.get(cacheKey);
68884
+ while (true) {
68885
+ if (fs6.existsSync(path5.join(dir, "package.json"))) {
68886
+ packageRootCache.set(cacheKey, dir);
68887
+ return dir;
68888
+ }
68889
+ const parent = path5.dirname(dir);
68890
+ if (parent === dir) {
68891
+ packageRootCache.set(cacheKey, null);
68892
+ return null;
68893
+ }
68894
+ dir = parent;
68895
+ }
68896
+ }
68897
+ function resolveAssetPath(assetPath, pugDir, srcWebRoot) {
68898
+ if (assetPath.startsWith("/")) {
68899
+ return path5.resolve(srcWebRoot, assetPath.slice(1));
68900
+ }
68901
+ return path5.resolve(pugDir, assetPath);
68902
+ }
68903
+ function assertWithinAllowedRoots(absolutePath, allowedRoots, originalValue, pugFilePath) {
68904
+ const normalizedPath = path5.resolve(absolutePath);
68905
+ const withinARoot = allowedRoots.some((root) => {
68906
+ const normalizedRoot = path5.resolve(root);
68907
+ return normalizedPath === normalizedRoot || normalizedPath.startsWith(normalizedRoot + path5.sep);
68908
+ });
68909
+ if (!withinARoot) {
68910
+ throw new Error(
68911
+ `Security: asset path "${originalValue}" resolves to "${normalizedPath}" which is outside the allowed source directories [${allowedRoots.join(", ")}]. Referenced in ${pugFilePath}. Use the copy: or build: prefix to resolve via Node module resolution.`
68912
+ );
68913
+ }
68914
+ }
68881
68915
  var discoveredPugReferences = /* @__PURE__ */ new Map();
68882
68916
  var discoveredLessReferences = /* @__PURE__ */ new Map();
68883
68917
  var discoveredScriptReferences = /* @__PURE__ */ new Map();
@@ -68912,7 +68946,7 @@ function computeHtmlAssetPath(opts) {
68912
68946
  }
68913
68947
  return path5.relative(opts.htmlOutputDir, opts.absoluteOutput);
68914
68948
  }
68915
- async function processHtmlAssets(html, pugFilePath, options, webRoot) {
68949
+ async function processHtmlAssets(html, pugFilePath, options, webRoot, collectedReferences = []) {
68916
68950
  const assets = [];
68917
68951
  const pugReferences = [];
68918
68952
  const lessReferences = [];
@@ -68923,30 +68957,24 @@ async function processHtmlAssets(html, pugFilePath, options, webRoot) {
68923
68957
  const outbase = options.outbase || path5.dirname(pugFilePath);
68924
68958
  const pugDir = path5.dirname(pugFilePath);
68925
68959
  const publicPath = options.publicPath || "";
68926
- const processedAssets = /* @__PURE__ */ new Map();
68927
- const allAttrs = [...new Set(Object.values(ASSET_ATTRIBUTES).flat())].join(
68928
- "|"
68960
+ const srcWebRoot = webRoot ? path5.join(outbase, webRoot) : outbase;
68961
+ const normalizedOutbase = path5.resolve(outbase);
68962
+ const normalizedPug = path5.resolve(pugFilePath);
68963
+ const pugWithinOutbase = normalizedPug === normalizedOutbase || normalizedPug.startsWith(normalizedOutbase + path5.sep);
68964
+ const packageRoot = pugWithinOutbase ? null : findPackageRoot(pugFilePath);
68965
+ const allowedRoots = [outbase, packageRoot].filter(
68966
+ (r) => r != null
68929
68967
  );
68930
- const attrRegex = new RegExp(`(${allAttrs})\\s*=\\s*["']([^"']+)["']`, "gi");
68968
+ const processedAssets = /* @__PURE__ */ new Map();
68931
68969
  let modifiedHtml = html;
68932
- const matches = [];
68933
- let match;
68934
- while ((match = attrRegex.exec(html)) !== null) {
68935
- const attr = match[1];
68936
- const value = match[2];
68937
- if (attr && value) {
68938
- matches.push({
68939
- fullMatch: match[0],
68940
- attr: attr.toLowerCase(),
68941
- value
68942
- });
68943
- }
68944
- }
68970
+ const matches = collectedReferences.map(
68971
+ (ref) => ({ attr: ref.attr, value: ref.value })
68972
+ );
68945
68973
  const discoveredPugPaths = /* @__PURE__ */ new Set();
68946
68974
  const discoveredLessPaths = /* @__PURE__ */ new Set();
68947
68975
  const discoveredScriptPaths = /* @__PURE__ */ new Set();
68948
68976
  const discoveredPackageCssPaths = /* @__PURE__ */ new Set();
68949
- for (const { fullMatch, attr, value } of matches) {
68977
+ for (const { attr, value } of matches) {
68950
68978
  let newValue = value;
68951
68979
  if (attr === "srcset") {
68952
68980
  const srcsetParts = value.split(",");
@@ -68959,7 +68987,13 @@ async function processHtmlAssets(html, pugFilePath, options, webRoot) {
68959
68987
  [getVerbatimPath(assetPath), ...descriptors].filter(Boolean).join(" ")
68960
68988
  );
68961
68989
  } else if (assetPath && isPugReference(assetPath)) {
68962
- const absolutePath = path5.resolve(pugDir, assetPath);
68990
+ const absolutePath = resolveAssetPath(assetPath, pugDir, srcWebRoot);
68991
+ assertWithinAllowedRoots(
68992
+ absolutePath,
68993
+ allowedRoots,
68994
+ assetPath,
68995
+ pugFilePath
68996
+ );
68963
68997
  if (!discoveredPugPaths.has(absolutePath)) {
68964
68998
  discoveredPugPaths.add(absolutePath);
68965
68999
  pugReferences.push({ originalHref: assetPath, absolutePath });
@@ -68976,6 +69010,8 @@ async function processHtmlAssets(html, pugFilePath, options, webRoot) {
68976
69010
  publicPath,
68977
69011
  assets,
68978
69012
  processedAssets,
69013
+ srcWebRoot,
69014
+ allowedRoots,
68979
69015
  webRoot
68980
69016
  );
68981
69017
  if (hashedPath) {
@@ -68993,19 +69029,22 @@ async function processHtmlAssets(html, pugFilePath, options, webRoot) {
68993
69029
  } else if (isVerbatimReference(value)) {
68994
69030
  newValue = getVerbatimPath(value);
68995
69031
  } else if (isPugReference(value)) {
68996
- const absolutePath = path5.resolve(pugDir, value);
69032
+ const absolutePath = resolveAssetPath(value, pugDir, srcWebRoot);
69033
+ assertWithinAllowedRoots(absolutePath, allowedRoots, value, pugFilePath);
68997
69034
  if (!discoveredPugPaths.has(absolutePath)) {
68998
69035
  discoveredPugPaths.add(absolutePath);
68999
69036
  pugReferences.push({ originalHref: value, absolutePath });
69000
69037
  }
69001
69038
  } else if (isLessReference(value)) {
69002
- const absolutePath = path5.resolve(pugDir, value);
69039
+ const absolutePath = resolveAssetPath(value, pugDir, srcWebRoot);
69040
+ assertWithinAllowedRoots(absolutePath, allowedRoots, value, pugFilePath);
69003
69041
  if (!discoveredLessPaths.has(absolutePath)) {
69004
69042
  discoveredLessPaths.add(absolutePath);
69005
69043
  lessReferences.push({ originalHref: value, absolutePath });
69006
69044
  }
69007
69045
  } else if (isScriptReference(value)) {
69008
- const absolutePath = path5.resolve(pugDir, value);
69046
+ const absolutePath = resolveAssetPath(value, pugDir, srcWebRoot);
69047
+ assertWithinAllowedRoots(absolutePath, allowedRoots, value, pugFilePath);
69009
69048
  if (!discoveredScriptPaths.has(absolutePath)) {
69010
69049
  discoveredScriptPaths.add(absolutePath);
69011
69050
  scriptReferences.push({ originalHref: value, absolutePath });
@@ -69137,6 +69176,8 @@ async function processHtmlAssets(html, pugFilePath, options, webRoot) {
69137
69176
  publicPath,
69138
69177
  assets,
69139
69178
  processedAssets,
69179
+ srcWebRoot,
69180
+ allowedRoots,
69140
69181
  webRoot
69141
69182
  );
69142
69183
  if (hashedPath) {
@@ -69144,8 +69185,9 @@ async function processHtmlAssets(html, pugFilePath, options, webRoot) {
69144
69185
  }
69145
69186
  }
69146
69187
  if (newValue !== value) {
69147
- const newFullMatch = fullMatch.replace(value, newValue);
69148
- modifiedHtml = modifiedHtml.replace(fullMatch, newFullMatch);
69188
+ const escapedValue = value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
69189
+ const valueRegex = new RegExp(`(["'])${escapedValue}\\1`);
69190
+ modifiedHtml = modifiedHtml.replace(valueRegex, `$1${newValue}$1`);
69149
69191
  }
69150
69192
  }
69151
69193
  return {
@@ -69157,8 +69199,14 @@ async function processHtmlAssets(html, pugFilePath, options, webRoot) {
69157
69199
  packageCssReferences
69158
69200
  };
69159
69201
  }
69160
- async function processAsset(assetPath, pugDir, pugFilePath, outdir, outbase, assetNames, publicPath, assets, processedAssets, webRoot) {
69161
- const absoluteSource = path5.resolve(pugDir, assetPath);
69202
+ async function processAsset(assetPath, pugDir, pugFilePath, outdir, outbase, assetNames, publicPath, assets, processedAssets, srcWebRoot, allowedRoots, webRoot) {
69203
+ const absoluteSource = resolveAssetPath(assetPath, pugDir, srcWebRoot);
69204
+ assertWithinAllowedRoots(
69205
+ absoluteSource,
69206
+ allowedRoots,
69207
+ assetPath,
69208
+ pugFilePath
69209
+ );
69162
69210
  if (processedAssets.has(absoluteSource)) {
69163
69211
  return processedAssets.get(absoluteSource);
69164
69212
  }
@@ -69312,11 +69360,11 @@ function createRebaseAssetsPlugin(entryFilePath) {
69312
69360
  if (node.nodes) {
69313
69361
  pending.push(...node.nodes);
69314
69362
  }
69315
- const assetAttrs = ASSET_ATTRIBUTES[node.name];
69316
- if (!assetAttrs) continue;
69363
+ const linkingAttrs = getLinkingAttrs(node.name);
69364
+ if (!linkingAttrs) continue;
69317
69365
  if (!node.attrs) continue;
69318
69366
  for (const attr of node.attrs) {
69319
- if (!assetAttrs.includes(attr.name)) continue;
69367
+ if (!linkingAttrs.includes(attr.name)) continue;
69320
69368
  const sourceFile = attr.filename || node.filename;
69321
69369
  if (!sourceFile) continue;
69322
69370
  const sourceDir = path5.dirname(sourceFile);
@@ -69353,6 +69401,36 @@ function createRebaseAssetsPlugin(entryFilePath) {
69353
69401
  }
69354
69402
  };
69355
69403
  }
69404
+ function createAssetCollectorPlugin(collectedReferences) {
69405
+ return {
69406
+ preCodeGen(ast) {
69407
+ const pending = [...ast?.nodes || []];
69408
+ while (pending.length > 0) {
69409
+ const node = pending.shift();
69410
+ if (!node) continue;
69411
+ if (node.block?.nodes) {
69412
+ pending.push(...node.block.nodes);
69413
+ }
69414
+ if (node.nodes) {
69415
+ pending.push(...node.nodes);
69416
+ }
69417
+ const linkingAttrs = getLinkingAttrs(node.name);
69418
+ if (!linkingAttrs) continue;
69419
+ if (!node.attrs) continue;
69420
+ for (const attr of node.attrs) {
69421
+ if (!linkingAttrs.includes(attr.name)) continue;
69422
+ const extracted = extractStaticString(attr.val);
69423
+ if (!extracted) continue;
69424
+ collectedReferences.push({
69425
+ attr: attr.name,
69426
+ value: extracted.value
69427
+ });
69428
+ }
69429
+ }
69430
+ return ast;
69431
+ }
69432
+ };
69433
+ }
69356
69434
  async function loadAsText2(_filepath) {
69357
69435
  return {
69358
69436
  loader: "text"
@@ -69394,12 +69472,14 @@ async function loadAsHtml(filepath, options) {
69394
69472
  }
69395
69473
  async function loadAsEntrypoint(filepath, options, webRoot) {
69396
69474
  const fileData = await fs6.promises.readFile(filepath, "utf8");
69475
+ const collectedReferences = [];
69397
69476
  let contents = import_pug.default.render(fileData, {
69398
69477
  filename: filepath,
69399
69478
  basedir: options.outbase,
69400
69479
  name: "render",
69401
69480
  plugins: [
69402
69481
  createRebaseAssetsPlugin(filepath),
69482
+ createAssetCollectorPlugin(collectedReferences),
69403
69483
  pugTransformer(isScriptNodeWithSrcAttr, (nodes) => {
69404
69484
  nodes;
69405
69485
  })
@@ -69413,7 +69493,13 @@ async function loadAsEntrypoint(filepath, options, webRoot) {
69413
69493
  lessReferences,
69414
69494
  scriptReferences,
69415
69495
  packageCssReferences
69416
- } = await processHtmlAssets(contents, filepath, options, webRoot);
69496
+ } = await processHtmlAssets(
69497
+ contents,
69498
+ filepath,
69499
+ options,
69500
+ webRoot,
69501
+ collectedReferences
69502
+ );
69417
69503
  contents = processedHtml;
69418
69504
  if (pugReferences.length > 0) {
69419
69505
  discoveredPugReferences.set(filepath, pugReferences);