@tinacms/cli 2.1.5 → 2.1.7

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/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { Cli, Builtins } from "clipanion";
3
3
 
4
4
  // package.json
5
- var version = "2.1.5";
5
+ var version = "2.1.7";
6
6
 
7
7
  // src/next/commands/dev-command/index.ts
8
8
  import path8 from "path";
@@ -417,6 +417,38 @@ var loadGraphQLDocuments = async (globPath) => {
417
417
  import { transform } from "esbuild";
418
418
  import { mapUserFields } from "@tinacms/graphql";
419
419
  import normalizePath from "normalize-path";
420
+
421
+ // src/next/codegen/stripSearchTokenFromConfig.ts
422
+ function stripSearchTokenFromConfig(config2) {
423
+ const cfg = config2;
424
+ if (!cfg?.search) {
425
+ return config2;
426
+ }
427
+ const search = cfg.search;
428
+ const tina = search?.tina;
429
+ if (tina) {
430
+ const { indexerToken, ...safeSearchConfig } = tina;
431
+ const newConfig = {};
432
+ for (const key of Object.keys(cfg)) {
433
+ if (key === "search") {
434
+ newConfig.search = { tina: safeSearchConfig };
435
+ } else {
436
+ newConfig[key] = cfg[key];
437
+ }
438
+ }
439
+ return newConfig;
440
+ } else {
441
+ const newConfig = {};
442
+ for (const key of Object.keys(cfg)) {
443
+ if (key !== "search") {
444
+ newConfig[key] = cfg[key];
445
+ }
446
+ }
447
+ return newConfig;
448
+ }
449
+ }
450
+
451
+ // src/next/codegen/index.ts
420
452
  var TINA_HOST = "content.tinajs.io";
421
453
  var Codegen = class {
422
454
  configManager;
@@ -487,27 +519,9 @@ var Codegen = class {
487
519
  "_graphql.json",
488
520
  JSON.stringify(this.graphqlSchemaDoc)
489
521
  );
490
- const config2 = this.tinaSchema.schema.config;
491
- if (config2?.search?.tina) {
492
- const { indexerToken, ...safeSearchConfig } = config2.search.tina;
493
- const newConfig = {};
494
- for (const key of Object.keys(config2)) {
495
- if (key === "search") {
496
- newConfig.search = { tina: safeSearchConfig };
497
- } else {
498
- newConfig[key] = config2[key];
499
- }
500
- }
501
- this.tinaSchema.schema.config = newConfig;
502
- } else if (config2?.search) {
503
- const newConfig = {};
504
- for (const key of Object.keys(config2)) {
505
- if (key !== "search") {
506
- newConfig[key] = config2[key];
507
- }
508
- }
509
- this.tinaSchema.schema.config = newConfig;
510
- }
522
+ this.tinaSchema.schema.config = stripSearchTokenFromConfig(
523
+ this.tinaSchema.schema.config
524
+ );
511
525
  await this.writeConfigFile(
512
526
  "_schema.json",
513
527
  JSON.stringify(this.tinaSchema.schema)
@@ -803,6 +817,14 @@ function stripNativeTrailingSlash(p) {
803
817
  }
804
818
  return str;
805
819
  }
820
+ var PathTraversalError = class extends Error {
821
+ constructor(attemptedPath) {
822
+ super(
823
+ `Path traversal detected: the path "${attemptedPath}" escapes the allowed directory`
824
+ );
825
+ this.name = "PathTraversalError";
826
+ }
827
+ };
806
828
 
807
829
  // src/next/config-manager.ts
808
830
  var TINA_FOLDER = "tina";
@@ -1017,13 +1039,15 @@ var ConfigManager = class {
1017
1039
  this.contentRootPath = this.rootPath;
1018
1040
  }
1019
1041
  this.generatedFolderPathContentRepo = path3.join(
1020
- await this.getTinaFolderPath(this.contentRootPath),
1042
+ await this.getTinaFolderPath(this.contentRootPath, {
1043
+ isContentRoot: this.hasSeparateContentRoot()
1044
+ }),
1021
1045
  GENERATED_FOLDER
1022
1046
  );
1023
1047
  this.spaMainPath = require2.resolve("@tinacms/app");
1024
1048
  this.spaRootPath = path3.join(this.spaMainPath, "..", "..");
1025
1049
  }
1026
- async getTinaFolderPath(rootPath) {
1050
+ async getTinaFolderPath(rootPath, { isContentRoot } = {}) {
1027
1051
  const tinaFolderPath = path3.join(rootPath, TINA_FOLDER);
1028
1052
  const tinaFolderExists = await fs2.pathExists(tinaFolderPath);
1029
1053
  if (tinaFolderExists) {
@@ -1036,6 +1060,11 @@ var ConfigManager = class {
1036
1060
  this.isUsingLegacyFolder = true;
1037
1061
  return legacyFolderPath;
1038
1062
  }
1063
+ if (isContentRoot) {
1064
+ throw new Error(
1065
+ `Unable to find a ${chalk3.cyan("tina/")} folder in your content root at ${chalk3.cyan(rootPath)}. When using localContentPath, the content directory must contain a ${chalk3.cyan("tina/")} folder for generated files. Create one with: mkdir ${path3.join(rootPath, TINA_FOLDER)}`
1066
+ );
1067
+ }
1039
1068
  throw new Error(
1040
1069
  `Unable to find Tina folder, if you're working in folder outside of the Tina config be sure to specify --rootPath`
1041
1070
  );
@@ -1557,6 +1586,29 @@ import {
1557
1586
  splitVendorChunkPlugin
1558
1587
  } from "vite";
1559
1588
 
1589
+ // src/next/vite/filterPublicEnv.ts
1590
+ function filterPublicEnv(env = process.env) {
1591
+ const publicEnv = {};
1592
+ Object.keys(env).forEach((key) => {
1593
+ if (key.startsWith("TINA_PUBLIC_") || key.startsWith("NEXT_PUBLIC_") || key === "NODE_ENV" || key === "HEAD") {
1594
+ try {
1595
+ const value = env[key];
1596
+ if (typeof value === "string") {
1597
+ publicEnv[key] = value;
1598
+ } else {
1599
+ publicEnv[key] = JSON.stringify(value);
1600
+ }
1601
+ } catch (error) {
1602
+ console.warn(
1603
+ `Could not stringify public env process.env.${key} env variable`
1604
+ );
1605
+ console.warn(error);
1606
+ }
1607
+ }
1608
+ });
1609
+ return publicEnv;
1610
+ }
1611
+
1560
1612
  // src/next/vite/tailwind.ts
1561
1613
  import path4 from "node:path";
1562
1614
  import aspectRatio from "@tailwindcss/aspect-ratio";
@@ -1889,23 +1941,7 @@ var createConfig = async ({
1889
1941
  noWatch,
1890
1942
  rollupOptions
1891
1943
  }) => {
1892
- const publicEnv = {};
1893
- Object.keys(process.env).forEach((key) => {
1894
- if (key.startsWith("TINA_PUBLIC_") || key.startsWith("NEXT_PUBLIC_") || key === "NODE_ENV" || key === "HEAD") {
1895
- try {
1896
- if (typeof process.env[key] === "string") {
1897
- publicEnv[key] = process.env[key];
1898
- } else {
1899
- publicEnv[key] = JSON.stringify(process.env[key]);
1900
- }
1901
- } catch (error) {
1902
- console.warn(
1903
- `Could not stringify public env process.env.${key} env variable`
1904
- );
1905
- console.warn(error);
1906
- }
1907
- }
1908
- });
1944
+ const publicEnv = filterPublicEnv();
1909
1945
  const staticMediaPath = path5.join(
1910
1946
  configManager.generatedFolderPath,
1911
1947
  "static-media.json"
@@ -2035,9 +2071,9 @@ import cors from "cors";
2035
2071
  import { resolve as gqlResolve } from "@tinacms/graphql";
2036
2072
 
2037
2073
  // src/next/commands/dev-command/server/media.ts
2038
- import fs5 from "fs-extra";
2039
2074
  import path6, { join } from "path";
2040
2075
  import busboy from "busboy";
2076
+ import fs5 from "fs-extra";
2041
2077
  var createMediaRouter = (config2) => {
2042
2078
  const mediaFolder = path6.join(
2043
2079
  config2.rootPath,
@@ -2046,31 +2082,68 @@ var createMediaRouter = (config2) => {
2046
2082
  );
2047
2083
  const mediaModel = new MediaModel(config2);
2048
2084
  const handleList = async (req, res) => {
2049
- const requestURL = new URL(req.url, config2.apiURL);
2050
- const folder = requestURL.pathname.replace("/media/list/", "");
2051
- const limit = requestURL.searchParams.get("limit");
2052
- const cursor = requestURL.searchParams.get("cursor");
2053
- const media = await mediaModel.listMedia({
2054
- searchPath: folder,
2055
- cursor,
2056
- limit
2057
- });
2058
- res.end(JSON.stringify(media));
2085
+ try {
2086
+ const requestURL = new URL(req.url, config2.apiURL);
2087
+ const folder = decodeURIComponent(
2088
+ requestURL.pathname.replace("/media/list/", "")
2089
+ );
2090
+ const limit = requestURL.searchParams.get("limit");
2091
+ const cursor = requestURL.searchParams.get("cursor");
2092
+ const media = await mediaModel.listMedia({
2093
+ searchPath: folder,
2094
+ cursor,
2095
+ limit
2096
+ });
2097
+ res.end(JSON.stringify(media));
2098
+ } catch (error) {
2099
+ if (error instanceof PathTraversalError) {
2100
+ res.statusCode = 403;
2101
+ res.end(JSON.stringify({ error: error.message }));
2102
+ return;
2103
+ }
2104
+ throw error;
2105
+ }
2059
2106
  };
2060
2107
  const handleDelete = async (req, res) => {
2061
- const file = decodeURIComponent(req.url.slice("/media/".length));
2062
- const didDelete = await mediaModel.deleteMedia({ searchPath: file });
2063
- res.end(JSON.stringify(didDelete));
2108
+ try {
2109
+ const file = decodeURIComponent(req.url.slice("/media/".length));
2110
+ const didDelete = await mediaModel.deleteMedia({ searchPath: file });
2111
+ res.end(JSON.stringify(didDelete));
2112
+ } catch (error) {
2113
+ if (error instanceof PathTraversalError) {
2114
+ res.statusCode = 403;
2115
+ res.end(JSON.stringify({ error: error.message }));
2116
+ return;
2117
+ }
2118
+ throw error;
2119
+ }
2064
2120
  };
2065
2121
  const handlePost = async function(req, res) {
2066
2122
  const bb = busboy({ headers: req.headers });
2123
+ let responded = false;
2067
2124
  bb.on("file", async (_name, file, _info) => {
2068
- const fullPath = decodeURI(req.url?.slice("/media/upload/".length));
2069
- const saveTo = path6.join(mediaFolder, ...fullPath.split("/"));
2125
+ const fullPath = decodeURIComponent(
2126
+ req.url?.slice("/media/upload/".length)
2127
+ );
2128
+ let saveTo;
2129
+ try {
2130
+ saveTo = resolveStrictlyWithinBase(fullPath, mediaFolder);
2131
+ } catch {
2132
+ responded = true;
2133
+ file.resume();
2134
+ res.statusCode = 403;
2135
+ res.end(
2136
+ JSON.stringify({
2137
+ error: `Path traversal detected: ${fullPath}`
2138
+ })
2139
+ );
2140
+ return;
2141
+ }
2070
2142
  await fs5.ensureDir(path6.dirname(saveTo));
2071
2143
  file.pipe(fs5.createWriteStream(saveTo));
2072
2144
  });
2073
2145
  bb.on("error", (error) => {
2146
+ responded = true;
2074
2147
  res.statusCode = 500;
2075
2148
  if (error instanceof Error) {
2076
2149
  res.end(JSON.stringify({ message: error }));
@@ -2079,6 +2152,7 @@ var createMediaRouter = (config2) => {
2079
2152
  }
2080
2153
  });
2081
2154
  bb.on("close", () => {
2155
+ if (responded) return;
2082
2156
  res.statusCode = 200;
2083
2157
  res.end(JSON.stringify({ success: true }));
2084
2158
  });
@@ -2093,6 +2167,32 @@ var parseMediaFolder = (str) => {
2093
2167
  returnString = returnString.substr(0, returnString.length - 1);
2094
2168
  return returnString;
2095
2169
  };
2170
+ var ENCODED_TRAVERSAL_RE = /%2e%2e|%2f|%5c/i;
2171
+ function resolveWithinBase(userPath, baseDir) {
2172
+ if (ENCODED_TRAVERSAL_RE.test(userPath)) {
2173
+ throw new PathTraversalError(userPath);
2174
+ }
2175
+ const resolvedBase = path6.resolve(baseDir);
2176
+ const resolved = path6.resolve(path6.join(baseDir, userPath));
2177
+ if (resolved === resolvedBase) {
2178
+ return resolvedBase;
2179
+ }
2180
+ if (resolved.startsWith(resolvedBase + path6.sep)) {
2181
+ return resolved;
2182
+ }
2183
+ throw new PathTraversalError(userPath);
2184
+ }
2185
+ function resolveStrictlyWithinBase(userPath, baseDir) {
2186
+ if (ENCODED_TRAVERSAL_RE.test(userPath)) {
2187
+ throw new PathTraversalError(userPath);
2188
+ }
2189
+ const resolvedBase = path6.resolve(baseDir) + path6.sep;
2190
+ const resolved = path6.resolve(path6.join(baseDir, userPath));
2191
+ if (!resolved.startsWith(resolvedBase)) {
2192
+ throw new PathTraversalError(userPath);
2193
+ }
2194
+ return resolved;
2195
+ }
2096
2196
  var MediaModel = class {
2097
2197
  rootPath;
2098
2198
  publicFolder;
@@ -2104,22 +2204,18 @@ var MediaModel = class {
2104
2204
  }
2105
2205
  async listMedia(args) {
2106
2206
  try {
2107
- const folderPath = join(
2108
- this.rootPath,
2109
- this.publicFolder,
2110
- this.mediaRoot,
2111
- decodeURIComponent(args.searchPath)
2112
- );
2207
+ const mediaBase = join(this.rootPath, this.publicFolder, this.mediaRoot);
2208
+ const validatedPath = resolveWithinBase(args.searchPath, mediaBase);
2113
2209
  const searchPath = parseMediaFolder(args.searchPath);
2114
- if (!await fs5.pathExists(folderPath)) {
2210
+ if (!await fs5.pathExists(validatedPath)) {
2115
2211
  return {
2116
2212
  files: [],
2117
2213
  directories: []
2118
2214
  };
2119
2215
  }
2120
- const filesStr = await fs5.readdir(folderPath);
2216
+ const filesStr = await fs5.readdir(validatedPath);
2121
2217
  const filesProm = filesStr.map(async (file) => {
2122
- const filePath = join(folderPath, file);
2218
+ const filePath = join(validatedPath, file);
2123
2219
  const stat = await fs5.stat(filePath);
2124
2220
  let src = `/${file}`;
2125
2221
  const isFile = stat.isFile();
@@ -2166,6 +2262,7 @@ var MediaModel = class {
2166
2262
  cursor
2167
2263
  };
2168
2264
  } catch (error) {
2265
+ if (error instanceof PathTraversalError) throw error;
2169
2266
  console.error(error);
2170
2267
  return {
2171
2268
  files: [],
@@ -2176,16 +2273,13 @@ var MediaModel = class {
2176
2273
  }
2177
2274
  async deleteMedia(args) {
2178
2275
  try {
2179
- const file = join(
2180
- this.rootPath,
2181
- this.publicFolder,
2182
- this.mediaRoot,
2183
- decodeURIComponent(args.searchPath)
2184
- );
2276
+ const mediaBase = join(this.rootPath, this.publicFolder, this.mediaRoot);
2277
+ const file = resolveStrictlyWithinBase(args.searchPath, mediaBase);
2185
2278
  await fs5.stat(file);
2186
2279
  await fs5.remove(file);
2187
2280
  return { ok: true };
2188
2281
  } catch (error) {
2282
+ if (error instanceof PathTraversalError) throw error;
2189
2283
  console.error(error);
2190
2284
  return { ok: false, message: error?.toString() };
2191
2285
  }
@@ -2555,7 +2649,8 @@ var DevCommand = class extends BaseCommand {
2555
2649
  );
2556
2650
  if (configManager.hasSeparateContentRoot()) {
2557
2651
  const rootPath = await configManager.getTinaFolderPath(
2558
- configManager.contentRootPath
2652
+ configManager.contentRootPath,
2653
+ { isContentRoot: true }
2559
2654
  );
2560
2655
  const filePath = path8.join(rootPath, tinaLockFilename);
2561
2656
  await fs7.ensureFile(filePath);
@@ -2731,6 +2826,13 @@ ${dangerText(e.message)}
2731
2826
  // },
2732
2827
  ]
2733
2828
  });
2829
+ if (configManager?.config?.telemetry === "anonymous") {
2830
+ logger.info(
2831
+ `
2832
+ \u{1F4CA} Note: TinaCMS now collects anonymous telemetry regarding usage. More information on TinaCMS Telemetry: https://tina.io/telemetry
2833
+ `
2834
+ );
2835
+ }
2734
2836
  await this.startSubCommand();
2735
2837
  }
2736
2838
  watchContentFiles(configManager, database, databaseLock, searchIndexer) {
@@ -2831,23 +2933,6 @@ async function sleepAndCallFunc({
2831
2933
  // src/next/commands/build-command/server.ts
2832
2934
  import { build as build2 } from "vite";
2833
2935
  var buildProductionSpa = async (configManager, database, apiURL) => {
2834
- const publicEnv = {};
2835
- Object.keys(process.env).forEach((key) => {
2836
- if (key.startsWith("TINA_PUBLIC_") || key.startsWith("NEXT_PUBLIC_") || key === "NODE_ENV" || key === "HEAD") {
2837
- try {
2838
- if (typeof process.env[key] === "string") {
2839
- publicEnv[key] = process.env[key];
2840
- } else {
2841
- publicEnv[key] = JSON.stringify(process.env[key]);
2842
- }
2843
- } catch (error) {
2844
- console.warn(
2845
- `Could not stringify public env process.env.${key} env variable`
2846
- );
2847
- console.warn(error);
2848
- }
2849
- }
2850
- });
2851
2936
  const config2 = await createConfig({
2852
2937
  plugins: [transformTsxPlugin({ configManager }), viteTransformExtension()],
2853
2938
  configManager,
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Strips `indexerToken` from `search.tina` before serialization to
3
+ * _schema.json / tina-lock.json.
4
+ *
5
+ * @see https://github.com/tinacms/tinacms/security/advisories/GHSA-4qrm-9h4r-v2fx
6
+ */
7
+ export declare function stripSearchTokenFromConfig<T extends object>(config: T): T;
@@ -1,5 +1,5 @@
1
- import type { Connect } from 'vite';
2
1
  import type { ServerResponse } from 'http';
2
+ import type { Connect } from 'vite';
3
3
  export declare const createMediaRouter: (config: PathConfig) => {
4
4
  handleList: (req: any, res: any) => Promise<void>;
5
5
  handleDelete: (req: Connect.IncomingMessage, res: any) => Promise<void>;
@@ -34,6 +34,23 @@ type SuccessRecord = {
34
34
  ok: false;
35
35
  message: string;
36
36
  };
37
+ /**
38
+ * Handles media file operations (list, delete) for the Vite-based dev server.
39
+ *
40
+ * @security Every method that accepts a user-supplied `searchPath` validates
41
+ * it against the media root using `resolveWithinBase` (list) or
42
+ * `resolveStrictlyWithinBase` (delete) before any filesystem access.
43
+ *
44
+ * - **list** uses `resolveWithinBase` because listing the media root itself
45
+ * (empty path / exact base match) is a valid operation.
46
+ * - **delete** uses `resolveStrictlyWithinBase` because deleting the media
47
+ * root directory itself must never be allowed.
48
+ *
49
+ * Both methods catch `PathTraversalError` and re-throw it so that the
50
+ * route handler can return a 403 response. Other errors are caught and
51
+ * returned as structured error responses (this avoids leaking stack traces
52
+ * to the client).
53
+ */
37
54
  export declare class MediaModel {
38
55
  readonly rootPath: string;
39
56
  readonly publicFolder: string;
@@ -54,7 +54,9 @@ export declare class ConfigManager {
54
54
  hasSeparateContentRoot(): boolean;
55
55
  shouldSkipSDK(): boolean;
56
56
  processConfig(): Promise<void>;
57
- getTinaFolderPath(rootPath: any): Promise<string>;
57
+ getTinaFolderPath(rootPath: string, { isContentRoot }?: {
58
+ isContentRoot?: boolean;
59
+ }): Promise<string>;
58
60
  getTinaGraphQLVersion(): {
59
61
  fullVersion: string;
60
62
  major: string;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Filters env vars to only those safe for client-side bundles.
3
+ *
4
+ * Allows: TINA_PUBLIC_*, NEXT_PUBLIC_*, NODE_ENV, HEAD.
5
+ * Everything else is excluded to prevent leaking secrets.
6
+ *
7
+ * @see https://github.com/tinacms/tinacms/security/advisories/GHSA-pc2q-jcxq-rjrr
8
+ */
9
+ export declare function filterPublicEnv(env?: Record<string, string | undefined>): Record<string, string>;
@@ -28,6 +28,23 @@ type SuccessRecord = {
28
28
  ok: false;
29
29
  message: string;
30
30
  };
31
+ /**
32
+ * Handles media file operations (list, delete) for the Express-based server.
33
+ *
34
+ * @security Every method that accepts a user-supplied `searchPath` validates
35
+ * it against the media root using `resolveWithinBase` (list) or
36
+ * `resolveStrictlyWithinBase` (delete) before any filesystem access.
37
+ *
38
+ * - **list** uses `resolveWithinBase` because listing the media root itself
39
+ * (empty path / exact base match) is a valid operation.
40
+ * - **delete** uses `resolveStrictlyWithinBase` because deleting the media
41
+ * root directory itself must never be allowed.
42
+ *
43
+ * Both methods catch `PathTraversalError` and re-throw it so that the
44
+ * route handler can return a 403 response. Other errors are caught and
45
+ * returned as structured error responses (this avoids leaking stack traces
46
+ * to the client).
47
+ */
31
48
  export declare class MediaModel {
32
49
  readonly rootPath: string;
33
50
  readonly publicFolder: string;
@@ -1,3 +1,29 @@
1
1
  /** Removes trailing slash from path. Separator to remove is chosen based on
2
2
  * operating system. */
3
3
  export declare function stripNativeTrailingSlash(p: string): string;
4
+ /**
5
+ * Validates that a user-supplied path does not escape the base directory
6
+ * via path traversal (CWE-22). Returns the resolved absolute path.
7
+ *
8
+ * Allows an exact base match (empty or `.` input) — use this for list/read
9
+ * operations where referencing the root directory itself is valid. For
10
+ * delete/write operations where you need to target an actual file, use the
11
+ * `resolveStrictlyWithinBase` variant (currently inlined in media models).
12
+ *
13
+ * As a safety net, also rejects paths that still contain URL-encoded
14
+ * traversal sequences (`%2e%2e`, `%2f`, `%5c`), catching cases where the
15
+ * caller forgot to decode.
16
+ *
17
+ * @security This is the canonical implementation. Inlined copies exist in
18
+ * the media model files for CodeQL compatibility — keep them in sync.
19
+ *
20
+ * @param userPath - The untrusted path from the request (must already be
21
+ * URI-decoded by the caller).
22
+ * @param baseDir - The trusted base directory the path must stay within.
23
+ * @returns The resolved absolute path.
24
+ * @throws {PathTraversalError} If the path escapes the base directory.
25
+ */
26
+ export declare function assertPathWithinBase(userPath: string, baseDir: string): string;
27
+ export declare class PathTraversalError extends Error {
28
+ constructor(attemptedPath: string);
29
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tinacms/cli",
3
3
  "type": "module",
4
- "version": "2.1.5",
4
+ "version": "2.1.7",
5
5
  "main": "dist/index.js",
6
6
  "typings": "dist/index.d.ts",
7
7
  "files": [
@@ -41,7 +41,7 @@
41
41
  "@types/progress": "^2.0.7",
42
42
  "@types/prompts": "^2.4.9",
43
43
  "jest": "^29.7.0",
44
- "@tinacms/scripts": "1.4.2"
44
+ "@tinacms/scripts": "1.5.0"
45
45
  },
46
46
  "dependencies": {
47
47
  "@graphql-codegen/core": "^2.6.8",
@@ -88,12 +88,12 @@
88
88
  "vite": "^4.5.9",
89
89
  "yup": "^1.6.1",
90
90
  "zod": "^3.24.2",
91
- "@tinacms/app": "2.3.24",
92
- "@tinacms/graphql": "2.1.1",
91
+ "@tinacms/app": "2.3.26",
92
+ "@tinacms/graphql": "2.1.3",
93
93
  "@tinacms/metrics": "2.0.1",
94
- "@tinacms/schema-tools": "2.5.0",
95
- "@tinacms/search": "1.2.2",
96
- "tinacms": "3.4.1"
94
+ "@tinacms/schema-tools": "2.6.0",
95
+ "@tinacms/search": "1.2.4",
96
+ "tinacms": "3.5.1"
97
97
  },
98
98
  "publishConfig": {
99
99
  "registry": "https://registry.npmjs.org"