@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;
|
package/dist/database/index.d.ts
CHANGED
|
@@ -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,
|
|
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
|
);
|
|
@@ -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.
|
|
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((
|
|
3666
|
-
if (filesSeen.has(
|
|
3667
|
-
filesSeen.get(
|
|
3668
|
-
duplicateFiles.add(
|
|
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(
|
|
3679
|
+
filesSeen.set(path9, [collection.name]);
|
|
3671
3680
|
}
|
|
3672
3681
|
});
|
|
3673
|
-
duplicateFiles.forEach((
|
|
3682
|
+
duplicateFiles.forEach((path9) => {
|
|
3674
3683
|
warnings.push(
|
|
3675
|
-
`"${
|
|
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,
|
|
4247
|
+
var stripCollectionFromPath = (collectionPath, path9) => {
|
|
4239
4248
|
const collectionPathParts = collectionPath.split("/");
|
|
4240
|
-
const pathParts =
|
|
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
|
|
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(
|
|
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
|
|
4385
|
-
const ref = JSONPath({ path:
|
|
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(
|
|
4404
|
+
references2[r].push(path9);
|
|
4396
4405
|
} else {
|
|
4397
|
-
references2[r] = [
|
|
4406
|
+
references2[r] = [path9];
|
|
4398
4407
|
}
|
|
4399
4408
|
}
|
|
4400
4409
|
} else {
|
|
4401
4410
|
if (references2[ref]) {
|
|
4402
|
-
references2[ref].push(
|
|
4411
|
+
references2[ref].push(path9);
|
|
4403
4412
|
} else {
|
|
4404
|
-
references2[ref] = [
|
|
4413
|
+
references2[ref] = [path9];
|
|
4405
4414
|
}
|
|
4406
4415
|
}
|
|
4407
4416
|
}
|
|
4408
4417
|
for (const ref of Object.keys(references2)) {
|
|
4409
|
-
for (const
|
|
4418
|
+
for (const path9 of references2[ref]) {
|
|
4410
4419
|
result.push({
|
|
4411
4420
|
type: opType,
|
|
4412
|
-
key: `${ref}${INDEX_KEY_FIELD_SEPARATOR}${
|
|
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 = (
|
|
4680
|
-
if (
|
|
4681
|
-
return `/${
|
|
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,
|
|
4902
|
+
var updateObjectWithJsonPath = (obj, path9, oldValue, newValue) => {
|
|
4892
4903
|
let updated = false;
|
|
4893
|
-
if (!
|
|
4894
|
-
if (
|
|
4895
|
-
obj[
|
|
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 =
|
|
4901
|
-
const keyToUpdate =
|
|
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
|
-
|
|
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
|
|
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 ?
|
|
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
|
|
5346
|
+
for (const path9 of referencePaths) {
|
|
5326
5347
|
const { object: object2, updated } = updateObjectWithJsonPath(
|
|
5327
5348
|
docWithRef,
|
|
5328
|
-
|
|
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
|
|
5414
|
+
for (const path9 of referencePaths) {
|
|
5394
5415
|
const { object: object2, updated } = updateObjectWithJsonPath(
|
|
5395
5416
|
refDoc,
|
|
5396
|
-
|
|
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: (
|
|
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,
|
|
6105
|
-
super(message, nodes, source, positions,
|
|
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:
|
|
7393
|
+
path: path9,
|
|
7368
7394
|
value
|
|
7369
7395
|
}) => {
|
|
7370
7396
|
try {
|
|
7371
|
-
const node = await hydrator(
|
|
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 && (!
|
|
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:
|
|
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 >=
|
|
7488
|
-
await this.contentLevel.batch(
|
|
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,
|
|
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 >=
|
|
7516
|
-
await this.contentLevel.batch(
|
|
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,
|
|
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 >=
|
|
7624
|
-
const batchOps = operations.splice(0,
|
|
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,
|
|
7694
|
+
await level.batch(operations.splice(0, this.levelBatchSize));
|
|
7665
7695
|
}
|
|
7666
7696
|
return { warnings };
|
|
7667
7697
|
};
|
|
7668
7698
|
};
|
|
7669
|
-
var hashPasswordVisitor = async (node,
|
|
7670
|
-
const passwordValuePath = [...
|
|
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,
|
|
7681
|
-
const [currentLevel, ...remainingLevels] =
|
|
7710
|
+
var visitNodes = async (node, path9, callback) => {
|
|
7711
|
+
const [currentLevel, ...remainingLevels] = path9;
|
|
7682
7712
|
if (!remainingLevels?.length) {
|
|
7683
|
-
return callback(node,
|
|
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 =
|
|
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
|
-
|
|
8082
|
+
const resolved = assertWithinBase(filepath, this.outputPath);
|
|
8083
|
+
await fs2.remove(resolved);
|
|
8039
8084
|
}
|
|
8040
8085
|
async get(filepath) {
|
|
8041
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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:
|
|
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 =
|
|
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 =
|
|
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(
|
|
8188
|
-
let pathParts =
|
|
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 (
|
|
8196
|
-
if (dirname(
|
|
8197
|
-
if (
|
|
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,
|
|
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 ===
|
|
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:
|
|
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:
|
|
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((
|
|
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),
|
package/dist/resolver/index.d.ts
CHANGED
|
@@ -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-
|
|
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": "
|
|
47
|
-
"@tinacms/schema-tools": "
|
|
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/
|
|
75
|
-
"@tinacms/
|
|
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
|
}
|