@tinacms/graphql 2.3.0 → 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.0",
3038
+ version: "2.4.0",
3039
3039
  main: "dist/index.js",
3040
3040
  module: "./dist/index.js",
3041
3041
  files: [
@@ -3779,6 +3779,9 @@ var getTemplateForFile = (templateInfo, data) => {
3779
3779
  throw new Error(`Unable to determine template`);
3780
3780
  };
3781
3781
  var loadAndParseWithAliases = async (bridge, filepath, collection, templateInfo) => {
3782
+ if (filepath.endsWith(".gitkeep")) {
3783
+ return { _is_tina_folder_placeholder: true };
3784
+ }
3782
3785
  const dataString = await bridge.get(normalizePath(filepath));
3783
3786
  const data = parseFile(
3784
3787
  dataString,
@@ -4637,17 +4640,18 @@ var resolveMediaCloudToRelative = (value, config = { useRelativeMedia: true }, s
4637
4640
  return value;
4638
4641
  }
4639
4642
  if (hasTinaMediaConfig(schema) === true) {
4640
- const assetsURL = `https://${config.assetsHost}/${config.clientId}`;
4641
4643
  const cleanMediaRoot = cleanUpSlashes(schema.config.media.tina.mediaRoot);
4642
- if (typeof value === "string" && value.includes(assetsURL)) {
4644
+ const cloudUrl = cloudUrlPattern(config.clientId);
4645
+ if (typeof value === "string" && cloudUrl.test(value)) {
4643
4646
  return `${cleanMediaRoot}${stripStagingPrefix(
4644
- value.replace(assetsURL, "")
4647
+ value.replace(cloudUrl, "")
4645
4648
  )}`;
4646
4649
  }
4647
4650
  if (Array.isArray(value)) {
4648
4651
  return value.map((v) => {
4649
4652
  if (!v || typeof v !== "string") return v;
4650
- const strippedURL = v.replace(assetsURL, "");
4653
+ if (!cloudUrl.test(v)) return v;
4654
+ const strippedURL = v.replace(cloudUrl, "");
4651
4655
  return `${cleanMediaRoot}${stripStagingPrefix(strippedURL)}`;
4652
4656
  });
4653
4657
  }
@@ -4683,12 +4687,14 @@ var resolveMediaRelativeToCloud = (value, config = { useRelativeMedia: true }, s
4683
4687
  return value;
4684
4688
  }
4685
4689
  };
4686
- var stagingPrefix = (config) => config.branch && config.branch !== config.mediaBranch ? `/__staging/${encodeURIComponent(config.branch)}` : "";
4687
- var STAGING_SEGMENT = /^\/__staging\/[^/]+(\/.*)$/;
4690
+ var stagingPrefix = (config) => config.branch && config.branch !== config.mediaBranch ? `/__staging/${config.branch}/__file` : "";
4691
+ var STAGING_SEGMENT = /^\/__staging\/.+?\/__file(\/.*)$/;
4688
4692
  var stripStagingPrefix = (path9) => {
4689
4693
  const match = path9.match(STAGING_SEGMENT);
4690
4694
  return match ? match[1] : path9;
4691
4695
  };
4696
+ var escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4697
+ var cloudUrlPattern = (clientId) => new RegExp(`^https://[^/]+/${escapeRegExp(clientId)}`);
4692
4698
  var cleanUpSlashes = (path9) => {
4693
4699
  if (path9) {
4694
4700
  return `/${path9.replace(/^\/+|\/+$/gm, "")}`;
@@ -5361,6 +5367,12 @@ var Resolver = class _Resolver {
5361
5367
  if (newRealPath === realPath) {
5362
5368
  return doc;
5363
5369
  }
5370
+ const newPathAlreadyExists = await this.database.documentExists(newRealPath);
5371
+ if (newPathAlreadyExists) {
5372
+ throw new Error(
5373
+ `Unable to rename document, ${newRealPath} already exists`
5374
+ );
5375
+ }
5364
5376
  await this.database.put(newRealPath, doc._rawData, collection.name);
5365
5377
  await this.deleteDocument(realPath);
5366
5378
  const collRefs = await this.findReferences(realPath, collection);
@@ -6915,7 +6927,10 @@ var Database = class {
6915
6927
  if (!collection) {
6916
6928
  throw new GraphQLError5(`Unable to find collection for ${filepath}.`);
6917
6929
  }
6918
- if (collection.match?.exclude || collection.match?.include) {
6930
+ const isFolderPlaceholder = filepath.endsWith(
6931
+ `.gitkeep.${collection.format || "md"}`
6932
+ );
6933
+ if (!isFolderPlaceholder && (collection.match?.exclude || collection.match?.include)) {
6919
6934
  const matches = this.tinaSchema.getMatches({ collection });
6920
6935
  const match = micromatch2.isMatch(filepath, matches);
6921
6936
  if (!match) {
@@ -7839,8 +7854,9 @@ var _indexContent = async ({
7839
7854
  ]);
7840
7855
  }
7841
7856
  }
7857
+ let putOps = [];
7842
7858
  if (!isGitKeep(filepath, collection)) {
7843
- await enqueueOps([
7859
+ putOps = [
7844
7860
  ...makeRefOpsForDocument(
7845
7861
  normalizedPath,
7846
7862
  collection?.name,
@@ -7865,18 +7881,21 @@ var _indexContent = async ({
7865
7881
  aliasedData,
7866
7882
  "put",
7867
7883
  level
7868
- ),
7869
- {
7870
- type: "put",
7871
- key: normalizedPath,
7872
- value: aliasedData,
7873
- sublevel: level.sublevel(
7874
- CONTENT_ROOT_PREFIX,
7875
- SUBLEVEL_OPTIONS
7876
- )
7877
- }
7878
- ]);
7884
+ )
7885
+ ];
7879
7886
  }
7887
+ await enqueueOps([
7888
+ ...putOps,
7889
+ {
7890
+ type: "put",
7891
+ key: normalizedPath,
7892
+ value: aliasedData,
7893
+ sublevel: level.sublevel(
7894
+ CONTENT_ROOT_PREFIX,
7895
+ SUBLEVEL_OPTIONS
7896
+ )
7897
+ }
7898
+ ]);
7880
7899
  } catch (error) {
7881
7900
  throw new TinaFetchError(`Unable to seed ${filepath}`, {
7882
7901
  originalError: error,
@@ -8105,6 +8124,13 @@ function assertWithinBase(filepath, baseDir) {
8105
8124
  }
8106
8125
  return resolved;
8107
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
+ }
8108
8134
  var FilesystemBridge = class {
8109
8135
  rootPath;
8110
8136
  outputPath;
@@ -8112,6 +8138,30 @@ var FilesystemBridge = class {
8112
8138
  this.rootPath = path7.resolve(rootPath);
8113
8139
  this.outputPath = outputPath ? path7.resolve(outputPath) : this.rootPath;
8114
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
+ }
8115
8165
  async glob(pattern, extension) {
8116
8166
  const basePath = assertWithinBase(pattern, this.outputPath);
8117
8167
  const items = await fg(
@@ -8127,16 +8177,19 @@ var FilesystemBridge = class {
8127
8177
  );
8128
8178
  }
8129
8179
  async delete(filepath) {
8130
- const resolved = assertWithinBase(filepath, this.outputPath);
8180
+ const resolved = assertWithinBase(filepath, this.baseFor(filepath));
8181
+ this.assertGeneratedSubtree(filepath, resolved);
8131
8182
  await fs2.remove(resolved);
8132
8183
  }
8133
8184
  async get(filepath) {
8134
- const resolved = assertWithinBase(filepath, this.outputPath);
8185
+ const resolved = assertWithinBase(filepath, this.baseFor(filepath));
8186
+ this.assertGeneratedSubtree(filepath, resolved);
8135
8187
  return (await fs2.readFile(resolved)).toString();
8136
8188
  }
8137
8189
  async put(filepath, data, basePathOverride) {
8138
- const basePath = basePathOverride || this.outputPath;
8190
+ const basePath = basePathOverride || this.baseFor(filepath);
8139
8191
  const resolved = assertWithinBase(filepath, basePath);
8192
+ this.assertGeneratedSubtree(filepath, resolved);
8140
8193
  await fs2.outputFile(resolved, data);
8141
8194
  }
8142
8195
  };
@@ -11,6 +11,11 @@ interface ResolverConfig {
11
11
  isAudit: boolean;
12
12
  }
13
13
  export declare const createResolver: (args: ResolverConfig) => Resolver;
14
+ export declare const resolveFieldData: ({ namespace, ...field }: TinaField<true>, rawData: unknown, accumulator: {
15
+ [key: string]: unknown;
16
+ }, tinaSchema: TinaSchema, config?: GraphQLConfig, isAudit?: boolean) => Promise<{
17
+ [key: string]: unknown;
18
+ }>;
14
19
  export declare const transformDocumentIntoPayload: (fullPath: string, rawData: {
15
20
  _collection: any;
16
21
  _template: any;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tinacms/graphql",
3
3
  "type": "module",
4
- "version": "2.3.0",
4
+ "version": "2.4.0",
5
5
  "main": "dist/index.js",
6
6
  "module": "./dist/index.js",
7
7
  "files": [
@@ -43,8 +43,8 @@
43
43
  "normalize-path": "^3.0.0",
44
44
  "readable-stream": "^4.7.0",
45
45
  "yup": "^1.6.1",
46
- "@tinacms/schema-tools": "2.7.3",
47
- "@tinacms/mdx": "2.1.3"
46
+ "@tinacms/mdx": "2.1.4",
47
+ "@tinacms/schema-tools": "2.7.4"
48
48
  },
49
49
  "publishConfig": {
50
50
  "registry": "https://registry.npmjs.org"
@@ -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.3",
76
- "@tinacms/scripts": "1.6.0"
75
+ "@tinacms/scripts": "1.6.1",
76
+ "@tinacms/schema-tools": "2.7.4"
77
77
  },
78
78
  "scripts": {
79
79
  "types": "pnpm tsc",