@easy-cms/core 0.47.2 → 0.47.4

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
@@ -2957,6 +2957,8 @@ interface EndpointRequest {
2957
2957
  readonly cms: EasyCMS;
2958
2958
  /** The request's JSON body, which must be an object (at most 1 MB). */
2959
2959
  json(): Promise<Record<string, unknown>>;
2960
+ /** The request's body as text, as sent (at most 1 MB), e.g. to check a webhook's signature. */
2961
+ text(): Promise<string>;
2960
2962
  }
2961
2963
  /**
2962
2964
  * Work that runs with the scheduled jobs: every minute in a server, or each time a cron calls
package/dist/index.js CHANGED
@@ -1690,6 +1690,14 @@ var SingleSignOn = class {
1690
1690
  if (!email2 || !profile.emailVerified)
1691
1691
  return await back("login", email2 ? "unverified" : "no-account");
1692
1692
  user = await this.userByEmail(email2);
1693
+ if (user && user.emailVerified === false) {
1694
+ const { id, ...rest } = user;
1695
+ user = await this.cms.db.update({
1696
+ collection: USERS,
1697
+ id,
1698
+ data: { ...rest, emailVerified: true, passwordHash: null }
1699
+ });
1700
+ }
1693
1701
  if (!user) user = await this.signUp(email2, profile.name);
1694
1702
  if (!user) return await back("login", "no-account");
1695
1703
  await this.addIdentity(user.id, provider.id, subject, email2);
@@ -1742,7 +1750,7 @@ var SingleSignOn = class {
1742
1750
  const row = await this.cms.db.findById({ collection: USER_IDENTITIES, id: identityId });
1743
1751
  if (!row) throw new NotFoundError(USER_IDENTITIES, identityId);
1744
1752
  const own = String(row.user) === String(actor.id);
1745
- if (!own && (actor.role !== "admin" || actor.apiKey)) throw new ForbiddenError();
1753
+ if (!own && (!isSystemAdmin(actor) || actor.apiKey)) throw new ForbiddenError();
1746
1754
  const user = await this.cms.db.findById({ collection: USERS, id: row.user });
1747
1755
  const others = (await this.identities(row.user)).length - 1;
1748
1756
  const password = !!user?.passwordHash && this.passwordAllowed(user);
@@ -1994,8 +2002,16 @@ var Auth = class {
1994
2002
  throw new QueryError("Signing up needs `email` in the config, and a site URL in production");
1995
2003
  const existing = email2 ? await this.findByEmail(email2) : void 0;
1996
2004
  if (existing && verify) {
1997
- if (existing.emailVerified === false && existing.active !== false)
1998
- await this.mailVerifyLink(existing, args);
2005
+ if (existing.emailVerified === false && existing.active !== false) {
2006
+ await this.cms.update(USERS, existing.id, { password: args.password }, {
2007
+ context: args.context ?? {}
2008
+ });
2009
+ const pending = await this.db.findById({
2010
+ collection: USERS,
2011
+ id: existing.id
2012
+ });
2013
+ await this.mailVerifyLink(pending, args);
2014
+ }
1999
2015
  return { verify: true };
2000
2016
  }
2001
2017
  const created = await this.cms.create(
@@ -2030,14 +2046,15 @@ var Auth = class {
2030
2046
  const claims = readPasswordToken(token);
2031
2047
  const id = claims && (/^\d+$/.test(claims.userId) ? Number(claims.userId) : claims.userId);
2032
2048
  const user = claims && claims.purpose === "verify" && claims.expiresAt > Date.now() && id !== null ? await this.db.findById({ collection: USERS, id }) : null;
2033
- if (!user || user.active === false || !passwordTokenMatches(this.config.secret, token, passwordFingerprint(user.passwordHash))) {
2049
+ if (!user || user.active === false || // A link confirms once: it doesn't sign in again afterwards.
2050
+ user.emailVerified !== false || !passwordTokenMatches(this.config.secret, token, passwordFingerprint(user.passwordHash))) {
2034
2051
  await this.recordFailure(key2);
2035
2052
  throw new ValidationError(USERS, [
2036
2053
  { field: "token", message: "This link has expired or is not valid" }
2037
2054
  ]);
2038
2055
  }
2039
2056
  await this.clearFailures(key2);
2040
- if (user.emailVerified === false) {
2057
+ {
2041
2058
  const { id: _id, ...rest } = user;
2042
2059
  await this.db.update({
2043
2060
  collection: USERS,
@@ -3988,12 +4005,41 @@ for (const [net, prefix] of [
3988
4005
  ["ff00::", 8]
3989
4006
  ])
3990
4007
  PRIVATE.addSubnet(net, prefix, "ipv6");
4008
+ var EMBEDS_IPV4 = new BlockList();
4009
+ EMBEDS_IPV4.addSubnet("64:ff9b::", 96, "ipv6");
4010
+ EMBEDS_IPV4.addSubnet("::", 96, "ipv6");
4011
+ function embeddedIPv4(address) {
4012
+ if (address === "::" || address === "::1" || !EMBEDS_IPV4.check(address, "ipv6")) return void 0;
4013
+ const bytes = ipv6Bytes(address);
4014
+ return bytes ? bytes.slice(12).join(".") : void 0;
4015
+ }
4016
+ function ipv6Bytes(address) {
4017
+ let text2 = address;
4018
+ const tail = /(\d+)\.(\d+)\.(\d+)\.(\d+)$/.exec(text2);
4019
+ if (tail) {
4020
+ const [a, b, c, d] = tail.slice(1).map(Number);
4021
+ text2 = `${text2.slice(0, tail.index)}${(a << 8 | b).toString(16)}:${(c << 8 | d).toString(16)}`;
4022
+ }
4023
+ const [head = "", rest] = text2.split("::");
4024
+ const left = head ? head.split(":") : [];
4025
+ const right = rest === void 0 ? [] : rest ? rest.split(":") : [];
4026
+ const groups = rest === void 0 ? left : [...left, ...Array(8 - left.length - right.length).fill("0"), ...right];
4027
+ if (groups.length !== 8) return void 0;
4028
+ return groups.flatMap((g) => {
4029
+ const n = Number.parseInt(g, 16);
4030
+ return [n >> 8, n & 255];
4031
+ });
4032
+ }
3991
4033
  function isPrivateAddress(address) {
3992
4034
  const mapped = /^::ffff:(\d+\.\d+\.\d+\.\d+)$/i.exec(address);
3993
4035
  if (mapped?.[1]) return PRIVATE.check(mapped[1], "ipv4");
3994
4036
  const family = isIP(address);
3995
4037
  if (family === 4) return PRIVATE.check(address, "ipv4");
3996
- if (family === 6) return PRIVATE.check(address, "ipv6");
4038
+ if (family === 6) {
4039
+ const embedded = embeddedIPv4(address);
4040
+ if (embedded) return PRIVATE.check(embedded, "ipv4");
4041
+ return PRIVATE.check(address, "ipv6");
4042
+ }
3997
4043
  return true;
3998
4044
  }
3999
4045
  function hostAllowed(host, allowedHosts) {
@@ -6584,9 +6630,10 @@ var EasyCMS = class {
6584
6630
  if (!Number.isInteger(limit) || limit < 0)
6585
6631
  throw new QueryError("limit must be a non-negative integer");
6586
6632
  if (!Number.isInteger(page) || page < 1) throw new QueryError("page must be a positive integer");
6633
+ const draft = readsDrafts(guard, options.draft);
6587
6634
  const where = this.whereFor(
6588
6635
  config,
6589
- draftWhere(config, options.draft, await this.readWhere(config, guard, options.where)),
6636
+ draftWhere(config, draft, await this.readWhere(config, guard, options.where)),
6590
6637
  options
6591
6638
  );
6592
6639
  const requested = options.sort === void 0 ? ["-createdAt"] : [options.sort].flat();
@@ -6599,8 +6646,8 @@ var EasyCMS = class {
6599
6646
  this.config.localization?.locales ?? []
6600
6647
  );
6601
6648
  const result = await this.db.find({ collection: collection2, where, sort, limit, page });
6602
- const found = options.draft ? await this.withDrafts(config, result.docs) : result.docs;
6603
- const docs = await this.output(config, found, guard, options);
6649
+ const found = draft ? await this.withDrafts(config, result.docs) : result.docs;
6650
+ const docs = await this.output(config, found, guard, { ...options, draft });
6604
6651
  return { ...result, docs };
6605
6652
  }
6606
6653
  async findById(collection2, id, options = {}) {
@@ -6608,9 +6655,10 @@ var EasyCMS = class {
6608
6655
  const guard = guardOf(options);
6609
6656
  const parsed = parseId(id);
6610
6657
  if (parsed === void 0) return null;
6658
+ const draft = readsDrafts(guard, options.draft);
6611
6659
  const where = this.whereFor(
6612
6660
  config,
6613
- draftWhere(config, options.draft, await this.readWhere(config, guard, void 0)),
6661
+ draftWhere(config, draft, await this.readWhere(config, guard, void 0)),
6614
6662
  options
6615
6663
  );
6616
6664
  const doc = where ? (await this.db.find({
@@ -6621,8 +6669,8 @@ var EasyCMS = class {
6621
6669
  page: 1
6622
6670
  })).docs[0] : await this.db.findById({ collection: collection2, id: parsed });
6623
6671
  if (!doc) return null;
6624
- const [current] = options.draft ? await this.withDrafts(config, [doc]) : [doc];
6625
- const [out] = await this.output(config, [current], guard, options);
6672
+ const [current] = draft ? await this.withDrafts(config, [doc]) : [doc];
6673
+ const [out] = await this.output(config, [current], guard, { ...options, draft });
6626
6674
  return out ?? null;
6627
6675
  }
6628
6676
  async count(collection2, options = {}) {
@@ -6631,7 +6679,7 @@ var EasyCMS = class {
6631
6679
  config,
6632
6680
  draftWhere(
6633
6681
  config,
6634
- options.draft,
6682
+ readsDrafts(guardOf(options), options.draft),
6635
6683
  await this.readWhere(config, guardOf(options), options.where)
6636
6684
  ),
6637
6685
  options
@@ -7492,7 +7540,7 @@ var EasyCMS = class {
7492
7540
  /** A global as stored under `key` (see `globalKey`); empty when there is no key. */
