@tinacms/graphql 2.1.1 → 2.1.3
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;
|
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,
|
|
49
|
-
const travel = (regexp) => String.prototype.split.call(
|
|
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.
|
|
3029
|
+
version: "2.1.3",
|
|
3030
3030
|
main: "dist/index.js",
|
|
3031
3031
|
module: "./dist/index.js",
|
|
3032
3032
|
files: [
|
|
@@ -3662,17 +3662,17 @@ var scanAllContent = async (tinaSchema, bridge, callback) => {
|
|
|
3662
3662
|
const documentPaths = await bridge.glob(normalPath, format);
|
|
3663
3663
|
const matches = tinaSchema.getMatches({ collection });
|
|
3664
3664
|
const filteredPaths = matches.length > 0 ? micromatch(documentPaths, matches) : documentPaths;
|
|
3665
|
-
filteredPaths.forEach((
|
|
3666
|
-
if (filesSeen.has(
|
|
3667
|
-
filesSeen.get(
|
|
3668
|
-
duplicateFiles.add(
|
|
3665
|
+
filteredPaths.forEach((path9) => {
|
|
3666
|
+
if (filesSeen.has(path9)) {
|
|
3667
|
+
filesSeen.get(path9).push(collection.name);
|
|
3668
|
+
duplicateFiles.add(path9);
|
|
3669
3669
|
} else {
|
|
3670
|
-
filesSeen.set(
|
|
3670
|
+
filesSeen.set(path9, [collection.name]);
|
|
3671
3671
|
}
|
|
3672
3672
|
});
|
|
3673
|
-
duplicateFiles.forEach((
|
|
3673
|
+
duplicateFiles.forEach((path9) => {
|
|
3674
3674
|
warnings.push(
|
|
3675
|
-
`"${
|
|
3675
|
+
`"${path9}" Found in multiple collections: ${filesSeen.get(path9).map((collection2) => `"${collection2}"`).join(
|
|
3676
3676
|
", "
|
|
3677
3677
|
)}. This can cause unexpected behavior. We recommend updating the \`match\` property of those collections so that each file is in only one collection.
|
|
3678
3678
|
This will be an error in the future. See https://tina.io/docs/errors/file-in-mutpliple-collections/
|
|
@@ -4235,9 +4235,9 @@ var makeFilterSuffixes = (filterChain, index) => {
|
|
|
4235
4235
|
}
|
|
4236
4236
|
};
|
|
4237
4237
|
var FOLDER_ROOT = "~";
|
|
4238
|
-
var stripCollectionFromPath = (collectionPath,
|
|
4238
|
+
var stripCollectionFromPath = (collectionPath, path9) => {
|
|
4239
4239
|
const collectionPathParts = collectionPath.split("/");
|
|
4240
|
-
const pathParts =
|
|
4240
|
+
const pathParts = path9.split("/");
|
|
4241
4241
|
const strippedPathParts = pathParts.slice(collectionPathParts.length);
|
|
4242
4242
|
return strippedPathParts.join("/");
|
|
4243
4243
|
};
|
|
@@ -4293,13 +4293,13 @@ var makeFolderOpsForCollection = (folderTree, collection, indexDefinitions, opTy
|
|
|
4293
4293
|
SUBLEVEL_OPTIONS
|
|
4294
4294
|
);
|
|
4295
4295
|
let folderSortingIdx = 0;
|
|
4296
|
-
for (const
|
|
4296
|
+
for (const path9 of Array.from(folder).sort()) {
|
|
4297
4297
|
for (const [sort] of Object.entries(indexDefinitions)) {
|
|
4298
4298
|
const indexSublevel = folderCollectionSublevel.sublevel(
|
|
4299
4299
|
sort,
|
|
4300
4300
|
SUBLEVEL_OPTIONS
|
|
4301
4301
|
);
|
|
4302
|
-
const subFolderKey = sha.hex(
|
|
4302
|
+
const subFolderKey = sha.hex(path9);
|
|
4303
4303
|
if (sort === DEFAULT_COLLECTION_SORT_KEY) {
|
|
4304
4304
|
result.push({
|
|
4305
4305
|
type: opType,
|
|
@@ -4381,8 +4381,8 @@ var makeRefOpsForDocument = (filepath, collection, references, data, opType, lev
|
|
|
4381
4381
|
SUBLEVEL_OPTIONS
|
|
4382
4382
|
);
|
|
4383
4383
|
const references2 = {};
|
|
4384
|
-
for (const
|
|
4385
|
-
const ref = JSONPath({ path:
|
|
4384
|
+
for (const path9 of referencePaths) {
|
|
4385
|
+
const ref = JSONPath({ path: path9, json: data });
|
|
4386
4386
|
if (!ref) {
|
|
4387
4387
|
continue;
|
|
4388
4388
|
}
|
|
@@ -4392,24 +4392,24 @@ var makeRefOpsForDocument = (filepath, collection, references, data, opType, lev
|
|
|
4392
4392
|
continue;
|
|
4393
4393
|
}
|
|
4394
4394
|
if (references2[r]) {
|
|
4395
|
-
references2[r].push(
|
|
4395
|
+
references2[r].push(path9);
|
|
4396
4396
|
} else {
|
|
4397
|
-
references2[r] = [
|
|
4397
|
+
references2[r] = [path9];
|
|
4398
4398
|
}
|
|
4399
4399
|
}
|
|
4400
4400
|
} else {
|
|
4401
4401
|
if (references2[ref]) {
|
|
4402
|
-
references2[ref].push(
|
|
4402
|
+
references2[ref].push(path9);
|
|
4403
4403
|
} else {
|
|
4404
|
-
references2[ref] = [
|
|
4404
|
+
references2[ref] = [path9];
|
|
4405
4405
|
}
|
|
4406
4406
|
}
|
|
4407
4407
|
}
|
|
4408
4408
|
for (const ref of Object.keys(references2)) {
|
|
4409
|
-
for (const
|
|
4409
|
+
for (const path9 of references2[ref]) {
|
|
4410
4410
|
result.push({
|
|
4411
4411
|
type: opType,
|
|
4412
|
-
key: `${ref}${INDEX_KEY_FIELD_SEPARATOR}${
|
|
4412
|
+
key: `${ref}${INDEX_KEY_FIELD_SEPARATOR}${path9}${INDEX_KEY_FIELD_SEPARATOR}${filepath}`,
|
|
4413
4413
|
sublevel: refSublevel,
|
|
4414
4414
|
value: opType === "put" ? {} : void 0
|
|
4415
4415
|
});
|
|
@@ -4676,9 +4676,9 @@ var resolveMediaRelativeToCloud = (value, config = { useRelativeMedia: true }, s
|
|
|
4676
4676
|
return value;
|
|
4677
4677
|
}
|
|
4678
4678
|
};
|
|
4679
|
-
var cleanUpSlashes = (
|
|
4680
|
-
if (
|
|
4681
|
-
return `/${
|
|
4679
|
+
var cleanUpSlashes = (path9) => {
|
|
4680
|
+
if (path9) {
|
|
4681
|
+
return `/${path9.replace(/^\/+|\/+$/gm, "")}`;
|
|
4682
4682
|
}
|
|
4683
4683
|
return "";
|
|
4684
4684
|
};
|
|
@@ -4888,17 +4888,17 @@ var transformDocumentIntoPayload = async (fullPath, rawData, tinaSchema, config,
|
|
|
4888
4888
|
throw e;
|
|
4889
4889
|
}
|
|
4890
4890
|
};
|
|
4891
|
-
var updateObjectWithJsonPath = (obj,
|
|
4891
|
+
var updateObjectWithJsonPath = (obj, path9, oldValue, newValue) => {
|
|
4892
4892
|
let updated = false;
|
|
4893
|
-
if (!
|
|
4894
|
-
if (
|
|
4895
|
-
obj[
|
|
4893
|
+
if (!path9.includes(".") && !path9.includes("[")) {
|
|
4894
|
+
if (path9 in obj && obj[path9] === oldValue) {
|
|
4895
|
+
obj[path9] = newValue;
|
|
4896
4896
|
updated = true;
|
|
4897
4897
|
}
|
|
4898
4898
|
return { object: obj, updated };
|
|
4899
4899
|
}
|
|
4900
|
-
const parentPath =
|
|
4901
|
-
const keyToUpdate =
|
|
4900
|
+
const parentPath = path9.replace(/\.[^.\[\]]+$/, "");
|
|
4901
|
+
const keyToUpdate = path9.match(/[^.\[\]]+$/)[0];
|
|
4902
4902
|
const parents = JSONPath2({
|
|
4903
4903
|
path: parentPath,
|
|
4904
4904
|
json: obj,
|
|
@@ -5100,8 +5100,10 @@ var Resolver = class {
|
|
|
5100
5100
|
relativePath,
|
|
5101
5101
|
templateName
|
|
5102
5102
|
}) => {
|
|
5103
|
-
const collection = this.
|
|
5104
|
-
|
|
5103
|
+
const { collection, realPath } = this.getValidatedPath(
|
|
5104
|
+
collectionName,
|
|
5105
|
+
relativePath
|
|
5106
|
+
);
|
|
5105
5107
|
const alreadyExists = await this.database.documentExists(realPath);
|
|
5106
5108
|
if (alreadyExists) {
|
|
5107
5109
|
throw new Error(`Unable to add document, ${realPath} already exists`);
|
|
@@ -5178,6 +5180,58 @@ var Resolver = class {
|
|
|
5178
5180
|
}
|
|
5179
5181
|
return this.tinaSchema.getCollection(collectionName);
|
|
5180
5182
|
};
|
|
5183
|
+
/**
|
|
5184
|
+
* validatePath ensures that the provided path remains within the boundaries
|
|
5185
|
+
* of the collection's directory and that the file extension matches the
|
|
5186
|
+
* collection's configured format. This is a critical security check to prevent
|
|
5187
|
+
* path traversal attacks where a user might attempt to read or write files
|
|
5188
|
+
* outside of the intended collection.
|
|
5189
|
+
*/
|
|
5190
|
+
validatePath = (fullPath, collection, relativePath) => {
|
|
5191
|
+
const normalizedPath = path3.normalize(fullPath);
|
|
5192
|
+
const normalizedCollectionPath = path3.normalize(collection.path);
|
|
5193
|
+
const relative = path3.relative(normalizedCollectionPath, normalizedPath);
|
|
5194
|
+
if (relative.startsWith("..")) {
|
|
5195
|
+
throw new Error(`Invalid path: path escapes the collection directory`);
|
|
5196
|
+
}
|
|
5197
|
+
if (path3.isAbsolute(relative)) {
|
|
5198
|
+
throw new Error(`Invalid path: absolute paths are not allowed`);
|
|
5199
|
+
}
|
|
5200
|
+
if (relativePath) {
|
|
5201
|
+
const collectionFormat = collection.format || "md";
|
|
5202
|
+
const fileExtension = path3.extname(relativePath).toLowerCase().slice(1);
|
|
5203
|
+
if (fileExtension !== collectionFormat) {
|
|
5204
|
+
throw new Error(
|
|
5205
|
+
`Invalid file extension: expected '.${collectionFormat}' but got '.${fileExtension}'`
|
|
5206
|
+
);
|
|
5207
|
+
}
|
|
5208
|
+
}
|
|
5209
|
+
};
|
|
5210
|
+
/**
|
|
5211
|
+
* Helper method to get collection and construct validated path.
|
|
5212
|
+
* This encapsulates the common pattern of getting a collection, joining paths,
|
|
5213
|
+
* and validating the result, ensuring security checks are always performed.
|
|
5214
|
+
*
|
|
5215
|
+
* @param collectionName - Name of the collection
|
|
5216
|
+
* @param relativePath - Relative path within the collection
|
|
5217
|
+
* @param options - Optional configuration
|
|
5218
|
+
* @returns Object containing the collection and validated real path
|
|
5219
|
+
*/
|
|
5220
|
+
getValidatedPath = (collectionName, relativePath, options) => {
|
|
5221
|
+
const collection = this.getCollectionWithName(collectionName);
|
|
5222
|
+
const pathSegments = [collection.path, relativePath];
|
|
5223
|
+
if (options?.extraSegments) {
|
|
5224
|
+
pathSegments.push(...options.extraSegments);
|
|
5225
|
+
}
|
|
5226
|
+
const realPath = path3.join(...pathSegments);
|
|
5227
|
+
const shouldValidateExtension = options?.validateExtension !== false;
|
|
5228
|
+
this.validatePath(
|
|
5229
|
+
realPath,
|
|
5230
|
+
collection,
|
|
5231
|
+
shouldValidateExtension ? relativePath : void 0
|
|
5232
|
+
);
|
|
5233
|
+
return { collection, realPath };
|
|
5234
|
+
};
|
|
5181
5235
|
/*
|
|
5182
5236
|
* Used for getDocument, get<Collection>Document.
|
|
5183
5237
|
*/
|
|
@@ -5185,8 +5239,10 @@ var Resolver = class {
|
|
|
5185
5239
|
collectionName,
|
|
5186
5240
|
relativePath
|
|
5187
5241
|
}) => {
|
|
5188
|
-
const collection = this.
|
|
5189
|
-
|
|
5242
|
+
const { collection, realPath } = this.getValidatedPath(
|
|
5243
|
+
collectionName,
|
|
5244
|
+
relativePath
|
|
5245
|
+
);
|
|
5190
5246
|
return this.getDocument(realPath, {
|
|
5191
5247
|
collection,
|
|
5192
5248
|
checkReferences: true
|
|
@@ -5205,6 +5261,7 @@ var Resolver = class {
|
|
|
5205
5261
|
relativePath,
|
|
5206
5262
|
`.gitkeep.${collection.format || "md"}`
|
|
5207
5263
|
);
|
|
5264
|
+
this.validatePath(realPath, collection);
|
|
5208
5265
|
const alreadyExists = await this.database.documentExists(realPath);
|
|
5209
5266
|
if (alreadyExists) {
|
|
5210
5267
|
throw new Error(`Unable to add folder, ${realPath} already exists`);
|
|
@@ -5221,8 +5278,10 @@ var Resolver = class {
|
|
|
5221
5278
|
relativePath,
|
|
5222
5279
|
body
|
|
5223
5280
|
}) => {
|
|
5224
|
-
const collection = this.
|
|
5225
|
-
|
|
5281
|
+
const { collection, realPath } = this.getValidatedPath(
|
|
5282
|
+
collectionName,
|
|
5283
|
+
relativePath
|
|
5284
|
+
);
|
|
5226
5285
|
const alreadyExists = await this.database.documentExists(realPath);
|
|
5227
5286
|
if (alreadyExists) {
|
|
5228
5287
|
throw new Error(`Unable to add document, ${realPath} already exists`);
|
|
@@ -5237,15 +5296,20 @@ var Resolver = class {
|
|
|
5237
5296
|
newRelativePath,
|
|
5238
5297
|
newBody
|
|
5239
5298
|
}) => {
|
|
5240
|
-
const collection = this.
|
|
5241
|
-
|
|
5299
|
+
const { collection, realPath } = this.getValidatedPath(
|
|
5300
|
+
collectionName,
|
|
5301
|
+
relativePath
|
|
5302
|
+
);
|
|
5242
5303
|
const alreadyExists = await this.database.documentExists(realPath);
|
|
5243
5304
|
if (!alreadyExists) {
|
|
5244
5305
|
throw new Error(`Unable to update document, ${realPath} does not exist`);
|
|
5245
5306
|
}
|
|
5246
5307
|
const doc = await this.getDocument(realPath);
|
|
5247
5308
|
if (newRelativePath) {
|
|
5248
|
-
const newRealPath =
|
|
5309
|
+
const { realPath: newRealPath } = this.getValidatedPath(
|
|
5310
|
+
collectionName,
|
|
5311
|
+
newRelativePath
|
|
5312
|
+
);
|
|
5249
5313
|
if (newRealPath === realPath) {
|
|
5250
5314
|
return doc;
|
|
5251
5315
|
}
|
|
@@ -5258,10 +5322,10 @@ var Resolver = class {
|
|
|
5258
5322
|
)) {
|
|
5259
5323
|
let docWithRef = await this.getRaw(pathToDocWithRef);
|
|
5260
5324
|
let hasUpdate = false;
|
|
5261
|
-
for (const
|
|
5325
|
+
for (const path9 of referencePaths) {
|
|
5262
5326
|
const { object: object2, updated } = updateObjectWithJsonPath(
|
|
5263
5327
|
docWithRef,
|
|
5264
|
-
|
|
5328
|
+
path9,
|
|
5265
5329
|
realPath,
|
|
5266
5330
|
newRealPath
|
|
5267
5331
|
);
|
|
@@ -5308,8 +5372,10 @@ var Resolver = class {
|
|
|
5308
5372
|
collectionName,
|
|
5309
5373
|
relativePath
|
|
5310
5374
|
}) => {
|
|
5311
|
-
const collection = this.
|
|
5312
|
-
|
|
5375
|
+
const { collection, realPath } = this.getValidatedPath(
|
|
5376
|
+
collectionName,
|
|
5377
|
+
relativePath
|
|
5378
|
+
);
|
|
5313
5379
|
const alreadyExists = await this.database.documentExists(realPath);
|
|
5314
5380
|
if (!alreadyExists) {
|
|
5315
5381
|
throw new Error(`Unable to delete document, ${realPath} does not exist`);
|
|
@@ -5324,10 +5390,10 @@ var Resolver = class {
|
|
|
5324
5390
|
)) {
|
|
5325
5391
|
let refDoc = await this.getRaw(pathToDocWithRef);
|
|
5326
5392
|
let hasUpdate = false;
|
|
5327
|
-
for (const
|
|
5393
|
+
for (const path9 of referencePaths) {
|
|
5328
5394
|
const { object: object2, updated } = updateObjectWithJsonPath(
|
|
5329
5395
|
refDoc,
|
|
5330
|
-
|
|
5396
|
+
path9,
|
|
5331
5397
|
realPath,
|
|
5332
5398
|
null
|
|
5333
5399
|
);
|
|
@@ -5528,7 +5594,10 @@ var Resolver = class {
|
|
|
5528
5594
|
params: yup3.object({ relativePath: yup3.string().required() }).required()
|
|
5529
5595
|
})
|
|
5530
5596
|
);
|
|
5531
|
-
const realPath =
|
|
5597
|
+
const realPath = this.getValidatedPath(
|
|
5598
|
+
collection.name,
|
|
5599
|
+
args.relativePath
|
|
5600
|
+
).realPath;
|
|
5532
5601
|
return this.updateResolveDocument({
|
|
5533
5602
|
collection,
|
|
5534
5603
|
realPath,
|
|
@@ -5548,7 +5617,10 @@ var Resolver = class {
|
|
|
5548
5617
|
});
|
|
5549
5618
|
}
|
|
5550
5619
|
} else {
|
|
5551
|
-
const realPath =
|
|
5620
|
+
const realPath = this.getValidatedPath(
|
|
5621
|
+
collection.name,
|
|
5622
|
+
args.relativePath
|
|
5623
|
+
).realPath;
|
|
5552
5624
|
return this.getDocument(realPath, {
|
|
5553
5625
|
collection,
|
|
5554
5626
|
checkReferences: true
|
|
@@ -5588,7 +5660,7 @@ var Resolver = class {
|
|
|
5588
5660
|
first: -1
|
|
5589
5661
|
},
|
|
5590
5662
|
collection: referencedCollection,
|
|
5591
|
-
hydrator: (
|
|
5663
|
+
hydrator: (path9) => path9
|
|
5592
5664
|
// just return the path
|
|
5593
5665
|
}
|
|
5594
5666
|
);
|
|
@@ -6029,8 +6101,8 @@ async function handleUpdatePassword({
|
|
|
6029
6101
|
// src/error.ts
|
|
6030
6102
|
import { GraphQLError as GraphQLError3 } from "graphql";
|
|
6031
6103
|
var NotFoundError = class extends GraphQLError3 {
|
|
6032
|
-
constructor(message, nodes, source, positions,
|
|
6033
|
-
super(message, nodes, source, positions,
|
|
6104
|
+
constructor(message, nodes, source, positions, path9, originalError, extensions) {
|
|
6105
|
+
super(message, nodes, source, positions, path9, originalError, extensions);
|
|
6034
6106
|
this.name = "NotFoundError";
|
|
6035
6107
|
}
|
|
6036
6108
|
};
|
|
@@ -7292,21 +7364,21 @@ var Database = class {
|
|
|
7292
7364
|
edges,
|
|
7293
7365
|
async ({
|
|
7294
7366
|
cursor,
|
|
7295
|
-
path:
|
|
7367
|
+
path: path9,
|
|
7296
7368
|
value
|
|
7297
7369
|
}) => {
|
|
7298
7370
|
try {
|
|
7299
|
-
const node = await hydrator(
|
|
7371
|
+
const node = await hydrator(path9, value);
|
|
7300
7372
|
return {
|
|
7301
7373
|
node,
|
|
7302
7374
|
cursor: btoa(cursor)
|
|
7303
7375
|
};
|
|
7304
7376
|
} catch (error) {
|
|
7305
7377
|
console.log(error);
|
|
7306
|
-
if (error instanceof Error && (!
|
|
7378
|
+
if (error instanceof Error && (!path9.includes(".tina/__generated__/_graphql.json") || !path9.includes("tina/__generated__/_graphql.json"))) {
|
|
7307
7379
|
throw new TinaQueryError({
|
|
7308
7380
|
originalError: error,
|
|
7309
|
-
file:
|
|
7381
|
+
file: path9,
|
|
7310
7382
|
collection: collection.name
|
|
7311
7383
|
});
|
|
7312
7384
|
}
|
|
@@ -7594,8 +7666,8 @@ var Database = class {
|
|
|
7594
7666
|
return { warnings };
|
|
7595
7667
|
};
|
|
7596
7668
|
};
|
|
7597
|
-
var hashPasswordVisitor = async (node,
|
|
7598
|
-
const passwordValuePath = [...
|
|
7669
|
+
var hashPasswordVisitor = async (node, path9) => {
|
|
7670
|
+
const passwordValuePath = [...path9, "value"];
|
|
7599
7671
|
const plaintextPassword = get(node, passwordValuePath);
|
|
7600
7672
|
if (plaintextPassword) {
|
|
7601
7673
|
set2(
|
|
@@ -7605,10 +7677,10 @@ var hashPasswordVisitor = async (node, path8) => {
|
|
|
7605
7677
|
);
|
|
7606
7678
|
}
|
|
7607
7679
|
};
|
|
7608
|
-
var visitNodes = async (node,
|
|
7609
|
-
const [currentLevel, ...remainingLevels] =
|
|
7680
|
+
var visitNodes = async (node, path9, callback) => {
|
|
7681
|
+
const [currentLevel, ...remainingLevels] = path9;
|
|
7610
7682
|
if (!remainingLevels?.length) {
|
|
7611
|
-
return callback(node,
|
|
7683
|
+
return callback(node, path9);
|
|
7612
7684
|
}
|
|
7613
7685
|
if (Array.isArray(node[currentLevel])) {
|
|
7614
7686
|
for (const item of node[currentLevel]) {
|
|
@@ -7937,10 +8009,20 @@ var shaExists = async ({
|
|
|
7937
8009
|
}) => git.readCommit({ fs: fs4, dir, oid: sha3 }).then(() => true).catch(() => false);
|
|
7938
8010
|
|
|
7939
8011
|
// src/database/bridge/filesystem.ts
|
|
7940
|
-
import fs2 from "fs-extra";
|
|
7941
|
-
import fg from "fast-glob";
|
|
7942
8012
|
import path7 from "path";
|
|
8013
|
+
import fg from "fast-glob";
|
|
8014
|
+
import fs2 from "fs-extra";
|
|
7943
8015
|
import normalize from "normalize-path";
|
|
8016
|
+
function assertWithinBase(filepath, baseDir) {
|
|
8017
|
+
const resolvedBase = path7.resolve(baseDir);
|
|
8018
|
+
const resolved = path7.resolve(path7.join(baseDir, filepath));
|
|
8019
|
+
if (resolved !== resolvedBase && !resolved.startsWith(resolvedBase + path7.sep)) {
|
|
8020
|
+
throw new Error(
|
|
8021
|
+
`Path traversal detected: "${filepath}" escapes the base directory`
|
|
8022
|
+
);
|
|
8023
|
+
}
|
|
8024
|
+
return resolved;
|
|
8025
|
+
}
|
|
7944
8026
|
var FilesystemBridge = class {
|
|
7945
8027
|
rootPath;
|
|
7946
8028
|
outputPath;
|
|
@@ -7949,7 +8031,7 @@ var FilesystemBridge = class {
|
|
|
7949
8031
|
this.outputPath = outputPath ? path7.resolve(outputPath) : this.rootPath;
|
|
7950
8032
|
}
|
|
7951
8033
|
async glob(pattern, extension) {
|
|
7952
|
-
const basePath =
|
|
8034
|
+
const basePath = assertWithinBase(pattern, this.outputPath);
|
|
7953
8035
|
const items = await fg(
|
|
7954
8036
|
path7.join(basePath, "**", `/*.${extension}`).replace(/\\/g, "/"),
|
|
7955
8037
|
{
|
|
@@ -7963,14 +8045,17 @@ var FilesystemBridge = class {
|
|
|
7963
8045
|
);
|
|
7964
8046
|
}
|
|
7965
8047
|
async delete(filepath) {
|
|
7966
|
-
|
|
8048
|
+
const resolved = assertWithinBase(filepath, this.outputPath);
|
|
8049
|
+
await fs2.remove(resolved);
|
|
7967
8050
|
}
|
|
7968
8051
|
async get(filepath) {
|
|
7969
|
-
|
|
8052
|
+
const resolved = assertWithinBase(filepath, this.outputPath);
|
|
8053
|
+
return (await fs2.readFile(resolved)).toString();
|
|
7970
8054
|
}
|
|
7971
8055
|
async put(filepath, data, basePathOverride) {
|
|
7972
8056
|
const basePath = basePathOverride || this.outputPath;
|
|
7973
|
-
|
|
8057
|
+
const resolved = assertWithinBase(filepath, basePath);
|
|
8058
|
+
await fs2.outputFile(resolved, data);
|
|
7974
8059
|
}
|
|
7975
8060
|
};
|
|
7976
8061
|
var AuditFileSystemBridge = class extends FilesystemBridge {
|
|
@@ -7990,12 +8075,21 @@ var AuditFileSystemBridge = class extends FilesystemBridge {
|
|
|
7990
8075
|
};
|
|
7991
8076
|
|
|
7992
8077
|
// src/database/bridge/isomorphic.ts
|
|
7993
|
-
import
|
|
8078
|
+
import path8, { dirname } from "path";
|
|
7994
8079
|
import fs3 from "fs-extra";
|
|
7995
8080
|
import globParent from "glob-parent";
|
|
7996
|
-
import normalize2 from "normalize-path";
|
|
7997
8081
|
import { GraphQLError as GraphQLError6 } from "graphql";
|
|
7998
|
-
import
|
|
8082
|
+
import git2 from "isomorphic-git";
|
|
8083
|
+
import normalize2 from "normalize-path";
|
|
8084
|
+
function assertWithinBase2(filepath, relativePath) {
|
|
8085
|
+
const qualified = relativePath ? `${relativePath}/${filepath}` : filepath;
|
|
8086
|
+
const normalized = path8.normalize(qualified);
|
|
8087
|
+
if (normalized.startsWith("..") || normalized.startsWith("/") || path8.isAbsolute(normalized) || relativePath && normalized !== relativePath && !normalized.startsWith(relativePath + "/")) {
|
|
8088
|
+
throw new Error(
|
|
8089
|
+
`Path traversal detected: "${filepath}" escapes the content root`
|
|
8090
|
+
);
|
|
8091
|
+
}
|
|
8092
|
+
}
|
|
7999
8093
|
var flat = typeof Array.prototype.flat === "undefined" ? (entries) => entries.reduce((acc, x) => acc.concat(x), []) : (entries) => entries.flat();
|
|
8000
8094
|
var toUint8Array = (buf) => {
|
|
8001
8095
|
const ab = new ArrayBuffer(buf.length);
|
|
@@ -8074,7 +8168,7 @@ var IsomorphicBridge = class {
|
|
|
8074
8168
|
async listEntries({
|
|
8075
8169
|
pattern,
|
|
8076
8170
|
entry,
|
|
8077
|
-
path:
|
|
8171
|
+
path: path9,
|
|
8078
8172
|
results
|
|
8079
8173
|
}) {
|
|
8080
8174
|
const treeResult = await git2.readTree({
|
|
@@ -8084,7 +8178,7 @@ var IsomorphicBridge = class {
|
|
|
8084
8178
|
});
|
|
8085
8179
|
const children = [];
|
|
8086
8180
|
for (const childEntry of treeResult.tree) {
|
|
8087
|
-
const childPath =
|
|
8181
|
+
const childPath = path9 ? `${path9}/${childEntry.path}` : childEntry.path;
|
|
8088
8182
|
if (childEntry.type === "tree") {
|
|
8089
8183
|
children.push(childEntry);
|
|
8090
8184
|
} else {
|
|
@@ -8094,7 +8188,7 @@ var IsomorphicBridge = class {
|
|
|
8094
8188
|
}
|
|
8095
8189
|
}
|
|
8096
8190
|
for (const childEntry of children) {
|
|
8097
|
-
const childPath =
|
|
8191
|
+
const childPath = path9 ? `${path9}/${childEntry.path}` : childEntry.path;
|
|
8098
8192
|
await this.listEntries({
|
|
8099
8193
|
pattern,
|
|
8100
8194
|
entry: childEntry,
|
|
@@ -8112,17 +8206,17 @@ var IsomorphicBridge = class {
|
|
|
8112
8206
|
* @param ref - ref to resolve path entries for
|
|
8113
8207
|
* @private
|
|
8114
8208
|
*/
|
|
8115
|
-
async resolvePathEntries(
|
|
8116
|
-
let pathParts =
|
|
8209
|
+
async resolvePathEntries(path9, ref) {
|
|
8210
|
+
let pathParts = path9.split("/");
|
|
8117
8211
|
const result = await git2.walk({
|
|
8118
8212
|
...this.isomorphicConfig,
|
|
8119
8213
|
map: async (filepath, [head]) => {
|
|
8120
8214
|
if (head._fullpath === ".") {
|
|
8121
8215
|
return head;
|
|
8122
8216
|
}
|
|
8123
|
-
if (
|
|
8124
|
-
if (dirname(
|
|
8125
|
-
if (
|
|
8217
|
+
if (path9.startsWith(filepath)) {
|
|
8218
|
+
if (dirname(path9) === dirname(filepath)) {
|
|
8219
|
+
if (path9 === filepath) {
|
|
8126
8220
|
return head;
|
|
8127
8221
|
}
|
|
8128
8222
|
} else {
|
|
@@ -8153,7 +8247,7 @@ var IsomorphicBridge = class {
|
|
|
8153
8247
|
* @param pathParts - parent path parts
|
|
8154
8248
|
* @private
|
|
8155
8249
|
*/
|
|
8156
|
-
async updateTreeHierarchy(existingOid, updatedOid,
|
|
8250
|
+
async updateTreeHierarchy(existingOid, updatedOid, path9, type, pathEntries, pathParts) {
|
|
8157
8251
|
const lastIdx = pathEntries.length - 1;
|
|
8158
8252
|
const parentEntry = pathEntries[lastIdx];
|
|
8159
8253
|
const parentPath = pathParts[lastIdx];
|
|
@@ -8168,7 +8262,7 @@ var IsomorphicBridge = class {
|
|
|
8168
8262
|
cache: this.cache
|
|
8169
8263
|
});
|
|
8170
8264
|
tree = existingOid ? treeResult.tree.map((entry) => {
|
|
8171
|
-
if (entry.path ===
|
|
8265
|
+
if (entry.path === path9) {
|
|
8172
8266
|
entry.oid = updatedOid;
|
|
8173
8267
|
}
|
|
8174
8268
|
return entry;
|
|
@@ -8177,7 +8271,7 @@ var IsomorphicBridge = class {
|
|
|
8177
8271
|
{
|
|
8178
8272
|
oid: updatedOid,
|
|
8179
8273
|
type,
|
|
8180
|
-
path:
|
|
8274
|
+
path: path9,
|
|
8181
8275
|
mode
|
|
8182
8276
|
}
|
|
8183
8277
|
];
|
|
@@ -8186,7 +8280,7 @@ var IsomorphicBridge = class {
|
|
|
8186
8280
|
{
|
|
8187
8281
|
oid: updatedOid,
|
|
8188
8282
|
type,
|
|
8189
|
-
path:
|
|
8283
|
+
path: path9,
|
|
8190
8284
|
mode
|
|
8191
8285
|
}
|
|
8192
8286
|
];
|
|
@@ -8262,6 +8356,7 @@ var IsomorphicBridge = class {
|
|
|
8262
8356
|
return ref;
|
|
8263
8357
|
}
|
|
8264
8358
|
async glob(pattern, extension) {
|
|
8359
|
+
assertWithinBase2(pattern, this.relativePath);
|
|
8265
8360
|
const ref = await this.getRef();
|
|
8266
8361
|
const parent = globParent(this.qualifyPath(pattern));
|
|
8267
8362
|
const { pathParts, pathEntries } = await this.resolvePathEntries(
|
|
@@ -8295,9 +8390,10 @@ var IsomorphicBridge = class {
|
|
|
8295
8390
|
path: parentPath,
|
|
8296
8391
|
results
|
|
8297
8392
|
});
|
|
8298
|
-
return results.map((
|
|
8393
|
+
return results.map((path9) => this.unqualifyPath(path9)).filter((path9) => path9.endsWith(extension));
|
|
8299
8394
|
}
|
|
8300
8395
|
async delete(filepath) {
|
|
8396
|
+
assertWithinBase2(filepath, this.relativePath);
|
|
8301
8397
|
const ref = await this.getRef();
|
|
8302
8398
|
const { pathParts, pathEntries } = await this.resolvePathEntries(
|
|
8303
8399
|
this.qualifyPath(filepath),
|
|
@@ -8369,6 +8465,7 @@ var IsomorphicBridge = class {
|
|
|
8369
8465
|
return this.relativePath ? filepath.slice(this.relativePath.length + 1) : filepath;
|
|
8370
8466
|
}
|
|
8371
8467
|
async get(filepath) {
|
|
8468
|
+
assertWithinBase2(filepath, this.relativePath);
|
|
8372
8469
|
const ref = await this.getRef();
|
|
8373
8470
|
const oid = await git2.resolveRef({
|
|
8374
8471
|
...this.isomorphicConfig,
|
|
@@ -8383,6 +8480,7 @@ var IsomorphicBridge = class {
|
|
|
8383
8480
|
return Buffer.from(blob).toString("utf8");
|
|
8384
8481
|
}
|
|
8385
8482
|
async put(filepath, data) {
|
|
8483
|
+
assertWithinBase2(filepath, this.relativePath);
|
|
8386
8484
|
const ref = await this.getRef();
|
|
8387
8485
|
const { pathParts, pathEntries } = await this.resolvePathEntries(
|
|
8388
8486
|
this.qualifyPath(filepath),
|
package/dist/resolver/index.d.ts
CHANGED
|
@@ -228,6 +228,25 @@ export declare class Resolver {
|
|
|
228
228
|
*/
|
|
229
229
|
resolveLegacyValues: (oldDoc: any, collection: Collection<true>) => {};
|
|
230
230
|
private getCollectionWithName;
|
|
231
|
+
/**
|
|
232
|
+
* validatePath ensures that the provided path remains within the boundaries
|
|
233
|
+
* of the collection's directory and that the file extension matches the
|
|
234
|
+
* collection's configured format. This is a critical security check to prevent
|
|
235
|
+
* path traversal attacks where a user might attempt to read or write files
|
|
236
|
+
* outside of the intended collection.
|
|
237
|
+
*/
|
|
238
|
+
private validatePath;
|
|
239
|
+
/**
|
|
240
|
+
* Helper method to get collection and construct validated path.
|
|
241
|
+
* This encapsulates the common pattern of getting a collection, joining paths,
|
|
242
|
+
* and validating the result, ensuring security checks are always performed.
|
|
243
|
+
*
|
|
244
|
+
* @param collectionName - Name of the collection
|
|
245
|
+
* @param relativePath - Relative path within the collection
|
|
246
|
+
* @param options - Optional configuration
|
|
247
|
+
* @returns Object containing the collection and validated real path
|
|
248
|
+
*/
|
|
249
|
+
private getValidatedPath;
|
|
231
250
|
resolveRetrievedDocument: ({ collectionName, relativePath, }: {
|
|
232
251
|
collectionName: string;
|
|
233
252
|
relativePath: string;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tinacms/graphql",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "2.1.
|
|
4
|
+
"version": "2.1.3",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "./dist/index.js",
|
|
7
7
|
"files": [
|
|
@@ -37,14 +37,14 @@
|
|
|
37
37
|
"isomorphic-git": "^1.29.0",
|
|
38
38
|
"js-sha1": "^0.6.0",
|
|
39
39
|
"js-yaml": "^3.14.1",
|
|
40
|
-
"jsonpath-plus": "10.
|
|
40
|
+
"jsonpath-plus": "^10.3.0",
|
|
41
41
|
"many-level": "^2.0.0",
|
|
42
42
|
"micromatch": "4.0.8",
|
|
43
43
|
"normalize-path": "^3.0.0",
|
|
44
44
|
"readable-stream": "^4.7.0",
|
|
45
45
|
"yup": "^1.6.1",
|
|
46
|
-
"@tinacms/mdx": "2.0.
|
|
47
|
-
"@tinacms/schema-tools": "2.
|
|
46
|
+
"@tinacms/mdx": "2.0.6",
|
|
47
|
+
"@tinacms/schema-tools": "2.6.0"
|
|
48
48
|
},
|
|
49
49
|
"publishConfig": {
|
|
50
50
|
"registry": "https://registry.npmjs.org"
|
|
@@ -71,8 +71,8 @@
|
|
|
71
71
|
"vite": "^4.5.9",
|
|
72
72
|
"vitest": "^0.32.4",
|
|
73
73
|
"zod": "^3.24.2",
|
|
74
|
-
"@tinacms/scripts": "1.
|
|
75
|
-
"@tinacms/schema-tools": "2.
|
|
74
|
+
"@tinacms/scripts": "1.5.0",
|
|
75
|
+
"@tinacms/schema-tools": "2.6.0"
|
|
76
76
|
},
|
|
77
77
|
"scripts": {
|
|
78
78
|
"types": "pnpm tsc",
|