@easy-cms/core 0.1.0 → 0.2.0
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.d.ts +8 -1
- package/dist/index.js +84 -8
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -412,6 +412,12 @@ interface Config {
|
|
|
412
412
|
* so frontends on other origins can use them. Default: relative URLs.
|
|
413
413
|
*/
|
|
414
414
|
readonly serverURL?: string;
|
|
415
|
+
/**
|
|
416
|
+
* Origins whose browser code may call the REST API (CORS), e.g. a Vite or React app on
|
|
417
|
+
* another origin. `'*'` allows any origin for anonymous requests. Origins in
|
|
418
|
+
* `auth.trustedOrigins` are always allowed, with cookies. Default: none.
|
|
419
|
+
*/
|
|
420
|
+
readonly cors?: readonly string[] | '*';
|
|
415
421
|
readonly routes?: RoutesConfig;
|
|
416
422
|
readonly admin?: AdminConfig;
|
|
417
423
|
readonly upload?: UploadConfig;
|
|
@@ -425,7 +431,8 @@ interface Config {
|
|
|
425
431
|
readonly plugins?: readonly Plugin[];
|
|
426
432
|
}
|
|
427
433
|
/** The config after plugins ran, validation passed and defaults were applied. */
|
|
428
|
-
interface ResolvedConfig extends Omit<Config, 'routes' | 'admin' | 'upload' | 'auth' | 'collections' | 'globals' | 'plugins'> {
|
|
434
|
+
interface ResolvedConfig extends Omit<Config, 'cors' | 'routes' | 'admin' | 'upload' | 'auth' | 'collections' | 'globals' | 'plugins'> {
|
|
435
|
+
readonly cors: readonly string[] | '*';
|
|
429
436
|
readonly routes: Required<RoutesConfig>;
|
|
430
437
|
readonly admin: Required<AdminConfig>;
|
|
431
438
|
readonly upload: Required<Omit<UploadConfig, 'storage'>> & Pick<UploadConfig, 'storage'>;
|
package/dist/index.js
CHANGED
|
@@ -867,6 +867,7 @@ function validateConfig(config) {
|
|
|
867
867
|
validateAdmin(config, add);
|
|
868
868
|
validateUpload(config, add);
|
|
869
869
|
validateAuth(config, add);
|
|
870
|
+
validateCors(config.cors, add);
|
|
870
871
|
const collections = asArray(config.collections, "collections", add);
|
|
871
872
|
const globals = asArray(config.globals, "globals", add);
|
|
872
873
|
const collectionSlugs = /* @__PURE__ */ new Set([...BUILTIN_COLLECTIONS]);
|
|
@@ -896,7 +897,11 @@ function validateConfig(config) {
|
|
|
896
897
|
function validateSecret(secret, add) {
|
|
897
898
|
const hint = `set EASY_CMS_SECRET to a random string of at least ${MIN_SECRET_LENGTH} characters, e.g. \`openssl rand -hex 32\``;
|
|
898
899
|
if (typeof secret !== "string" || secret.length === 0) {
|
|
899
|
-
add(
|
|
900
|
+
add(
|
|
901
|
+
"secret",
|
|
902
|
+
"is required",
|
|
903
|
+
`${hint}. In production, set it in the server's environment: not every server reads .env (Nuxt's does not)`
|
|
904
|
+
);
|
|
900
905
|
} else if (secret.length < MIN_SECRET_LENGTH) {
|
|
901
906
|
add("secret", `must be at least ${MIN_SECRET_LENGTH} characters (got ${secret.length})`, hint);
|
|
902
907
|
}
|
|
@@ -939,6 +944,21 @@ function validateAuth(config, add) {
|
|
|
939
944
|
}
|
|
940
945
|
}
|
|
941
946
|
}
|
|
947
|
+
function validateCors(cors, add) {
|
|
948
|
+
if (cors === void 0 || cors === "*") return;
|
|
949
|
+
if (!Array.isArray(cors)) {
|
|
950
|
+
add("cors", 'must be a list of origins or "*"', 'cors: ["https://my-site.com"]');
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
for (const [i, origin] of cors.entries()) {
|
|
954
|
+
if (typeof origin !== "string" || !/^https?:\/\/[^/]+$/.test(origin)) {
|
|
955
|
+
add(
|
|
956
|
+
`cors[${i}]`,
|
|
957
|
+
`must be an origin like "https://example.com" (got ${JSON.stringify(origin)})`
|
|
958
|
+
);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
}
|
|
942
962
|
function validateUpload(config, add) {
|
|
943
963
|
const upload = config.upload;
|
|
944
964
|
if (upload === void 0) return;
|
|
@@ -1121,6 +1141,7 @@ async function resolveConfig(input) {
|
|
|
1121
1141
|
const { plugins: _plugins, ...rest } = config;
|
|
1122
1142
|
const result = {
|
|
1123
1143
|
...rest,
|
|
1144
|
+
cors: config.cors ?? [],
|
|
1124
1145
|
routes: { api: `/${(config.routes?.api ?? DEFAULT_API_PATH).replace(/^\/+|\/+$/g, "")}` },
|
|
1125
1146
|
admin: {
|
|
1126
1147
|
path: config.admin?.path ?? DEFAULT_ADMIN_PATH,
|
|
@@ -1156,8 +1177,15 @@ var CONFIG_FILE_NAMES = [
|
|
|
1156
1177
|
];
|
|
1157
1178
|
function findConfigFile(cwd = process.cwd()) {
|
|
1158
1179
|
for (const name of CONFIG_FILE_NAMES) {
|
|
1159
|
-
const path = resolve(
|
|
1160
|
-
|
|
1180
|
+
const path = resolve(
|
|
1181
|
+
/* turbopackIgnore: true */
|
|
1182
|
+
cwd,
|
|
1183
|
+
name
|
|
1184
|
+
);
|
|
1185
|
+
if (existsSync(
|
|
1186
|
+
/* turbopackIgnore: true */
|
|
1187
|
+
path
|
|
1188
|
+
)) return path;
|
|
1161
1189
|
}
|
|
1162
1190
|
return void 0;
|
|
1163
1191
|
}
|
|
@@ -1361,9 +1389,16 @@ function collectSlots(fields, data, bySlug, slots) {
|
|
|
1361
1389
|
import { mkdir, readFile, rm, stat, writeFile } from "fs/promises";
|
|
1362
1390
|
import { dirname, isAbsolute, join, normalize, resolve as resolve2, sep } from "path";
|
|
1363
1391
|
function localStorage(options = {}) {
|
|
1364
|
-
let root = resolve2(
|
|
1392
|
+
let root = resolve2(
|
|
1393
|
+
/* turbopackIgnore: true */
|
|
1394
|
+
options.dir ?? "uploads"
|
|
1395
|
+
);
|
|
1365
1396
|
const pathOf = (key2) => {
|
|
1366
|
-
const path = normalize(join(
|
|
1397
|
+
const path = normalize(join(
|
|
1398
|
+
/* turbopackIgnore: true */
|
|
1399
|
+
root,
|
|
1400
|
+
key2
|
|
1401
|
+
));
|
|
1367
1402
|
if (!path.startsWith(root + sep)) throw new Error(`Invalid storage key "${key2}"`);
|
|
1368
1403
|
return path;
|
|
1369
1404
|
};
|
|
@@ -1371,7 +1406,11 @@ function localStorage(options = {}) {
|
|
|
1371
1406
|
name: "local",
|
|
1372
1407
|
init({ cwd }) {
|
|
1373
1408
|
const dir = options.dir ?? "uploads";
|
|
1374
|
-
root = isAbsolute(dir) ? dir : resolve2(
|
|
1409
|
+
root = isAbsolute(dir) ? dir : resolve2(
|
|
1410
|
+
/* turbopackIgnore: true */
|
|
1411
|
+
cwd,
|
|
1412
|
+
dir
|
|
1413
|
+
);
|
|
1375
1414
|
},
|
|
1376
1415
|
async put(key2, data) {
|
|
1377
1416
|
const path = pathOf(key2);
|
|
@@ -2206,6 +2245,13 @@ function createRestHandler(instance, options = {}) {
|
|
|
2206
2245
|
const basePath = (options.basePath ?? cms.config.routes.api).replace(/\/+$/, "");
|
|
2207
2246
|
const production = process.env.NODE_ENV === "production";
|
|
2208
2247
|
return async (request) => {
|
|
2248
|
+
const cors = corsHeaders(cms, request);
|
|
2249
|
+
if (request.method.toUpperCase() === "OPTIONS") {
|
|
2250
|
+
return new Response(null, { status: 204, headers: cors });
|
|
2251
|
+
}
|
|
2252
|
+
return withHeaders(await handle(request), cors);
|
|
2253
|
+
};
|
|
2254
|
+
async function handle(request) {
|
|
2209
2255
|
const headers = new Headers({
|
|
2210
2256
|
"content-type": "application/json; charset=utf-8",
|
|
2211
2257
|
"cache-control": "no-store",
|
|
@@ -2221,14 +2267,44 @@ function createRestHandler(instance, options = {}) {
|
|
|
2221
2267
|
const user = token ? await cms.auth.verify(token) : null;
|
|
2222
2268
|
const ctx = { request, url, user, via: user ? via : null, token, headers };
|
|
2223
2269
|
const method = request.method.toUpperCase();
|
|
2224
|
-
if (method !== "GET" && method !== "HEAD"
|
|
2270
|
+
if (method !== "GET" && method !== "HEAD") checkCsrf(cms, ctx);
|
|
2225
2271
|
const result = await route(cms, ctx, method, segments, options);
|
|
2226
2272
|
if (result.body instanceof Response) return result.body;
|
|
2227
2273
|
return new Response(JSON.stringify(result.body), { status: result.status ?? 200, headers });
|
|
2228
2274
|
} catch (error) {
|
|
2229
2275
|
return errorResponse(cms, error, headers, production);
|
|
2230
2276
|
}
|
|
2231
|
-
}
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
var CORS_METHODS = "GET, HEAD, POST, PATCH, PUT, DELETE";
|
|
2280
|
+
var CORS_HEADERS = `authorization, content-type, ${CSRF_HEADER}`;
|
|
2281
|
+
function corsHeaders(cms, request) {
|
|
2282
|
+
const headers = new Headers({ vary: "Origin" });
|
|
2283
|
+
const origin = request.headers.get("origin");
|
|
2284
|
+
if (!origin) return headers;
|
|
2285
|
+
const { cors, auth } = cms.config;
|
|
2286
|
+
const trusted = auth.trustedOrigins.includes(origin);
|
|
2287
|
+
if (!trusted && cors !== "*" && !cors.includes(origin)) return headers;
|
|
2288
|
+
headers.set("access-control-allow-origin", origin);
|
|
2289
|
+
if (trusted) headers.set("access-control-allow-credentials", "true");
|
|
2290
|
+
if (request.method.toUpperCase() === "OPTIONS") {
|
|
2291
|
+
headers.set("access-control-allow-methods", CORS_METHODS);
|
|
2292
|
+
headers.set("access-control-allow-headers", CORS_HEADERS);
|
|
2293
|
+
headers.set("access-control-max-age", "600");
|
|
2294
|
+
}
|
|
2295
|
+
return headers;
|
|
2296
|
+
}
|
|
2297
|
+
function withHeaders(response, extra) {
|
|
2298
|
+
const headers = new Headers(response.headers);
|
|
2299
|
+
extra.forEach((value, key2) => {
|
|
2300
|
+
if (key2 === "vary") headers.append("vary", value);
|
|
2301
|
+
else headers.set(key2, value);
|
|
2302
|
+
});
|
|
2303
|
+
return new Response(response.body, {
|
|
2304
|
+
status: response.status,
|
|
2305
|
+
statusText: response.statusText,
|
|
2306
|
+
headers
|
|
2307
|
+
});
|
|
2232
2308
|
}
|
|
2233
2309
|
async function route(cms, ctx, method, segments, options) {
|
|
2234
2310
|
const [first, second, third] = segments;
|