@tinacms/graphql 0.0.0-e2d2d7e-20260217062658 → 0.0.0-e2e04ae-20260325061046

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.
@@ -3,6 +3,10 @@ import type { Bridge } from './index';
3
3
  * This is the bridge from whatever datasource we need for I/O.
4
4
  * The basic example here is for the filesystem, one is needed
5
5
  * for GitHub has well.
6
+ *
7
+ * @security All public methods validate their `filepath` / `pattern`
8
+ * argument via `assertWithinBase` before performing any I/O. If you add a
9
+ * new method that accepts a path, you MUST validate it the same way.
6
10
  */
7
11
  export declare class FilesystemBridge implements Bridge {
8
12
  rootPath: string;
@@ -1,8 +1,36 @@
1
+ /**
2
+ * I/O abstraction layer for reading/writing content files.
3
+ *
4
+ * @security **Path traversal (CWE-22):** All `filepath` and `pattern`
5
+ * parameters may originate from user input (e.g. GraphQL mutations or media
6
+ * API requests). Implementations MUST validate that resolved paths stay
7
+ * within their root/output directory before performing any filesystem
8
+ * operation. See `FilesystemBridge.assertWithinBase` and
9
+ * `IsomorphicBridge.assertWithinBase` for reference implementations.
10
+ *
11
+ * The recommended validation pattern is:
12
+ * ```ts
13
+ * const resolved = path.resolve(path.join(baseDir, filepath));
14
+ * if (!resolved.startsWith(path.resolve(baseDir) + path.sep)) {
15
+ * throw new Error('Path traversal detected');
16
+ * }
17
+ * ```
18
+ *
19
+ * If you are adding a new Bridge implementation, add path traversal
20
+ * validation to every method that accepts a filepath from the caller.
21
+ */
1
22
  export interface Bridge {
2
23
  rootPath: string;
24
+ /**
25
+ * @param pattern - Glob pattern prefix (untrusted — validate before use).
26
+ * @param extension - File extension to match.
27
+ */
3
28
  glob(pattern: string, extension: string): Promise<string[]>;
29
+ /** @param filepath - Relative path to delete (untrusted — validate before use). */
4
30
  delete(filepath: string): Promise<void>;
31
+ /** @param filepath - Relative path to read (untrusted — validate before use). */
5
32
  get(filepath: string): Promise<string>;
33
+ /** @param filepath - Relative path to write (untrusted — validate before use). */
6
34
  put(filepath: string, data: string): Promise<void>;
7
35
  /**
8
36
  * Optionally, the bridge can perform
@@ -18,6 +18,11 @@ export type IsomorphicGitBridgeOptions = {
18
18
  };
19
19
  /**
20
20
  * Bridge backed by isomorphic-git
21
+ *
22
+ * @security All public methods (glob, get, put, delete) validate their
23
+ * `filepath` / `pattern` argument via `assertWithinBase` before performing
24
+ * any git operations. If you add a new method that accepts a path, you
25
+ * MUST validate it the same way.
21
26
  */
22
27
  export declare class IsomorphicBridge implements Bridge {
23
28
  rootPath: string;
@@ -19,6 +19,7 @@ export interface DatabaseArgs {
19
19
  indexStatusCallback?: IndexStatusCallback;
20
20
  version?: boolean;
21
21
  namespace?: string;
22
+ levelBatchSize?: number;
22
23
  }
23
24
  export interface GitProvider {
24
25
  onPut: (key: string, value: string) => Promise<void>;
@@ -69,6 +70,7 @@ export declare class Database {
69
70
  indexStatusCallback: IndexStatusCallback | undefined;
70
71
  private readonly onPut;
71
72
  private readonly onDelete;
73
+ private readonly levelBatchSize;
72
74
  private tinaSchema;
73
75
  private contentNamespace;
74
76
  private collectionIndexDefinitions;
@@ -104,7 +106,7 @@ export declare class Database {
104
106
  defaultItem?: import("@tinacms/schema-tools").DefaultItem<Record<string, any>>;
105
107
  previewSrc?: string;
106
108
  };
107
- 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>) & {})[];
108
110
  };
109
111
  info: CollectionTemplateable;
110
112
  }>;
package/dist/index.js CHANGED
@@ -45,8 +45,8 @@ var btoa = (string2) => {
45
45
  var lastItem = (arr) => {
46
46
  return arr[arr.length - 1];
47
47
  };
48
- var get = (obj, path8, defaultValue = void 0) => {
49
- const travel = (regexp) => String.prototype.split.call(path8, regexp).filter(Boolean).reduce(
48
+ var get = (obj, path9, defaultValue = void 0) => {
49
+ const travel = (regexp) => String.prototype.split.call(path9, regexp).filter(Boolean).reduce(
50
50
  (res, key) => res !== null && res !== void 0 ? res[key] : res,
51
51
  obj
52
52
  );
@@ -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":
@@ -3026,7 +3034,7 @@ var validateField = async (field) => {
3026
3034
  var package_default = {
3027
3035
  name: "@tinacms/graphql",
3028
3036
  type: "module",
3029
- version: "2.1.1",
3037
+ version: "2.2.1",
3030
3038
  main: "dist/index.js",
3031
3039
  module: "./dist/index.js",
3032
3040
  files: [
@@ -3053,7 +3061,7 @@ var package_default = {
3053
3061
  types: "pnpm tsc",
3054
3062
  build: "tinacms-scripts build",
3055
3063
  docs: "pnpm typedoc",
3056
- test: "vitest run",
3064
+ test: "vitest run --coverage.enabled",
3057
3065
  "test-watch": "vitest"
3058
3066
  },
3059
3067
  dependencies: {
@@ -3099,6 +3107,7 @@ var package_default = {
3099
3107
  "@types/node": "^22.13.1",
3100
3108
  "@types/normalize-path": "catalog:",
3101
3109
  "@types/ws": "catalog:",
3110
+ "@vitest/coverage-v8": "0.32.4",
3102
3111
  "jest-file-snapshot": "^0.5.0",
3103
3112
  "memory-level": "catalog:",
3104
3113
  typescript: "^5.7.3",
@@ -3662,17 +3671,17 @@ var scanAllContent = async (tinaSchema, bridge, callback) => {
3662
3671
  const documentPaths = await bridge.glob(normalPath, format);
3663
3672
  const matches = tinaSchema.getMatches({ collection });
3664
3673
  const filteredPaths = matches.length > 0 ? micromatch(documentPaths, matches) : documentPaths;
3665
- filteredPaths.forEach((path8) => {
3666
- if (filesSeen.has(path8)) {
3667
- filesSeen.get(path8).push(collection.name);
3668
- duplicateFiles.add(path8);
3674
+ filteredPaths.forEach((path9) => {
3675
+ if (filesSeen.has(path9)) {
3676
+ filesSeen.get(path9).push(collection.name);
3677
+ duplicateFiles.add(path9);
3669
3678
  } else {
3670
- filesSeen.set(path8, [collection.name]);
3679
+ filesSeen.set(path9, [collection.name]);
3671
3680
  }
3672
3681
  });
3673
- duplicateFiles.forEach((path8) => {
3682
+ duplicateFiles.forEach((path9) => {
3674
3683
  warnings.push(
3675
- `"${path8}" Found in multiple collections: ${filesSeen.get(path8).map((collection2) => `"${collection2}"`).join(
3684
+ `"${path9}" Found in multiple collections: ${filesSeen.get(path9).map((collection2) => `"${collection2}"`).join(
3676
3685
  ", "
3677
3686
  )}. This can cause unexpected behavior. We recommend updating the \`match\` property of those collections so that each file is in only one collection.
3678
3687
  This will be an error in the future. See https://tina.io/docs/errors/file-in-mutpliple-collections/
@@ -4235,9 +4244,9 @@ var makeFilterSuffixes = (filterChain, index) => {
4235
4244
  }
4236
4245
  };
4237
4246
  var FOLDER_ROOT = "~";
4238
- var stripCollectionFromPath = (collectionPath, path8) => {
4247
+ var stripCollectionFromPath = (collectionPath, path9) => {
4239
4248
  const collectionPathParts = collectionPath.split("/");
4240
- const pathParts = path8.split("/");
4249
+ const pathParts = path9.split("/");
4241
4250
  const strippedPathParts = pathParts.slice(collectionPathParts.length);
4242
4251
  return strippedPathParts.join("/");
4243
4252
  };
@@ -4293,13 +4302,13 @@ var makeFolderOpsForCollection = (folderTree, collection, indexDefinitions, opTy
4293
4302
  SUBLEVEL_OPTIONS
4294
4303
  );
4295
4304
  let folderSortingIdx = 0;
4296
- for (const path8 of Array.from(folder).sort()) {
4305
+ for (const path9 of Array.from(folder).sort()) {
4297
4306
  for (const [sort] of Object.entries(indexDefinitions)) {
4298
4307
  const indexSublevel = folderCollectionSublevel.sublevel(
4299
4308
  sort,
4300
4309
  SUBLEVEL_OPTIONS
4301
4310
  );
4302
- const subFolderKey = sha.hex(path8);
4311
+ const subFolderKey = sha.hex(path9);
4303
4312
  if (sort === DEFAULT_COLLECTION_SORT_KEY) {
4304
4313
  result.push({
4305
4314
  type: opType,
@@ -4381,8 +4390,8 @@ var makeRefOpsForDocument = (filepath, collection, references, data, opType, lev
4381
4390
  SUBLEVEL_OPTIONS
4382
4391
  );
4383
4392
  const references2 = {};
4384
- for (const path8 of referencePaths) {
4385
- const ref = JSONPath({ path: path8, json: data });
4393
+ for (const path9 of referencePaths) {
4394
+ const ref = JSONPath({ path: path9, json: data });
4386
4395
  if (!ref) {
4387
4396
  continue;
4388
4397
  }
@@ -4392,24 +4401,24 @@ var makeRefOpsForDocument = (filepath, collection, references, data, opType, lev
4392
4401
  continue;
4393
4402
  }
4394
4403
  if (references2[r]) {
4395
- references2[r].push(path8);
4404
+ references2[r].push(path9);
4396
4405
  } else {
4397
- references2[r] = [path8];
4406
+ references2[r] = [path9];
4398
4407
  }
4399
4408
  }
4400
4409
  } else {
4401
4410
  if (references2[ref]) {
4402
- references2[ref].push(path8);
4411
+ references2[ref].push(path9);
4403
4412
  } else {
4404
- references2[ref] = [path8];
4413
+ references2[ref] = [path9];
4405
4414
  }
4406
4415
  }
4407
4416
  }
4408
4417
  for (const ref of Object.keys(references2)) {
4409
- for (const path8 of references2[ref]) {
4418
+ for (const path9 of references2[ref]) {
4410
4419
  result.push({
4411
4420
  type: opType,
4412
- key: `${ref}${INDEX_KEY_FIELD_SEPARATOR}${path8}${INDEX_KEY_FIELD_SEPARATOR}${filepath}`,
4421
+ key: `${ref}${INDEX_KEY_FIELD_SEPARATOR}${path9}${INDEX_KEY_FIELD_SEPARATOR}${filepath}`,
4413
4422
  sublevel: refSublevel,
4414
4423
  value: opType === "put" ? {} : void 0
4415
4424
  });
@@ -4676,9 +4685,9 @@ var resolveMediaRelativeToCloud = (value, config = { useRelativeMedia: true }, s
4676
4685
  return value;
4677
4686
  }
4678
4687
  };
4679
- var cleanUpSlashes = (path8) => {
4680
- if (path8) {
4681
- return `/${path8.replace(/^\/+|\/+$/gm, "")}`;
4688
+ var cleanUpSlashes = (path9) => {
4689
+ if (path9) {
4690
+ return `/${path9.replace(/^\/+|\/+$/gm, "")}`;
4682
4691
  }
4683
4692
  return "";
4684
4693
  };
@@ -4700,6 +4709,8 @@ var resolveFieldData = async ({ namespace, ...field }, rawData, accumulator, tin
4700
4709
  assertShape(rawData, (yup3) => yup3.object());
4701
4710
  const value = rawData[field.name];
4702
4711
  switch (field.type) {
4712
+ case "displayOnly":
4713
+ break;
4703
4714
  case "datetime":
4704
4715
  if (value instanceof Date) {
4705
4716
  accumulator[field.name] = value.toISOString();
@@ -4888,17 +4899,17 @@ var transformDocumentIntoPayload = async (fullPath, rawData, tinaSchema, config,
4888
4899
  throw e;
4889
4900
  }
4890
4901
  };
4891
- var updateObjectWithJsonPath = (obj, path8, oldValue, newValue) => {
4902
+ var updateObjectWithJsonPath = (obj, path9, oldValue, newValue) => {
4892
4903
  let updated = false;
4893
- if (!path8.includes(".") && !path8.includes("[")) {
4894
- if (path8 in obj && obj[path8] === oldValue) {
4895
- obj[path8] = newValue;
4904
+ if (!path9.includes(".") && !path9.includes("[")) {
4905
+ if (path9 in obj && obj[path9] === oldValue) {
4906
+ obj[path9] = newValue;
4896
4907
  updated = true;
4897
4908
  }
4898
4909
  return { object: obj, updated };
4899
4910
  }
4900
- const parentPath = path8.replace(/\.[^.\[\]]+$/, "");
4901
- const keyToUpdate = path8.match(/[^.\[\]]+$/)[0];
4911
+ const parentPath = path9.replace(/\.[^.\[\]]+$/, "");
4912
+ const keyToUpdate = path9.match(/[^.\[\]]+$/)[0];
4902
4913
  const parents = JSONPath2({
4903
4914
  path: parentPath,
4904
4915
  json: obj,
@@ -4916,7 +4927,7 @@ var updateObjectWithJsonPath = (obj, path8, oldValue, newValue) => {
4916
4927
  }
4917
4928
  return { object: obj, updated };
4918
4929
  };
4919
- var Resolver = class {
4930
+ var Resolver = class _Resolver {
4920
4931
  constructor(init) {
4921
4932
  this.init = init;
4922
4933
  this.config = init.config;
@@ -5187,8 +5198,17 @@ var Resolver = class {
5187
5198
  * path traversal attacks where a user might attempt to read or write files
5188
5199
  * outside of the intended collection.
5189
5200
  */
5201
+ static sanitizePath(input) {
5202
+ if (input.includes("\0")) {
5203
+ throw new Error("Invalid path: null bytes are not allowed");
5204
+ }
5205
+ return input.replace(/\\/g, "/");
5206
+ }
5190
5207
  validatePath = (fullPath, collection, relativePath) => {
5191
- const normalizedPath = path3.normalize(fullPath);
5208
+ if (fullPath.includes("\0")) {
5209
+ throw new Error("Invalid path: null bytes are not allowed");
5210
+ }
5211
+ const normalizedPath = path3.normalize(fullPath.replace(/\\/g, "/"));
5192
5212
  const normalizedCollectionPath = path3.normalize(collection.path);
5193
5213
  const relative = path3.relative(normalizedCollectionPath, normalizedPath);
5194
5214
  if (relative.startsWith("..")) {
@@ -5219,16 +5239,17 @@ var Resolver = class {
5219
5239
  */
5220
5240
  getValidatedPath = (collectionName, relativePath, options) => {
5221
5241
  const collection = this.getCollectionWithName(collectionName);
5222
- const pathSegments = [collection.path, relativePath];
5242
+ const sanitizedRelativePath = _Resolver.sanitizePath(relativePath);
5243
+ const pathSegments = [collection.path, sanitizedRelativePath];
5223
5244
  if (options?.extraSegments) {
5224
- pathSegments.push(...options.extraSegments);
5245
+ pathSegments.push(...options.extraSegments.map(_Resolver.sanitizePath));
5225
5246
  }
5226
5247
  const realPath = path3.join(...pathSegments);
5227
5248
  const shouldValidateExtension = options?.validateExtension !== false;
5228
5249
  this.validatePath(
5229
5250
  realPath,
5230
5251
  collection,
5231
- shouldValidateExtension ? relativePath : void 0
5252
+ shouldValidateExtension ? sanitizedRelativePath : void 0
5232
5253
  );
5233
5254
  return { collection, realPath };
5234
5255
  };
@@ -5322,10 +5343,10 @@ var Resolver = class {
5322
5343
  )) {
5323
5344
  let docWithRef = await this.getRaw(pathToDocWithRef);
5324
5345
  let hasUpdate = false;
5325
- for (const path8 of referencePaths) {
5346
+ for (const path9 of referencePaths) {
5326
5347
  const { object: object2, updated } = updateObjectWithJsonPath(
5327
5348
  docWithRef,
5328
- path8,
5349
+ path9,
5329
5350
  realPath,
5330
5351
  newRealPath
5331
5352
  );
@@ -5390,10 +5411,10 @@ var Resolver = class {
5390
5411
  )) {
5391
5412
  let refDoc = await this.getRaw(pathToDocWithRef);
5392
5413
  let hasUpdate = false;
5393
- for (const path8 of referencePaths) {
5414
+ for (const path9 of referencePaths) {
5394
5415
  const { object: object2, updated } = updateObjectWithJsonPath(
5395
5416
  refDoc,
5396
- path8,
5417
+ path9,
5397
5418
  realPath,
5398
5419
  null
5399
5420
  );
@@ -5660,7 +5681,7 @@ var Resolver = class {
5660
5681
  first: -1
5661
5682
  },
5662
5683
  collection: referencedCollection,
5663
- hydrator: (path8) => path8
5684
+ hydrator: (path9) => path9
5664
5685
  // just return the path
5665
5686
  }
5666
5687
  );
@@ -5862,6 +5883,8 @@ var Resolver = class {
5862
5883
  throw new Error(`Expected to find field by name ${fieldName}`);
5863
5884
  }
5864
5885
  switch (field.type) {
5886
+ case "displayOnly":
5887
+ break;
5865
5888
  case "datetime":
5866
5889
  accum[fieldName] = resolveDateInput(fieldValue, field);
5867
5890
  break;
@@ -6101,8 +6124,8 @@ async function handleUpdatePassword({
6101
6124
  // src/error.ts
6102
6125
  import { GraphQLError as GraphQLError3 } from "graphql";
6103
6126
  var NotFoundError = class extends GraphQLError3 {
6104
- constructor(message, nodes, source, positions, path8, originalError, extensions) {
6105
- super(message, nodes, source, positions, path8, originalError, extensions);
6127
+ constructor(message, nodes, source, positions, path9, originalError, extensions) {
6128
+ super(message, nodes, source, positions, path9, originalError, extensions);
6106
6129
  this.name = "NotFoundError";
6107
6130
  }
6108
6131
  };
@@ -6598,6 +6621,7 @@ var createDatabaseInternal = (config) => {
6598
6621
  });
6599
6622
  };
6600
6623
  var SYSTEM_FILES = ["_schema", "_graphql", "_lookup"];
6624
+ var DEFAULT_LEVEL_BATCH_SIZE = 25;
6601
6625
  var defaultStatusCallback = () => Promise.resolve();
6602
6626
  var defaultOnPut = () => Promise.resolve();
6603
6627
  var defaultOnDelete = () => Promise.resolve();
@@ -6610,6 +6634,7 @@ var Database = class {
6610
6634
  this.indexStatusCallback = config.indexStatusCallback || defaultStatusCallback;
6611
6635
  this.onPut = config.onPut || defaultOnPut;
6612
6636
  this.onDelete = config.onDelete || defaultOnDelete;
6637
+ this.levelBatchSize = config.levelBatchSize ?? DEFAULT_LEVEL_BATCH_SIZE;
6613
6638
  this.contentNamespace = config.namespace;
6614
6639
  }
6615
6640
  bridge;
@@ -6620,6 +6645,7 @@ var Database = class {
6620
6645
  indexStatusCallback;
6621
6646
  onPut;
6622
6647
  onDelete;
6648
+ levelBatchSize;
6623
6649
  tinaSchema;
6624
6650
  contentNamespace;
6625
6651
  collectionIndexDefinitions;
@@ -7364,21 +7390,21 @@ var Database = class {
7364
7390
  edges,
7365
7391
  async ({
7366
7392
  cursor,
7367
- path: path8,
7393
+ path: path9,
7368
7394
  value
7369
7395
  }) => {
7370
7396
  try {
7371
- const node = await hydrator(path8, value);
7397
+ const node = await hydrator(path9, value);
7372
7398
  return {
7373
7399
  node,
7374
7400
  cursor: btoa(cursor)
7375
7401
  };
7376
7402
  } catch (error) {
7377
7403
  console.log(error);
7378
- if (error instanceof Error && (!path8.includes(".tina/__generated__/_graphql.json") || !path8.includes("tina/__generated__/_graphql.json"))) {
7404
+ if (error instanceof Error && (!path9.includes(".tina/__generated__/_graphql.json") || !path9.includes("tina/__generated__/_graphql.json"))) {
7379
7405
  throw new TinaQueryError({
7380
7406
  originalError: error,
7381
- file: path8,
7407
+ file: path9,
7382
7408
  collection: collection.name
7383
7409
  });
7384
7410
  }
@@ -7484,8 +7510,10 @@ var Database = class {
7484
7510
  const operations = [];
7485
7511
  const enqueueOps = async (ops) => {
7486
7512
  operations.push(...ops);
7487
- while (operations.length >= 25) {
7488
- await this.contentLevel.batch(operations.splice(0, 25));
7513
+ while (operations.length >= this.levelBatchSize) {
7514
+ await this.contentLevel.batch(
7515
+ operations.splice(0, this.levelBatchSize)
7516
+ );
7489
7517
  }
7490
7518
  };
7491
7519
  const tinaSchema = await this.getSchema(this.contentLevel);
@@ -7504,7 +7532,7 @@ var Database = class {
7504
7532
  }
7505
7533
  });
7506
7534
  while (operations.length) {
7507
- await this.contentLevel.batch(operations.splice(0, 25));
7535
+ await this.contentLevel.batch(operations.splice(0, this.levelBatchSize));
7508
7536
  }
7509
7537
  };
7510
7538
  indexContentByPaths = async (documentPaths) => {
@@ -7512,8 +7540,10 @@ var Database = class {
7512
7540
  const operations = [];
7513
7541
  const enqueueOps = async (ops) => {
7514
7542
  operations.push(...ops);
7515
- while (operations.length >= 25) {
7516
- await this.contentLevel.batch(operations.splice(0, 25));
7543
+ while (operations.length >= this.levelBatchSize) {
7544
+ await this.contentLevel.batch(
7545
+ operations.splice(0, this.levelBatchSize)
7546
+ );
7517
7547
  }
7518
7548
  };
7519
7549
  const tinaSchema = await this.getSchema(this.contentLevel);
@@ -7536,7 +7566,7 @@ var Database = class {
7536
7566
  );
7537
7567
  });
7538
7568
  while (operations.length) {
7539
- await this.contentLevel.batch(operations.splice(0, 25));
7569
+ await this.contentLevel.batch(operations.splice(0, this.levelBatchSize));
7540
7570
  }
7541
7571
  };
7542
7572
  delete = async (filepath) => {
@@ -7620,8 +7650,8 @@ var Database = class {
7620
7650
  const operations = [];
7621
7651
  const enqueueOps = async (ops) => {
7622
7652
  operations.push(...ops);
7623
- while (operations.length >= 25) {
7624
- const batchOps = operations.splice(0, 25);
7653
+ while (operations.length >= this.levelBatchSize) {
7654
+ const batchOps = operations.splice(0, this.levelBatchSize);
7625
7655
  await level.batch(batchOps);
7626
7656
  }
7627
7657
  };
@@ -7661,13 +7691,13 @@ var Database = class {
7661
7691
  }
7662
7692
  );
7663
7693
  while (operations.length) {
7664
- await level.batch(operations.splice(0, 25));
7694
+ await level.batch(operations.splice(0, this.levelBatchSize));
7665
7695
  }
7666
7696
  return { warnings };
7667
7697
  };
7668
7698
  };
7669
- var hashPasswordVisitor = async (node, path8) => {
7670
- const passwordValuePath = [...path8, "value"];
7699
+ var hashPasswordVisitor = async (node, path9) => {
7700
+ const passwordValuePath = [...path9, "value"];
7671
7701
  const plaintextPassword = get(node, passwordValuePath);
7672
7702
  if (plaintextPassword) {
7673
7703
  set2(
@@ -7677,10 +7707,10 @@ var hashPasswordVisitor = async (node, path8) => {
7677
7707
  );
7678
7708
  }
7679
7709
  };
7680
- var visitNodes = async (node, path8, callback) => {
7681
- const [currentLevel, ...remainingLevels] = path8;
7710
+ var visitNodes = async (node, path9, callback) => {
7711
+ const [currentLevel, ...remainingLevels] = path9;
7682
7712
  if (!remainingLevels?.length) {
7683
- return callback(node, path8);
7713
+ return callback(node, path9);
7684
7714
  }
7685
7715
  if (Array.isArray(node[currentLevel])) {
7686
7716
  for (const item of node[currentLevel]) {
@@ -8009,10 +8039,24 @@ var shaExists = async ({
8009
8039
  }) => git.readCommit({ fs: fs4, dir, oid: sha3 }).then(() => true).catch(() => false);
8010
8040
 
8011
8041
  // src/database/bridge/filesystem.ts
8012
- import fs2 from "fs-extra";
8013
- import fg from "fast-glob";
8014
8042
  import path7 from "path";
8043
+ import fg from "fast-glob";
8044
+ import fs2 from "fs-extra";
8015
8045
  import normalize from "normalize-path";
8046
+ function assertWithinBase(filepath, baseDir) {
8047
+ if (filepath.includes("\0")) {
8048
+ throw new Error("Invalid path: null bytes are not allowed");
8049
+ }
8050
+ const sanitized = filepath.replace(/\\/g, "/");
8051
+ const resolvedBase = path7.resolve(baseDir);
8052
+ const resolved = path7.resolve(path7.join(baseDir, sanitized));
8053
+ if (resolved !== resolvedBase && !resolved.startsWith(resolvedBase + path7.sep)) {
8054
+ throw new Error(
8055
+ `Path traversal detected: "${filepath}" escapes the base directory`
8056
+ );
8057
+ }
8058
+ return resolved;
8059
+ }
8016
8060
  var FilesystemBridge = class {
8017
8061
  rootPath;
8018
8062
  outputPath;
@@ -8021,7 +8065,7 @@ var FilesystemBridge = class {
8021
8065
  this.outputPath = outputPath ? path7.resolve(outputPath) : this.rootPath;
8022
8066
  }
8023
8067
  async glob(pattern, extension) {
8024
- const basePath = path7.join(this.outputPath, ...pattern.split("/"));
8068
+ const basePath = assertWithinBase(pattern, this.outputPath);
8025
8069
  const items = await fg(
8026
8070
  path7.join(basePath, "**", `/*.${extension}`).replace(/\\/g, "/"),
8027
8071
  {
@@ -8035,14 +8079,17 @@ var FilesystemBridge = class {
8035
8079
  );
8036
8080
  }
8037
8081
  async delete(filepath) {
8038
- await fs2.remove(path7.join(this.outputPath, filepath));
8082
+ const resolved = assertWithinBase(filepath, this.outputPath);
8083
+ await fs2.remove(resolved);
8039
8084
  }
8040
8085
  async get(filepath) {
8041
- return (await fs2.readFile(path7.join(this.outputPath, filepath))).toString();
8086
+ const resolved = assertWithinBase(filepath, this.outputPath);
8087
+ return (await fs2.readFile(resolved)).toString();
8042
8088
  }
8043
8089
  async put(filepath, data, basePathOverride) {
8044
8090
  const basePath = basePathOverride || this.outputPath;
8045
- await fs2.outputFile(path7.join(basePath, filepath), data);
8091
+ const resolved = assertWithinBase(filepath, basePath);
8092
+ await fs2.outputFile(resolved, data);
8046
8093
  }
8047
8094
  };
8048
8095
  var AuditFileSystemBridge = class extends FilesystemBridge {
@@ -8062,12 +8109,25 @@ var AuditFileSystemBridge = class extends FilesystemBridge {
8062
8109
  };
8063
8110
 
8064
8111
  // src/database/bridge/isomorphic.ts
8065
- import git2 from "isomorphic-git";
8112
+ import path8, { dirname } from "path";
8066
8113
  import fs3 from "fs-extra";
8067
8114
  import globParent from "glob-parent";
8068
- import normalize2 from "normalize-path";
8069
8115
  import { GraphQLError as GraphQLError6 } from "graphql";
8070
- import { dirname } from "path";
8116
+ import git2 from "isomorphic-git";
8117
+ import normalize2 from "normalize-path";
8118
+ function assertWithinBase2(filepath, relativePath) {
8119
+ if (filepath.includes("\0")) {
8120
+ throw new Error("Invalid path: null bytes are not allowed");
8121
+ }
8122
+ const sanitized = filepath.replace(/\\/g, "/");
8123
+ const qualified = relativePath ? `${relativePath}/${sanitized}` : sanitized;
8124
+ const normalized = path8.normalize(qualified);
8125
+ if (normalized.startsWith("..") || normalized.startsWith("/") || path8.isAbsolute(normalized) || relativePath && normalized !== relativePath && !normalized.startsWith(relativePath + "/")) {
8126
+ throw new Error(
8127
+ `Path traversal detected: "${filepath}" escapes the content root`
8128
+ );
8129
+ }
8130
+ }
8071
8131
  var flat = typeof Array.prototype.flat === "undefined" ? (entries) => entries.reduce((acc, x) => acc.concat(x), []) : (entries) => entries.flat();
8072
8132
  var toUint8Array = (buf) => {
8073
8133
  const ab = new ArrayBuffer(buf.length);
@@ -8146,7 +8206,7 @@ var IsomorphicBridge = class {
8146
8206
  async listEntries({
8147
8207
  pattern,
8148
8208
  entry,
8149
- path: path8,
8209
+ path: path9,
8150
8210
  results
8151
8211
  }) {
8152
8212
  const treeResult = await git2.readTree({
@@ -8156,7 +8216,7 @@ var IsomorphicBridge = class {
8156
8216
  });
8157
8217
  const children = [];
8158
8218
  for (const childEntry of treeResult.tree) {
8159
- const childPath = path8 ? `${path8}/${childEntry.path}` : childEntry.path;
8219
+ const childPath = path9 ? `${path9}/${childEntry.path}` : childEntry.path;
8160
8220
  if (childEntry.type === "tree") {
8161
8221
  children.push(childEntry);
8162
8222
  } else {
@@ -8166,7 +8226,7 @@ var IsomorphicBridge = class {
8166
8226
  }
8167
8227
  }
8168
8228
  for (const childEntry of children) {
8169
- const childPath = path8 ? `${path8}/${childEntry.path}` : childEntry.path;
8229
+ const childPath = path9 ? `${path9}/${childEntry.path}` : childEntry.path;
8170
8230
  await this.listEntries({
8171
8231
  pattern,
8172
8232
  entry: childEntry,
@@ -8184,17 +8244,17 @@ var IsomorphicBridge = class {
8184
8244
  * @param ref - ref to resolve path entries for
8185
8245
  * @private
8186
8246
  */
8187
- async resolvePathEntries(path8, ref) {
8188
- let pathParts = path8.split("/");
8247
+ async resolvePathEntries(path9, ref) {
8248
+ let pathParts = path9.split("/");
8189
8249
  const result = await git2.walk({
8190
8250
  ...this.isomorphicConfig,
8191
8251
  map: async (filepath, [head]) => {
8192
8252
  if (head._fullpath === ".") {
8193
8253
  return head;
8194
8254
  }
8195
- if (path8.startsWith(filepath)) {
8196
- if (dirname(path8) === dirname(filepath)) {
8197
- if (path8 === filepath) {
8255
+ if (path9.startsWith(filepath)) {
8256
+ if (dirname(path9) === dirname(filepath)) {
8257
+ if (path9 === filepath) {
8198
8258
  return head;
8199
8259
  }
8200
8260
  } else {
@@ -8225,7 +8285,7 @@ var IsomorphicBridge = class {
8225
8285
  * @param pathParts - parent path parts
8226
8286
  * @private
8227
8287
  */
8228
- async updateTreeHierarchy(existingOid, updatedOid, path8, type, pathEntries, pathParts) {
8288
+ async updateTreeHierarchy(existingOid, updatedOid, path9, type, pathEntries, pathParts) {
8229
8289
  const lastIdx = pathEntries.length - 1;
8230
8290
  const parentEntry = pathEntries[lastIdx];
8231
8291
  const parentPath = pathParts[lastIdx];
@@ -8240,7 +8300,7 @@ var IsomorphicBridge = class {
8240
8300
  cache: this.cache
8241
8301
  });
8242
8302
  tree = existingOid ? treeResult.tree.map((entry) => {
8243
- if (entry.path === path8) {
8303
+ if (entry.path === path9) {
8244
8304
  entry.oid = updatedOid;
8245
8305
  }
8246
8306
  return entry;
@@ -8249,7 +8309,7 @@ var IsomorphicBridge = class {
8249
8309
  {
8250
8310
  oid: updatedOid,
8251
8311
  type,
8252
- path: path8,
8312
+ path: path9,
8253
8313
  mode
8254
8314
  }
8255
8315
  ];
@@ -8258,7 +8318,7 @@ var IsomorphicBridge = class {
8258
8318
  {
8259
8319
  oid: updatedOid,
8260
8320
  type,
8261
- path: path8,
8321
+ path: path9,
8262
8322
  mode
8263
8323
  }
8264
8324
  ];
@@ -8334,6 +8394,7 @@ var IsomorphicBridge = class {
8334
8394
  return ref;
8335
8395
  }
8336
8396
  async glob(pattern, extension) {
8397
+ assertWithinBase2(pattern, this.relativePath);
8337
8398
  const ref = await this.getRef();
8338
8399
  const parent = globParent(this.qualifyPath(pattern));
8339
8400
  const { pathParts, pathEntries } = await this.resolvePathEntries(
@@ -8367,9 +8428,10 @@ var IsomorphicBridge = class {
8367
8428
  path: parentPath,
8368
8429
  results
8369
8430
  });
8370
- return results.map((path8) => this.unqualifyPath(path8)).filter((path8) => path8.endsWith(extension));
8431
+ return results.map((path9) => this.unqualifyPath(path9)).filter((path9) => path9.endsWith(extension));
8371
8432
  }
8372
8433
  async delete(filepath) {
8434
+ assertWithinBase2(filepath, this.relativePath);
8373
8435
  const ref = await this.getRef();
8374
8436
  const { pathParts, pathEntries } = await this.resolvePathEntries(
8375
8437
  this.qualifyPath(filepath),
@@ -8441,6 +8503,7 @@ var IsomorphicBridge = class {
8441
8503
  return this.relativePath ? filepath.slice(this.relativePath.length + 1) : filepath;
8442
8504
  }
8443
8505
  async get(filepath) {
8506
+ assertWithinBase2(filepath, this.relativePath);
8444
8507
  const ref = await this.getRef();
8445
8508
  const oid = await git2.resolveRef({
8446
8509
  ...this.isomorphicConfig,
@@ -8455,6 +8518,7 @@ var IsomorphicBridge = class {
8455
8518
  return Buffer.from(blob).toString("utf8");
8456
8519
  }
8457
8520
  async put(filepath, data) {
8521
+ assertWithinBase2(filepath, this.relativePath);
8458
8522
  const ref = await this.getRef();
8459
8523
  const { pathParts, pathEntries } = await this.resolvePathEntries(
8460
8524
  this.qualifyPath(filepath),
@@ -235,6 +235,7 @@ export declare class Resolver {
235
235
  * path traversal attacks where a user might attempt to read or write files
236
236
  * outside of the intended collection.
237
237
  */
238
+ private static sanitizePath;
238
239
  private validatePath;
239
240
  /**
240
241
  * Helper method to get collection and construct validated path.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tinacms/graphql",
3
3
  "type": "module",
4
- "version": "0.0.0-e2d2d7e-20260217062658",
4
+ "version": "0.0.0-e2e04ae-20260325061046",
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-e2d2d7e-20260217062658",
47
- "@tinacms/schema-tools": "0.0.0-e2d2d7e-20260217062658"
46
+ "@tinacms/mdx": "2.1.0",
47
+ "@tinacms/schema-tools": "2.7.0"
48
48
  },
49
49
  "publishConfig": {
50
50
  "registry": "https://registry.npmjs.org"
@@ -65,20 +65,21 @@
65
65
  "@types/node": "^22.13.1",
66
66
  "@types/normalize-path": "^3.0.2",
67
67
  "@types/ws": "^7.4.7",
68
+ "@vitest/coverage-v8": "0.32.4",
68
69
  "jest-file-snapshot": "^0.5.0",
69
70
  "memory-level": "^1.0.0",
70
71
  "typescript": "^5.7.3",
71
72
  "vite": "^4.5.9",
72
73
  "vitest": "^0.32.4",
73
74
  "zod": "^3.24.2",
74
- "@tinacms/schema-tools": "0.0.0-e2d2d7e-20260217062658",
75
- "@tinacms/scripts": "0.0.0-e2d2d7e-20260217062658"
75
+ "@tinacms/scripts": "1.6.0",
76
+ "@tinacms/schema-tools": "2.7.0"
76
77
  },
77
78
  "scripts": {
78
79
  "types": "pnpm tsc",
79
80
  "build": "tinacms-scripts build",
80
81
  "docs": "pnpm typedoc",
81
- "test": "vitest run",
82
+ "test": "vitest run --coverage.enabled",
82
83
  "test-watch": "vitest"
83
84
  }
84
85
  }