7493
7541
  async readGlobal(config, key2, guard, options) {
7494
7542
  const saved = key2 ? await this.db.findGlobal({ slug: key2 }) : null;
7495
- const stored = saved && options.draft ? await this.globalDraft(config, saved, key2) : saved;
7543
+ const stored = saved && readsDrafts(guard, options.draft) ? await this.globalDraft(config, saved, key2) : saved;
7496
7544
  const data = fillMissing(
7497
7545
  config.fields,
7498
7546
  applyDefaults(config.fields, stored ?? {}, this.config.localization)
@@ -8220,6 +8268,7 @@ var EasyCMS = class {
8220
8268
  );
8221
8269
  return await stripFields(target.fields, hooked, read2);
8222
8270
  };
8271
+ const drafts = readsDrafts(guard, options.draft);
8223
8272
  const load = async (target, ids) => {
8224
8273
  const where = await this.readWhere(target, guard, { id: { in: [...ids] } }).catch((error) => {
8225
8274
  if (error instanceof ForbiddenError || error instanceof UnauthorizedError) return null;
@@ -8228,12 +8277,12 @@ var EasyCMS = class {
8228
8277
  if (where === null) return [];
8229
8278
  const found = await this.db.find({
8230
8279
  collection: target.slug,
8231
- where: this.whereFor(target, draftWhere(target, options.draft, where), options),
8280
+ where: this.whereFor(target, draftWhere(target, drafts, where), options),
8232
8281
  sort: [],
8233
8282
  limit: 0,
8234
8283
  page: 1
8235
8284
  });
8236
- const docs2 = options.draft ? await this.withDrafts(target, found.docs) : found.docs;
8285
+ const docs2 = drafts ? await this.withDrafts(target, found.docs) : found.docs;
8237
8286
  return Promise.all(
8238
8287
  docs2.map((d) => finish(target, this.pick(target, d, options)))
8239
8288
  );
@@ -8761,6 +8810,9 @@ function guardOf(options) {
8761
8810
  context: options.context ?? NO_CONTEXT
8762
8811
  };
8763
8812
  }
8813
+ function readsDrafts(guard, draft) {
8814
+ return draft === true && (!guard.enforce || guard.user !== null && guard.user.member !== true);
8815
+ }
8764
8816
  function deny(user) {
8765
8817
  return user ? new ForbiddenError() : new UnauthorizedError();
8766
8818
  }
@@ -9517,7 +9569,7 @@ async function adminSearch(cms, who, q, perCollection = 5) {
9517
9569
  }
9518
9570
 
9519
9571
  // src/version.ts
9520
- var VERSION = true ? "0.47.2" : "0.0.0-dev";
9572
+ var VERSION = true ? "0.47.4" : "0.0.0-dev";
9521
9573
 
9522
9574
  // src/rest/admin-status.ts
9523
9575
  var DAY2 = 864e5;
@@ -9860,7 +9912,8 @@ async function customEndpoint(cms, ctx, method, segments, root, options) {
9860
9912
  context: ctx.context,
9861
9913
  ip: options.getClientIp?.(ctx.request),
9862
9914
  cms,
9863
- json: () => readJson(ctx.request)
9915
+ json: () => readJson(ctx.request),
9916
+ text: async () => new TextDecoder().decode(await readBody(ctx.request, MAX_BODY_BYTES))
9864
9917
  });
9865
9918
  return { body: body === void 0 ? null : body };
9866
9919
  }
@@ -10703,12 +10756,16 @@ async function readUpload(request, maxFileSize) {
10703
10756
  415
10704
10757
  );
10705
10758
  }
10706
- const declared = Number(request.headers.get("content-length") ?? 0);
10707
- if (declared > maxFileSize + 64 * 1024)
10708
- throw new PayloadTooLargeError(`File is larger than ${maxFileSize} bytes`);
10759
+ const bytes = await readBody(request, maxFileSize + 64 * 1024).catch((error) => {
10760
+ throw error instanceof HttpError && error.status === 413 ? new PayloadTooLargeError(`File is larger than ${maxFileSize} bytes`) : error;
10761
+ });
10709
10762
  let form;
10710
10763
  try {
10711
- form = await request.formData();
10764
+ form = await new Request(request.url, {
10765
+ method: "POST",
10766
+ headers: { "content-type": request.headers.get("content-type") ?? "" },
10767
+ body: bytes
10768
+ }).formData();
10712
10769
  } catch {
10713
10770
  throw new HttpError("Malformed multipart body", 400);
10714
10771
  }
@@ -10724,15 +10781,37 @@ async function readUpload(request, maxFileSize) {
10724
10781
  data
10725
10782
  };
10726
10783
  }
