@easy-cms/core 0.47.3 → 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 +2 -0
- package/dist/index.js +105 -26
- package/package.json +1 -1
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
|
|
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.
|
|
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 ||
|
|
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
|
-
|
|
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)
|
|
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,
|
|
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 =
|
|
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,
|
|
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] =
|
|
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,
|
|
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 =
|
|
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.
|
|
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
|
|
10707
|
-
|
|
10708
|
-
|
|
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.
|
|
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
|
|
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);
|