@tinacms/graphql 0.0.0-059f480-20260324232913 → 0.0.0-06c643a-20260521045804

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/README.md CHANGED
@@ -137,7 +137,7 @@ Visit [Tina's documentation](https://tina.io/docs/) to learn more.
137
137
  [![Tweet](https://img.shields.io/twitter/url/http/shields.io.svg?style=social)](https://twitter.com/intent/tweet?url=https%3A%2F%2Ftinacms.org&text=I%20just%20checked%20out%20@tinacms%20on%20GitHub%20and%20it%20is%20sweet%21&hashtags=TinaCMS%2Cjamstack%2Cheadlesscms)
138
138
  [![Forum](https://shields.io/github/discussions/tinacms/tinacms)](https://github.com/tinacms/tinacms/discussions)
139
139
 
140
- Visit the [GitHub Discussions](https://github.com/tinacms/tinacms/discussions) or our [Community Discord](https://discord.com/invite/zumN63Ybpf) to ask questions, or look us up on on Twitter at [@tinacms](https://twitter.com/tinacms).
140
+ Visit the [GitHub Discussions](https://github.com/tinacms/tinacms/discussions) or our [Community Discord](https://discord.com/invite/zumN63Ybpf) to ask questions, or look us up on Twitter at [@tinacms](https://twitter.com/tinacms).
141
141
 
142
142
  ## Contributing
143
143
 
@@ -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>;
@@ -106,7 +106,7 @@ export declare class Database {
106
106
  defaultItem?: import("@tinacms/schema-tools").DefaultItem<Record<string, any>>;
107
107
  previewSrc?: string;
108
108
  };
109
- fields: ((import("@tinacms/schema-tools").StringField | import("@tinacms/schema-tools").NumberField | import("@tinacms/schema-tools").BooleanField | import("@tinacms/schema-tools").DateTimeField | import("@tinacms/schema-tools").ImageField | import("@tinacms/schema-tools").ReferenceField | import("@tinacms/schema-tools").PasswordField | import("@tinacms/schema-tools").RichTextField<false> | import("@tinacms/schema-tools").ObjectField<false>) & {})[];
109
+ fields: ((import("@tinacms/schema-tools").StringField | import("@tinacms/schema-tools").NumberField | import("@tinacms/schema-tools").BooleanField | import("@tinacms/schema-tools").DateTimeField | import("@tinacms/schema-tools").ImageField | import("@tinacms/schema-tools").ReferenceField | import("@tinacms/schema-tools").PasswordField | import("@tinacms/schema-tools").DisplayOnlyField | import("@tinacms/schema-tools").RichTextField<false> | import("@tinacms/schema-tools").ObjectField<false>) & {})[];
110
110
  };
111
111
  info: CollectionTemplateable;
112
112
  }>;
@@ -16,8 +16,6 @@ yupSchema: (args: typeof yup) => yup.ObjectSchema<any>, markdownParseConfig?: {
16
16
  lineWidth?: number;
17
17
  frontmatterDelimiters?: [string, string] | string;
18
18
  }) => T;
19
- export declare const atob: (b64Encoded: string) => string;
20
- export declare const btoa: (string: string) => string;
21
19
  export declare const scanAllContent: (tinaSchema: TinaSchema, bridge: Bridge, callback: (collection: Collection<true>, contentPaths: string[]) => Promise<void>) => Promise<string[]>;
22
20
  export declare const scanContentByPaths: (tinaSchema: TinaSchema, documentPaths: string[], callback: (collection: Collection<true> | undefined, documentPaths: string[]) => Promise<void>) => Promise<void>;
23
21
  export declare const partitionPathsByCollection: (tinaSchema: TinaSchema, documentPaths: string[]) => Promise<{
package/dist/index.js CHANGED
@@ -1909,6 +1909,8 @@ var Builder = class {
1909
1909
  };
1910
1910
  _buildFieldNodeForFragments = async (field, depth) => {
1911
1911
  switch (field.type) {
1912
+ case "displayOnly":
1913
+ return false;
1912
1914
  case "string":
1913
1915
  case "image":
1914
1916
  case "datetime":
@@ -2350,6 +2352,8 @@ var Builder = class {
2350
2352
  };
2351
2353
  _buildFieldFilter = async (field) => {
2352
2354
  switch (field.type) {
2355
+ case "displayOnly":
2356
+ return void 0;
2353
2357
  case "boolean":
2354
2358
  return astBuilder.InputValueDefinition({
2355
2359
  name: field.name,
@@ -2511,6 +2515,8 @@ var Builder = class {
2511
2515
  };
2512
2516
  _buildFieldMutation = async (field) => {
2513
2517
  switch (field.type) {
2518
+ case "displayOnly":
2519
+ return void 0;
2514
2520
  case "boolean":
2515
2521
  return astBuilder.InputValueDefinition({
2516
2522
  name: field.name,
@@ -2739,6 +2745,8 @@ Visit https://tina.io/docs/r/content-fields/#list-fields/ for more information
2739
2745
 
2740
2746
  `;
2741
2747
  switch (field.type) {
2748
+ case "displayOnly":
2749
+ return void 0;
2742
2750
  case "boolean":
2743
2751
  case "datetime":
2744
2752
  case "number":
@@ -2884,7 +2892,8 @@ var FIELD_TYPES = [
2884
2892
  "reference",
2885
2893
  "object",
2886
2894
  "rich-text",
2887
- "password"
2895
+ "password",
2896
+ "displayOnly"
2888
2897
  ];
2889
2898
  var validateSchema = async (schema) => {
2890
2899
  const schema2 = addNamespaceToSchema(
@@ -3026,7 +3035,7 @@ var validateField = async (field) => {
3026
3035
  var package_default = {
3027
3036
  name: "@tinacms/graphql",
3028
3037
  type: "module",
3029
- version: "2.2.0",
3038
+ version: "2.4.1",
3030
3039
  main: "dist/index.js",
3031
3040
  module: "./dist/index.js",
3032
3041
  files: [
@@ -3676,7 +3685,7 @@ var scanAllContent = async (tinaSchema, bridge, callback) => {
3676
3685
  `"${path9}" Found in multiple collections: ${filesSeen.get(path9).map((collection2) => `"${collection2}"`).join(
3677
3686
  ", "
3678
3687
  )}. This can cause unexpected behavior. We recommend updating the \`match\` property of those collections so that each file is in only one collection.
3679
- This will be an error in the future. See https://tina.io/docs/errors/file-in-mutpliple-collections/
3688
+ This will be an error in the future. See https://tina.io/docs/r/content-modelling-collections#path-matching
3680
3689
  `
3681
3690
  );
3682
3691
  });
@@ -3770,6 +3779,9 @@ var getTemplateForFile = (templateInfo, data) => {
3770
3779
  throw new Error(`Unable to determine template`);
3771
3780
  };
3772
3781
  var loadAndParseWithAliases = async (bridge, filepath, collection, templateInfo) => {
3782
+ if (filepath.endsWith(".gitkeep")) {
3783
+ return { _is_tina_folder_placeholder: true };
3784
+ }
3773
3785
  const dataString = await bridge.get(normalizePath(filepath));
3774
3786
  const data = parseFile(
3775
3787
  dataString,
@@ -3783,7 +3795,7 @@ var loadAndParseWithAliases = async (bridge, filepath, collection, templateInfo)
3783
3795
  const template = getTemplateForFile(templateInfo, data);
3784
3796
  if (!template) {
3785
3797
  console.warn(
3786
- `Document: ${filepath} has an ambiguous template, skipping from indexing. See https://tina.io/docs/errors/ambiguous-template/ for more info.`
3798
+ `Document: ${filepath} has an ambiguous template, skipping from indexing. See https://tina.io/docs/r/ambiguous-template for more info.`
3787
3799
  );
3788
3800
  return;
3789
3801
  }
@@ -4628,22 +4640,19 @@ var resolveMediaCloudToRelative = (value, config = { useRelativeMedia: true }, s
4628
4640
  return value;
4629
4641
  }
4630
4642
  if (hasTinaMediaConfig(schema) === true) {
4631
- const assetsURL = `https://${config.assetsHost}/${config.clientId}`;
4632
- if (typeof value === "string" && value.includes(assetsURL)) {
4633
- const cleanMediaRoot = cleanUpSlashes(
4634
- schema.config.media.tina.mediaRoot
4635
- );
4636
- const strippedURL = value.replace(assetsURL, "");
4637
- return `${cleanMediaRoot}${strippedURL}`;
4643
+ const cleanMediaRoot = cleanUpSlashes(schema.config.media.tina.mediaRoot);
4644
+ const cloudUrl = cloudUrlPattern(config.clientId);
4645
+ if (typeof value === "string" && cloudUrl.test(value)) {
4646
+ return `${cleanMediaRoot}${stripStagingPrefix(
4647
+ value.replace(cloudUrl, "")
4648
+ )}`;
4638
4649
  }
4639
4650
  if (Array.isArray(value)) {
4640
4651
  return value.map((v) => {
4641
4652
  if (!v || typeof v !== "string") return v;
4642
- const cleanMediaRoot = cleanUpSlashes(
4643
- schema.config.media.tina.mediaRoot
4644
- );
4645
- const strippedURL = v.replace(assetsURL, "");
4646
- return `${cleanMediaRoot}${strippedURL}`;
4653
+ if (!cloudUrl.test(v)) return v;
4654
+ const strippedURL = v.replace(cloudUrl, "");
4655
+ return `${cleanMediaRoot}${stripStagingPrefix(strippedURL)}`;
4647
4656
  });
4648
4657
  }
4649
4658
  return value;
@@ -4660,15 +4669,20 @@ var resolveMediaRelativeToCloud = (value, config = { useRelativeMedia: true }, s
4660
4669
  }
4661
4670
  if (hasTinaMediaConfig(schema) === true) {
4662
4671
  const cleanMediaRoot = cleanUpSlashes(schema.config.media.tina.mediaRoot);
4672
+ const prefix = stagingPrefix(config);
4663
4673
  if (typeof value === "string") {
4674
+ if (ABSOLUTE_URL.test(value)) return value;
4664
4675
  const strippedValue = value.replace(cleanMediaRoot, "");
4665
- return `https://${config.assetsHost}/${config.clientId}${strippedValue}`;
4676
+ if (ABSOLUTE_URL.test(strippedValue)) return strippedValue;
4677
+ return `https://${config.assetsHost}/${config.clientId}${prefix}${strippedValue}`;
4666
4678
  }
4667
4679
  if (Array.isArray(value)) {
4668
4680
  return value.map((v) => {
4669
4681
  if (!v || typeof v !== "string") return v;
4682
+ if (ABSOLUTE_URL.test(v)) return v;
4670
4683
  const strippedValue = v.replace(cleanMediaRoot, "");
4671
- return `https://${config.assetsHost}/${config.clientId}${strippedValue}`;
4684
+ if (ABSOLUTE_URL.test(strippedValue)) return strippedValue;
4685
+ return `https://${config.assetsHost}/${config.clientId}${prefix}${strippedValue}`;
4672
4686
  });
4673
4687
  }
4674
4688
  }
@@ -4677,6 +4691,15 @@ var resolveMediaRelativeToCloud = (value, config = { useRelativeMedia: true }, s
4677
4691
  return value;
4678
4692
  }
4679
4693
  };
4694
+ var stagingPrefix = (config) => config.branch && config.branch !== config.mediaBranch ? `/__staging/${config.branch}/__file` : "";
4695
+ var STAGING_SEGMENT = /^\/__staging\/.+?\/__file(\/.*)$/;
4696
+ var stripStagingPrefix = (path9) => {
4697
+ const match = path9.match(STAGING_SEGMENT);
4698
+ return match ? match[1] : path9;
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)}`);
4680
4703
  var cleanUpSlashes = (path9) => {
4681
4704
  if (path9) {
4682
4705
  return `/${path9.replace(/^\/+|\/+$/gm, "")}`;
@@ -4701,6 +4724,8 @@ var resolveFieldData = async ({ namespace, ...field }, rawData, accumulator, tin
4701
4724
  assertShape(rawData, (yup3) => yup3.object());
4702
4725
  const value = rawData[field.name];
4703
4726
  switch (field.type) {
4727
+ case "displayOnly":
4728
+ break;
4704
4729
  case "datetime":
4705
4730
  if (value instanceof Date) {
4706
4731
  accumulator[field.name] = value.toISOString();
@@ -4917,7 +4942,7 @@ var updateObjectWithJsonPath = (obj, path9, oldValue, newValue) => {
4917
4942
  }
4918
4943
  return { object: obj, updated };
4919
4944
  };
4920
- var Resolver = class {
4945
+ var Resolver = class _Resolver {
4921
4946
  constructor(init) {
4922
4947
  this.init = init;
4923
4948
  this.config = init.config;
@@ -5188,8 +5213,37 @@ var Resolver = class {
5188
5213
  * path traversal attacks where a user might attempt to read or write files
5189
5214
  * outside of the intended collection.
5190
5215
  */
5216
+ static sanitizePath(input) {
5217
+ if (input.includes("\0")) {
5218
+ throw new Error("Invalid path: null bytes are not allowed");
5219
+ }
5220
+ return input.replace(/\\/g, "/");
5221
+ }
5222
+ /**
5223
+ * Validates that relativePath is non-empty and contains only allowed
5224
+ * characters: a-z, A-Z, 0-9, hyphens, underscores, periods, and
5225
+ * forward slashes.
5226
+ */
5227
+ static validateRelativePath(relativePath) {
5228
+ if (!relativePath.trim()) {
5229
+ throw new Error(
5230
+ "Invalid path: relativePath cannot be empty or whitespace"
5231
+ );
5232
+ }
5233
+ if (relativePath !== relativePath.trim()) {
5234
+ throw new Error(
5235
+ "Invalid path: relativePath cannot have leading or trailing whitespace"
5236
+ );
5237
+ }
5238
+ if (!/^[a-zA-Z0-9\-_./]+$/.test(relativePath)) {
5239
+ throw new Error("Invalid path: relativePath contains invalid characters");
5240
+ }
5241
+ }
5191
5242
  validatePath = (fullPath, collection, relativePath) => {
5192
- const normalizedPath = path3.normalize(fullPath);
5243
+ if (fullPath.includes("\0")) {
5244
+ throw new Error("Invalid path: null bytes are not allowed");
5245
+ }
5246
+ const normalizedPath = path3.normalize(fullPath.replace(/\\/g, "/"));
5193
5247
  const normalizedCollectionPath = path3.normalize(collection.path);
5194
5248
  const relative = path3.relative(normalizedCollectionPath, normalizedPath);
5195
5249
  if (relative.startsWith("..")) {
@@ -5199,6 +5253,7 @@ var Resolver = class {
5199
5253
  throw new Error(`Invalid path: absolute paths are not allowed`);
5200
5254
  }
5201
5255
  if (relativePath) {
5256
+ _Resolver.validateRelativePath(relativePath);
5202
5257
  const collectionFormat = collection.format || "md";
5203
5258
  const fileExtension = path3.extname(relativePath).toLowerCase().slice(1);
5204
5259
  if (fileExtension !== collectionFormat) {
@@ -5219,17 +5274,19 @@ var Resolver = class {
5219
5274
  * @returns Object containing the collection and validated real path
5220
5275
  */
5221
5276
  getValidatedPath = (collectionName, relativePath, options) => {
5277
+ _Resolver.validateRelativePath(relativePath);
5222
5278
  const collection = this.getCollectionWithName(collectionName);
5223
- const pathSegments = [collection.path, relativePath];
5279
+ const sanitizedRelativePath = _Resolver.sanitizePath(relativePath);
5280
+ const pathSegments = [collection.path, sanitizedRelativePath];
5224
5281
  if (options?.extraSegments) {
5225
- pathSegments.push(...options.extraSegments);
5282
+ pathSegments.push(...options.extraSegments.map(_Resolver.sanitizePath));
5226
5283
  }
5227
5284
  const realPath = path3.join(...pathSegments);
5228
5285
  const shouldValidateExtension = options?.validateExtension !== false;
5229
5286
  this.validatePath(
5230
5287
  realPath,
5231
5288
  collection,
5232
- shouldValidateExtension ? relativePath : void 0
5289
+ shouldValidateExtension ? sanitizedRelativePath : void 0
5233
5290
  );
5234
5291
  return { collection, realPath };
5235
5292
  };
@@ -5257,6 +5314,7 @@ var Resolver = class {
5257
5314
  relativePath
5258
5315
  }) => {
5259
5316
  const collection = this.getCollectionWithName(collectionName);
5317
+ _Resolver.validateRelativePath(relativePath);
5260
5318
  const realPath = path3.join(
5261
5319
  collection.path,
5262
5320
  relativePath,
@@ -5314,6 +5372,12 @@ var Resolver = class {
5314
5372
  if (newRealPath === realPath) {
5315
5373
  return doc;
5316
5374
  }
5375
+ const newPathAlreadyExists = await this.database.documentExists(newRealPath);
5376
+ if (newPathAlreadyExists) {
5377
+ throw new Error(
5378
+ `Unable to rename document, ${newRealPath} already exists`
5379
+ );
5380
+ }
5317
5381
  await this.database.put(newRealPath, doc._rawData, collection.name);
5318
5382
  await this.deleteDocument(realPath);
5319
5383
  const collRefs = await this.findReferences(realPath, collection);
@@ -5863,6 +5927,8 @@ var Resolver = class {
5863
5927
  throw new Error(`Expected to find field by name ${fieldName}`);
5864
5928
  }
5865
5929
  switch (field.type) {
5930
+ case "displayOnly":
5931
+ break;
5866
5932
  case "datetime":
5867
5933
  accum[fieldName] = resolveDateInput(fieldValue, field);
5868
5934
  break;
@@ -6866,7 +6932,10 @@ var Database = class {
6866
6932
  if (!collection) {
6867
6933
  throw new GraphQLError5(`Unable to find collection for ${filepath}.`);
6868
6934
  }
6869
- if (collection.match?.exclude || collection.match?.include) {
6935
+ const isFolderPlaceholder = filepath.endsWith(
6936
+ `.gitkeep.${collection.format || "md"}`
6937
+ );
6938
+ if (!isFolderPlaceholder && (collection.match?.exclude || collection.match?.include)) {
6870
6939
  const matches = this.tinaSchema.getMatches({ collection });
6871
6940
  const match = micromatch2.isMatch(filepath, matches);
6872
6941
  if (!match) {
@@ -7790,8 +7859,9 @@ var _indexContent = async ({
7790
7859
  ]);
7791
7860
  }
7792
7861
  }
7862
+ let putOps = [];
7793
7863
  if (!isGitKeep(filepath, collection)) {
7794
- await enqueueOps([
7864
+ putOps = [
7795
7865
  ...makeRefOpsForDocument(
7796
7866
  normalizedPath,
7797
7867
  collection?.name,
@@ -7816,18 +7886,21 @@ var _indexContent = async ({
7816
7886
  aliasedData,
7817
7887
  "put",
7818
7888
  level
7819
- ),
7820
- {
7821
- type: "put",
7822
- key: normalizedPath,
7823
- value: aliasedData,
7824
- sublevel: level.sublevel(
7825
- CONTENT_ROOT_PREFIX,
7826
- SUBLEVEL_OPTIONS
7827
- )
7828
- }
7829
- ]);
7889
+ )
7890
+ ];
7830
7891
  }
7892
+ await enqueueOps([
7893
+ ...putOps,
7894
+ {
7895
+ type: "put",
7896
+ key: normalizedPath,
7897
+ value: aliasedData,
7898
+ sublevel: level.sublevel(
7899
+ CONTENT_ROOT_PREFIX,
7900
+ SUBLEVEL_OPTIONS
7901
+ )
7902
+ }
7903
+ ]);
7831
7904
  } catch (error) {
7832
7905
  throw new TinaFetchError(`Unable to seed ${filepath}`, {
7833
7906
  originalError: error,
@@ -8021,16 +8094,48 @@ import path7 from "path";
8021
8094
  import fg from "fast-glob";
8022
8095
  import fs2 from "fs-extra";
8023
8096
  import normalize from "normalize-path";
8097
+ function resolveRealPath(candidate) {
8098
+ try {
8099
+ return fs2.realpathSync(candidate);
8100
+ } catch {
8101
+ const parent = path7.dirname(candidate);
8102
+ if (parent === candidate) return candidate;
8103
+ return path7.join(resolveRealPath(parent), path7.basename(candidate));
8104
+ }
8105
+ }
8024
8106
  function assertWithinBase(filepath, baseDir) {
8107
+ if (filepath.includes("\0")) {
8108
+ throw new Error("Invalid path: null bytes are not allowed");
8109
+ }
8110
+ const sanitized = filepath.replace(/\\/g, "/");
8025
8111
  const resolvedBase = path7.resolve(baseDir);
8026
- const resolved = path7.resolve(path7.join(baseDir, filepath));
8112
+ const resolved = path7.resolve(path7.join(baseDir, sanitized));
8027
8113
  if (resolved !== resolvedBase && !resolved.startsWith(resolvedBase + path7.sep)) {
8028
8114
  throw new Error(
8029
8115
  `Path traversal detected: "${filepath}" escapes the base directory`
8030
8116
  );
8031
8117
  }
8118
+ try {
8119
+ const realBase = fs2.realpathSync(resolvedBase);
8120
+ const realResolved = resolveRealPath(resolved);
8121
+ if (realResolved !== realBase && !realResolved.startsWith(realBase + path7.sep)) {
8122
+ throw new Error(
8123
+ `Path traversal detected: "${filepath}" escapes the base directory`
8124
+ );
8125
+ }
8126
+ } catch (err) {
8127
+ if (err instanceof Error && err.message.startsWith("Path traversal"))
8128
+ throw err;
8129
+ }
8032
8130
  return resolved;
8033
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
+ }
8034
8139
  var FilesystemBridge = class {
8035
8140
  rootPath;
8036
8141
  outputPath;
@@ -8038,6 +8143,30 @@ var FilesystemBridge = class {
8038
8143
  this.rootPath = path7.resolve(rootPath);
8039
8144
  this.outputPath = outputPath ? path7.resolve(outputPath) : this.rootPath;
8040
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
+ }
8041
8170
  async glob(pattern, extension) {
8042
8171
  const basePath = assertWithinBase(pattern, this.outputPath);
8043
8172
  const items = await fg(
@@ -8053,16 +8182,19 @@ var FilesystemBridge = class {
8053
8182
  );
8054
8183
  }
8055
8184
  async delete(filepath) {
8056
- const resolved = assertWithinBase(filepath, this.outputPath);
8185
+ const resolved = assertWithinBase(filepath, this.baseFor(filepath));
8186
+ this.assertGeneratedSubtree(filepath, resolved);
8057
8187
  await fs2.remove(resolved);
8058
8188
  }
8059
8189
  async get(filepath) {
8060
- const resolved = assertWithinBase(filepath, this.outputPath);
8190
+ const resolved = assertWithinBase(filepath, this.baseFor(filepath));
8191
+ this.assertGeneratedSubtree(filepath, resolved);
8061
8192
  return (await fs2.readFile(resolved)).toString();
8062
8193
  }
8063
8194
  async put(filepath, data, basePathOverride) {
8064
- const basePath = basePathOverride || this.outputPath;
8195
+ const basePath = basePathOverride || this.baseFor(filepath);
8065
8196
  const resolved = assertWithinBase(filepath, basePath);
8197
+ this.assertGeneratedSubtree(filepath, resolved);
8066
8198
  await fs2.outputFile(resolved, data);
8067
8199
  }
8068
8200
  };
@@ -8090,7 +8222,11 @@ import { GraphQLError as GraphQLError6 } from "graphql";
8090
8222
  import git2 from "isomorphic-git";
8091
8223
  import normalize2 from "normalize-path";
8092
8224
  function assertWithinBase2(filepath, relativePath) {
8093
- const qualified = relativePath ? `${relativePath}/${filepath}` : filepath;
8225
+ if (filepath.includes("\0")) {
8226
+ throw new Error("Invalid path: null bytes are not allowed");
8227
+ }
8228
+ const sanitized = filepath.replace(/\\/g, "/");
8229
+ const qualified = relativePath ? `${relativePath}/${sanitized}` : sanitized;
8094
8230
  const normalized = path8.normalize(qualified);
8095
8231
  if (normalized.startsWith("..") || normalized.startsWith("/") || path8.isAbsolute(normalized) || relativePath && normalized !== relativePath && !normalized.startsWith(relativePath + "/")) {
8096
8232
  throw new Error(
@@ -8573,3 +8709,8 @@ export {
8573
8709
  };
8574
8710
  //! Replaces _.get()
8575
8711
  //! Replaces _.flattenDeep()
8712
+ /**
8713
+ * @license
8714
+ * Copyright 2023 Forestry.io Holdings, Inc.
8715
+ * SPDX-License-Identifier: Apache-2.0
8716
+ */
@@ -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;
@@ -235,6 +240,13 @@ export declare class Resolver {
235
240
  * path traversal attacks where a user might attempt to read or write files
236
241
  * outside of the intended collection.
237
242
  */
243
+ private static sanitizePath;
244
+ /**
245
+ * Validates that relativePath is non-empty and contains only allowed
246
+ * characters: a-z, A-Z, 0-9, hyphens, underscores, periods, and
247
+ * forward slashes.
248
+ */
249
+ private static validateRelativePath;
238
250
  private validatePath;
239
251
  /**
240
252
  * Helper method to get collection and construct validated path.
@@ -1,6 +1,8 @@
1
1
  /**
2
-
3
- */
2
+ * @license
3
+ * Copyright 2023 Forestry.io Holdings, Inc.
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
4
6
  import type { Schema } from '@tinacms/schema-tools';
5
7
  import type { GraphQLConfig } from '../types';
6
8
  /**
package/dist/types.d.ts CHANGED
@@ -7,4 +7,6 @@ export type GraphQLConfig = {
7
7
  useRelativeMedia: false;
8
8
  clientId: string;
9
9
  assetsHost: string;
10
+ branch?: string;
11
+ mediaBranch?: string;
10
12
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tinacms/graphql",
3
3
  "type": "module",
4
- "version": "0.0.0-059f480-20260324232913",
4
+ "version": "0.0.0-06c643a-20260521045804",
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/mdx": "0.0.0-059f480-20260324232913",
47
- "@tinacms/schema-tools": "2.7.0"
46
+ "@tinacms/schema-tools": "0.0.0-06c643a-20260521045804",
47
+ "@tinacms/mdx": "0.0.0-06c643a-20260521045804"
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.0",
76
- "@tinacms/scripts": "0.0.0-059f480-20260324232913"
75
+ "@tinacms/scripts": "1.6.1",
76
+ "@tinacms/schema-tools": "0.0.0-06c643a-20260521045804"
77
77
  },
78
78
  "scripts": {
79
79
  "types": "pnpm tsc",