@tinacms/graphql 2.3.1 → 2.4.0

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.0",
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
  }
@@ -4686,12 +4687,14 @@ var resolveMediaRelativeToCloud = (value, config = { useRelativeMedia: true }, s
4686
4687
  return value;
4687
4688
  }
4688
4689
  };
4689
- var stagingPrefix = (config) => config.branch && config.branch !== config.mediaBranch ? `/__staging/${encodeURIComponent(config.branch)}` : "";
4690
- var STAGING_SEGMENT = /^\/__staging\/[^/]+(\/.*)$/;
4690
+ var stagingPrefix = (config) => config.branch && config.branch !== config.mediaBranch ? `/__staging/${config.branch}/__file` : "";
4691
+ var STAGING_SEGMENT = /^\/__staging\/.+?\/__file(\/.*)$/;
4691
4692
  var stripStagingPrefix = (path9) => {
4692
4693
  const match = path9.match(STAGING_SEGMENT);
4693
4694
  return match ? match[1] : path9;
4694
4695
  };
4696
+ var escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4697
+ var cloudUrlPattern = (clientId) => new RegExp(`^https://[^/]+/${escapeRegExp(clientId)}`);
4695
4698
  var cleanUpSlashes = (path9) => {
4696
4699
  if (path9) {
4697
4700
  return `/${path9.replace(/^\/+|\/+$/gm, "")}`;
@@ -8121,6 +8124,13 @@ function assertWithinBase(filepath, baseDir) {
8121
8124
  }
8122
8125
  return resolved;
8123
8126
  }
8127
+ var GENERATED_PATH_PREFIXES = ["tina/__generated__/", ".tina/__generated__/"];
8128
+ function isGeneratedPath(filepath) {
8129
+ const normalized = filepath.replace(/\\/g, "/");
8130
+ return GENERATED_PATH_PREFIXES.some(
8131
+ (prefix) => normalized.startsWith(prefix)
8132
+ );
8133
+ }
8124
8134
  var FilesystemBridge = class {
8125
8135
  rootPath;
8126
8136
  outputPath;
@@ -8128,6 +8138,30 @@ var FilesystemBridge = class {
8128
8138
  this.rootPath = path7.resolve(rootPath);
8129
8139
  this.outputPath = outputPath ? path7.resolve(outputPath) : this.rootPath;
8130
8140
  }
8141
+ // Picks the base directory for a given path. The `assertWithinBase` check
8142
+ // in delete/get/put still runs against the chosen base, so a path like
8143
+ // `tina/__generated__/../../escape.txt` is still rejected — just rejected
8144
+ // against `rootPath` rather than `outputPath`.
8145
+ baseFor(filepath) {
8146
+ return isGeneratedPath(filepath) ? this.rootPath : this.outputPath;
8147
+ }
8148
+ // Defense-in-depth for generated-path routing: assertWithinBase already
8149
+ // rejects paths that escape rootPath, but a path like
8150
+ // `tina/__generated__/../../.env` would resolve to `<rootPath>/.env` —
8151
+ // technically inside rootPath but outside the generated subtree the
8152
+ // routing is supposed to grant access to. Verify the resolved path stays
8153
+ // inside the matching generated subdirectory.
8154
+ assertGeneratedSubtree(filepath, resolved) {
8155
+ if (!isGeneratedPath(filepath)) return;
8156
+ const normalized = filepath.replace(/\\/g, "/");
8157
+ const subdir = normalized.startsWith(".tina/__generated__/") ? ".tina/__generated__" : "tina/__generated__";
8158
+ const generatedRoot = path7.resolve(path7.join(this.rootPath, subdir));
8159
+ if (resolved !== generatedRoot && !resolved.startsWith(generatedRoot + path7.sep)) {
8160
+ throw new Error(
8161
+ `Path traversal detected: "${filepath}" routed via generated prefix but resolved outside ${generatedRoot}`
8162
+ );
8163
+ }
8164
+ }
8131
8165
  async glob(pattern, extension) {
8132
8166
  const basePath = assertWithinBase(pattern, this.outputPath);
8133
8167
  const items = await fg(
@@ -8143,16 +8177,19 @@ var FilesystemBridge = class {
8143
8177
  );
8144
8178
  }
8145
8179
  async delete(filepath) {
8146
- const resolved = assertWithinBase(filepath, this.outputPath);
8180
+ const resolved = assertWithinBase(filepath, this.baseFor(filepath));
8181
+ this.assertGeneratedSubtree(filepath, resolved);
8147
8182
  await fs2.remove(resolved);
8148
8183
  }
8149
8184
  async get(filepath) {
8150
- const resolved = assertWithinBase(filepath, this.outputPath);
8185
+ const resolved = assertWithinBase(filepath, this.baseFor(filepath));
8186
+ this.assertGeneratedSubtree(filepath, resolved);
8151
8187
  return (await fs2.readFile(resolved)).toString();
8152
8188
  }
8153
8189
  async put(filepath, data, basePathOverride) {
8154
- const basePath = basePathOverride || this.outputPath;
8190
+ const basePath = basePathOverride || this.baseFor(filepath);
8155
8191
  const resolved = assertWithinBase(filepath, basePath);
8192
+ this.assertGeneratedSubtree(filepath, resolved);
8156
8193
  await fs2.outputFile(resolved, data);
8157
8194
  }
8158
8195
  };
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.0",
5
5
  "main": "dist/index.js",
6
6
  "module": "./dist/index.js",
7
7
  "files": [
@@ -72,8 +72,8 @@
72
72
  "vite": "^4.5.9",
73
73
  "vitest": "^0.32.4",
74
74
  "zod": "^3.24.2",
75
- "@tinacms/schema-tools": "2.7.4",
76
- "@tinacms/scripts": "1.6.1"
75
+ "@tinacms/scripts": "1.6.1",
76
+ "@tinacms/schema-tools": "2.7.4"
77
77
  },
78
78
  "scripts": {
79
79
  "types": "pnpm tsc",