@tinacms/graphql 2.3.1 → 2.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/database/bridge/filesystem.d.ts +7 -0
- package/dist/index.js +52 -10
- package/package.json +1 -1
|
@@ -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/index.js
CHANGED
|
@@ -3035,7 +3035,7 @@ var validateField = async (field) => {
|
|
|
3035
3035
|
var package_default = {
|
|
3036
3036
|
name: "@tinacms/graphql",
|
|
3037
3037
|
type: "module",
|
|
3038
|
-
version: "2.
|
|
3038
|
+
version: "2.4.1",
|
|
3039
3039
|
main: "dist/index.js",
|
|
3040
3040
|
module: "./dist/index.js",
|
|
3041
3041
|
files: [
|
|
@@ -4640,17 +4640,18 @@ var resolveMediaCloudToRelative = (value, config = { useRelativeMedia: true }, s
|
|
|
4640
4640
|
return value;
|
|
4641
4641
|
}
|
|
4642
4642
|
if (hasTinaMediaConfig(schema) === true) {
|
|
4643
|
-
const assetsURL = `https://${config.assetsHost}/${config.clientId}`;
|
|
4644
4643
|
const cleanMediaRoot = cleanUpSlashes(schema.config.media.tina.mediaRoot);
|
|
4645
|
-
|
|
4644
|
+
const cloudUrl = cloudUrlPattern(config.clientId);
|
|
4645
|
+
if (typeof value === "string" && cloudUrl.test(value)) {
|
|
4646
4646
|
return `${cleanMediaRoot}${stripStagingPrefix(
|
|
4647
|
-
value.replace(
|
|
4647
|
+
value.replace(cloudUrl, "")
|
|
4648
4648
|
)}`;
|
|
4649
4649
|
}
|
|
4650
4650
|
if (Array.isArray(value)) {
|
|
4651
4651
|
return value.map((v) => {
|
|
4652
4652
|
if (!v || typeof v !== "string") return v;
|
|
4653
|
-
|
|
4653
|
+
if (!cloudUrl.test(v)) return v;
|
|
4654
|
+
const strippedURL = v.replace(cloudUrl, "");
|
|
4654
4655
|
return `${cleanMediaRoot}${stripStagingPrefix(strippedURL)}`;
|
|
4655
4656
|
});
|
|
4656
4657
|
}
|
|
@@ -4670,13 +4671,17 @@ var resolveMediaRelativeToCloud = (value, config = { useRelativeMedia: true }, s
|
|
|
4670
4671
|
const cleanMediaRoot = cleanUpSlashes(schema.config.media.tina.mediaRoot);
|
|
4671
4672
|
const prefix = stagingPrefix(config);
|
|
4672
4673
|
if (typeof value === "string") {
|
|
4674
|
+
if (ABSOLUTE_URL.test(value)) return value;
|
|
4673
4675
|
const strippedValue = value.replace(cleanMediaRoot, "");
|
|
4676
|
+
if (ABSOLUTE_URL.test(strippedValue)) return strippedValue;
|
|
4674
4677
|
return `https://${config.assetsHost}/${config.clientId}${prefix}${strippedValue}`;
|
|
4675
4678
|
}
|
|
4676
4679
|
if (Array.isArray(value)) {
|
|
4677
4680
|
return value.map((v) => {
|
|
4678
4681
|
if (!v || typeof v !== "string") return v;
|
|
4682
|
+
if (ABSOLUTE_URL.test(v)) return v;
|
|
4679
4683
|
const strippedValue = v.replace(cleanMediaRoot, "");
|
|
4684
|
+
if (ABSOLUTE_URL.test(strippedValue)) return strippedValue;
|
|
4680
4685
|
return `https://${config.assetsHost}/${config.clientId}${prefix}${strippedValue}`;
|
|
4681
4686
|
});
|
|
4682
4687
|
}
|
|
@@ -4686,12 +4691,15 @@ var resolveMediaRelativeToCloud = (value, config = { useRelativeMedia: true }, s
|
|
|
4686
4691
|
return value;
|
|
4687
4692
|
}
|
|
4688
4693
|
};
|
|
4689
|
-
var stagingPrefix = (config) => config.branch && config.branch !== config.mediaBranch ? `/__staging/${
|
|
4690
|
-
var STAGING_SEGMENT = /^\/__staging
|
|
4694
|
+
var stagingPrefix = (config) => config.branch && config.branch !== config.mediaBranch ? `/__staging/${config.branch}/__file` : "";
|
|
4695
|
+
var STAGING_SEGMENT = /^\/__staging\/.+?\/__file(\/.*)$/;
|
|
4691
4696
|
var stripStagingPrefix = (path9) => {
|
|
4692
4697
|
const match = path9.match(STAGING_SEGMENT);
|
|
4693
4698
|
return match ? match[1] : path9;
|
|
4694
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)}`);
|
|
4695
4703
|
var cleanUpSlashes = (path9) => {
|
|
4696
4704
|
if (path9) {
|
|
4697
4705
|
return `/${path9.replace(/^\/+|\/+$/gm, "")}`;
|
|
@@ -8121,6 +8129,13 @@ function assertWithinBase(filepath, baseDir) {
|
|
|
8121
8129
|
}
|
|
8122
8130
|
return resolved;
|
|
8123
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
|
+
}
|
|
8124
8139
|
var FilesystemBridge = class {
|
|
8125
8140
|
rootPath;
|
|
8126
8141
|
outputPath;
|
|
@@ -8128,6 +8143,30 @@ var FilesystemBridge = class {
|
|
|
8128
8143
|
this.rootPath = path7.resolve(rootPath);
|
|
8129
8144
|
this.outputPath = outputPath ? path7.resolve(outputPath) : this.rootPath;
|
|
8130
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
|
+
}
|
|
8131
8170
|
async glob(pattern, extension) {
|
|
8132
8171
|
const basePath = assertWithinBase(pattern, this.outputPath);
|
|
8133
8172
|
const items = await fg(
|
|
@@ -8143,16 +8182,19 @@ var FilesystemBridge = class {
|
|
|
8143
8182
|
);
|
|
8144
8183
|
}
|
|
8145
8184
|
async delete(filepath) {
|
|
8146
|
-
const resolved = assertWithinBase(filepath, this.
|
|
8185
|
+
const resolved = assertWithinBase(filepath, this.baseFor(filepath));
|
|
8186
|
+
this.assertGeneratedSubtree(filepath, resolved);
|
|
8147
8187
|
await fs2.remove(resolved);
|
|
8148
8188
|
}
|
|
8149
8189
|
async get(filepath) {
|
|
8150
|
-
const resolved = assertWithinBase(filepath, this.
|
|
8190
|
+
const resolved = assertWithinBase(filepath, this.baseFor(filepath));
|
|
8191
|
+
this.assertGeneratedSubtree(filepath, resolved);
|
|
8151
8192
|
return (await fs2.readFile(resolved)).toString();
|
|
8152
8193
|
}
|
|
8153
8194
|
async put(filepath, data, basePathOverride) {
|
|
8154
|
-
const basePath = basePathOverride || this.
|
|
8195
|
+
const basePath = basePathOverride || this.baseFor(filepath);
|
|
8155
8196
|
const resolved = assertWithinBase(filepath, basePath);
|
|
8197
|
+
this.assertGeneratedSubtree(filepath, resolved);
|
|
8156
8198
|
await fs2.outputFile(resolved, data);
|
|
8157
8199
|
}
|
|
8158
8200
|
};
|