@tinacms/graphql 0.0.0-04a60ca-20260217063130 → 0.0.0-059f480-20260324232913

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;
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
  );
@@ -3026,7 +3026,7 @@ var validateField = async (field) => {
3026
3026
  var package_default = {
3027
3027
  name: "@tinacms/graphql",
3028
3028
  type: "module",
3029
- version: "2.1.1",
3029
+ version: "2.2.0",
3030
3030
  main: "dist/index.js",
3031
3031
  module: "./dist/index.js",
3032
3032
  files: [
@@ -3053,7 +3053,7 @@ var package_default = {
3053
3053
  types: "pnpm tsc",
3054
3054
  build: "tinacms-scripts build",
3055
3055
  docs: "pnpm typedoc",
3056
- test: "vitest run",
3056
+ test: "vitest run --coverage.enabled",
3057
3057
  "test-watch": "vitest"
3058
3058
  },
3059
3059
  dependencies: {
@@ -3099,6 +3099,7 @@ var package_default = {
3099
3099
  "@types/node": "^22.13.1",
3100
3100
  "@types/normalize-path": "catalog:",
3101
3101
  "@types/ws": "catalog:",
3102
+ "@vitest/coverage-v8": "0.32.4",
3102
3103
  "jest-file-snapshot": "^0.5.0",
3103
3104
  "memory-level": "catalog:",
3104
3105
  typescript: "^5.7.3",
@@ -3662,17 +3663,17 @@ var scanAllContent = async (tinaSchema, bridge, callback) => {
3662
3663
  const documentPaths = await bridge.glob(normalPath, format);
3663
3664
  const matches = tinaSchema.getMatches({ collection });
3664
3665
  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);
3666
+ filteredPaths.forEach((path9) => {
3667
+ if (filesSeen.has(path9)) {
3668
+ filesSeen.get(path9).push(collection.name);
3669
+ duplicateFiles.add(path9);
3669
3670
  } else {
3670
- filesSeen.set(path8, [collection.name]);
3671
+ filesSeen.set(path9, [collection.name]);
3671
3672
  }
3672
3673
  });
3673
- duplicateFiles.forEach((path8) => {
3674
+ duplicateFiles.forEach((path9) => {
3674
3675
  warnings.push(
3675
- `"${path8}" Found in multiple collections: ${filesSeen.get(path8).map((collection2) => `"${collection2}"`).join(
3676
+ `"${path9}" Found in multiple collections: ${filesSeen.get(path9).map((collection2) => `"${collection2}"`).join(
3676
3677
  ", "
3677
3678
  )}. This can cause unexpected behavior. We recommend updating the \`match\` property of those collections so that each file is in only one collection.
3678
3679
  This will be an error in the future. See https://tina.io/docs/errors/file-in-mutpliple-collections/
@@ -4235,9 +4236,9 @@ var makeFilterSuffixes = (filterChain, index) => {
4235
4236
  }
4236
4237
  };
4237
4238
  var FOLDER_ROOT = "~";
4238
- var stripCollectionFromPath = (collectionPath, path8) => {
4239
+ var stripCollectionFromPath = (collectionPath, path9) => {
4239
4240
  const collectionPathParts = collectionPath.split("/");
4240
- const pathParts = path8.split("/");
4241
+ const pathParts = path9.split("/");
4241
4242
  const strippedPathParts = pathParts.slice(collectionPathParts.length);
4242
4243
  return strippedPathParts.join("/");
4243
4244
  };
@@ -4293,13 +4294,13 @@ var makeFolderOpsForCollection = (folderTree, collection, indexDefinitions, opTy
4293
4294
  SUBLEVEL_OPTIONS
4294
4295
  );
4295
4296
  let folderSortingIdx = 0;
4296
- for (const path8 of Array.from(folder).sort()) {
4297
+ for (const path9 of Array.from(folder).sort()) {
4297
4298
  for (const [sort] of Object.entries(indexDefinitions)) {
4298
4299
  const indexSublevel = folderCollectionSublevel.sublevel(
4299
4300
  sort,
4300
4301
  SUBLEVEL_OPTIONS
4301
4302
  );
4302
- const subFolderKey = sha.hex(path8);
4303
+ const subFolderKey = sha.hex(path9);
4303
4304
  if (sort === DEFAULT_COLLECTION_SORT_KEY) {
4304
4305
  result.push({
4305
4306
  type: opType,
@@ -4381,8 +4382,8 @@ var makeRefOpsForDocument = (filepath, collection, references, data, opType, lev
4381
4382
  SUBLEVEL_OPTIONS
4382
4383
  );
4383
4384
  const references2 = {};
4384
- for (const path8 of referencePaths) {
4385
- const ref = JSONPath({ path: path8, json: data });
4385
+ for (const path9 of referencePaths) {
4386
+ const ref = JSONPath({ path: path9, json: data });
4386
4387
  if (!ref) {
4387
4388
  continue;
4388
4389
  }
@@ -4392,24 +4393,24 @@ var makeRefOpsForDocument = (filepath, collection, references, data, opType, lev
4392
4393
  continue;
4393
4394
  }
4394
4395
  if (references2[r]) {
4395
- references2[r].push(path8);
4396
+ references2[r].push(path9);
4396
4397
  } else {
4397
- references2[r] = [path8];
4398
+ references2[r] = [path9];
4398
4399
  }
4399
4400
  }
4400
4401
  } else {
4401
4402
  if (references2[ref]) {
4402
- references2[ref].push(path8);
4403
+ references2[ref].push(path9);
4403
4404
  } else {
4404
- references2[ref] = [path8];
4405
+ references2[ref] = [path9];
4405
4406
  }
4406
4407
  }
4407
4408
  }
4408
4409
  for (const ref of Object.keys(references2)) {
4409
- for (const path8 of references2[ref]) {
4410
+ for (const path9 of references2[ref]) {
4410
4411
  result.push({
4411
4412
  type: opType,
4412
- key: `${ref}${INDEX_KEY_FIELD_SEPARATOR}${path8}${INDEX_KEY_FIELD_SEPARATOR}${filepath}`,
4413
+ key: `${ref}${INDEX_KEY_FIELD_SEPARATOR}${path9}${INDEX_KEY_FIELD_SEPARATOR}${filepath}`,
4413
4414
  sublevel: refSublevel,
4414
4415
  value: opType === "put" ? {} : void 0
4415
4416
  });
@@ -4676,9 +4677,9 @@ var resolveMediaRelativeToCloud = (value, config = { useRelativeMedia: true }, s
4676
4677
  return value;
4677
4678
  }
4678
4679
  };
4679
- var cleanUpSlashes = (path8) => {
4680
- if (path8) {
4681
- return `/${path8.replace(/^\/+|\/+$/gm, "")}`;
4680
+ var cleanUpSlashes = (path9) => {
4681
+ if (path9) {
4682
+ return `/${path9.replace(/^\/+|\/+$/gm, "")}`;
4682
4683
  }
4683
4684
  return "";
4684
4685
  };
@@ -4888,17 +4889,17 @@ var transformDocumentIntoPayload = async (fullPath, rawData, tinaSchema, config,
4888
4889
  throw e;
4889
4890
  }
4890
4891
  };
4891
- var updateObjectWithJsonPath = (obj, path8, oldValue, newValue) => {
4892
+ var updateObjectWithJsonPath = (obj, path9, oldValue, newValue) => {
4892
4893
  let updated = false;
4893
- if (!path8.includes(".") && !path8.includes("[")) {
4894
- if (path8 in obj && obj[path8] === oldValue) {
4895
- obj[path8] = newValue;
4894
+ if (!path9.includes(".") && !path9.includes("[")) {
4895
+ if (path9 in obj && obj[path9] === oldValue) {
4896
+ obj[path9] = newValue;
4896
4897
  updated = true;
4897
4898
  }
4898
4899
  return { object: obj, updated };
4899
4900
  }
4900
- const parentPath = path8.replace(/\.[^.\[\]]+$/, "");
4901
- const keyToUpdate = path8.match(/[^.\[\]]+$/)[0];
4901
+ const parentPath = path9.replace(/\.[^.\[\]]+$/, "");
4902
+ const keyToUpdate = path9.match(/[^.\[\]]+$/)[0];
4902
4903
  const parents = JSONPath2({
4903
4904
  path: parentPath,
4904
4905
  json: obj,
@@ -5322,10 +5323,10 @@ var Resolver = class {
5322
5323
  )) {
5323
5324
  let docWithRef = await this.getRaw(pathToDocWithRef);
5324
5325
  let hasUpdate = false;
5325
- for (const path8 of referencePaths) {
5326
+ for (const path9 of referencePaths) {
5326
5327
  const { object: object2, updated } = updateObjectWithJsonPath(
5327
5328
  docWithRef,
5328
- path8,
5329
+ path9,
5329
5330
  realPath,
5330
5331
  newRealPath
5331
5332
  );
@@ -5390,10 +5391,10 @@ var Resolver = class {
5390
5391
  )) {
5391
5392
  let refDoc = await this.getRaw(pathToDocWithRef);
5392
5393
  let hasUpdate = false;
5393
- for (const path8 of referencePaths) {
5394
+ for (const path9 of referencePaths) {
5394
5395
  const { object: object2, updated } = updateObjectWithJsonPath(
5395
5396
  refDoc,
5396
- path8,
5397
+ path9,
5397
5398
  realPath,
5398
5399
  null
5399
5400
  );
@@ -5660,7 +5661,7 @@ var Resolver = class {
5660
5661
  first: -1
5661
5662
  },
5662
5663
  collection: referencedCollection,
5663
- hydrator: (path8) => path8
5664
+ hydrator: (path9) => path9
5664
5665
  // just return the path
5665
5666
  }
5666
5667
  );
@@ -6101,8 +6102,8 @@ async function handleUpdatePassword({
6101
6102
  // src/error.ts
6102
6103
  import { GraphQLError as GraphQLError3 } from "graphql";
6103
6104
  var NotFoundError = class extends GraphQLError3 {
6104
- constructor(message, nodes, source, positions, path8, originalError, extensions) {
6105
- super(message, nodes, source, positions, path8, originalError, extensions);
6105
+ constructor(message, nodes, source, positions, path9, originalError, extensions) {
6106
+ super(message, nodes, source, positions, path9, originalError, extensions);
6106
6107
  this.name = "NotFoundError";
6107
6108
  }
6108
6109
  };
@@ -6598,6 +6599,7 @@ var createDatabaseInternal = (config) => {
6598
6599
  });
6599
6600
  };
6600
6601
  var SYSTEM_FILES = ["_schema", "_graphql", "_lookup"];
6602
+ var DEFAULT_LEVEL_BATCH_SIZE = 25;
6601
6603
  var defaultStatusCallback = () => Promise.resolve();
6602
6604
  var defaultOnPut = () => Promise.resolve();
6603
6605
  var defaultOnDelete = () => Promise.resolve();
@@ -6610,6 +6612,7 @@ var Database = class {
6610
6612
  this.indexStatusCallback = config.indexStatusCallback || defaultStatusCallback;
6611
6613
  this.onPut = config.onPut || defaultOnPut;
6612
6614
  this.onDelete = config.onDelete || defaultOnDelete;
6615
+ this.levelBatchSize = config.levelBatchSize ?? DEFAULT_LEVEL_BATCH_SIZE;
6613
6616
  this.contentNamespace = config.namespace;
6614
6617
  }
6615
6618
  bridge;
@@ -6620,6 +6623,7 @@ var Database = class {
6620
6623
  indexStatusCallback;
6621
6624
  onPut;
6622
6625
  onDelete;
6626
+ levelBatchSize;
6623
6627
  tinaSchema;
6624
6628
  contentNamespace;
6625
6629
  collectionIndexDefinitions;
@@ -7364,21 +7368,21 @@ var Database = class {
7364
7368
  edges,
7365
7369
  async ({
7366
7370
  cursor,
7367
- path: path8,
7371
+ path: path9,
7368
7372
  value
7369
7373
  }) => {
7370
7374
  try {
7371
- const node = await hydrator(path8, value);
7375
+ const node = await hydrator(path9, value);
7372
7376
  return {
7373
7377
  node,
7374
7378
  cursor: btoa(cursor)
7375
7379
  };
7376
7380
  } catch (error) {
7377
7381
  console.log(error);
7378
- if (error instanceof Error && (!path8.includes(".tina/__generated__/_graphql.json") || !path8.includes("tina/__generated__/_graphql.json"))) {
7382
+ if (error instanceof Error && (!path9.includes(".tina/__generated__/_graphql.json") || !path9.includes("tina/__generated__/_graphql.json"))) {
7379
7383
  throw new TinaQueryError({
7380
7384
  originalError: error,
7381
- file: path8,
7385
+ file: path9,
7382
7386
  collection: collection.name
7383
7387
  });
7384
7388
  }
@@ -7484,8 +7488,10 @@ var Database = class {
7484
7488
  const operations = [];
7485
7489
  const enqueueOps = async (ops) => {
7486
7490
  operations.push(...ops);
7487
- while (operations.length >= 25) {
7488
- await this.contentLevel.batch(operations.splice(0, 25));
7491
+ while (operations.length >= this.levelBatchSize) {
7492
+ await this.contentLevel.batch(
7493
+ operations.splice(0, this.levelBatchSize)
7494
+ );
7489
7495
  }
7490
7496
  };
7491
7497
  const tinaSchema = await this.getSchema(this.contentLevel);
@@ -7504,7 +7510,7 @@ var Database = class {
7504
7510
  }
7505
7511
  });
7506
7512
  while (operations.length) {
7507
- await this.contentLevel.batch(operations.splice(0, 25));
7513
+ await this.contentLevel.batch(operations.splice(0, this.levelBatchSize));
7508
7514
  }
7509
7515
  };
7510
7516
  indexContentByPaths = async (documentPaths) => {
@@ -7512,8 +7518,10 @@ var Database = class {
7512
7518
  const operations = [];
7513
7519
  const enqueueOps = async (ops) => {
7514
7520
  operations.push(...ops);
7515
- while (operations.length >= 25) {
7516
- await this.contentLevel.batch(operations.splice(0, 25));
7521
+ while (operations.length >= this.levelBatchSize) {
7522
+ await this.contentLevel.batch(
7523
+ operations.splice(0, this.levelBatchSize)
7524
+ );
7517
7525
  }
7518
7526
  };
7519
7527
  const tinaSchema = await this.getSchema(this.contentLevel);
@@ -7536,7 +7544,7 @@ var Database = class {
7536
7544
  );
7537
7545
  });
7538
7546
  while (operations.length) {
7539
- await this.contentLevel.batch(operations.splice(0, 25));
7547
+ await this.contentLevel.batch(operations.splice(0, this.levelBatchSize));
7540
7548
  }
7541
7549
  };
7542
7550
  delete = async (filepath) => {
@@ -7620,8 +7628,8 @@ var Database = class {
7620
7628
  const operations = [];
7621
7629
  const enqueueOps = async (ops) => {
7622
7630
  operations.push(...ops);
7623
- while (operations.length >= 25) {
7624
- const batchOps = operations.splice(0, 25);
7631
+ while (operations.length >= this.levelBatchSize) {
7632
+ const batchOps = operations.splice(0, this.levelBatchSize);
7625
7633
  await level.batch(batchOps);
7626
7634
  }
7627
7635
  };
@@ -7661,13 +7669,13 @@ var Database = class {
7661
7669
  }
7662
7670
  );
7663
7671
  while (operations.length) {
7664
- await level.batch(operations.splice(0, 25));
7672
+ await level.batch(operations.splice(0, this.levelBatchSize));
7665
7673
  }
7666
7674
  return { warnings };
7667
7675
  };
7668
7676
  };
7669
- var hashPasswordVisitor = async (node, path8) => {
7670
- const passwordValuePath = [...path8, "value"];
7677
+ var hashPasswordVisitor = async (node, path9) => {
7678
+ const passwordValuePath = [...path9, "value"];
7671
7679
  const plaintextPassword = get(node, passwordValuePath);
7672
7680
  if (plaintextPassword) {
7673
7681
  set2(
@@ -7677,10 +7685,10 @@ var hashPasswordVisitor = async (node, path8) => {
7677
7685
  );
7678
7686
  }
7679
7687
  };
7680
- var visitNodes = async (node, path8, callback) => {
7681
- const [currentLevel, ...remainingLevels] = path8;
7688
+ var visitNodes = async (node, path9, callback) => {
7689
+ const [currentLevel, ...remainingLevels] = path9;
7682
7690
  if (!remainingLevels?.length) {
7683
- return callback(node, path8);
7691
+ return callback(node, path9);
7684
7692
  }
7685
7693
  if (Array.isArray(node[currentLevel])) {
7686
7694
  for (const item of node[currentLevel]) {
@@ -8009,10 +8017,20 @@ var shaExists = async ({
8009
8017
  }) => git.readCommit({ fs: fs4, dir, oid: sha3 }).then(() => true).catch(() => false);
8010
8018
 
8011
8019
  // src/database/bridge/filesystem.ts
8012
- import fs2 from "fs-extra";
8013
- import fg from "fast-glob";
8014
8020
  import path7 from "path";
8021
+ import fg from "fast-glob";
8022
+ import fs2 from "fs-extra";
8015
8023
  import normalize from "normalize-path";
8024
+ function assertWithinBase(filepath, baseDir) {
8025
+ const resolvedBase = path7.resolve(baseDir);
8026
+ const resolved = path7.resolve(path7.join(baseDir, filepath));
8027
+ if (resolved !== resolvedBase && !resolved.startsWith(resolvedBase + path7.sep)) {
8028
+ throw new Error(
8029
+ `Path traversal detected: "${filepath}" escapes the base directory`
8030
+ );
8031
+ }
8032
+ return resolved;
8033
+ }
8016
8034
  var FilesystemBridge = class {
8017
8035
  rootPath;
8018
8036
  outputPath;
@@ -8021,7 +8039,7 @@ var FilesystemBridge = class {
8021
8039
  this.outputPath = outputPath ? path7.resolve(outputPath) : this.rootPath;
8022
8040
  }
8023
8041
  async glob(pattern, extension) {
8024
- const basePath = path7.join(this.outputPath, ...pattern.split("/"));
8042
+ const basePath = assertWithinBase(pattern, this.outputPath);
8025
8043
  const items = await fg(
8026
8044
  path7.join(basePath, "**", `/*.${extension}`).replace(/\\/g, "/"),
8027
8045
  {
@@ -8035,14 +8053,17 @@ var FilesystemBridge = class {
8035
8053
  );
8036
8054
  }
8037
8055
  async delete(filepath) {
8038
- await fs2.remove(path7.join(this.outputPath, filepath));
8056
+ const resolved = assertWithinBase(filepath, this.outputPath);
8057
+ await fs2.remove(resolved);
8039
8058
  }
8040
8059
  async get(filepath) {
8041
- return (await fs2.readFile(path7.join(this.outputPath, filepath))).toString();
8060
+ const resolved = assertWithinBase(filepath, this.outputPath);
8061
+ return (await fs2.readFile(resolved)).toString();
8042
8062
  }
8043
8063
  async put(filepath, data, basePathOverride) {
8044
8064
  const basePath = basePathOverride || this.outputPath;
8045
- await fs2.outputFile(path7.join(basePath, filepath), data);
8065
+ const resolved = assertWithinBase(filepath, basePath);
8066
+ await fs2.outputFile(resolved, data);
8046
8067
  }
8047
8068
  };
8048
8069
  var AuditFileSystemBridge = class extends FilesystemBridge {
@@ -8062,12 +8083,21 @@ var AuditFileSystemBridge = class extends FilesystemBridge {
8062
8083
  };
8063
8084
 
8064
8085
  // src/database/bridge/isomorphic.ts
8065
- import git2 from "isomorphic-git";
8086
+ import path8, { dirname } from "path";
8066
8087
  import fs3 from "fs-extra";
8067
8088
  import globParent from "glob-parent";
8068
- import normalize2 from "normalize-path";
8069
8089
  import { GraphQLError as GraphQLError6 } from "graphql";
8070
- import { dirname } from "path";
8090
+ import git2 from "isomorphic-git";
8091
+ import normalize2 from "normalize-path";
8092
+ function assertWithinBase2(filepath, relativePath) {
8093
+ const qualified = relativePath ? `${relativePath}/${filepath}` : filepath;
8094
+ const normalized = path8.normalize(qualified);
8095
+ if (normalized.startsWith("..") || normalized.startsWith("/") || path8.isAbsolute(normalized) || relativePath && normalized !== relativePath && !normalized.startsWith(relativePath + "/")) {
8096
+ throw new Error(
8097
+ `Path traversal detected: "${filepath}" escapes the content root`
8098
+ );
8099
+ }
8100
+ }
8071
8101
  var flat = typeof Array.prototype.flat === "undefined" ? (entries) => entries.reduce((acc, x) => acc.concat(x), []) : (entries) => entries.flat();
8072
8102
  var toUint8Array = (buf) => {
8073
8103
  const ab = new ArrayBuffer(buf.length);
@@ -8146,7 +8176,7 @@ var IsomorphicBridge = class {
8146
8176
  async listEntries({
8147
8177
  pattern,
8148
8178
  entry,
8149
- path: path8,
8179
+ path: path9,
8150
8180
  results
8151
8181
  }) {
8152
8182
  const treeResult = await git2.readTree({
@@ -8156,7 +8186,7 @@ var IsomorphicBridge = class {
8156
8186
  });
8157
8187
  const children = [];
8158
8188
  for (const childEntry of treeResult.tree) {
8159
- const childPath = path8 ? `${path8}/${childEntry.path}` : childEntry.path;
8189
+ const childPath = path9 ? `${path9}/${childEntry.path}` : childEntry.path;
8160
8190
  if (childEntry.type === "tree") {
8161
8191
  children.push(childEntry);
8162
8192
  } else {
@@ -8166,7 +8196,7 @@ var IsomorphicBridge = class {
8166
8196
  }
8167
8197
  }
8168
8198
  for (const childEntry of children) {
8169
- const childPath = path8 ? `${path8}/${childEntry.path}` : childEntry.path;
8199
+ const childPath = path9 ? `${path9}/${childEntry.path}` : childEntry.path;
8170
8200
  await this.listEntries({
8171
8201
  pattern,
8172
8202
  entry: childEntry,
@@ -8184,17 +8214,17 @@ var IsomorphicBridge = class {
8184
8214
  * @param ref - ref to resolve path entries for
8185
8215
  * @private
8186
8216
  */
8187
- async resolvePathEntries(path8, ref) {
8188
- let pathParts = path8.split("/");
8217
+ async resolvePathEntries(path9, ref) {
8218
+ let pathParts = path9.split("/");
8189
8219
  const result = await git2.walk({
8190
8220
  ...this.isomorphicConfig,
8191
8221
  map: async (filepath, [head]) => {
8192
8222
  if (head._fullpath === ".") {
8193
8223
  return head;
8194
8224
  }
8195
- if (path8.startsWith(filepath)) {
8196
- if (dirname(path8) === dirname(filepath)) {
8197
- if (path8 === filepath) {
8225
+ if (path9.startsWith(filepath)) {
8226
+ if (dirname(path9) === dirname(filepath)) {
8227
+ if (path9 === filepath) {
8198
8228
  return head;
8199
8229
  }
8200
8230
  } else {
@@ -8225,7 +8255,7 @@ var IsomorphicBridge = class {
8225
8255
  * @param pathParts - parent path parts
8226
8256
  * @private
8227
8257
  */
8228
- async updateTreeHierarchy(existingOid, updatedOid, path8, type, pathEntries, pathParts) {
8258
+ async updateTreeHierarchy(existingOid, updatedOid, path9, type, pathEntries, pathParts) {
8229
8259
  const lastIdx = pathEntries.length - 1;
8230
8260
  const parentEntry = pathEntries[lastIdx];
8231
8261
  const parentPath = pathParts[lastIdx];
@@ -8240,7 +8270,7 @@ var IsomorphicBridge = class {
8240
8270
  cache: this.cache
8241
8271
  });
8242
8272
  tree = existingOid ? treeResult.tree.map((entry) => {
8243
- if (entry.path === path8) {
8273
+ if (entry.path === path9) {
8244
8274
  entry.oid = updatedOid;
8245
8275
  }
8246
8276
  return entry;
@@ -8249,7 +8279,7 @@ var IsomorphicBridge = class {
8249
8279
  {
8250
8280
  oid: updatedOid,
8251
8281
  type,
8252
- path: path8,
8282
+ path: path9,
8253
8283
  mode
8254
8284
  }
8255
8285
  ];
@@ -8258,7 +8288,7 @@ var IsomorphicBridge = class {
8258
8288
  {
8259
8289
  oid: updatedOid,
8260
8290
  type,
8261
- path: path8,
8291
+ path: path9,
8262
8292
  mode
8263
8293
  }
8264
8294
  ];
@@ -8334,6 +8364,7 @@ var IsomorphicBridge = class {
8334
8364
  return ref;
8335
8365
  }
8336
8366
  async glob(pattern, extension) {
8367
+ assertWithinBase2(pattern, this.relativePath);
8337
8368
  const ref = await this.getRef();
8338
8369
  const parent = globParent(this.qualifyPath(pattern));
8339
8370
  const { pathParts, pathEntries } = await this.resolvePathEntries(
@@ -8367,9 +8398,10 @@ var IsomorphicBridge = class {
8367
8398
  path: parentPath,
8368
8399
  results
8369
8400
  });
8370
- return results.map((path8) => this.unqualifyPath(path8)).filter((path8) => path8.endsWith(extension));
8401
+ return results.map((path9) => this.unqualifyPath(path9)).filter((path9) => path9.endsWith(extension));
8371
8402
  }
8372
8403
  async delete(filepath) {
8404
+ assertWithinBase2(filepath, this.relativePath);
8373
8405
  const ref = await this.getRef();
8374
8406
  const { pathParts, pathEntries } = await this.resolvePathEntries(
8375
8407
  this.qualifyPath(filepath),
@@ -8441,6 +8473,7 @@ var IsomorphicBridge = class {
8441
8473
  return this.relativePath ? filepath.slice(this.relativePath.length + 1) : filepath;
8442
8474
  }
8443
8475
  async get(filepath) {
8476
+ assertWithinBase2(filepath, this.relativePath);
8444
8477
  const ref = await this.getRef();
8445
8478
  const oid = await git2.resolveRef({
8446
8479
  ...this.isomorphicConfig,
@@ -8455,6 +8488,7 @@ var IsomorphicBridge = class {
8455
8488
  return Buffer.from(blob).toString("utf8");
8456
8489
  }
8457
8490
  async put(filepath, data) {
8491
+ assertWithinBase2(filepath, this.relativePath);
8458
8492
  const ref = await this.getRef();
8459
8493
  const { pathParts, pathEntries } = await this.resolvePathEntries(
8460
8494
  this.qualifyPath(filepath),
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tinacms/graphql",
3
3
  "type": "module",
4
- "version": "0.0.0-04a60ca-20260217063130",
4
+ "version": "0.0.0-059f480-20260324232913",
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": "0.0.0-04a60ca-20260217063130",
47
- "@tinacms/mdx": "0.0.0-04a60ca-20260217063130"
46
+ "@tinacms/mdx": "0.0.0-059f480-20260324232913",
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-04a60ca-20260217063130",
75
- "@tinacms/scripts": "0.0.0-04a60ca-20260217063130"
75
+ "@tinacms/schema-tools": "2.7.0",
76
+ "@tinacms/scripts": "0.0.0-059f480-20260324232913"
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
  }