@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 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("secret", "is required", hint);
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(cwd, name);
1160
- if (existsSync(path)) return path;
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(options.dir ?? "uploads");
1392
+ let root = resolve2(
1393
+ /* turbopackIgnore: true */
1394
+ options.dir ?? "uploads"
1395
+ );
1365
1396
  const pathOf = (key2) => {
1366
- const path = normalize(join(root, key2));
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(cwd, dir);
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" && method !== "OPTIONS") checkCsrf(cms, ctx);
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@easy-cms/core",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Embedded, code-first headless CMS for Nuxt and Next.js",
5
5
  "license": "MIT",
6
6
  "type": "module",