@tinacms/graphql 2.3.1 → 2.4.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.
@@ -4,6 +4,11 @@ import type { Bridge } from './index';
4
4
  * The basic example here is for the filesystem, one is needed
5
5
  * for GitHub has well.
6
6
  *
7
+ * When `outputPath` differs from `rootPath` (multi-repo: generator +
8
+ * sibling content repo), paths under `tina/__generated__/` still resolve
9
+ * against `rootPath` — schema/graphql/lookup artifacts live only in the
10
+ * generator. Everything else (content files) resolves against `outputPath`.
11
+ *
7
12
  * @security All public methods validate their `filepath` / `pattern`
8
13
  * argument via `assertWithinBase` before performing any I/O. If you add a
9
14
  * new method that accepts a path, you MUST validate it the same way.
@@ -12,6 +17,8 @@ export declare class FilesystemBridge implements Bridge {
12
17
  rootPath: string;
13
18
  outputPath: string;
14
19
  constructor(rootPath: string, outputPath?: string);
20
+ private baseFor;
21
+ private assertGeneratedSubtree;
15
22
  glob(pattern: string, extension: string): Promise<string[]>;
16
23
  delete(filepath: string): Promise<void>;
17
24
  get(filepath: string): Promise<string>;
package/dist/index.js CHANGED
@@ -3035,7 +3035,7 @@ var validateField = async (field) => {
3035
3035
  var package_default = {
3036
3036
  name: "@tinacms/graphql",
3037
3037
  type: "module",
3038
- version: "2.3.1",
3038
+ version: "2.4.1",
3039
3039
  main: "dist/index.js",
3040
3040
  module: "./dist/index.js",
3041
3041
  files: [
@@ -4640,17 +4640,18 @@ var resolveMediaCloudToRelative = (value, config = { useRelativeMedia: true }, s
4640
4640
  return value;
4641
4641
  }
4642
4642
  if (hasTinaMediaConfig(schema) === true) {
4643
- const assetsURL = `https://${config.assetsHost}/${config.clientId}`;
4644
4643
  const cleanMediaRoot = cleanUpSlashes(schema.config.media.tina.mediaRoot);
4645
- if (typeof value === "string" && value.includes(assetsURL)) {
4644
+ const cloudUrl = cloudUrlPattern(config.clientId);
4645
+ if (typeof value === "string" && cloudUrl.test(value)) {
4646
4646
  return `${cleanMediaRoot}${stripStagingPrefix(
4647
- value.replace(assetsURL, "")
4647
+ value.replace(cloudUrl, "")
4648
4648
  )}`;
4649
4649
  }
4650
4650
  if (Array.isArray(value)) {
4651
4651
  return value.map((v) => {
4652
4652
  if (!v || typeof v !== "string") return v;
4653
- const strippedURL = v.replace(assetsURL, "");
4653
+ if (!cloudUrl.test(v)) return v;
4654
+ const strippedURL = v.replace(cloudUrl, "");
4654
4655
  return `${cleanMediaRoot}${stripStagingPrefix(strippedURL)}`;
4655
4656
  });
4656
4657
  }
@@ -4670,13 +4671,17 @@ var resolveMediaRelativeToCloud = (value, config = { useRelativeMedia: true }, s
4670
4671
  const cleanMediaRoot = cleanUpSlashes(schema.config.media.tina.mediaRoot);
4671
4672
  const prefix = stagingPrefix(config);
4672
4673
  if (typeof value === "string") {
4674
+ if (ABSOLUTE_URL.test(value)) return value;
4673
4675
  const strippedValue = value.replace(cleanMediaRoot, "");
4676
+ if (ABSOLUTE_URL.test(strippedValue)) return strippedValue;
4674
4677
  return `https://${config.assetsHost}/${config.clientId}${prefix}${strippedValue}`;
4675
4678
  }
4676
4679
  if (Array.isArray(value)) {
4677
4680
  return value.map((v) => {
4678
4681
  if (!v || typeof v !== "string") return v;
4682
+ if (ABSOLUTE_URL.test(v)) return v;
4679
4683
  const strippedValue = v.replace(cleanMediaRoot, "");
4684
+ if (ABSOLUTE_URL.test(strippedValue)) return strippedValue;
4680
4685
  return `https://${config.assetsHost}/${config.clientId}${prefix}${strippedValue}`;
4681
4686
  });
4682
4687
  }
@@ -4686,12 +4691,15 @@ var resolveMediaRelativeToCloud = (value, config = { useRelativeMedia: true }, s
4686
4691
  return value;
4687
4692
  }
4688
4693
  };
4689
- var stagingPrefix = (config) => config.branch && config.branch !== config.mediaBranch ? `/__staging/${encodeURIComponent(config.branch)}` : "";
4690
- var STAGING_SEGMENT = /^\/__staging\/[^/]+(\/.*)$/;
4694
+ var stagingPrefix = (config) => config.branch && config.branch !== config.mediaBranch ? `/__staging/${config.branch}/__file` : "";
4695
+ var STAGING_SEGMENT = /^\/__staging\/.+?\/__file(\/.*)$/;
4691
4696
  var stripStagingPrefix = (path9) => {
4692
4697
  const match = path9.match(STAGING_SEGMENT);
4693
4698
  return match ? match[1] : path9;
4694
4699
  };
4700
+ var ABSOLUTE_URL = /^[a-z][a-z0-9+.\-]*:|^\/\//i;
4701
+ var escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4702
+ var cloudUrlPattern = (clientId) => new RegExp(`^https://[^/]+/${escapeRegExp(clientId)}`);
4695
4703
  var cleanUpSlashes = (path9) => {
4696
4704
  if (path9) {
4697
4705
  return `/${path9.replace(/^\/+|\/+$/gm, "")}`;
@@ -8121,6 +8129,13 @@ function assertWithinBase(filepath, baseDir) {
8121
8129
  }
8122
8130
  return resolved;
8123
8131
  }
8132
+ var GENERATED_PATH_PREFIXES = ["tina/__generated__/", ".tina/__generated__/"];
8133
+ function isGeneratedPath(filepath) {
8134
+ const normalized = filepath.replace(/\\/g, "/");
8135
+ return GENERATED_PATH_PREFIXES.some(
8136
+ (prefix) => normalized.startsWith(prefix)
8137
+ );
8138
+ }
8124
8139
  var FilesystemBridge = class {
8125
8140
  rootPath;
8126
8141
  outputPath;
@@ -8128,6 +8143,30 @@ var FilesystemBridge = class {
8128
8143
  this.rootPath = path7.resolve(rootPath);
8129
8144
  this.outputPath = outputPath ? path7.resolve(outputPath) : this.rootPath;
8130
8145
  }
8146
+ // Picks the base directory for a given path. The `assertWithinBase` check
8147
+ // in delete/get/put still runs against the chosen base, so a path like
8148
+ // `tina/__generated__/../../escape.txt` is still rejected — just rejected
8149
+ // against `rootPath` rather than `outputPath`.
8150
+ baseFor(filepath) {
8151
+ return isGeneratedPath(filepath) ? this.rootPath : this.outputPath;
8152
+ }
8153
+ // Defense-in-depth for generated-path routing: assertWithinBase already
8154
+ // rejects paths that escape rootPath, but a path like
8155
+ // `tina/__generated__/../../.env` would resolve to `<rootPath>/.env` —
8156
+ // technically inside rootPath but outside the generated subtree the
8157
+ // routing is supposed to grant access to. Verify the resolved path stays
8158
+ // inside the matching generated subdirectory.
8159
+ assertGeneratedSubtree(filepath, resolved) {
8160
+ if (!isGeneratedPath(filepath)) return;
8161
+ const normalized = filepath.replace(/\\/g, "/");
8162
+ const subdir = normalized.startsWith(".tina/__generated__/") ? ".tina/__generated__" : "tina/__generated__";
8163
+ const generatedRoot = path7.resolve(path7.join(this.rootPath, subdir));
8164
+ if (resolved !== generatedRoot && !resolved.startsWith(generatedRoot + path7.sep)) {
8165
+ throw new Error(
8166
+ `Path traversal detected: "${filepath}" routed via generated prefix but resolved outside ${generatedRoot}`
8167
+ );
8168
+ }
8169
+ }
8131
8170
  async glob(pattern, extension) {
8132
8171
  const basePath = assertWithinBase(pattern, this.outputPath);
8133
8172
  const items = await fg(
@@ -8143,16 +8182,19 @@ var FilesystemBridge = class {
8143
8182
  );
8144
8183
  }
8145
8184
  async delete(filepath) {
8146
- const resolved = assertWithinBase(filepath, this.outputPath);
8185
+ const resolved = assertWithinBase(filepath, this.baseFor(filepath));
8186
+ this.assertGeneratedSubtree(filepath, resolved);
8147
8187
  await fs2.remove(resolved);
8148
8188
  }
8149
8189
  async get(filepath) {
8150
- const resolved = assertWithinBase(filepath, this.outputPath);
8190
+ const resolved = assertWithinBase(filepath, this.baseFor(filepath));
8191
+ this.assertGeneratedSubtree(filepath, resolved);
8151
8192
  return (await fs2.readFile(resolved)).toString();
8152
8193
  }
8153
8194
  async put(filepath, data, basePathOverride) {
8154
- const basePath = basePathOverride || this.outputPath;
8195
+ const basePath = basePathOverride || this.baseFor(filepath);
8155
8196
  const resolved = assertWithinBase(filepath, basePath);
8197
+ this.assertGeneratedSubtree(filepath, resolved);
8156
8198
  await fs2.outputFile(resolved, data);
8157
8199
  }
8158
8200
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tinacms/graphql",
3
3
  "type": "module",
4
- "version": "2.3.1",
4
+ "version": "2.4.1",
5
5
  "main": "dist/index.js",
6
6
  "module": "./dist/index.js",
7
7
  "files": [