@easy-cms/core 0.1.1 → 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]);
@@ -943,6 +944,21 @@ function validateAuth(config, add) {
943
944
  }
944
945
  }
945
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
+ }
946
962
  function validateUpload(config, add) {
947
963
  const upload = config.upload;
948
964
  if (upload === void 0) return;
@@ -1125,6 +1141,7 @@ async function resolveConfig(input) {
1125
1141
  const { plugins: _plugins, ...rest } = config;
1126
1142
  const result = {
1127
1143
  ...rest,
1144
+ cors: config.cors ?? [],
1128
1145
  routes: { api: `/${(config.routes?.api ?? DEFAULT_API_PATH).replace(/^\/+|\/+$/g, "")}` },
1129
1146
  admin: {
1130
1147
  path: config.admin?.path ?? DEFAULT_ADMIN_PATH,
@@ -2228,6 +2245,13 @@ function createRestHandler(instance, options = {}) {
2228
2245
  const basePath = (options.basePath ?? cms.config.routes.api).replace(/\/+$/, "");
2229
2246
  const production = process.env.NODE_ENV === "production";
2230
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) {
2231
2255
  const headers = new Headers({
2232
2256
  "content-type": "application/json; charset=utf-8",
2233
2257
  "cache-control": "no-store",
@@ -2243,14 +2267,44 @@ function createRestHandler(instance, options = {}) {
2243
2267
  const user = token ? await cms.auth.verify(token) : null;
2244
2268
  const ctx = { request, url, user, via: user ? via : null, token, headers };
2245
2269
  const method = request.method.toUpperCase();
2246
- if (method !== "GET" && method !== "HEAD" && method !== "OPTIONS") checkCsrf(cms, ctx);
2270
+ if (method !== "GET" && method !== "HEAD") checkCsrf(cms, ctx);
2247
2271
  const result = await route(cms, ctx, method, segments, options);
2248
2272
  if (result.body instanceof Response) return result.body;
2249
2273
  return new Response(JSON.stringify(result.body), { status: result.status ?? 200, headers });
2250
2274
  } catch (error) {
2251
2275
  return errorResponse(cms, error, headers, production);
2252
2276
  }
2253
- };
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
+ });
2254
2308
  }
2255
2309
  async function route(cms, ctx, method, segments, options) {
2256
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.1",
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",