@tinacms/graphql 0.0.0-059f480-20260324232913 → 0.0.0-06c643a-20260521045804
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/README.md +1 -1
- package/dist/database/bridge/filesystem.d.ts +7 -0
- package/dist/database/index.d.ts +1 -1
- package/dist/database/util.d.ts +0 -2
- package/dist/index.js +182 -41
- package/dist/resolver/index.d.ts +12 -0
- package/dist/resolver/media-utils.d.ts +4 -2
- package/dist/types.d.ts +2 -0
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -137,7 +137,7 @@ Visit [Tina's documentation](https://tina.io/docs/) to learn more.
|
|
|
137
137
|
[](https://twitter.com/intent/tweet?url=https%3A%2F%2Ftinacms.org&text=I%20just%20checked%20out%20@tinacms%20on%20GitHub%20and%20it%20is%20sweet%21&hashtags=TinaCMS%2Cjamstack%2Cheadlesscms)
|
|
138
138
|
[](https://github.com/tinacms/tinacms/discussions)
|
|
139
139
|
|
|
140
|
-
Visit the [GitHub Discussions](https://github.com/tinacms/tinacms/discussions) or our [Community Discord](https://discord.com/invite/zumN63Ybpf) to ask questions, or look us up on
|
|
140
|
+
Visit the [GitHub Discussions](https://github.com/tinacms/tinacms/discussions) or our [Community Discord](https://discord.com/invite/zumN63Ybpf) to ask questions, or look us up on Twitter at [@tinacms](https://twitter.com/tinacms).
|
|
141
141
|
|
|
142
142
|
## Contributing
|
|
143
143
|
|
|
@@ -4,6 +4,11 @@ import type { Bridge } from './index';
|
|
|
4
4
|
* The basic example here is for the filesystem, one is needed
|
|
5
5
|
* for GitHub has well.
|
|
6
6
|
*
|
|
7
|
+
* When `outputPath` differs from `rootPath` (multi-repo: generator +
|
|
8
|
+
* sibling content repo), paths under `tina/__generated__/` still resolve
|
|
9
|
+
* against `rootPath` — schema/graphql/lookup artifacts live only in the
|
|
10
|
+
* generator. Everything else (content files) resolves against `outputPath`.
|
|
11
|
+
*
|
|
7
12
|
* @security All public methods validate their `filepath` / `pattern`
|
|
8
13
|
* argument via `assertWithinBase` before performing any I/O. If you add a
|
|
9
14
|
* new method that accepts a path, you MUST validate it the same way.
|
|
@@ -12,6 +17,8 @@ export declare class FilesystemBridge implements Bridge {
|
|
|
12
17
|
rootPath: string;
|
|
13
18
|
outputPath: string;
|
|
14
19
|
constructor(rootPath: string, outputPath?: string);
|
|
20
|
+
private baseFor;
|
|
21
|
+
private assertGeneratedSubtree;
|
|
15
22
|
glob(pattern: string, extension: string): Promise<string[]>;
|
|
16
23
|
delete(filepath: string): Promise<void>;
|
|
17
24
|
get(filepath: string): Promise<string>;
|
package/dist/database/index.d.ts
CHANGED
|
@@ -106,7 +106,7 @@ export declare class Database {
|
|
|
106
106
|
defaultItem?: import("@tinacms/schema-tools").DefaultItem<Record<string, any>>;
|
|
107
107
|
previewSrc?: string;
|
|
108
108
|
};
|
|
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").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>) & {})[];
|
|
110
110
|
};
|
|
111
111
|
info: CollectionTemplateable;
|
|
112
112
|
}>;
|
package/dist/database/util.d.ts
CHANGED
|
@@ -16,8 +16,6 @@ yupSchema: (args: typeof yup) => yup.ObjectSchema<any>, markdownParseConfig?: {
|
|
|
16
16
|
lineWidth?: number;
|
|
17
17
|
frontmatterDelimiters?: [string, string] | string;
|
|
18
18
|
}) => T;
|
|
19
|
-
export declare const atob: (b64Encoded: string) => string;
|
|
20
|
-
export declare const btoa: (string: string) => string;
|
|
21
19
|
export declare const scanAllContent: (tinaSchema: TinaSchema, bridge: Bridge, callback: (collection: Collection<true>, contentPaths: string[]) => Promise<void>) => Promise<string[]>;
|
|
22
20
|
export declare const scanContentByPaths: (tinaSchema: TinaSchema, documentPaths: string[], callback: (collection: Collection<true> | undefined, documentPaths: string[]) => Promise<void>) => Promise<void>;
|
|
23
21
|
export declare const partitionPathsByCollection: (tinaSchema: TinaSchema, documentPaths: string[]) => Promise<{
|
package/dist/index.js
CHANGED
|
@@ -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":
|
|
@@ -2884,7 +2892,8 @@ var FIELD_TYPES = [
|
|
|
2884
2892
|
"reference",
|
|
2885
2893
|
"object",
|
|
2886
2894
|
"rich-text",
|
|
2887
|
-
"password"
|
|
2895
|
+
"password",
|
|
2896
|
+
"displayOnly"
|
|
2888
2897
|
];
|
|
2889
2898
|
var validateSchema = async (schema) => {
|
|
2890
2899
|
const schema2 = addNamespaceToSchema(
|
|
@@ -3026,7 +3035,7 @@ var validateField = async (field) => {
|
|
|
3026
3035
|
var package_default = {
|
|
3027
3036
|
name: "@tinacms/graphql",
|
|
3028
3037
|
type: "module",
|
|
3029
|
-
version: "2.
|
|
3038
|
+
version: "2.4.1",
|
|
3030
3039
|
main: "dist/index.js",
|
|
3031
3040
|
module: "./dist/index.js",
|
|
3032
3041
|
files: [
|
|
@@ -3676,7 +3685,7 @@ var scanAllContent = async (tinaSchema, bridge, callback) => {
|
|
|
3676
3685
|
`"${path9}" Found in multiple collections: ${filesSeen.get(path9).map((collection2) => `"${collection2}"`).join(
|
|
3677
3686
|
", "
|
|
3678
3687
|
)}. This can cause unexpected behavior. We recommend updating the \`match\` property of those collections so that each file is in only one collection.
|
|
3679
|
-
This will be an error in the future. See https://tina.io/docs/
|
|
3688
|
+
This will be an error in the future. See https://tina.io/docs/r/content-modelling-collections#path-matching
|
|
3680
3689
|
`
|
|
3681
3690
|
);
|
|
3682
3691
|
});
|
|
@@ -3770,6 +3779,9 @@ var getTemplateForFile = (templateInfo, data) => {
|
|
|
3770
3779
|
throw new Error(`Unable to determine template`);
|
|
3771
3780
|
};
|
|
3772
3781
|
var loadAndParseWithAliases = async (bridge, filepath, collection, templateInfo) => {
|
|
3782
|
+
if (filepath.endsWith(".gitkeep")) {
|
|
3783
|
+
return { _is_tina_folder_placeholder: true };
|
|
3784
|
+
}
|
|
3773
3785
|
const dataString = await bridge.get(normalizePath(filepath));
|
|
3774
3786
|
const data = parseFile(
|
|
3775
3787
|
dataString,
|
|
@@ -3783,7 +3795,7 @@ var loadAndParseWithAliases = async (bridge, filepath, collection, templateInfo)
|
|
|
3783
3795
|
const template = getTemplateForFile(templateInfo, data);
|
|
3784
3796
|
if (!template) {
|
|
3785
3797
|
console.warn(
|
|
3786
|
-
`Document: ${filepath} has an ambiguous template, skipping from indexing. See https://tina.io/docs/
|
|
3798
|
+
`Document: ${filepath} has an ambiguous template, skipping from indexing. See https://tina.io/docs/r/ambiguous-template for more info.`
|
|
3787
3799
|
);
|
|
3788
3800
|
return;
|
|
3789
3801
|
}
|
|
@@ -4628,22 +4640,19 @@ var resolveMediaCloudToRelative = (value, config = { useRelativeMedia: true }, s
|
|
|
4628
4640
|
return value;
|
|
4629
4641
|
}
|
|
4630
4642
|
if (hasTinaMediaConfig(schema) === true) {
|
|
4631
|
-
const
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
return `${cleanMediaRoot}${strippedURL}`;
|
|
4643
|
+
const cleanMediaRoot = cleanUpSlashes(schema.config.media.tina.mediaRoot);
|
|
4644
|
+
const cloudUrl = cloudUrlPattern(config.clientId);
|
|
4645
|
+
if (typeof value === "string" && cloudUrl.test(value)) {
|
|
4646
|
+
return `${cleanMediaRoot}${stripStagingPrefix(
|
|
4647
|
+
value.replace(cloudUrl, "")
|
|
4648
|
+
)}`;
|
|
4638
4649
|
}
|
|
4639
4650
|
if (Array.isArray(value)) {
|
|
4640
4651
|
return value.map((v) => {
|
|
4641
4652
|
if (!v || typeof v !== "string") return v;
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
)
|
|
4645
|
-
const strippedURL = v.replace(assetsURL, "");
|
|
4646
|
-
return `${cleanMediaRoot}${strippedURL}`;
|
|
4653
|
+
if (!cloudUrl.test(v)) return v;
|
|
4654
|
+
const strippedURL = v.replace(cloudUrl, "");
|
|
4655
|
+
return `${cleanMediaRoot}${stripStagingPrefix(strippedURL)}`;
|
|
4647
4656
|
});
|
|
4648
4657
|
}
|
|
4649
4658
|
return value;
|
|
@@ -4660,15 +4669,20 @@ var resolveMediaRelativeToCloud = (value, config = { useRelativeMedia: true }, s
|
|
|
4660
4669
|
}
|
|
4661
4670
|
if (hasTinaMediaConfig(schema) === true) {
|
|
4662
4671
|
const cleanMediaRoot = cleanUpSlashes(schema.config.media.tina.mediaRoot);
|
|
4672
|
+
const prefix = stagingPrefix(config);
|
|
4663
4673
|
if (typeof value === "string") {
|
|
4674
|
+
if (ABSOLUTE_URL.test(value)) return value;
|
|
4664
4675
|
const strippedValue = value.replace(cleanMediaRoot, "");
|
|
4665
|
-
|
|
4676
|
+
if (ABSOLUTE_URL.test(strippedValue)) return strippedValue;
|
|
4677
|
+
return `https://${config.assetsHost}/${config.clientId}${prefix}${strippedValue}`;
|
|
4666
4678
|
}
|
|
4667
4679
|
if (Array.isArray(value)) {
|
|
4668
4680
|
return value.map((v) => {
|
|
4669
4681
|
if (!v || typeof v !== "string") return v;
|
|
4682
|
+
if (ABSOLUTE_URL.test(v)) return v;
|
|
4670
4683
|
const strippedValue = v.replace(cleanMediaRoot, "");
|
|
4671
|
-
|
|
4684
|
+
if (ABSOLUTE_URL.test(strippedValue)) return strippedValue;
|
|
4685
|
+
return `https://${config.assetsHost}/${config.clientId}${prefix}${strippedValue}`;
|
|
4672
4686
|
});
|
|
4673
4687
|
}
|
|
4674
4688
|
}
|
|
@@ -4677,6 +4691,15 @@ var resolveMediaRelativeToCloud = (value, config = { useRelativeMedia: true }, s
|
|
|
4677
4691
|
return value;
|
|
4678
4692
|
}
|
|
4679
4693
|
};
|
|
4694
|
+
var stagingPrefix = (config) => config.branch && config.branch !== config.mediaBranch ? `/__staging/${config.branch}/__file` : "";
|
|
4695
|
+
var STAGING_SEGMENT = /^\/__staging\/.+?\/__file(\/.*)$/;
|
|
4696
|
+
var stripStagingPrefix = (path9) => {
|
|
4697
|
+
const match = path9.match(STAGING_SEGMENT);
|
|
4698
|
+
return match ? match[1] : path9;
|
|
4699
|
+
};
|
|
4700
|
+
var ABSOLUTE_URL = /^[a-z][a-z0-9+.\-]*:|^\/\//i;
|
|
4701
|
+
var escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4702
|
+
var cloudUrlPattern = (clientId) => new RegExp(`^https://[^/]+/${escapeRegExp(clientId)}`);
|
|
4680
4703
|
var cleanUpSlashes = (path9) => {
|
|
4681
4704
|
if (path9) {
|
|
4682
4705
|
return `/${path9.replace(/^\/+|\/+$/gm, "")}`;
|
|
@@ -4701,6 +4724,8 @@ var resolveFieldData = async ({ namespace, ...field }, rawData, accumulator, tin
|
|
|
4701
4724
|
assertShape(rawData, (yup3) => yup3.object());
|
|
4702
4725
|
const value = rawData[field.name];
|
|
4703
4726
|
switch (field.type) {
|
|
4727
|
+
case "displayOnly":
|
|
4728
|
+
break;
|
|
4704
4729
|
case "datetime":
|
|
4705
4730
|
if (value instanceof Date) {
|
|
4706
4731
|
accumulator[field.name] = value.toISOString();
|
|
@@ -4917,7 +4942,7 @@ var updateObjectWithJsonPath = (obj, path9, oldValue, newValue) => {
|
|
|
4917
4942
|
}
|
|
4918
4943
|
return { object: obj, updated };
|
|
4919
4944
|
};
|
|
4920
|
-
var Resolver = class {
|
|
4945
|
+
var Resolver = class _Resolver {
|
|
4921
4946
|
constructor(init) {
|
|
4922
4947
|
this.init = init;
|
|
4923
4948
|
this.config = init.config;
|
|
@@ -5188,8 +5213,37 @@ var Resolver = class {
|
|
|
5188
5213
|
* path traversal attacks where a user might attempt to read or write files
|
|
5189
5214
|
* outside of the intended collection.
|
|
5190
5215
|
*/
|
|
5216
|
+
static sanitizePath(input) {
|
|
5217
|
+
if (input.includes("\0")) {
|
|
5218
|
+
throw new Error("Invalid path: null bytes are not allowed");
|
|
5219
|
+
}
|
|
5220
|
+
return input.replace(/\\/g, "/");
|
|
5221
|
+
}
|
|
5222
|
+
/**
|
|
5223
|
+
* Validates that relativePath is non-empty and contains only allowed
|
|
5224
|
+
* characters: a-z, A-Z, 0-9, hyphens, underscores, periods, and
|
|
5225
|
+
* forward slashes.
|
|
5226
|
+
*/
|
|
5227
|
+
static validateRelativePath(relativePath) {
|
|
5228
|
+
if (!relativePath.trim()) {
|
|
5229
|
+
throw new Error(
|
|
5230
|
+
"Invalid path: relativePath cannot be empty or whitespace"
|
|
5231
|
+
);
|
|
5232
|
+
}
|
|
5233
|
+
if (relativePath !== relativePath.trim()) {
|
|
5234
|
+
throw new Error(
|
|
5235
|
+
"Invalid path: relativePath cannot have leading or trailing whitespace"
|
|
5236
|
+
);
|
|
5237
|
+
}
|
|
5238
|
+
if (!/^[a-zA-Z0-9\-_./]+$/.test(relativePath)) {
|
|
5239
|
+
throw new Error("Invalid path: relativePath contains invalid characters");
|
|
5240
|
+
}
|
|
5241
|
+
}
|
|
5191
5242
|
validatePath = (fullPath, collection, relativePath) => {
|
|
5192
|
-
|
|
5243
|
+
if (fullPath.includes("\0")) {
|
|
5244
|
+
throw new Error("Invalid path: null bytes are not allowed");
|
|
5245
|
+
}
|
|
5246
|
+
const normalizedPath = path3.normalize(fullPath.replace(/\\/g, "/"));
|
|
5193
5247
|
const normalizedCollectionPath = path3.normalize(collection.path);
|
|
5194
5248
|
const relative = path3.relative(normalizedCollectionPath, normalizedPath);
|
|
5195
5249
|
if (relative.startsWith("..")) {
|
|
@@ -5199,6 +5253,7 @@ var Resolver = class {
|
|
|
5199
5253
|
throw new Error(`Invalid path: absolute paths are not allowed`);
|
|
5200
5254
|
}
|
|
5201
5255
|
if (relativePath) {
|
|
5256
|
+
_Resolver.validateRelativePath(relativePath);
|
|
5202
5257
|
const collectionFormat = collection.format || "md";
|
|
5203
5258
|
const fileExtension = path3.extname(relativePath).toLowerCase().slice(1);
|
|
5204
5259
|
if (fileExtension !== collectionFormat) {
|
|
@@ -5219,17 +5274,19 @@ var Resolver = class {
|
|
|
5219
5274
|
* @returns Object containing the collection and validated real path
|
|
5220
5275
|
*/
|
|
5221
5276
|
getValidatedPath = (collectionName, relativePath, options) => {
|
|
5277
|
+
_Resolver.validateRelativePath(relativePath);
|
|
5222
5278
|
const collection = this.getCollectionWithName(collectionName);
|
|
5223
|
-
const
|
|
5279
|
+
const sanitizedRelativePath = _Resolver.sanitizePath(relativePath);
|
|
5280
|
+
const pathSegments = [collection.path, sanitizedRelativePath];
|
|
5224
5281
|
if (options?.extraSegments) {
|
|
5225
|
-
pathSegments.push(...options.extraSegments);
|
|
5282
|
+
pathSegments.push(...options.extraSegments.map(_Resolver.sanitizePath));
|
|
5226
5283
|
}
|
|
5227
5284
|
const realPath = path3.join(...pathSegments);
|
|
5228
5285
|
const shouldValidateExtension = options?.validateExtension !== false;
|
|
5229
5286
|
this.validatePath(
|
|
5230
5287
|
realPath,
|
|
5231
5288
|
collection,
|
|
5232
|
-
shouldValidateExtension ?
|
|
5289
|
+
shouldValidateExtension ? sanitizedRelativePath : void 0
|
|
5233
5290
|
);
|
|
5234
5291
|
return { collection, realPath };
|
|
5235
5292
|
};
|
|
@@ -5257,6 +5314,7 @@ var Resolver = class {
|
|
|
5257
5314
|
relativePath
|
|
5258
5315
|
}) => {
|
|
5259
5316
|
const collection = this.getCollectionWithName(collectionName);
|
|
5317
|
+
_Resolver.validateRelativePath(relativePath);
|
|
5260
5318
|
const realPath = path3.join(
|
|
5261
5319
|
collection.path,
|
|
5262
5320
|
relativePath,
|
|
@@ -5314,6 +5372,12 @@ var Resolver = class {
|
|
|
5314
5372
|
if (newRealPath === realPath) {
|
|
5315
5373
|
return doc;
|
|
5316
5374
|
}
|
|
5375
|
+
const newPathAlreadyExists = await this.database.documentExists(newRealPath);
|
|
5376
|
+
if (newPathAlreadyExists) {
|
|
5377
|
+
throw new Error(
|
|
5378
|
+
`Unable to rename document, ${newRealPath} already exists`
|
|
5379
|
+
);
|
|
5380
|
+
}
|
|
5317
5381
|
await this.database.put(newRealPath, doc._rawData, collection.name);
|
|
5318
5382
|
await this.deleteDocument(realPath);
|
|
5319
5383
|
const collRefs = await this.findReferences(realPath, collection);
|
|
@@ -5863,6 +5927,8 @@ var Resolver = class {
|
|
|
5863
5927
|
throw new Error(`Expected to find field by name ${fieldName}`);
|
|
5864
5928
|
}
|
|
5865
5929
|
switch (field.type) {
|
|
5930
|
+
case "displayOnly":
|
|
5931
|
+
break;
|
|
5866
5932
|
case "datetime":
|
|
5867
5933
|
accum[fieldName] = resolveDateInput(fieldValue, field);
|
|
5868
5934
|
break;
|
|
@@ -6866,7 +6932,10 @@ var Database = class {
|
|
|
6866
6932
|
if (!collection) {
|
|
6867
6933
|
throw new GraphQLError5(`Unable to find collection for ${filepath}.`);
|
|
6868
6934
|
}
|
|
6869
|
-
|
|
6935
|
+
const isFolderPlaceholder = filepath.endsWith(
|
|
6936
|
+
`.gitkeep.${collection.format || "md"}`
|
|
6937
|
+
);
|
|
6938
|
+
if (!isFolderPlaceholder && (collection.match?.exclude || collection.match?.include)) {
|
|
6870
6939
|
const matches = this.tinaSchema.getMatches({ collection });
|
|
6871
6940
|
const match = micromatch2.isMatch(filepath, matches);
|
|
6872
6941
|
if (!match) {
|
|
@@ -7790,8 +7859,9 @@ var _indexContent = async ({
|
|
|
7790
7859
|
]);
|
|
7791
7860
|
}
|
|
7792
7861
|
}
|
|
7862
|
+
let putOps = [];
|
|
7793
7863
|
if (!isGitKeep(filepath, collection)) {
|
|
7794
|
-
|
|
7864
|
+
putOps = [
|
|
7795
7865
|
...makeRefOpsForDocument(
|
|
7796
7866
|
normalizedPath,
|
|
7797
7867
|
collection?.name,
|
|
@@ -7816,18 +7886,21 @@ var _indexContent = async ({
|
|
|
7816
7886
|
aliasedData,
|
|
7817
7887
|
"put",
|
|
7818
7888
|
level
|
|
7819
|
-
)
|
|
7820
|
-
|
|
7821
|
-
type: "put",
|
|
7822
|
-
key: normalizedPath,
|
|
7823
|
-
value: aliasedData,
|
|
7824
|
-
sublevel: level.sublevel(
|
|
7825
|
-
CONTENT_ROOT_PREFIX,
|
|
7826
|
-
SUBLEVEL_OPTIONS
|
|
7827
|
-
)
|
|
7828
|
-
}
|
|
7829
|
-
]);
|
|
7889
|
+
)
|
|
7890
|
+
];
|
|
7830
7891
|
}
|
|
7892
|
+
await enqueueOps([
|
|
7893
|
+
...putOps,
|
|
7894
|
+
{
|
|
7895
|
+
type: "put",
|
|
7896
|
+
key: normalizedPath,
|
|
7897
|
+
value: aliasedData,
|
|
7898
|
+
sublevel: level.sublevel(
|
|
7899
|
+
CONTENT_ROOT_PREFIX,
|
|
7900
|
+
SUBLEVEL_OPTIONS
|
|
7901
|
+
)
|
|
7902
|
+
}
|
|
7903
|
+
]);
|
|
7831
7904
|
} catch (error) {
|
|
7832
7905
|
throw new TinaFetchError(`Unable to seed ${filepath}`, {
|
|
7833
7906
|
originalError: error,
|
|
@@ -8021,16 +8094,48 @@ import path7 from "path";
|
|
|
8021
8094
|
import fg from "fast-glob";
|
|
8022
8095
|
import fs2 from "fs-extra";
|
|
8023
8096
|
import normalize from "normalize-path";
|
|
8097
|
+
function resolveRealPath(candidate) {
|
|
8098
|
+
try {
|
|
8099
|
+
return fs2.realpathSync(candidate);
|
|
8100
|
+
} catch {
|
|
8101
|
+
const parent = path7.dirname(candidate);
|
|
8102
|
+
if (parent === candidate) return candidate;
|
|
8103
|
+
return path7.join(resolveRealPath(parent), path7.basename(candidate));
|
|
8104
|
+
}
|
|
8105
|
+
}
|
|
8024
8106
|
function assertWithinBase(filepath, baseDir) {
|
|
8107
|
+
if (filepath.includes("\0")) {
|
|
8108
|
+
throw new Error("Invalid path: null bytes are not allowed");
|
|
8109
|
+
}
|
|
8110
|
+
const sanitized = filepath.replace(/\\/g, "/");
|
|
8025
8111
|
const resolvedBase = path7.resolve(baseDir);
|
|
8026
|
-
const resolved = path7.resolve(path7.join(baseDir,
|
|
8112
|
+
const resolved = path7.resolve(path7.join(baseDir, sanitized));
|
|
8027
8113
|
if (resolved !== resolvedBase && !resolved.startsWith(resolvedBase + path7.sep)) {
|
|
8028
8114
|
throw new Error(
|
|
8029
8115
|
`Path traversal detected: "${filepath}" escapes the base directory`
|
|
8030
8116
|
);
|
|
8031
8117
|
}
|
|
8118
|
+
try {
|
|
8119
|
+
const realBase = fs2.realpathSync(resolvedBase);
|
|
8120
|
+
const realResolved = resolveRealPath(resolved);
|
|
8121
|
+
if (realResolved !== realBase && !realResolved.startsWith(realBase + path7.sep)) {
|
|
8122
|
+
throw new Error(
|
|
8123
|
+
`Path traversal detected: "${filepath}" escapes the base directory`
|
|
8124
|
+
);
|
|
8125
|
+
}
|
|
8126
|
+
} catch (err) {
|
|
8127
|
+
if (err instanceof Error && err.message.startsWith("Path traversal"))
|
|
8128
|
+
throw err;
|
|
8129
|
+
}
|
|
8032
8130
|
return resolved;
|
|
8033
8131
|
}
|
|
8132
|
+
var GENERATED_PATH_PREFIXES = ["tina/__generated__/", ".tina/__generated__/"];
|
|
8133
|
+
function isGeneratedPath(filepath) {
|
|
8134
|
+
const normalized = filepath.replace(/\\/g, "/");
|
|
8135
|
+
return GENERATED_PATH_PREFIXES.some(
|
|
8136
|
+
(prefix) => normalized.startsWith(prefix)
|
|
8137
|
+
);
|
|
8138
|
+
}
|
|
8034
8139
|
var FilesystemBridge = class {
|
|
8035
8140
|
rootPath;
|
|
8036
8141
|
outputPath;
|
|
@@ -8038,6 +8143,30 @@ var FilesystemBridge = class {
|
|
|
8038
8143
|
this.rootPath = path7.resolve(rootPath);
|
|
8039
8144
|
this.outputPath = outputPath ? path7.resolve(outputPath) : this.rootPath;
|
|
8040
8145
|
}
|
|
8146
|
+
// Picks the base directory for a given path. The `assertWithinBase` check
|
|
8147
|
+
// in delete/get/put still runs against the chosen base, so a path like
|
|
8148
|
+
// `tina/__generated__/../../escape.txt` is still rejected — just rejected
|
|
8149
|
+
// against `rootPath` rather than `outputPath`.
|
|
8150
|
+
baseFor(filepath) {
|
|
8151
|
+
return isGeneratedPath(filepath) ? this.rootPath : this.outputPath;
|
|
8152
|
+
}
|
|
8153
|
+
// Defense-in-depth for generated-path routing: assertWithinBase already
|
|
8154
|
+
// rejects paths that escape rootPath, but a path like
|
|
8155
|
+
// `tina/__generated__/../../.env` would resolve to `<rootPath>/.env` —
|
|
8156
|
+
// technically inside rootPath but outside the generated subtree the
|
|
8157
|
+
// routing is supposed to grant access to. Verify the resolved path stays
|
|
8158
|
+
// inside the matching generated subdirectory.
|
|
8159
|
+
assertGeneratedSubtree(filepath, resolved) {
|
|
8160
|
+
if (!isGeneratedPath(filepath)) return;
|
|
8161
|
+
const normalized = filepath.replace(/\\/g, "/");
|
|
8162
|
+
const subdir = normalized.startsWith(".tina/__generated__/") ? ".tina/__generated__" : "tina/__generated__";
|
|
8163
|
+
const generatedRoot = path7.resolve(path7.join(this.rootPath, subdir));
|
|
8164
|
+
if (resolved !== generatedRoot && !resolved.startsWith(generatedRoot + path7.sep)) {
|
|
8165
|
+
throw new Error(
|
|
8166
|
+
`Path traversal detected: "${filepath}" routed via generated prefix but resolved outside ${generatedRoot}`
|
|
8167
|
+
);
|
|
8168
|
+
}
|
|
8169
|
+
}
|
|
8041
8170
|
async glob(pattern, extension) {
|
|
8042
8171
|
const basePath = assertWithinBase(pattern, this.outputPath);
|
|
8043
8172
|
const items = await fg(
|
|
@@ -8053,16 +8182,19 @@ var FilesystemBridge = class {
|
|
|
8053
8182
|
);
|
|
8054
8183
|
}
|
|
8055
8184
|
async delete(filepath) {
|
|
8056
|
-
const resolved = assertWithinBase(filepath, this.
|
|
8185
|
+
const resolved = assertWithinBase(filepath, this.baseFor(filepath));
|
|
8186
|
+
this.assertGeneratedSubtree(filepath, resolved);
|
|
8057
8187
|
await fs2.remove(resolved);
|
|
8058
8188
|
}
|
|
8059
8189
|
async get(filepath) {
|
|
8060
|
-
const resolved = assertWithinBase(filepath, this.
|
|
8190
|
+
const resolved = assertWithinBase(filepath, this.baseFor(filepath));
|
|
8191
|
+
this.assertGeneratedSubtree(filepath, resolved);
|
|
8061
8192
|
return (await fs2.readFile(resolved)).toString();
|
|
8062
8193
|
}
|
|
8063
8194
|
async put(filepath, data, basePathOverride) {
|
|
8064
|
-
const basePath = basePathOverride || this.
|
|
8195
|
+
const basePath = basePathOverride || this.baseFor(filepath);
|
|
8065
8196
|
const resolved = assertWithinBase(filepath, basePath);
|
|
8197
|
+
this.assertGeneratedSubtree(filepath, resolved);
|
|
8066
8198
|
await fs2.outputFile(resolved, data);
|
|
8067
8199
|
}
|
|
8068
8200
|
};
|
|
@@ -8090,7 +8222,11 @@ import { GraphQLError as GraphQLError6 } from "graphql";
|
|
|
8090
8222
|
import git2 from "isomorphic-git";
|
|
8091
8223
|
import normalize2 from "normalize-path";
|
|
8092
8224
|
function assertWithinBase2(filepath, relativePath) {
|
|
8093
|
-
|
|
8225
|
+
if (filepath.includes("\0")) {
|
|
8226
|
+
throw new Error("Invalid path: null bytes are not allowed");
|
|
8227
|
+
}
|
|
8228
|
+
const sanitized = filepath.replace(/\\/g, "/");
|
|
8229
|
+
const qualified = relativePath ? `${relativePath}/${sanitized}` : sanitized;
|
|
8094
8230
|
const normalized = path8.normalize(qualified);
|
|
8095
8231
|
if (normalized.startsWith("..") || normalized.startsWith("/") || path8.isAbsolute(normalized) || relativePath && normalized !== relativePath && !normalized.startsWith(relativePath + "/")) {
|
|
8096
8232
|
throw new Error(
|
|
@@ -8573,3 +8709,8 @@ export {
|
|
|
8573
8709
|
};
|
|
8574
8710
|
//! Replaces _.get()
|
|
8575
8711
|
//! Replaces _.flattenDeep()
|
|
8712
|
+
/**
|
|
8713
|
+
* @license
|
|
8714
|
+
* Copyright 2023 Forestry.io Holdings, Inc.
|
|
8715
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
8716
|
+
*/
|
package/dist/resolver/index.d.ts
CHANGED
|
@@ -11,6 +11,11 @@ interface ResolverConfig {
|
|
|
11
11
|
isAudit: boolean;
|
|
12
12
|
}
|
|
13
13
|
export declare const createResolver: (args: ResolverConfig) => Resolver;
|
|
14
|
+
export declare const resolveFieldData: ({ namespace, ...field }: TinaField<true>, rawData: unknown, accumulator: {
|
|
15
|
+
[key: string]: unknown;
|
|
16
|
+
}, tinaSchema: TinaSchema, config?: GraphQLConfig, isAudit?: boolean) => Promise<{
|
|
17
|
+
[key: string]: unknown;
|
|
18
|
+
}>;
|
|
14
19
|
export declare const transformDocumentIntoPayload: (fullPath: string, rawData: {
|
|
15
20
|
_collection: any;
|
|
16
21
|
_template: any;
|
|
@@ -235,6 +240,13 @@ export declare class Resolver {
|
|
|
235
240
|
* path traversal attacks where a user might attempt to read or write files
|
|
236
241
|
* outside of the intended collection.
|
|
237
242
|
*/
|
|
243
|
+
private static sanitizePath;
|
|
244
|
+
/**
|
|
245
|
+
* Validates that relativePath is non-empty and contains only allowed
|
|
246
|
+
* characters: a-z, A-Z, 0-9, hyphens, underscores, periods, and
|
|
247
|
+
* forward slashes.
|
|
248
|
+
*/
|
|
249
|
+
private static validateRelativePath;
|
|
238
250
|
private validatePath;
|
|
239
251
|
/**
|
|
240
252
|
* Helper method to get collection and construct validated path.
|
package/dist/types.d.ts
CHANGED
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-06c643a-20260521045804",
|
|
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/
|
|
47
|
-
"@tinacms/
|
|
46
|
+
"@tinacms/schema-tools": "0.0.0-06c643a-20260521045804",
|
|
47
|
+
"@tinacms/mdx": "0.0.0-06c643a-20260521045804"
|
|
48
48
|
},
|
|
49
49
|
"publishConfig": {
|
|
50
50
|
"registry": "https://registry.npmjs.org"
|
|
@@ -72,8 +72,8 @@
|
|
|
72
72
|
"vite": "^4.5.9",
|
|
73
73
|
"vitest": "^0.32.4",
|
|
74
74
|
"zod": "^3.24.2",
|
|
75
|
-
"@tinacms/
|
|
76
|
-
"@tinacms/
|
|
75
|
+
"@tinacms/scripts": "1.6.1",
|
|
76
|
+
"@tinacms/schema-tools": "0.0.0-06c643a-20260521045804"
|
|
77
77
|
},
|
|
78
78
|
"scripts": {
|
|
79
79
|
"types": "pnpm tsc",
|