10784
+ async function readBody(request, max) {
10785
+ const tooLarge = () => new HttpError("Request body too large", 413);
10786
+ if (Number(request.headers.get("content-length") ?? 0) > max) throw tooLarge();
10787
+ if (!request.body) return new Uint8Array();
10788
+ const reader = request.body.getReader();
10789
+ const chunks = [];
10790
+ let size = 0;
10791
+ for (; ; ) {
10792
+ const { done, value } = await reader.read();
10793
+ if (done) break;
10794
+ size += value.byteLength;
10795
+ if (size > max) {
10796
+ reader.releaseLock();
10797
+ throw tooLarge();
10798
+ }
10799
+ chunks.push(value);
10800
+ }
10801
+ const body = new Uint8Array(size);
10802
+ let at = 0;
10803
+ for (const chunk of chunks) {
10804
+ body.set(chunk, at);
10805
+ at += chunk.byteLength;
10806
+ }
10807
+ return body;
10808
+ }
10727
10809
  async function readJson(request) {
10728
10810
  const type = request.headers.get("content-type") ?? "";
10729
10811
  if (!type.toLowerCase().startsWith("application/json")) {
10730
10812
  throw new HttpError("Content-Type must be application/json", 415);
10731
10813
  }
10732
- const declared = Number(request.headers.get("content-length") ?? 0);
10733
- if (declared > MAX_BODY_BYTES) throw new HttpError("Request body too large", 413);
10734
- const text2 = await request.text();
10735
- if (Buffer.byteLength(text2) > MAX_BODY_BYTES) throw new HttpError("Request body too large", 413);
10814
+ const text2 = new TextDecoder().decode(await readBody(request, MAX_BODY_BYTES));
10736
10815
  let body;
10737
10816
  try {
10738
10817
  body = JSON.parse(text2);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@easy-cms/core",
3
- "version": "0.47.2",
3
+ "version": "0.47.4",
4
4
  "description": "Embedded, code-first headless CMS for Nuxt and Next.js",
5
5
  "license": "MIT",
6
6
  "type": "module",