@tinacms/cli 0.0.0-e6ffde4-20251216055147 → 0.0.0-e797ebd-20260331104743
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +359 -99
- package/dist/next/codegen/stripSearchTokenFromConfig.d.ts +7 -0
- package/dist/next/commands/dev-command/server/media.d.ts +18 -1
- package/dist/next/commands/dev-command/server/searchIndex.d.ts +39 -4
- package/dist/next/config-manager.d.ts +3 -1
- package/dist/next/database.d.ts +1 -1
- package/dist/next/vite/cors.d.ts +13 -0
- package/dist/next/vite/filterPublicEnv.d.ts +9 -0
- package/dist/next/vite/plugins.d.ts +2 -2
- package/dist/server/models/media.d.ts +17 -0
- package/dist/utils/host.d.ts +5 -0
- package/dist/utils/path.d.ts +26 -0
- package/package.json +7 -7
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { Cli, Builtins } from "clipanion";
|
|
3
3
|
|
|
4
4
|
// package.json
|
|
5
|
-
var version = "2.
|
|
5
|
+
var version = "2.2.1";
|
|
6
6
|
|
|
7
7
|
// src/next/commands/dev-command/index.ts
|
|
8
8
|
import path8 from "path";
|
|
@@ -124,6 +124,15 @@ var S_SUCCESS = s("\u25C6", "*");
|
|
|
124
124
|
var S_WARN = s("\u25B2", "!");
|
|
125
125
|
var S_ERROR = s("\u25A0", "x");
|
|
126
126
|
|
|
127
|
+
// src/utils/host.ts
|
|
128
|
+
var LOCALHOST_ADDRESSES = ["localhost", "127.0.0.1", "::1"];
|
|
129
|
+
function isHostExposed(host) {
|
|
130
|
+
if (host === true) return true;
|
|
131
|
+
if (typeof host === "string" && !LOCALHOST_ADDRESSES.includes(host))
|
|
132
|
+
return true;
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
|
|
127
136
|
// src/utils/spinner.ts
|
|
128
137
|
import { Spinner } from "cli-spinner";
|
|
129
138
|
async function localSpin({
|
|
@@ -417,6 +426,38 @@ var loadGraphQLDocuments = async (globPath) => {
|
|
|
417
426
|
import { transform } from "esbuild";
|
|
418
427
|
import { mapUserFields } from "@tinacms/graphql";
|
|
419
428
|
import normalizePath from "normalize-path";
|
|
429
|
+
|
|
430
|
+
// src/next/codegen/stripSearchTokenFromConfig.ts
|
|
431
|
+
function stripSearchTokenFromConfig(config2) {
|
|
432
|
+
const cfg = config2;
|
|
433
|
+
if (!cfg?.search) {
|
|
434
|
+
return config2;
|
|
435
|
+
}
|
|
436
|
+
const search = cfg.search;
|
|
437
|
+
const tina = search?.tina;
|
|
438
|
+
if (tina) {
|
|
439
|
+
const { indexerToken, ...safeSearchConfig } = tina;
|
|
440
|
+
const newConfig = {};
|
|
441
|
+
for (const key of Object.keys(cfg)) {
|
|
442
|
+
if (key === "search") {
|
|
443
|
+
newConfig.search = { tina: safeSearchConfig };
|
|
444
|
+
} else {
|
|
445
|
+
newConfig[key] = cfg[key];
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return newConfig;
|
|
449
|
+
} else {
|
|
450
|
+
const newConfig = {};
|
|
451
|
+
for (const key of Object.keys(cfg)) {
|
|
452
|
+
if (key !== "search") {
|
|
453
|
+
newConfig[key] = cfg[key];
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return newConfig;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// src/next/codegen/index.ts
|
|
420
461
|
var TINA_HOST = "content.tinajs.io";
|
|
421
462
|
var Codegen = class {
|
|
422
463
|
configManager;
|
|
@@ -487,8 +528,9 @@ var Codegen = class {
|
|
|
487
528
|
"_graphql.json",
|
|
488
529
|
JSON.stringify(this.graphqlSchemaDoc)
|
|
489
530
|
);
|
|
490
|
-
|
|
491
|
-
|
|
531
|
+
this.tinaSchema.schema.config = stripSearchTokenFromConfig(
|
|
532
|
+
this.tinaSchema.schema.config
|
|
533
|
+
);
|
|
492
534
|
await this.writeConfigFile(
|
|
493
535
|
"_schema.json",
|
|
494
536
|
JSON.stringify(this.tinaSchema.schema)
|
|
@@ -784,6 +826,14 @@ function stripNativeTrailingSlash(p) {
|
|
|
784
826
|
}
|
|
785
827
|
return str;
|
|
786
828
|
}
|
|
829
|
+
var PathTraversalError = class extends Error {
|
|
830
|
+
constructor(attemptedPath) {
|
|
831
|
+
super(
|
|
832
|
+
`Path traversal detected: the path "${attemptedPath}" escapes the allowed directory`
|
|
833
|
+
);
|
|
834
|
+
this.name = "PathTraversalError";
|
|
835
|
+
}
|
|
836
|
+
};
|
|
787
837
|
|
|
788
838
|
// src/next/config-manager.ts
|
|
789
839
|
var TINA_FOLDER = "tina";
|
|
@@ -998,13 +1048,15 @@ var ConfigManager = class {
|
|
|
998
1048
|
this.contentRootPath = this.rootPath;
|
|
999
1049
|
}
|
|
1000
1050
|
this.generatedFolderPathContentRepo = path3.join(
|
|
1001
|
-
await this.getTinaFolderPath(this.contentRootPath
|
|
1051
|
+
await this.getTinaFolderPath(this.contentRootPath, {
|
|
1052
|
+
isContentRoot: this.hasSeparateContentRoot()
|
|
1053
|
+
}),
|
|
1002
1054
|
GENERATED_FOLDER
|
|
1003
1055
|
);
|
|
1004
1056
|
this.spaMainPath = require2.resolve("@tinacms/app");
|
|
1005
1057
|
this.spaRootPath = path3.join(this.spaMainPath, "..", "..");
|
|
1006
1058
|
}
|
|
1007
|
-
async getTinaFolderPath(rootPath) {
|
|
1059
|
+
async getTinaFolderPath(rootPath, { isContentRoot } = {}) {
|
|
1008
1060
|
const tinaFolderPath = path3.join(rootPath, TINA_FOLDER);
|
|
1009
1061
|
const tinaFolderExists = await fs2.pathExists(tinaFolderPath);
|
|
1010
1062
|
if (tinaFolderExists) {
|
|
@@ -1017,6 +1069,11 @@ var ConfigManager = class {
|
|
|
1017
1069
|
this.isUsingLegacyFolder = true;
|
|
1018
1070
|
return legacyFolderPath;
|
|
1019
1071
|
}
|
|
1072
|
+
if (isContentRoot) {
|
|
1073
|
+
throw new Error(
|
|
1074
|
+
`Unable to find a ${chalk3.cyan("tina/")} folder in your content root at ${chalk3.cyan(rootPath)}. When using localContentPath, the content directory must contain a ${chalk3.cyan("tina/")} folder for generated files. Create one with: mkdir ${path3.join(rootPath, TINA_FOLDER)}`
|
|
1075
|
+
);
|
|
1076
|
+
}
|
|
1020
1077
|
throw new Error(
|
|
1021
1078
|
`Unable to find Tina folder, if you're working in folder outside of the Tina config be sure to specify --rootPath`
|
|
1022
1079
|
);
|
|
@@ -1207,15 +1264,15 @@ var loaders = {
|
|
|
1207
1264
|
};
|
|
1208
1265
|
|
|
1209
1266
|
// src/next/database.ts
|
|
1267
|
+
import { createServer } from "net";
|
|
1210
1268
|
import {
|
|
1211
|
-
createDatabaseInternal,
|
|
1212
1269
|
FilesystemBridge,
|
|
1213
|
-
TinaLevelClient
|
|
1270
|
+
TinaLevelClient,
|
|
1271
|
+
createDatabaseInternal
|
|
1214
1272
|
} from "@tinacms/graphql";
|
|
1215
|
-
import { pipeline } from "readable-stream";
|
|
1216
|
-
import { createServer } from "net";
|
|
1217
1273
|
import { ManyLevelHost } from "many-level";
|
|
1218
1274
|
import { MemoryLevel } from "memory-level";
|
|
1275
|
+
import { pipeline } from "readable-stream";
|
|
1219
1276
|
var createDBServer = (port) => {
|
|
1220
1277
|
const levelHost = new ManyLevelHost(
|
|
1221
1278
|
// @ts-ignore
|
|
@@ -1234,7 +1291,7 @@ var createDBServer = (port) => {
|
|
|
1234
1291
|
);
|
|
1235
1292
|
}
|
|
1236
1293
|
});
|
|
1237
|
-
dbServer.listen(port);
|
|
1294
|
+
dbServer.listen(port, "localhost");
|
|
1238
1295
|
};
|
|
1239
1296
|
async function createAndInitializeDatabase(configManager, datalayerPort, bridgeOverride) {
|
|
1240
1297
|
let database;
|
|
@@ -1538,6 +1595,66 @@ import {
|
|
|
1538
1595
|
splitVendorChunkPlugin
|
|
1539
1596
|
} from "vite";
|
|
1540
1597
|
|
|
1598
|
+
// src/next/vite/cors.ts
|
|
1599
|
+
var LOCALHOST_RE = /^https?:\/\/(?:localhost|127\.0\.0\.1|\[::1\])(:\d+)?$/;
|
|
1600
|
+
var PRIVATE_NETWORK_RE = /^https?:\/\/(?:10\.\d{1,3}\.\d{1,3}\.\d{1,3}|172\.(?:1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3})(:\d+)?$/;
|
|
1601
|
+
function expandOrigins(raw) {
|
|
1602
|
+
const hasPrivate = raw.some((o) => o === "private");
|
|
1603
|
+
const filtered = raw.filter((o) => o !== "private");
|
|
1604
|
+
return hasPrivate ? [...filtered, PRIVATE_NETWORK_RE] : filtered;
|
|
1605
|
+
}
|
|
1606
|
+
function buildCorsOriginCheck(allowedOrigins = []) {
|
|
1607
|
+
const extra = expandOrigins(allowedOrigins);
|
|
1608
|
+
return (origin, callback) => {
|
|
1609
|
+
if (!origin) {
|
|
1610
|
+
callback(null, true);
|
|
1611
|
+
return;
|
|
1612
|
+
}
|
|
1613
|
+
if (LOCALHOST_RE.test(origin)) {
|
|
1614
|
+
callback(null, true);
|
|
1615
|
+
return;
|
|
1616
|
+
}
|
|
1617
|
+
for (const allowed of extra) {
|
|
1618
|
+
if (typeof allowed === "string") {
|
|
1619
|
+
if (allowed === origin) {
|
|
1620
|
+
callback(null, true);
|
|
1621
|
+
return;
|
|
1622
|
+
}
|
|
1623
|
+
} else {
|
|
1624
|
+
allowed.lastIndex = 0;
|
|
1625
|
+
if (allowed.test(origin)) {
|
|
1626
|
+
callback(null, true);
|
|
1627
|
+
return;
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
callback(null, false);
|
|
1632
|
+
};
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
// src/next/vite/filterPublicEnv.ts
|
|
1636
|
+
function filterPublicEnv(env = process.env) {
|
|
1637
|
+
const publicEnv = {};
|
|
1638
|
+
Object.keys(env).forEach((key) => {
|
|
1639
|
+
if (key.startsWith("TINA_PUBLIC_") || key.startsWith("NEXT_PUBLIC_") || key === "NODE_ENV" || key === "HEAD") {
|
|
1640
|
+
try {
|
|
1641
|
+
const value = env[key];
|
|
1642
|
+
if (typeof value === "string") {
|
|
1643
|
+
publicEnv[key] = value;
|
|
1644
|
+
} else {
|
|
1645
|
+
publicEnv[key] = JSON.stringify(value);
|
|
1646
|
+
}
|
|
1647
|
+
} catch (error) {
|
|
1648
|
+
console.warn(
|
|
1649
|
+
`Could not stringify public env process.env.${key} env variable`
|
|
1650
|
+
);
|
|
1651
|
+
console.warn(error);
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
});
|
|
1655
|
+
return publicEnv;
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1541
1658
|
// src/next/vite/tailwind.ts
|
|
1542
1659
|
import path4 from "node:path";
|
|
1543
1660
|
import aspectRatio from "@tailwindcss/aspect-ratio";
|
|
@@ -1870,23 +1987,7 @@ var createConfig = async ({
|
|
|
1870
1987
|
noWatch,
|
|
1871
1988
|
rollupOptions
|
|
1872
1989
|
}) => {
|
|
1873
|
-
const publicEnv =
|
|
1874
|
-
Object.keys(process.env).forEach((key) => {
|
|
1875
|
-
if (key.startsWith("TINA_PUBLIC_") || key.startsWith("NEXT_PUBLIC_") || key === "NODE_ENV" || key === "HEAD") {
|
|
1876
|
-
try {
|
|
1877
|
-
if (typeof process.env[key] === "string") {
|
|
1878
|
-
publicEnv[key] = process.env[key];
|
|
1879
|
-
} else {
|
|
1880
|
-
publicEnv[key] = JSON.stringify(process.env[key]);
|
|
1881
|
-
}
|
|
1882
|
-
} catch (error) {
|
|
1883
|
-
console.warn(
|
|
1884
|
-
`Could not stringify public env process.env.${key} env variable`
|
|
1885
|
-
);
|
|
1886
|
-
console.warn(error);
|
|
1887
|
-
}
|
|
1888
|
-
}
|
|
1889
|
-
});
|
|
1990
|
+
const publicEnv = filterPublicEnv();
|
|
1890
1991
|
const staticMediaPath = path5.join(
|
|
1891
1992
|
configManager.generatedFolderPath,
|
|
1892
1993
|
"static-media.json"
|
|
@@ -1967,6 +2068,13 @@ var createConfig = async ({
|
|
|
1967
2068
|
},
|
|
1968
2069
|
server: {
|
|
1969
2070
|
host: configManager.config?.build?.host ?? false,
|
|
2071
|
+
// Restrict Vite's built-in CORS to the same origins our custom
|
|
2072
|
+
// middleware allows (localhost + user-configured allowedOrigins).
|
|
2073
|
+
cors: {
|
|
2074
|
+
origin: buildCorsOriginCheck(
|
|
2075
|
+
configManager.config?.server?.allowedOrigins
|
|
2076
|
+
)
|
|
2077
|
+
},
|
|
1970
2078
|
watch: noWatch ? {
|
|
1971
2079
|
ignored: ["**/*"]
|
|
1972
2080
|
} : {
|
|
@@ -1976,7 +2084,15 @@ var createConfig = async ({
|
|
|
1976
2084
|
]
|
|
1977
2085
|
},
|
|
1978
2086
|
fs: {
|
|
1979
|
-
strict:
|
|
2087
|
+
strict: true,
|
|
2088
|
+
// Allow serving files from the project root and the SPA package.
|
|
2089
|
+
// Without this, Vite would block access to tina config/generated
|
|
2090
|
+
// files since the Vite root is the @tinacms/app package directory.
|
|
2091
|
+
allow: [
|
|
2092
|
+
configManager.spaRootPath,
|
|
2093
|
+
configManager.rootPath,
|
|
2094
|
+
...configManager.contentRootPath && configManager.contentRootPath !== configManager.rootPath ? [configManager.contentRootPath] : []
|
|
2095
|
+
]
|
|
1980
2096
|
}
|
|
1981
2097
|
},
|
|
1982
2098
|
build: {
|
|
@@ -2006,19 +2122,19 @@ var createConfig = async ({
|
|
|
2006
2122
|
};
|
|
2007
2123
|
|
|
2008
2124
|
// src/next/vite/plugins.ts
|
|
2009
|
-
import { createFilter } from "@rollup/pluginutils";
|
|
2010
2125
|
import fs6 from "fs";
|
|
2011
|
-
import { transformWithEsbuild } from "vite";
|
|
2012
|
-
import { transform as esbuildTransform } from "esbuild";
|
|
2013
2126
|
import path7 from "path";
|
|
2127
|
+
import { createFilter } from "@rollup/pluginutils";
|
|
2128
|
+
import { resolve as gqlResolve } from "@tinacms/graphql";
|
|
2014
2129
|
import bodyParser from "body-parser";
|
|
2015
2130
|
import cors from "cors";
|
|
2016
|
-
import {
|
|
2131
|
+
import { transform as esbuildTransform } from "esbuild";
|
|
2132
|
+
import { transformWithEsbuild } from "vite";
|
|
2017
2133
|
|
|
2018
2134
|
// src/next/commands/dev-command/server/media.ts
|
|
2019
|
-
import fs5 from "fs-extra";
|
|
2020
2135
|
import path6, { join } from "path";
|
|
2021
2136
|
import busboy from "busboy";
|
|
2137
|
+
import fs5 from "fs-extra";
|
|
2022
2138
|
var createMediaRouter = (config2) => {
|
|
2023
2139
|
const mediaFolder = path6.join(
|
|
2024
2140
|
config2.rootPath,
|
|
@@ -2027,31 +2143,68 @@ var createMediaRouter = (config2) => {
|
|
|
2027
2143
|
);
|
|
2028
2144
|
const mediaModel = new MediaModel(config2);
|
|
2029
2145
|
const handleList = async (req, res) => {
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
cursor
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2146
|
+
try {
|
|
2147
|
+
const requestURL = new URL(req.url, config2.apiURL);
|
|
2148
|
+
const folder = decodeURIComponent(
|
|
2149
|
+
requestURL.pathname.replace("/media/list/", "")
|
|
2150
|
+
);
|
|
2151
|
+
const limit = requestURL.searchParams.get("limit");
|
|
2152
|
+
const cursor = requestURL.searchParams.get("cursor");
|
|
2153
|
+
const media = await mediaModel.listMedia({
|
|
2154
|
+
searchPath: folder,
|
|
2155
|
+
cursor,
|
|
2156
|
+
limit
|
|
2157
|
+
});
|
|
2158
|
+
res.end(JSON.stringify(media));
|
|
2159
|
+
} catch (error) {
|
|
2160
|
+
if (error instanceof PathTraversalError) {
|
|
2161
|
+
res.statusCode = 403;
|
|
2162
|
+
res.end(JSON.stringify({ error: error.message }));
|
|
2163
|
+
return;
|
|
2164
|
+
}
|
|
2165
|
+
throw error;
|
|
2166
|
+
}
|
|
2040
2167
|
};
|
|
2041
2168
|
const handleDelete = async (req, res) => {
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2169
|
+
try {
|
|
2170
|
+
const file = decodeURIComponent(req.url.slice("/media/".length));
|
|
2171
|
+
const didDelete = await mediaModel.deleteMedia({ searchPath: file });
|
|
2172
|
+
res.end(JSON.stringify(didDelete));
|
|
2173
|
+
} catch (error) {
|
|
2174
|
+
if (error instanceof PathTraversalError) {
|
|
2175
|
+
res.statusCode = 403;
|
|
2176
|
+
res.end(JSON.stringify({ error: error.message }));
|
|
2177
|
+
return;
|
|
2178
|
+
}
|
|
2179
|
+
throw error;
|
|
2180
|
+
}
|
|
2045
2181
|
};
|
|
2046
2182
|
const handlePost = async function(req, res) {
|
|
2047
2183
|
const bb = busboy({ headers: req.headers });
|
|
2184
|
+
let responded = false;
|
|
2048
2185
|
bb.on("file", async (_name, file, _info) => {
|
|
2049
|
-
const fullPath =
|
|
2050
|
-
|
|
2186
|
+
const fullPath = decodeURIComponent(
|
|
2187
|
+
req.url?.slice("/media/upload/".length)
|
|
2188
|
+
);
|
|
2189
|
+
let saveTo;
|
|
2190
|
+
try {
|
|
2191
|
+
saveTo = resolveStrictlyWithinBase(fullPath, mediaFolder);
|
|
2192
|
+
} catch {
|
|
2193
|
+
responded = true;
|
|
2194
|
+
file.resume();
|
|
2195
|
+
res.statusCode = 403;
|
|
2196
|
+
res.end(
|
|
2197
|
+
JSON.stringify({
|
|
2198
|
+
error: `Path traversal detected: ${fullPath}`
|
|
2199
|
+
})
|
|
2200
|
+
);
|
|
2201
|
+
return;
|
|
2202
|
+
}
|
|
2051
2203
|
await fs5.ensureDir(path6.dirname(saveTo));
|
|
2052
2204
|
file.pipe(fs5.createWriteStream(saveTo));
|
|
2053
2205
|
});
|
|
2054
2206
|
bb.on("error", (error) => {
|
|
2207
|
+
responded = true;
|
|
2055
2208
|
res.statusCode = 500;
|
|
2056
2209
|
if (error instanceof Error) {
|
|
2057
2210
|
res.end(JSON.stringify({ message: error }));
|
|
@@ -2060,6 +2213,7 @@ var createMediaRouter = (config2) => {
|
|
|
2060
2213
|
}
|
|
2061
2214
|
});
|
|
2062
2215
|
bb.on("close", () => {
|
|
2216
|
+
if (responded) return;
|
|
2063
2217
|
res.statusCode = 200;
|
|
2064
2218
|
res.end(JSON.stringify({ success: true }));
|
|
2065
2219
|
});
|
|
@@ -2074,6 +2228,55 @@ var parseMediaFolder = (str) => {
|
|
|
2074
2228
|
returnString = returnString.substr(0, returnString.length - 1);
|
|
2075
2229
|
return returnString;
|
|
2076
2230
|
};
|
|
2231
|
+
var ENCODED_TRAVERSAL_RE = /%2e%2e|%2f|%5c/i;
|
|
2232
|
+
function resolveRealPath(candidate) {
|
|
2233
|
+
try {
|
|
2234
|
+
return fs5.realpathSync(candidate);
|
|
2235
|
+
} catch {
|
|
2236
|
+
const parent = path6.dirname(candidate);
|
|
2237
|
+
if (parent === candidate) return candidate;
|
|
2238
|
+
return path6.join(resolveRealPath(parent), path6.basename(candidate));
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
function assertSymlinkWithinBase(resolved, resolvedBase, userPath) {
|
|
2242
|
+
try {
|
|
2243
|
+
const realBase = fs5.realpathSync(resolvedBase);
|
|
2244
|
+
const realResolved = resolveRealPath(resolved);
|
|
2245
|
+
if (realResolved !== realBase && !realResolved.startsWith(realBase + path6.sep)) {
|
|
2246
|
+
throw new PathTraversalError(userPath);
|
|
2247
|
+
}
|
|
2248
|
+
} catch (err) {
|
|
2249
|
+
if (err instanceof PathTraversalError) throw err;
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
function resolveWithinBase(userPath, baseDir) {
|
|
2253
|
+
if (ENCODED_TRAVERSAL_RE.test(userPath)) {
|
|
2254
|
+
throw new PathTraversalError(userPath);
|
|
2255
|
+
}
|
|
2256
|
+
const resolvedBase = path6.resolve(baseDir);
|
|
2257
|
+
const resolved = path6.resolve(path6.join(baseDir, userPath));
|
|
2258
|
+
if (resolved === resolvedBase) {
|
|
2259
|
+
assertSymlinkWithinBase(resolved, resolvedBase, userPath);
|
|
2260
|
+
return resolvedBase;
|
|
2261
|
+
}
|
|
2262
|
+
if (resolved.startsWith(resolvedBase + path6.sep)) {
|
|
2263
|
+
assertSymlinkWithinBase(resolved, resolvedBase, userPath);
|
|
2264
|
+
return resolved;
|
|
2265
|
+
}
|
|
2266
|
+
throw new PathTraversalError(userPath);
|
|
2267
|
+
}
|
|
2268
|
+
function resolveStrictlyWithinBase(userPath, baseDir) {
|
|
2269
|
+
if (ENCODED_TRAVERSAL_RE.test(userPath)) {
|
|
2270
|
+
throw new PathTraversalError(userPath);
|
|
2271
|
+
}
|
|
2272
|
+
const resolvedBase = path6.resolve(baseDir) + path6.sep;
|
|
2273
|
+
const resolved = path6.resolve(path6.join(baseDir, userPath));
|
|
2274
|
+
if (!resolved.startsWith(resolvedBase)) {
|
|
2275
|
+
throw new PathTraversalError(userPath);
|
|
2276
|
+
}
|
|
2277
|
+
assertSymlinkWithinBase(resolved, path6.resolve(baseDir), userPath);
|
|
2278
|
+
return resolved;
|
|
2279
|
+
}
|
|
2077
2280
|
var MediaModel = class {
|
|
2078
2281
|
rootPath;
|
|
2079
2282
|
publicFolder;
|
|
@@ -2085,22 +2288,18 @@ var MediaModel = class {
|
|
|
2085
2288
|
}
|
|
2086
2289
|
async listMedia(args) {
|
|
2087
2290
|
try {
|
|
2088
|
-
const
|
|
2089
|
-
|
|
2090
|
-
this.publicFolder,
|
|
2091
|
-
this.mediaRoot,
|
|
2092
|
-
decodeURIComponent(args.searchPath)
|
|
2093
|
-
);
|
|
2291
|
+
const mediaBase = join(this.rootPath, this.publicFolder, this.mediaRoot);
|
|
2292
|
+
const validatedPath = resolveWithinBase(args.searchPath, mediaBase);
|
|
2094
2293
|
const searchPath = parseMediaFolder(args.searchPath);
|
|
2095
|
-
if (!await fs5.pathExists(
|
|
2294
|
+
if (!await fs5.pathExists(validatedPath)) {
|
|
2096
2295
|
return {
|
|
2097
2296
|
files: [],
|
|
2098
2297
|
directories: []
|
|
2099
2298
|
};
|
|
2100
2299
|
}
|
|
2101
|
-
const filesStr = await fs5.readdir(
|
|
2300
|
+
const filesStr = await fs5.readdir(validatedPath);
|
|
2102
2301
|
const filesProm = filesStr.map(async (file) => {
|
|
2103
|
-
const filePath = join(
|
|
2302
|
+
const filePath = join(validatedPath, file);
|
|
2104
2303
|
const stat = await fs5.stat(filePath);
|
|
2105
2304
|
let src = `/${file}`;
|
|
2106
2305
|
const isFile = stat.isFile();
|
|
@@ -2147,6 +2346,7 @@ var MediaModel = class {
|
|
|
2147
2346
|
cursor
|
|
2148
2347
|
};
|
|
2149
2348
|
} catch (error) {
|
|
2349
|
+
if (error instanceof PathTraversalError) throw error;
|
|
2150
2350
|
console.error(error);
|
|
2151
2351
|
return {
|
|
2152
2352
|
files: [],
|
|
@@ -2157,16 +2357,13 @@ var MediaModel = class {
|
|
|
2157
2357
|
}
|
|
2158
2358
|
async deleteMedia(args) {
|
|
2159
2359
|
try {
|
|
2160
|
-
const
|
|
2161
|
-
|
|
2162
|
-
this.publicFolder,
|
|
2163
|
-
this.mediaRoot,
|
|
2164
|
-
decodeURIComponent(args.searchPath)
|
|
2165
|
-
);
|
|
2360
|
+
const mediaBase = join(this.rootPath, this.publicFolder, this.mediaRoot);
|
|
2361
|
+
const file = resolveStrictlyWithinBase(args.searchPath, mediaBase);
|
|
2166
2362
|
await fs5.stat(file);
|
|
2167
2363
|
await fs5.remove(file);
|
|
2168
2364
|
return { ok: true };
|
|
2169
2365
|
} catch (error) {
|
|
2366
|
+
if (error instanceof PathTraversalError) throw error;
|
|
2170
2367
|
console.error(error);
|
|
2171
2368
|
return { ok: false, message: error?.toString() };
|
|
2172
2369
|
}
|
|
@@ -2179,34 +2376,83 @@ var createSearchIndexRouter = ({
|
|
|
2179
2376
|
searchIndex
|
|
2180
2377
|
}) => {
|
|
2181
2378
|
const put = async (req, res) => {
|
|
2182
|
-
const
|
|
2379
|
+
const docs = req.body?.docs ?? [];
|
|
2183
2380
|
const result = await searchIndex.PUT(docs);
|
|
2184
2381
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2185
2382
|
res.end(JSON.stringify({ result }));
|
|
2186
2383
|
};
|
|
2187
2384
|
const get = async (req, res) => {
|
|
2188
|
-
const requestURL = new URL(req.url, config2.apiURL);
|
|
2385
|
+
const requestURL = new URL(req.url ?? "", config2.apiURL);
|
|
2386
|
+
const isV2 = requestURL.pathname.startsWith("/v2/searchIndex");
|
|
2387
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2388
|
+
if (isV2) {
|
|
2389
|
+
const queryParam = requestURL.searchParams.get("query");
|
|
2390
|
+
const collectionParam = requestURL.searchParams.get("collection");
|
|
2391
|
+
const limitParam = requestURL.searchParams.get("limit");
|
|
2392
|
+
const cursorParam = requestURL.searchParams.get("cursor");
|
|
2393
|
+
if (!queryParam) {
|
|
2394
|
+
res.end(JSON.stringify({ RESULT: [], RESULT_LENGTH: 0 }));
|
|
2395
|
+
return;
|
|
2396
|
+
}
|
|
2397
|
+
if (!searchIndex.fuzzySearchWrapper) {
|
|
2398
|
+
res.end(JSON.stringify({ RESULT: [], RESULT_LENGTH: 0 }));
|
|
2399
|
+
return;
|
|
2400
|
+
}
|
|
2401
|
+
try {
|
|
2402
|
+
const paginationOptions = {};
|
|
2403
|
+
if (limitParam) {
|
|
2404
|
+
paginationOptions.limit = parseInt(limitParam, 10);
|
|
2405
|
+
}
|
|
2406
|
+
if (cursorParam) {
|
|
2407
|
+
paginationOptions.cursor = cursorParam;
|
|
2408
|
+
}
|
|
2409
|
+
const searchQuery = collectionParam ? `${queryParam} _collection:${collectionParam}` : queryParam;
|
|
2410
|
+
const result2 = await searchIndex.fuzzySearchWrapper.query(searchQuery, {
|
|
2411
|
+
...paginationOptions
|
|
2412
|
+
});
|
|
2413
|
+
if (collectionParam) {
|
|
2414
|
+
result2.results = result2.results.filter(
|
|
2415
|
+
(r) => r._id && r._id.startsWith(`${collectionParam}:`)
|
|
2416
|
+
);
|
|
2417
|
+
}
|
|
2418
|
+
res.end(
|
|
2419
|
+
JSON.stringify({
|
|
2420
|
+
RESULT: result2.results,
|
|
2421
|
+
RESULT_LENGTH: result2.total,
|
|
2422
|
+
NEXT_CURSOR: result2.nextCursor,
|
|
2423
|
+
PREV_CURSOR: result2.prevCursor,
|
|
2424
|
+
FUZZY_MATCHES: result2.fuzzyMatches || {}
|
|
2425
|
+
})
|
|
2426
|
+
);
|
|
2427
|
+
return;
|
|
2428
|
+
} catch (error) {
|
|
2429
|
+
console.warn(
|
|
2430
|
+
"[search] v2 fuzzy search failed:",
|
|
2431
|
+
error instanceof Error ? error.message : error
|
|
2432
|
+
);
|
|
2433
|
+
res.end(JSON.stringify({ RESULT: [], RESULT_LENGTH: 0 }));
|
|
2434
|
+
return;
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2189
2437
|
const query = requestURL.searchParams.get("q");
|
|
2190
2438
|
const optionsParam = requestURL.searchParams.get("options");
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2439
|
+
if (!query) {
|
|
2440
|
+
res.end(JSON.stringify({ RESULT: [] }));
|
|
2441
|
+
return;
|
|
2442
|
+
}
|
|
2443
|
+
let searchIndexOptions = { DOCUMENTS: false };
|
|
2194
2444
|
if (optionsParam) {
|
|
2195
|
-
|
|
2196
|
-
...
|
|
2445
|
+
searchIndexOptions = {
|
|
2446
|
+
...searchIndexOptions,
|
|
2197
2447
|
...JSON.parse(optionsParam)
|
|
2198
2448
|
};
|
|
2199
2449
|
}
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
res.end(JSON.stringify(result));
|
|
2204
|
-
} else {
|
|
2205
|
-
res.end(JSON.stringify({ RESULT: [] }));
|
|
2206
|
-
}
|
|
2450
|
+
const queryObj = JSON.parse(query);
|
|
2451
|
+
const result = await searchIndex.QUERY(queryObj, searchIndexOptions);
|
|
2452
|
+
res.end(JSON.stringify(result));
|
|
2207
2453
|
};
|
|
2208
2454
|
const del = async (req, res) => {
|
|
2209
|
-
const requestURL = new URL(req.url, config2.apiURL);
|
|
2455
|
+
const requestURL = new URL(req.url ?? "", config2.apiURL);
|
|
2210
2456
|
const docId = requestURL.pathname.split("/").filter(Boolean).slice(1).join("/");
|
|
2211
2457
|
const result = await searchIndex.DELETE(docId);
|
|
2212
2458
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
@@ -2240,10 +2486,18 @@ var devServerEndPointsPlugin = ({
|
|
|
2240
2486
|
searchIndex,
|
|
2241
2487
|
databaseLock
|
|
2242
2488
|
}) => {
|
|
2489
|
+
const corsOriginCheck = buildCorsOriginCheck(
|
|
2490
|
+
configManager.config?.server?.allowedOrigins
|
|
2491
|
+
);
|
|
2243
2492
|
const plug = {
|
|
2244
2493
|
name: "graphql-endpoints",
|
|
2245
2494
|
configureServer(server) {
|
|
2246
|
-
server.middlewares.use(
|
|
2495
|
+
server.middlewares.use(
|
|
2496
|
+
cors({
|
|
2497
|
+
origin: corsOriginCheck,
|
|
2498
|
+
methods: ["GET", "HEAD", "PUT", "PATCH", "POST", "DELETE"]
|
|
2499
|
+
})
|
|
2500
|
+
);
|
|
2247
2501
|
server.middlewares.use(bodyParser.json({ limit: "5mb" }));
|
|
2248
2502
|
server.middlewares.use(async (req, res, next) => {
|
|
2249
2503
|
const mediaPaths = configManager.config.media?.tina;
|
|
@@ -2296,7 +2550,7 @@ var devServerEndPointsPlugin = ({
|
|
|
2296
2550
|
res.end(JSON.stringify(result));
|
|
2297
2551
|
return;
|
|
2298
2552
|
}
|
|
2299
|
-
if (req.url.startsWith("/searchIndex")) {
|
|
2553
|
+
if (req.url.startsWith("/searchIndex") || req.url.startsWith("/v2/searchIndex")) {
|
|
2300
2554
|
if (req.method === "POST") {
|
|
2301
2555
|
await searchIndexRouter.put(req, res);
|
|
2302
2556
|
} else if (req.method === "GET") {
|
|
@@ -2487,7 +2741,8 @@ var DevCommand = class extends BaseCommand {
|
|
|
2487
2741
|
);
|
|
2488
2742
|
if (configManager.hasSeparateContentRoot()) {
|
|
2489
2743
|
const rootPath = await configManager.getTinaFolderPath(
|
|
2490
|
-
configManager.contentRootPath
|
|
2744
|
+
configManager.contentRootPath,
|
|
2745
|
+
{ isContentRoot: true }
|
|
2491
2746
|
);
|
|
2492
2747
|
const filePath = path8.join(rootPath, tinaLockFilename);
|
|
2493
2748
|
await fs7.ensureFile(filePath);
|
|
@@ -2577,15 +2832,24 @@ ${dangerText(e.message)}
|
|
|
2577
2832
|
configManager.config.search && searchIndexer
|
|
2578
2833
|
);
|
|
2579
2834
|
}
|
|
2835
|
+
const searchIndexWithFuzzy = searchIndexClient.searchIndex;
|
|
2836
|
+
if (searchIndexWithFuzzy && searchIndexClient.fuzzySearchWrapper) {
|
|
2837
|
+
searchIndexWithFuzzy.fuzzySearchWrapper = searchIndexClient.fuzzySearchWrapper;
|
|
2838
|
+
}
|
|
2580
2839
|
const server = await createDevServer(
|
|
2581
2840
|
configManager,
|
|
2582
2841
|
database,
|
|
2583
|
-
|
|
2842
|
+
searchIndexWithFuzzy,
|
|
2584
2843
|
apiURL,
|
|
2585
2844
|
this.noWatch,
|
|
2586
2845
|
dbLock
|
|
2587
2846
|
);
|
|
2588
2847
|
await server.listen(Number(this.port));
|
|
2848
|
+
if (isHostExposed(server.config.server.host)) {
|
|
2849
|
+
logger.warn(
|
|
2850
|
+
"\u26A0\uFE0F The TinaCMS dev server is listening on a non-localhost address. It has no authentication and is not intended to be exposed to the internet."
|
|
2851
|
+
);
|
|
2852
|
+
}
|
|
2589
2853
|
if (!this.noWatch) {
|
|
2590
2854
|
chokidar.watch(configManager.watchList).on("change", async () => {
|
|
2591
2855
|
await dbLock(async () => {
|
|
@@ -2659,6 +2923,13 @@ ${dangerText(e.message)}
|
|
|
2659
2923
|
// },
|
|
2660
2924
|
]
|
|
2661
2925
|
});
|
|
2926
|
+
if (configManager?.config?.telemetry === "anonymous") {
|
|
2927
|
+
logger.info(
|
|
2928
|
+
`
|
|
2929
|
+
\u{1F4CA} Note: TinaCMS now collects anonymous telemetry regarding usage. More information on TinaCMS Telemetry: https://tina.io/telemetry
|
|
2930
|
+
`
|
|
2931
|
+
);
|
|
2932
|
+
}
|
|
2662
2933
|
await this.startSubCommand();
|
|
2663
2934
|
}
|
|
2664
2935
|
watchContentFiles(configManager, database, databaseLock, searchIndexer) {
|
|
@@ -2759,23 +3030,6 @@ async function sleepAndCallFunc({
|
|
|
2759
3030
|
// src/next/commands/build-command/server.ts
|
|
2760
3031
|
import { build as build2 } from "vite";
|
|
2761
3032
|
var buildProductionSpa = async (configManager, database, apiURL) => {
|
|
2762
|
-
const publicEnv = {};
|
|
2763
|
-
Object.keys(process.env).forEach((key) => {
|
|
2764
|
-
if (key.startsWith("TINA_PUBLIC_") || key.startsWith("NEXT_PUBLIC_") || key === "NODE_ENV" || key === "HEAD") {
|
|
2765
|
-
try {
|
|
2766
|
-
if (typeof process.env[key] === "string") {
|
|
2767
|
-
publicEnv[key] = process.env[key];
|
|
2768
|
-
} else {
|
|
2769
|
-
publicEnv[key] = JSON.stringify(process.env[key]);
|
|
2770
|
-
}
|
|
2771
|
-
} catch (error) {
|
|
2772
|
-
console.warn(
|
|
2773
|
-
`Could not stringify public env process.env.${key} env variable`
|
|
2774
|
-
);
|
|
2775
|
-
console.warn(error);
|
|
2776
|
-
}
|
|
2777
|
-
}
|
|
2778
|
-
});
|
|
2779
3033
|
const config2 = await createConfig({
|
|
2780
3034
|
plugins: [transformTsxPlugin({ configManager }), viteTransformExtension()],
|
|
2781
3035
|
configManager,
|
|
@@ -5715,6 +5969,12 @@ import { LocalAuthProvider } from "tinacms";`;
|
|
|
5715
5969
|
outputFolder: "admin",
|
|
5716
5970
|
publicFolder: "${args.publicFolder}",
|
|
5717
5971
|
},
|
|
5972
|
+
// Uncomment to allow cross-origin requests from non-localhost origins
|
|
5973
|
+
// during local development (e.g. GitHub Codespaces, Gitpod, Docker).
|
|
5974
|
+
// Use 'private' to allow all private-network IPs (WSL2, Docker, etc.)
|
|
5975
|
+
// server: {
|
|
5976
|
+
// allowedOrigins: ['https://your-codespace.github.dev'],
|
|
5977
|
+
// },
|
|
5718
5978
|
media: {
|
|
5719
5979
|
tina: {
|
|
5720
5980
|
mediaRoot: "",
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strips `indexerToken` from `search.tina` before serialization to
|
|
3
|
+
* _schema.json / tina-lock.json.
|
|
4
|
+
*
|
|
5
|
+
* @see https://github.com/tinacms/tinacms/security/advisories/GHSA-4qrm-9h4r-v2fx
|
|
6
|
+
*/
|
|
7
|
+
export declare function stripSearchTokenFromConfig<T extends object>(config: T): T;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { Connect } from 'vite';
|
|
2
1
|
import type { ServerResponse } from 'http';
|
|
2
|
+
import type { Connect } from 'vite';
|
|
3
3
|
export declare const createMediaRouter: (config: PathConfig) => {
|
|
4
4
|
handleList: (req: any, res: any) => Promise<void>;
|
|
5
5
|
handleDelete: (req: Connect.IncomingMessage, res: any) => Promise<void>;
|
|
@@ -34,6 +34,23 @@ type SuccessRecord = {
|
|
|
34
34
|
ok: false;
|
|
35
35
|
message: string;
|
|
36
36
|
};
|
|
37
|
+
/**
|
|
38
|
+
* Handles media file operations (list, delete) for the Vite-based dev server.
|
|
39
|
+
*
|
|
40
|
+
* @security Every method that accepts a user-supplied `searchPath` validates
|
|
41
|
+
* it against the media root using `resolveWithinBase` (list) or
|
|
42
|
+
* `resolveStrictlyWithinBase` (delete) before any filesystem access.
|
|
43
|
+
*
|
|
44
|
+
* - **list** uses `resolveWithinBase` because listing the media root itself
|
|
45
|
+
* (empty path / exact base match) is a valid operation.
|
|
46
|
+
* - **delete** uses `resolveStrictlyWithinBase` because deleting the media
|
|
47
|
+
* root directory itself must never be allowed.
|
|
48
|
+
*
|
|
49
|
+
* Both methods catch `PathTraversalError` and re-throw it so that the
|
|
50
|
+
* route handler can return a 403 response. Other errors are caught and
|
|
51
|
+
* returned as structured error responses (this avoids leaking stack traces
|
|
52
|
+
* to the client).
|
|
53
|
+
*/
|
|
37
54
|
export declare class MediaModel {
|
|
38
55
|
readonly rootPath: string;
|
|
39
56
|
readonly publicFolder: string;
|
|
@@ -1,12 +1,47 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
import type { SearchQueryResponse, SearchResult } from '@tinacms/search';
|
|
1
3
|
export interface PathConfig {
|
|
2
4
|
apiURL: string;
|
|
3
5
|
searchPath: string;
|
|
4
6
|
}
|
|
7
|
+
interface SearchIndexOptions {
|
|
8
|
+
DOCUMENTS?: boolean;
|
|
9
|
+
PAGE?: {
|
|
10
|
+
NUMBER: number;
|
|
11
|
+
SIZE: number;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
interface SearchIndexResult {
|
|
15
|
+
RESULT: SearchResult[];
|
|
16
|
+
RESULT_LENGTH: number;
|
|
17
|
+
}
|
|
18
|
+
interface FuzzySearchWrapper {
|
|
19
|
+
query: (query: string, options: {
|
|
20
|
+
limit?: number;
|
|
21
|
+
cursor?: string;
|
|
22
|
+
fuzzyOptions?: Record<string, unknown>;
|
|
23
|
+
}) => Promise<SearchQueryResponse>;
|
|
24
|
+
}
|
|
25
|
+
interface SearchIndex {
|
|
26
|
+
PUT: (docs: Record<string, unknown>[]) => Promise<unknown>;
|
|
27
|
+
DELETE: (id: string) => Promise<unknown>;
|
|
28
|
+
QUERY: (query: {
|
|
29
|
+
AND?: string[];
|
|
30
|
+
OR?: string[];
|
|
31
|
+
}, options: SearchIndexOptions) => Promise<SearchIndexResult>;
|
|
32
|
+
fuzzySearchWrapper?: FuzzySearchWrapper;
|
|
33
|
+
}
|
|
34
|
+
interface RequestWithBody extends IncomingMessage {
|
|
35
|
+
body?: {
|
|
36
|
+
docs?: Record<string, unknown>[];
|
|
37
|
+
};
|
|
38
|
+
}
|
|
5
39
|
export declare const createSearchIndexRouter: ({ config, searchIndex, }: {
|
|
6
40
|
config: PathConfig;
|
|
7
|
-
searchIndex:
|
|
41
|
+
searchIndex: SearchIndex;
|
|
8
42
|
}) => {
|
|
9
|
-
del: (req:
|
|
10
|
-
get: (req:
|
|
11
|
-
put: (req:
|
|
43
|
+
del: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
44
|
+
get: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
45
|
+
put: (req: RequestWithBody, res: ServerResponse) => Promise<void>;
|
|
12
46
|
};
|
|
47
|
+
export {};
|
|
@@ -54,7 +54,9 @@ export declare class ConfigManager {
|
|
|
54
54
|
hasSeparateContentRoot(): boolean;
|
|
55
55
|
shouldSkipSDK(): boolean;
|
|
56
56
|
processConfig(): Promise<void>;
|
|
57
|
-
getTinaFolderPath(rootPath:
|
|
57
|
+
getTinaFolderPath(rootPath: string, { isContentRoot }?: {
|
|
58
|
+
isContentRoot?: boolean;
|
|
59
|
+
}): Promise<string>;
|
|
58
60
|
getTinaGraphQLVersion(): {
|
|
59
61
|
fullVersion: string;
|
|
60
62
|
major: string;
|
package/dist/next/database.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Bridge, Database } from '@tinacms/graphql';
|
|
2
2
|
import { ConfigManager } from './config-manager';
|
|
3
3
|
export declare const createDBServer: (port: number) => void;
|
|
4
4
|
export declare function createAndInitializeDatabase(configManager: ConfigManager, datalayerPort: number, bridgeOverride?: Bridge): Promise<Database>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared CORS origin-checking logic for the TinaCMS dev server.
|
|
3
|
+
*
|
|
4
|
+
* By default only localhost / 127.0.0.1 / [::1] (any port) are allowed.
|
|
5
|
+
* Users can extend this via `server.allowedOrigins` in their tina config.
|
|
6
|
+
* The special keyword `'private'` expands to all RFC 1918 private-network
|
|
7
|
+
* IP ranges (10.x, 172.16-31.x, 192.168.x) — useful for WSL2, Docker
|
|
8
|
+
* bridge networks, etc.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Build a CORS `origin` callback compatible with the `cors` npm package.
|
|
12
|
+
*/
|
|
13
|
+
export declare function buildCorsOriginCheck(allowedOrigins?: (string | RegExp)[]): (origin: string | undefined, callback: (err: Error | null, allow?: boolean) => void) => void;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filters env vars to only those safe for client-side bundles.
|
|
3
|
+
*
|
|
4
|
+
* Allows: TINA_PUBLIC_*, NEXT_PUBLIC_*, NODE_ENV, HEAD.
|
|
5
|
+
* Everything else is excluded to prevent leaking secrets.
|
|
6
|
+
*
|
|
7
|
+
* @see https://github.com/tinacms/tinacms/security/advisories/GHSA-pc2q-jcxq-rjrr
|
|
8
|
+
*/
|
|
9
|
+
export declare function filterPublicEnv(env?: Record<string, string | undefined>): Record<string, string>;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type { Plugin } from 'vite';
|
|
2
1
|
import { FilterPattern } from '@rollup/pluginutils';
|
|
3
2
|
import type { Config } from '@svgr/core';
|
|
4
|
-
import { transformWithEsbuild } from 'vite';
|
|
5
3
|
import type { Database } from '@tinacms/graphql';
|
|
4
|
+
import type { Plugin } from 'vite';
|
|
5
|
+
import { transformWithEsbuild } from 'vite';
|
|
6
6
|
import type { ConfigManager } from '../config-manager';
|
|
7
7
|
export declare const transformTsxPlugin: ({ configManager: _configManager, }: {
|
|
8
8
|
configManager: ConfigManager;
|
|
@@ -28,6 +28,23 @@ type SuccessRecord = {
|
|
|
28
28
|
ok: false;
|
|
29
29
|
message: string;
|
|
30
30
|
};
|
|
31
|
+
/**
|
|
32
|
+
* Handles media file operations (list, delete) for the Express-based server.
|
|
33
|
+
*
|
|
34
|
+
* @security Every method that accepts a user-supplied `searchPath` validates
|
|
35
|
+
* it against the media root using `resolveWithinBase` (list) or
|
|
36
|
+
* `resolveStrictlyWithinBase` (delete) before any filesystem access.
|
|
37
|
+
*
|
|
38
|
+
* - **list** uses `resolveWithinBase` because listing the media root itself
|
|
39
|
+
* (empty path / exact base match) is a valid operation.
|
|
40
|
+
* - **delete** uses `resolveStrictlyWithinBase` because deleting the media
|
|
41
|
+
* root directory itself must never be allowed.
|
|
42
|
+
*
|
|
43
|
+
* Both methods catch `PathTraversalError` and re-throw it so that the
|
|
44
|
+
* route handler can return a 403 response. Other errors are caught and
|
|
45
|
+
* returned as structured error responses (this avoids leaking stack traces
|
|
46
|
+
* to the client).
|
|
47
|
+
*/
|
|
31
48
|
export declare class MediaModel {
|
|
32
49
|
readonly rootPath: string;
|
|
33
50
|
readonly publicFolder: string;
|
package/dist/utils/path.d.ts
CHANGED
|
@@ -1,3 +1,29 @@
|
|
|
1
1
|
/** Removes trailing slash from path. Separator to remove is chosen based on
|
|
2
2
|
* operating system. */
|
|
3
3
|
export declare function stripNativeTrailingSlash(p: string): string;
|
|
4
|
+
/**
|
|
5
|
+
* Validates that a user-supplied path does not escape the base directory
|
|
6
|
+
* via path traversal (CWE-22). Returns the resolved absolute path.
|
|
7
|
+
*
|
|
8
|
+
* Allows an exact base match (empty or `.` input) — use this for list/read
|
|
9
|
+
* operations where referencing the root directory itself is valid. For
|
|
10
|
+
* delete/write operations where you need to target an actual file, use the
|
|
11
|
+
* `resolveStrictlyWithinBase` variant (currently inlined in media models).
|
|
12
|
+
*
|
|
13
|
+
* As a safety net, also rejects paths that still contain URL-encoded
|
|
14
|
+
* traversal sequences (`%2e%2e`, `%2f`, `%5c`), catching cases where the
|
|
15
|
+
* caller forgot to decode.
|
|
16
|
+
*
|
|
17
|
+
* @security This is the canonical implementation. Inlined copies exist in
|
|
18
|
+
* the media model files for CodeQL compatibility — keep them in sync.
|
|
19
|
+
*
|
|
20
|
+
* @param userPath - The untrusted path from the request (must already be
|
|
21
|
+
* URI-decoded by the caller).
|
|
22
|
+
* @param baseDir - The trusted base directory the path must stay within.
|
|
23
|
+
* @returns The resolved absolute path.
|
|
24
|
+
* @throws {PathTraversalError} If the path escapes the base directory.
|
|
25
|
+
*/
|
|
26
|
+
export declare function assertPathWithinBase(userPath: string, baseDir: string): string;
|
|
27
|
+
export declare class PathTraversalError extends Error {
|
|
28
|
+
constructor(attemptedPath: string);
|
|
29
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tinacms/cli",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0.0-
|
|
4
|
+
"version": "0.0.0-e797ebd-20260331104743",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"typings": "dist/index.d.ts",
|
|
7
7
|
"files": [
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"@types/progress": "^2.0.7",
|
|
42
42
|
"@types/prompts": "^2.4.9",
|
|
43
43
|
"jest": "^29.7.0",
|
|
44
|
-
"@tinacms/scripts": "1.
|
|
44
|
+
"@tinacms/scripts": "1.6.0"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@graphql-codegen/core": "^2.6.8",
|
|
@@ -88,12 +88,12 @@
|
|
|
88
88
|
"vite": "^4.5.9",
|
|
89
89
|
"yup": "^1.6.1",
|
|
90
90
|
"zod": "^3.24.2",
|
|
91
|
-
"@tinacms/app": "0.0.0-
|
|
92
|
-
"@tinacms/graphql": "2.
|
|
91
|
+
"@tinacms/app": "0.0.0-e797ebd-20260331104743",
|
|
92
|
+
"@tinacms/graphql": "2.2.2",
|
|
93
93
|
"@tinacms/metrics": "2.0.1",
|
|
94
|
-
"@tinacms/schema-tools": "2.
|
|
95
|
-
"tinacms": "
|
|
96
|
-
"
|
|
94
|
+
"@tinacms/schema-tools": "2.7.0",
|
|
95
|
+
"@tinacms/search": "1.2.8",
|
|
96
|
+
"tinacms": "0.0.0-e797ebd-20260331104743"
|
|
97
97
|
},
|
|
98
98
|
"publishConfig": {
|
|
99
99
|
"registry": "https://registry.npmjs.org"
|