@easy-cms/core 1.32.0 → 1.33.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/{chunk-J4NASOMO.js → chunk-KUEGUD2X.js} +790 -225
- package/dist/{config-Dr_NZGm6.d.ts → config-BZ4fsHL4.d.ts} +766 -527
- package/dist/{handler-B0m4VOAy.d.ts → handler-CMTJ99sx.d.ts} +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +3 -1
- package/dist/internal.d.ts +2 -2
- package/dist/internal.js +1 -1
- package/dist/{plugin-DBHFjM-r.d.ts → plugin-GcNICm6a.d.ts} +1 -1
- package/dist/plugin.d.ts +2 -2
- package/package.json +1 -1
|
@@ -50,100 +50,6 @@ var isStaff = ({ user }) => user !== null && user.member !== true;
|
|
|
50
50
|
var isSignedIn = ({ user }) => user !== null;
|
|
51
51
|
var isAdmin = ({ user }) => user?.role === "admin" && user.scoped !== true;
|
|
52
52
|
|
|
53
|
-
// src/auth/tokens.ts
|
|
54
|
-
import { createHash, createHmac, randomBytes, timingSafeEqual } from "crypto";
|
|
55
|
-
function newToken() {
|
|
56
|
-
return randomBytes(32).toString("base64url");
|
|
57
|
-
}
|
|
58
|
-
function hashToken(token) {
|
|
59
|
-
return createHash("sha256").update(token).digest("hex");
|
|
60
|
-
}
|
|
61
|
-
function hmac(secret, value) {
|
|
62
|
-
return createHmac("sha256", secret).update(value).digest("base64url");
|
|
63
|
-
}
|
|
64
|
-
var signValue = (secret, value) => hmac(secret, value);
|
|
65
|
-
function secretsOf(config) {
|
|
66
|
-
return [config.secret, ...config.previousSecrets ?? []];
|
|
67
|
-
}
|
|
68
|
-
function signedWith(secrets, value, signature) {
|
|
69
|
-
return [secrets].flat().some((secret) => safeEqual(signature, hmac(secret, value)));
|
|
70
|
-
}
|
|
71
|
-
function signToken(secret, token) {
|
|
72
|
-
return `${token}.${hmac(secret, `session:${token}`)}`;
|
|
73
|
-
}
|
|
74
|
-
function unsignToken(secrets, value) {
|
|
75
|
-
const dot = value.lastIndexOf(".");
|
|
76
|
-
if (dot <= 0) return void 0;
|
|
77
|
-
const token = value.slice(0, dot);
|
|
78
|
-
return signedWith(secrets, `session:${token}`, value.slice(dot + 1)) ? token : void 0;
|
|
79
|
-
}
|
|
80
|
-
function csrfToken(secret, token) {
|
|
81
|
-
return hmac(secret, `csrf:${token}`);
|
|
82
|
-
}
|
|
83
|
-
function csrfMatches(secrets, token, given) {
|
|
84
|
-
return signedWith(secrets, `csrf:${token}`, given);
|
|
85
|
-
}
|
|
86
|
-
function safeEqual(a, b) {
|
|
87
|
-
const left = Buffer.from(a);
|
|
88
|
-
const right = Buffer.from(b);
|
|
89
|
-
return left.length === right.length && timingSafeEqual(left, right);
|
|
90
|
-
}
|
|
91
|
-
function signPreviewToken(secret, target, expiresAt, user) {
|
|
92
|
-
const payload = Buffer.from(
|
|
93
|
-
JSON.stringify({
|
|
94
|
-
..."global" in target ? { g: target.global } : { c: target.collection, i: target.id },
|
|
95
|
-
e: expiresAt,
|
|
96
|
-
...user !== void 0 ? { u: user } : {}
|
|
97
|
-
})
|
|
98
|
-
).toString("base64url");
|
|
99
|
-
return `${payload}.${hmac(secret, `preview:${payload}`)}`;
|
|
100
|
-
}
|
|
101
|
-
function verifyPreviewToken(secrets, token, now = Date.now()) {
|
|
102
|
-
const dot = token.lastIndexOf(".");
|
|
103
|
-
if (dot <= 0) return null;
|
|
104
|
-
const payload = token.slice(0, dot);
|
|
105
|
-
if (!signedWith(secrets, `preview:${payload}`, token.slice(dot + 1))) return null;
|
|
106
|
-
try {
|
|
107
|
-
const data = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
108
|
-
if (typeof data.e !== "number" || data.e <= now) return null;
|
|
109
|
-
const user = typeof data.u === "string" ? { user: data.u } : {};
|
|
110
|
-
if (typeof data.g === "string") return { global: data.g, expiresAt: data.e, ...user };
|
|
111
|
-
if (typeof data.c === "string" && typeof data.i === "string")
|
|
112
|
-
return { collection: data.c, id: data.i, expiresAt: data.e, ...user };
|
|
113
|
-
return null;
|
|
114
|
-
} catch {
|
|
115
|
-
return null;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
function passwordFingerprint(passwordHash) {
|
|
119
|
-
return createHash("sha256").update(typeof passwordHash === "string" ? passwordHash : "no-password").digest("base64url").slice(0, 22);
|
|
120
|
-
}
|
|
121
|
-
function signPasswordToken(secret, args) {
|
|
122
|
-
const payload = Buffer.from(
|
|
123
|
-
JSON.stringify({ u: args.userId, p: args.purpose, e: args.expiresAt })
|
|
124
|
-
).toString("base64url");
|
|
125
|
-
return `${payload}.${hmac(secret, `password:${payload}:${args.fingerprint}`)}`;
|
|
126
|
-
}
|
|
127
|
-
function readPasswordToken(token) {
|
|
128
|
-
const dot = token.lastIndexOf(".");
|
|
129
|
-
if (dot <= 0) return null;
|
|
130
|
-
const payload = token.slice(0, dot);
|
|
131
|
-
try {
|
|
132
|
-
const data = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
133
|
-
if (typeof data.u !== "string" || typeof data.e !== "number") return null;
|
|
134
|
-
if (data.p !== "reset" && data.p !== "invite" && data.p !== "verify") return null;
|
|
135
|
-
return { userId: data.u, purpose: data.p, expiresAt: data.e, payload };
|
|
136
|
-
} catch {
|
|
137
|
-
return null;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
function passwordTokenMatches(secrets, token, fingerprint) {
|
|
141
|
-
const dot = token.lastIndexOf(".");
|
|
142
|
-
if (dot <= 0) return false;
|
|
143
|
-
const payload = token.slice(0, dot);
|
|
144
|
-
return signedWith(secrets, `password:${payload}:${fingerprint}`, token.slice(dot + 1));
|
|
145
|
-
}
|
|
146
|
-
|
|
147
53
|
// src/builtins.ts
|
|
148
54
|
var USERS = "users";
|
|
149
55
|
var MEDIA = "media";
|
|
@@ -164,6 +70,7 @@ var RATE_LIMITS = "rate-limits";
|
|
|
164
70
|
var BULK_OPERATIONS = "bulk-operations";
|
|
165
71
|
var SAVED_VIEWS = "saved-views";
|
|
166
72
|
var CONNECTIONS = "connections";
|
|
73
|
+
var AI_USAGE = "ai-usage";
|
|
167
74
|
var INTERNAL_COLLECTIONS = /* @__PURE__ */ new Set([
|
|
168
75
|
SESSIONS,
|
|
169
76
|
LOGIN_ATTEMPTS,
|
|
@@ -180,7 +87,8 @@ var INTERNAL_COLLECTIONS = /* @__PURE__ */ new Set([
|
|
|
180
87
|
RATE_LIMITS,
|
|
181
88
|
BULK_OPERATIONS,
|
|
182
89
|
SAVED_VIEWS,
|
|
183
|
-
CONNECTIONS
|
|
90
|
+
CONNECTIONS,
|
|
91
|
+
AI_USAGE
|
|
184
92
|
]);
|
|
185
93
|
var DEFAULT_ROLES = ["admin", "editor"];
|
|
186
94
|
var CREATED_BY_FIELD = {
|
|
@@ -602,6 +510,22 @@ var connectionsCollection = {
|
|
|
602
510
|
{ name: "enabled", type: "boolean" }
|
|
603
511
|
]
|
|
604
512
|
};
|
|
513
|
+
var aiUsageCollection = {
|
|
514
|
+
slug: AI_USAGE,
|
|
515
|
+
access: { read: nobody, create: nobody, update: nobody, delete: nobody },
|
|
516
|
+
fields: [
|
|
517
|
+
{ name: "user", type: "text", index: true },
|
|
518
|
+
{ name: "feature", type: "text", required: true },
|
|
519
|
+
{ name: "integration", type: "text", required: true },
|
|
520
|
+
{ name: "connection", type: "text" },
|
|
521
|
+
{ name: "input", type: "number" },
|
|
522
|
+
{ name: "output", type: "number" },
|
|
523
|
+
{ name: "ok", type: "boolean" },
|
|
524
|
+
{ name: "error", type: "text" },
|
|
525
|
+
// The document it was for, e.g. the image of alt text.
|
|
526
|
+
{ name: "about", type: "text" }
|
|
527
|
+
]
|
|
528
|
+
};
|
|
605
529
|
var webhookDeliveriesCollection = {
|
|
606
530
|
slug: WEBHOOK_DELIVERIES,
|
|
607
531
|
access: { read: nobody, create: nobody, update: nobody, delete: nobody },
|
|
@@ -741,6 +665,602 @@ var emailDeliveriesCollection = {
|
|
|
741
665
|
]
|
|
742
666
|
};
|
|
743
667
|
|
|
668
|
+
// src/time-of-day.ts
|
|
669
|
+
var DAY = 864e5;
|
|
670
|
+
function isTimeZone(zone) {
|
|
671
|
+
try {
|
|
672
|
+
new Intl.DateTimeFormat("en-US", { timeZone: zone });
|
|
673
|
+
return true;
|
|
674
|
+
} catch {
|
|
675
|
+
return false;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
var formatters = /* @__PURE__ */ new Map();
|
|
679
|
+
function wallClock(instant, zone) {
|
|
680
|
+
let format = formatters.get(zone);
|
|
681
|
+
if (!format) {
|
|
682
|
+
format = new Intl.DateTimeFormat("en-US", {
|
|
683
|
+
timeZone: zone,
|
|
684
|
+
hourCycle: "h23",
|
|
685
|
+
year: "numeric",
|
|
686
|
+
month: "numeric",
|
|
687
|
+
day: "numeric",
|
|
688
|
+
hour: "numeric",
|
|
689
|
+
minute: "numeric",
|
|
690
|
+
second: "numeric"
|
|
691
|
+
});
|
|
692
|
+
formatters.set(zone, format);
|
|
693
|
+
}
|
|
694
|
+
const parts = {};
|
|
695
|
+
for (const part of format.formatToParts(instant))
|
|
696
|
+
if (part.type !== "literal") parts[part.type] = Number(part.value);
|
|
697
|
+
return Date.UTC(
|
|
698
|
+
parts.year,
|
|
699
|
+
parts.month - 1,
|
|
700
|
+
parts.day,
|
|
701
|
+
parts.hour,
|
|
702
|
+
parts.minute,
|
|
703
|
+
parts.second
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
function instantOf(wall, zone) {
|
|
707
|
+
let instant = wall - (wallClock(wall, zone) - wall);
|
|
708
|
+
instant = wall - (wallClock(instant, zone) - instant);
|
|
709
|
+
return instant;
|
|
710
|
+
}
|
|
711
|
+
var parse = (at) => at.split(":").map(Number);
|
|
712
|
+
function lastSlot(now, at, zone) {
|
|
713
|
+
const [hours2, minutes] = parse(at);
|
|
714
|
+
if (!zone) {
|
|
715
|
+
const slot2 = new Date(now);
|
|
716
|
+
slot2.setHours(hours2, minutes, 0, 0);
|
|
717
|
+
if (slot2.getTime() > now.getTime()) slot2.setDate(slot2.getDate() - 1);
|
|
718
|
+
return slot2;
|
|
719
|
+
}
|
|
720
|
+
const today = wallClock(now.getTime(), zone);
|
|
721
|
+
const midnight = today - today % DAY;
|
|
722
|
+
let slot = instantOf(midnight + hours2 * 36e5 + minutes * 6e4, zone);
|
|
723
|
+
if (slot > now.getTime())
|
|
724
|
+
slot = instantOf(midnight - DAY + hours2 * 36e5 + minutes * 6e4, zone);
|
|
725
|
+
return new Date(slot);
|
|
726
|
+
}
|
|
727
|
+
function nextSlot(after, at, zone) {
|
|
728
|
+
const last = lastSlot(after, at, zone);
|
|
729
|
+
return lastSlot(new Date(last.getTime() + DAY + 2 * 36e5), at, zone);
|
|
730
|
+
}
|
|
731
|
+
function dueAt(now, at, every, lastRun, zone) {
|
|
732
|
+
if (lastRun === void 0) return true;
|
|
733
|
+
const last = Date.parse(lastRun);
|
|
734
|
+
if (Number.isNaN(last)) return true;
|
|
735
|
+
const slot = lastSlot(now, at, zone).getTime();
|
|
736
|
+
return last < slot - (every * 1e3 - DAY);
|
|
737
|
+
}
|
|
738
|
+
function nextAt(at, every, lastRun, zone) {
|
|
739
|
+
return nextSlot(new Date(Date.parse(lastRun) + (every * 1e3 - DAY)), at, zone);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// src/ai.ts
|
|
743
|
+
var AiProviderError = class extends Error {
|
|
744
|
+
status;
|
|
745
|
+
constructor(message2, status) {
|
|
746
|
+
super(message2);
|
|
747
|
+
this.name = "AiProviderError";
|
|
748
|
+
this.status = status;
|
|
749
|
+
}
|
|
750
|
+
};
|
|
751
|
+
var SETTINGS = "ai";
|
|
752
|
+
var DEFAULT_LIMIT = 100;
|
|
753
|
+
var TIMEOUT = 6e4;
|
|
754
|
+
var MAX_IMAGE = 4 * 1024 * 1024;
|
|
755
|
+
var KEEP = 90 * 24 * 36e5;
|
|
756
|
+
var IMAGE_TYPES = /* @__PURE__ */ new Set(["image/jpeg", "image/png", "image/webp", "image/gif"]);
|
|
757
|
+
var AI_TEST_FEATURE = "test";
|
|
758
|
+
var ref = (integration, name) => `${integration}:${name}`;
|
|
759
|
+
var Ai = class {
|
|
760
|
+
constructor(cms, ai) {
|
|
761
|
+
this.cms = cms;
|
|
762
|
+
this.ai = ai;
|
|
763
|
+
}
|
|
764
|
+
cms;
|
|
765
|
+
ai;
|
|
766
|
+
get config() {
|
|
767
|
+
return this.cms.config;
|
|
768
|
+
}
|
|
769
|
+
get defaultLimit() {
|
|
770
|
+
return this.ai.limits?.perUserPerDay ?? DEFAULT_LIMIT;
|
|
771
|
+
}
|
|
772
|
+
/** The features of the config. */
|
|
773
|
+
get features() {
|
|
774
|
+
return this.config.aiFeatures;
|
|
775
|
+
}
|
|
776
|
+
async settingsRow() {
|
|
777
|
+
const found = await this.cms.db.find({
|
|
778
|
+
collection: CONNECTIONS,
|
|
779
|
+
where: { and: [{ kind: { equals: "settings" } }, { integration: { equals: SETTINGS } }] },
|
|
780
|
+
sort: ["id"],
|
|
781
|
+
limit: 1,
|
|
782
|
+
page: 1
|
|
783
|
+
});
|
|
784
|
+
return found.docs[0];
|
|
785
|
+
}
|
|
786
|
+
async settings() {
|
|
787
|
+
return (await this.settingsRow())?.values ?? {};
|
|
788
|
+
}
|
|
789
|
+
/** Every connection of every provider, in the providers' order. */
|
|
790
|
+
async connections() {
|
|
791
|
+
const out = [];
|
|
792
|
+
for (const provider of this.ai.providers)
|
|
793
|
+
for (const conn of await this.cms.integrations.all(provider.integration))
|
|
794
|
+
out.push({ ...conn, integration: provider.integration });
|
|
795
|
+
return out;
|
|
796
|
+
}
|
|
797
|
+
/** Whether a feature is on: admins' choice, else its `default`. */
|
|
798
|
+
async enabled(feature) {
|
|
799
|
+
const def = this.features.find((f) => f.id === feature);
|
|
800
|
+
if (!def) return false;
|
|
801
|
+
return (await this.settings()).features?.[feature]?.enabled ?? def.default ?? true;
|
|
802
|
+
}
|
|
803
|
+
/** Requests a user may make today: their role's, else everyone's. */
|
|
804
|
+
async limitFor(user) {
|
|
805
|
+
const role = typeof user.role === "string" ? user.role : void 0;
|
|
806
|
+
if (role) {
|
|
807
|
+
const fromRoles = await this.cms.roles.aiLimit(role);
|
|
808
|
+
if (fromRoles !== void 0) return fromRoles;
|
|
809
|
+
const fromConfig = this.ai.limits?.roles?.[role];
|
|
810
|
+
if (fromConfig !== void 0) return fromConfig;
|
|
811
|
+
}
|
|
812
|
+
return (await this.settings()).perUserPerDay ?? this.defaultLimit;
|
|
813
|
+
}
|
|
814
|
+
/** Requests the user made today (in `timeZone`) and how many are left. */
|
|
815
|
+
async remaining(user, now = /* @__PURE__ */ new Date()) {
|
|
816
|
+
const since = lastSlot(now, "00:00", this.config.timeZone).toISOString();
|
|
817
|
+
const used = await this.cms.db.count({
|
|
818
|
+
collection: AI_USAGE,
|
|
819
|
+
where: {
|
|
820
|
+
and: [
|
|
821
|
+
{ user: { equals: String(user.id) } },
|
|
822
|
+
{ ok: { equals: true } },
|
|
823
|
+
{ createdAt: { gte: since } }
|
|
824
|
+
]
|
|
825
|
+
}
|
|
826
|
+
});
|
|
827
|
+
return { used, limit: await this.limitFor(user) };
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
830
|
+
* Asks the feature's connection (or the main one). With `user`, the request counts toward their
|
|
831
|
+
* daily limit; it is refused once they reach it. Usage is recorded, never the prompt.
|
|
832
|
+
*/
|
|
833
|
+
async generate(request, options = {}) {
|
|
834
|
+
const feature = this.features.find((f) => f.id === request.feature);
|
|
835
|
+
if (!feature)
|
|
836
|
+
throw new QueryError(
|
|
837
|
+
`Unknown AI feature "${request.feature}": add it to \`aiFeatures\` in the config`
|
|
838
|
+
);
|
|
839
|
+
if (!await this.enabled(feature.id))
|
|
840
|
+
throw new QueryError(`The AI feature "${feature.id}" is turned off`);
|
|
841
|
+
const user = options.user ?? null;
|
|
842
|
+
if (user) {
|
|
843
|
+
const { used, limit } = await this.remaining(user);
|
|
844
|
+
if (used >= limit)
|
|
845
|
+
throw new TooManyRequestsError(
|
|
846
|
+
limit === 0 ? "Your role may not use AI" : `You used AI ${limit} times today, the most allowed`
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
const conn = await this.connectionFor(feature.id);
|
|
850
|
+
const images = await Promise.all((request.images ?? []).map((i) => this.image(i)));
|
|
851
|
+
return this.ask(conn, {
|
|
852
|
+
feature: feature.id,
|
|
853
|
+
user,
|
|
854
|
+
about: options.about,
|
|
855
|
+
request: {
|
|
856
|
+
...request.system ? { system: request.system } : {},
|
|
857
|
+
prompt: request.prompt,
|
|
858
|
+
images,
|
|
859
|
+
...request.json ? { json: request.json } : {},
|
|
860
|
+
maxTokens: request.maxTokens ?? 1024
|
|
861
|
+
}
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
/**
|
|
865
|
+
* The Test button of a provider's connection: a short prompt; its tokens count on the card,
|
|
866
|
+
* not toward anyone's limit.
|
|
867
|
+
*/
|
|
868
|
+
async test(integration, values, name) {
|
|
869
|
+
try {
|
|
870
|
+
await this.ask(
|
|
871
|
+
{ integration, name: name ?? "", values, env: false },
|
|
872
|
+
{
|
|
873
|
+
feature: AI_TEST_FEATURE,
|
|
874
|
+
user: null,
|
|
875
|
+
markConnection: false,
|
|
876
|
+
request: { prompt: "Reply with the word OK.", images: [], maxTokens: 16 }
|
|
877
|
+
}
|
|
878
|
+
);
|
|
879
|
+
return { ok: true, ...values.model ? { account: values.model } : {} };
|
|
880
|
+
} catch (error) {
|
|
881
|
+
return { ok: false, message: error.message };
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
async connectionFor(feature) {
|
|
885
|
+
const settings = await this.settings();
|
|
886
|
+
const wanted = settings.features?.[feature]?.connection ?? settings.main;
|
|
887
|
+
const all = await this.connections();
|
|
888
|
+
const conn = wanted && all.find((c) => ref(c.integration, c.name) === wanted) || all[0];
|
|
889
|
+
if (!conn)
|
|
890
|
+
throw new QueryError("No AI provider is connected: connect one in Settings \u2192 Integrations");
|
|
891
|
+
return conn;
|
|
892
|
+
}
|
|
893
|
+
async ask(conn, args) {
|
|
894
|
+
const provider = this.ai.providers.find((p) => p.integration === conn.integration);
|
|
895
|
+
if (!provider) throw new QueryError(`No AI provider answers for "${conn.integration}"`);
|
|
896
|
+
const record = (data) => this.cms.db.create({
|
|
897
|
+
collection: AI_USAGE,
|
|
898
|
+
data: {
|
|
899
|
+
user: args.user ? String(args.user.id) : null,
|
|
900
|
+
feature: args.feature,
|
|
901
|
+
integration: conn.integration,
|
|
902
|
+
connection: conn.name || null,
|
|
903
|
+
input: 0,
|
|
904
|
+
output: 0,
|
|
905
|
+
error: null,
|
|
906
|
+
about: args.about ? `${args.about.collection}:${args.about.id}` : null,
|
|
907
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
908
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
909
|
+
...data
|
|
910
|
+
}
|
|
911
|
+
});
|
|
912
|
+
let result;
|
|
913
|
+
try {
|
|
914
|
+
result = await this.call(provider, conn.values, args.request);
|
|
915
|
+
} catch (error) {
|
|
916
|
+
const status = error instanceof AiProviderError ? error.status : void 0;
|
|
917
|
+
const message2 = error.message;
|
|
918
|
+
await record({ ok: false, error: message2.slice(0, 500) });
|
|
919
|
+
if ((status === 401 || status === 403) && args.markConnection !== false && conn.name)
|
|
920
|
+
await this.cms.integrations.markError(conn.integration, message2, conn.name);
|
|
921
|
+
throw new QueryError(message2);
|
|
922
|
+
}
|
|
923
|
+
await record({ ok: true, input: result.usage.input, output: result.usage.output });
|
|
924
|
+
if (args.markConnection !== false && conn.name)
|
|
925
|
+
await this.cms.integrations.markUsed(conn.integration, conn.name);
|
|
926
|
+
let json2;
|
|
927
|
+
if (args.request.json) {
|
|
928
|
+
try {
|
|
929
|
+
json2 = JSON.parse(stripFence(result.text));
|
|
930
|
+
} catch {
|
|
931
|
+
throw new QueryError("The AI answered with something other than JSON; try again");
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
return {
|
|
935
|
+
text: result.text,
|
|
936
|
+
...args.request.json ? { json: json2 } : {},
|
|
937
|
+
usage: result.usage,
|
|
938
|
+
connection: ref(conn.integration, conn.name)
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
/** One try, and another after a second when the provider is busy or failing (429, 5xx). */
|
|
942
|
+
async call(provider, values, request) {
|
|
943
|
+
for (let attempt = 0; ; attempt++) {
|
|
944
|
+
try {
|
|
945
|
+
return await provider.generate(values, { ...request, signal: AbortSignal.timeout(TIMEOUT) });
|
|
946
|
+
} catch (error) {
|
|
947
|
+
const status = error instanceof AiProviderError ? error.status : void 0;
|
|
948
|
+
const busy = status === 429 || status !== void 0 && status >= 500;
|
|
949
|
+
if (!busy || attempt > 0) {
|
|
950
|
+
if (error.name === "TimeoutError")
|
|
951
|
+
throw new AiProviderError("The AI provider took longer than 60 seconds");
|
|
952
|
+
throw error;
|
|
953
|
+
}
|
|
954
|
+
await new Promise((resolve3) => setTimeout(resolve3, 1e3));
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
/** An image as base64: given, or read from a media document. */
|
|
959
|
+
async image(image) {
|
|
960
|
+
if (image.data !== void 0) {
|
|
961
|
+
if (!image.mediaType || !IMAGE_TYPES.has(image.mediaType))
|
|
962
|
+
throw new ValidationError(AI_USAGE, [
|
|
963
|
+
{ field: "images", message: "must be JPEG, PNG, WebP or GIF" }
|
|
964
|
+
]);
|
|
965
|
+
return { data: image.data, mediaType: image.mediaType };
|
|
966
|
+
}
|
|
967
|
+
if (image.media === void 0)
|
|
968
|
+
throw new ValidationError(AI_USAGE, [{ field: "images", message: "needs media or data" }]);
|
|
969
|
+
const doc = await this.cms.db.findById({ collection: MEDIA, id: image.media });
|
|
970
|
+
if (!doc) throw new ValidationError(AI_USAGE, [{ field: "images", message: "media not found" }]);
|
|
971
|
+
const sizes = Object.values(doc.sizes ?? {}).filter((s) => typeof s?.filename === "string" && Number(s.width) >= 1024).sort((a, b) => Number(a.width) - Number(b.width));
|
|
972
|
+
const filename = String(sizes[0]?.filename ?? doc.filename);
|
|
973
|
+
const mediaType = String(doc.mimeType ?? "");
|
|
974
|
+
if (!IMAGE_TYPES.has(mediaType))
|
|
975
|
+
throw new ValidationError(AI_USAGE, [
|
|
976
|
+
{ field: "images", message: `${mediaType || "this file"} is not an image AI can read` }
|
|
977
|
+
]);
|
|
978
|
+
const file = await this.cms.storageFor(doc.private === true).get(filename);
|
|
979
|
+
if (!file) throw new ValidationError(AI_USAGE, [{ field: "images", message: "file not found" }]);
|
|
980
|
+
if (file.body.byteLength > MAX_IMAGE)
|
|
981
|
+
throw new ValidationError(AI_USAGE, [
|
|
982
|
+
{ field: "images", message: "is larger than 4 MB; add an image size of about 1024 pixels" }
|
|
983
|
+
]);
|
|
984
|
+
return { data: Buffer.from(file.body).toString("base64"), mediaType };
|
|
985
|
+
}
|
|
986
|
+
// --- Admins ----------------------------------------------------------------------------------
|
|
987
|
+
/** Settings → Integrations, AI: the main connection, the features, the limit and this month. */
|
|
988
|
+
async overview(now = /* @__PURE__ */ new Date()) {
|
|
989
|
+
const settings = await this.settings();
|
|
990
|
+
const conns = await this.connections();
|
|
991
|
+
const labels = new Map(this.config.integrations.map((i) => [i.id, i.label]));
|
|
992
|
+
const month = monthStart(now, this.config.timeZone).toISOString();
|
|
993
|
+
const rows2 = (await this.cms.db.find({
|
|
994
|
+
collection: AI_USAGE,
|
|
995
|
+
where: { createdAt: { gte: month } },
|
|
996
|
+
sort: ["id"],
|
|
997
|
+
limit: 0,
|
|
998
|
+
page: 1
|
|
999
|
+
})).docs;
|
|
1000
|
+
const sum = (key) => {
|
|
1001
|
+
const totals = /* @__PURE__ */ new Map();
|
|
1002
|
+
for (const row of rows2) {
|
|
1003
|
+
if (key === "user" && !row.user) continue;
|
|
1004
|
+
const k = String(row[key]);
|
|
1005
|
+
const t = totals.get(k) ?? { requests: 0, input: 0, output: 0 };
|
|
1006
|
+
t.requests++;
|
|
1007
|
+
t.input += Number(row.input ?? 0);
|
|
1008
|
+
t.output += Number(row.output ?? 0);
|
|
1009
|
+
totals.set(k, t);
|
|
1010
|
+
}
|
|
1011
|
+
return [...totals].sort((a, b) => b[1].requests - a[1].requests);
|
|
1012
|
+
};
|
|
1013
|
+
const users = sum("user");
|
|
1014
|
+
const emails = /* @__PURE__ */ new Map();
|
|
1015
|
+
if (users.length > 0) {
|
|
1016
|
+
const found = await this.cms.db.findByIds({
|
|
1017
|
+
collection: "users",
|
|
1018
|
+
ids: users.map(([id]) => /^\d+$/.test(id) ? Number(id) : id)
|
|
1019
|
+
});
|
|
1020
|
+
for (const u of found) emails.set(String(u.id), String(u.email ?? u.id));
|
|
1021
|
+
}
|
|
1022
|
+
return {
|
|
1023
|
+
main: settings.main ?? null,
|
|
1024
|
+
perUserPerDay: settings.perUserPerDay ?? this.defaultLimit,
|
|
1025
|
+
defaultPerUserPerDay: this.defaultLimit,
|
|
1026
|
+
connections: conns.map((c) => ({
|
|
1027
|
+
id: ref(c.integration, c.name),
|
|
1028
|
+
integration: c.integration,
|
|
1029
|
+
name: c.name,
|
|
1030
|
+
label: labels.get(c.integration) ?? c.integration
|
|
1031
|
+
})),
|
|
1032
|
+
features: await Promise.all(
|
|
1033
|
+
this.features.map(async (f) => ({
|
|
1034
|
+
id: f.id,
|
|
1035
|
+
label: f.label,
|
|
1036
|
+
...f.description ? { description: f.description } : {},
|
|
1037
|
+
enabled: await this.enabled(f.id),
|
|
1038
|
+
connection: settings.features?.[f.id]?.connection ?? null
|
|
1039
|
+
}))
|
|
1040
|
+
),
|
|
1041
|
+
usage: {
|
|
1042
|
+
byUser: users.map(([id, t]) => ({ user: emails.get(id) ?? id, ...t })),
|
|
1043
|
+
byFeature: sum("feature").map(([feature, t]) => ({ feature, ...t }))
|
|
1044
|
+
}
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
/** Tokens and requests of each connection this month, for the cards. */
|
|
1048
|
+
async usageByConnection(now = /* @__PURE__ */ new Date()) {
|
|
1049
|
+
const month = monthStart(now, this.config.timeZone).toISOString();
|
|
1050
|
+
const rows2 = (await this.cms.db.find({
|
|
1051
|
+
collection: AI_USAGE,
|
|
1052
|
+
where: { createdAt: { gte: month } },
|
|
1053
|
+
sort: ["id"],
|
|
1054
|
+
limit: 0,
|
|
1055
|
+
page: 1
|
|
1056
|
+
})).docs;
|
|
1057
|
+
const out = /* @__PURE__ */ new Map();
|
|
1058
|
+
for (const row of rows2) {
|
|
1059
|
+
const k = ref(String(row.integration), String(row.connection ?? ""));
|
|
1060
|
+
const t = out.get(k) ?? { requests: 0, input: 0, output: 0 };
|
|
1061
|
+
t.requests++;
|
|
1062
|
+
t.input += Number(row.input ?? 0);
|
|
1063
|
+
t.output += Number(row.output ?? 0);
|
|
1064
|
+
out.set(k, t);
|
|
1065
|
+
}
|
|
1066
|
+
return out;
|
|
1067
|
+
}
|
|
1068
|
+
/** Saves admins' choices; audit-logged. */
|
|
1069
|
+
async update(body, user) {
|
|
1070
|
+
const input = body ?? {};
|
|
1071
|
+
const current = await this.settings();
|
|
1072
|
+
const conns = new Set((await this.connections()).map((c) => ref(c.integration, c.name)));
|
|
1073
|
+
const next = { ...current };
|
|
1074
|
+
const fail = (field, message2) => {
|
|
1075
|
+
throw new ValidationError(AI_USAGE, [{ field, message: message2 }]);
|
|
1076
|
+
};
|
|
1077
|
+
if (input.main !== void 0) {
|
|
1078
|
+
if (input.main === null) delete next.main;
|
|
1079
|
+
else if (typeof input.main === "string" && conns.has(input.main)) next.main = input.main;
|
|
1080
|
+
else fail("main", "is not a connection");
|
|
1081
|
+
}
|
|
1082
|
+
if (input.perUserPerDay !== void 0) {
|
|
1083
|
+
if (input.perUserPerDay === null) delete next.perUserPerDay;
|
|
1084
|
+
else if (Number.isInteger(input.perUserPerDay) && input.perUserPerDay >= 0)
|
|
1085
|
+
next.perUserPerDay = input.perUserPerDay;
|
|
1086
|
+
else fail("perUserPerDay", "must be a whole number, 0 or more");
|
|
1087
|
+
}
|
|
1088
|
+
if (input.features !== void 0) {
|
|
1089
|
+
if (typeof input.features !== "object" || input.features === null)
|
|
1090
|
+
fail("features", "must be an object");
|
|
1091
|
+
const features = { ...current.features ?? {} };
|
|
1092
|
+
for (const [id, raw] of Object.entries(input.features)) {
|
|
1093
|
+
if (!this.features.some((f) => f.id === id)) fail("features", `unknown feature "${id}"`);
|
|
1094
|
+
const change = raw ?? {};
|
|
1095
|
+
const entry = { ...features[id] ?? {} };
|
|
1096
|
+
if (typeof change.enabled === "boolean") entry.enabled = change.enabled;
|
|
1097
|
+
if (change.connection === null) delete entry.connection;
|
|
1098
|
+
else if (change.connection !== void 0) {
|
|
1099
|
+
if (typeof change.connection !== "string" || !conns.has(change.connection))
|
|
1100
|
+
fail("features", "connection is not a connection");
|
|
1101
|
+
entry.connection = change.connection;
|
|
1102
|
+
}
|
|
1103
|
+
features[id] = entry;
|
|
1104
|
+
}
|
|
1105
|
+
next.features = features;
|
|
1106
|
+
}
|
|
1107
|
+
const row = await this.settingsRow();
|
|
1108
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1109
|
+
if (row)
|
|
1110
|
+
await this.cms.db.update({
|
|
1111
|
+
collection: CONNECTIONS,
|
|
1112
|
+
id: row.id,
|
|
1113
|
+
data: { ...row, values: next, updatedAt: now }
|
|
1114
|
+
});
|
|
1115
|
+
else
|
|
1116
|
+
await this.cms.db.create({
|
|
1117
|
+
collection: CONNECTIONS,
|
|
1118
|
+
data: {
|
|
1119
|
+
kind: "settings",
|
|
1120
|
+
integration: SETTINGS,
|
|
1121
|
+
name: null,
|
|
1122
|
+
isDefault: false,
|
|
1123
|
+
values: next,
|
|
1124
|
+
secret: null,
|
|
1125
|
+
account: null,
|
|
1126
|
+
lastUsedAt: null,
|
|
1127
|
+
lastError: null,
|
|
1128
|
+
enabled: null,
|
|
1129
|
+
createdAt: now,
|
|
1130
|
+
updatedAt: now
|
|
1131
|
+
}
|
|
1132
|
+
});
|
|
1133
|
+
await this.cms.audit.record({
|
|
1134
|
+
action: "integration.ai",
|
|
1135
|
+
target: SETTINGS,
|
|
1136
|
+
user,
|
|
1137
|
+
detail: { ...next }
|
|
1138
|
+
});
|
|
1139
|
+
return this.overview();
|
|
1140
|
+
}
|
|
1141
|
+
/** Forgets usage older than 90 days (with the scheduled jobs). */
|
|
1142
|
+
async upkeep(now = /* @__PURE__ */ new Date()) {
|
|
1143
|
+
const before = new Date(now.getTime() - KEEP).toISOString();
|
|
1144
|
+
for (; ; ) {
|
|
1145
|
+
const old = await this.cms.db.find({
|
|
1146
|
+
collection: AI_USAGE,
|
|
1147
|
+
where: { createdAt: { lt: before } },
|
|
1148
|
+
sort: ["id"],
|
|
1149
|
+
limit: 500,
|
|
1150
|
+
page: 1
|
|
1151
|
+
});
|
|
1152
|
+
for (const row of old.docs) await this.cms.db.delete({ collection: AI_USAGE, id: row.id });
|
|
1153
|
+
if (old.docs.length < 500) return;
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
};
|
|
1157
|
+
function stripFence(text4) {
|
|
1158
|
+
const trimmed = text4.trim();
|
|
1159
|
+
const match = /^```(?:json)?\s*\n([\s\S]*?)\n?```$/.exec(trimmed);
|
|
1160
|
+
return match ? match[1] : trimmed;
|
|
1161
|
+
}
|
|
1162
|
+
function monthStart(now, zone) {
|
|
1163
|
+
let day = lastSlot(now, "00:00", zone);
|
|
1164
|
+
const dayOf = (d) => Number(new Intl.DateTimeFormat("en-US", { day: "numeric", timeZone: zone }).format(d));
|
|
1165
|
+
for (let i = 0; i < 31 && dayOf(day) > 1; i++)
|
|
1166
|
+
day = lastSlot(new Date(day.getTime() - 1), "00:00", zone);
|
|
1167
|
+
return day;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
// src/auth/tokens.ts
|
|
1171
|
+
import { createHash, createHmac, randomBytes, timingSafeEqual } from "crypto";
|
|
1172
|
+
function newToken() {
|
|
1173
|
+
return randomBytes(32).toString("base64url");
|
|
1174
|
+
}
|
|
1175
|
+
function hashToken(token) {
|
|
1176
|
+
return createHash("sha256").update(token).digest("hex");
|
|
1177
|
+
}
|
|
1178
|
+
function hmac(secret, value) {
|
|
1179
|
+
return createHmac("sha256", secret).update(value).digest("base64url");
|
|
1180
|
+
}
|
|
1181
|
+
var signValue = (secret, value) => hmac(secret, value);
|
|
1182
|
+
function secretsOf(config) {
|
|
1183
|
+
return [config.secret, ...config.previousSecrets ?? []];
|
|
1184
|
+
}
|
|
1185
|
+
function signedWith(secrets, value, signature) {
|
|
1186
|
+
return [secrets].flat().some((secret) => safeEqual(signature, hmac(secret, value)));
|
|
1187
|
+
}
|
|
1188
|
+
function signToken(secret, token) {
|
|
1189
|
+
return `${token}.${hmac(secret, `session:${token}`)}`;
|
|
1190
|
+
}
|
|
1191
|
+
function unsignToken(secrets, value) {
|
|
1192
|
+
const dot = value.lastIndexOf(".");
|
|
1193
|
+
if (dot <= 0) return void 0;
|
|
1194
|
+
const token = value.slice(0, dot);
|
|
1195
|
+
return signedWith(secrets, `session:${token}`, value.slice(dot + 1)) ? token : void 0;
|
|
1196
|
+
}
|
|
1197
|
+
function csrfToken(secret, token) {
|
|
1198
|
+
return hmac(secret, `csrf:${token}`);
|
|
1199
|
+
}
|
|
1200
|
+
function csrfMatches(secrets, token, given) {
|
|
1201
|
+
return signedWith(secrets, `csrf:${token}`, given);
|
|
1202
|
+
}
|
|
1203
|
+
function safeEqual(a, b) {
|
|
1204
|
+
const left = Buffer.from(a);
|
|
1205
|
+
const right = Buffer.from(b);
|
|
1206
|
+
return left.length === right.length && timingSafeEqual(left, right);
|
|
1207
|
+
}
|
|
1208
|
+
function signPreviewToken(secret, target, expiresAt, user) {
|
|
1209
|
+
const payload = Buffer.from(
|
|
1210
|
+
JSON.stringify({
|
|
1211
|
+
..."global" in target ? { g: target.global } : { c: target.collection, i: target.id },
|
|
1212
|
+
e: expiresAt,
|
|
1213
|
+
...user !== void 0 ? { u: user } : {}
|
|
1214
|
+
})
|
|
1215
|
+
).toString("base64url");
|
|
1216
|
+
return `${payload}.${hmac(secret, `preview:${payload}`)}`;
|
|
1217
|
+
}
|
|
1218
|
+
function verifyPreviewToken(secrets, token, now = Date.now()) {
|
|
1219
|
+
const dot = token.lastIndexOf(".");
|
|
1220
|
+
if (dot <= 0) return null;
|
|
1221
|
+
const payload = token.slice(0, dot);
|
|
1222
|
+
if (!signedWith(secrets, `preview:${payload}`, token.slice(dot + 1))) return null;
|
|
1223
|
+
try {
|
|
1224
|
+
const data = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
1225
|
+
if (typeof data.e !== "number" || data.e <= now) return null;
|
|
1226
|
+
const user = typeof data.u === "string" ? { user: data.u } : {};
|
|
1227
|
+
if (typeof data.g === "string") return { global: data.g, expiresAt: data.e, ...user };
|
|
1228
|
+
if (typeof data.c === "string" && typeof data.i === "string")
|
|
1229
|
+
return { collection: data.c, id: data.i, expiresAt: data.e, ...user };
|
|
1230
|
+
return null;
|
|
1231
|
+
} catch {
|
|
1232
|
+
return null;
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
function passwordFingerprint(passwordHash) {
|
|
1236
|
+
return createHash("sha256").update(typeof passwordHash === "string" ? passwordHash : "no-password").digest("base64url").slice(0, 22);
|
|
1237
|
+
}
|
|
1238
|
+
function signPasswordToken(secret, args) {
|
|
1239
|
+
const payload = Buffer.from(
|
|
1240
|
+
JSON.stringify({ u: args.userId, p: args.purpose, e: args.expiresAt })
|
|
1241
|
+
).toString("base64url");
|
|
1242
|
+
return `${payload}.${hmac(secret, `password:${payload}:${args.fingerprint}`)}`;
|
|
1243
|
+
}
|
|
1244
|
+
function readPasswordToken(token) {
|
|
1245
|
+
const dot = token.lastIndexOf(".");
|
|
1246
|
+
if (dot <= 0) return null;
|
|
1247
|
+
const payload = token.slice(0, dot);
|
|
1248
|
+
try {
|
|
1249
|
+
const data = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
1250
|
+
if (typeof data.u !== "string" || typeof data.e !== "number") return null;
|
|
1251
|
+
if (data.p !== "reset" && data.p !== "invite" && data.p !== "verify") return null;
|
|
1252
|
+
return { userId: data.u, purpose: data.p, expiresAt: data.e, payload };
|
|
1253
|
+
} catch {
|
|
1254
|
+
return null;
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
function passwordTokenMatches(secrets, token, fingerprint) {
|
|
1258
|
+
const dot = token.lastIndexOf(".");
|
|
1259
|
+
if (dot <= 0) return false;
|
|
1260
|
+
const payload = token.slice(0, dot);
|
|
1261
|
+
return signedWith(secrets, `password:${payload}:${fingerprint}`, token.slice(dot + 1));
|
|
1262
|
+
}
|
|
1263
|
+
|
|
744
1264
|
// src/audit.ts
|
|
745
1265
|
import { AsyncLocalStorage } from "async_hooks";
|
|
746
1266
|
import { createHmac as createHmac2 } from "crypto";
|
|
@@ -758,7 +1278,7 @@ var DEFAULT_FAILED_LOGINS = 20;
|
|
|
758
1278
|
var MAX_VALUE = 500;
|
|
759
1279
|
var PAGE = 50;
|
|
760
1280
|
var CSV_LIMIT = 1e4;
|
|
761
|
-
var
|
|
1281
|
+
var DAY2 = 864e5;
|
|
762
1282
|
var BULKY = /* @__PURE__ */ new Set(["richText", "blocks", "array", "json"]);
|
|
763
1283
|
var auditContext = new AsyncLocalStorage();
|
|
764
1284
|
var SIGN_IN_FAILURES = /* @__PURE__ */ new Set(["login.failed", "login.locked", "sso.failed"]);
|
|
@@ -1033,10 +1553,10 @@ var AuditLog = class {
|
|
|
1033
1553
|
async upkeep(now = /* @__PURE__ */ new Date()) {
|
|
1034
1554
|
if (!this.enabled) return;
|
|
1035
1555
|
const last = await this.lastVerification();
|
|
1036
|
-
if (last && now.getTime() - Date.parse(last.at) <
|
|
1556
|
+
if (last && now.getTime() - Date.parse(last.at) < DAY2) return;
|
|
1037
1557
|
const keep = this.settings.keepDays;
|
|
1038
1558
|
if (keep > 0) {
|
|
1039
|
-
const before = new Date(now.getTime() - keep *
|
|
1559
|
+
const before = new Date(now.getTime() - keep * DAY2).toISOString();
|
|
1040
1560
|
for (; ; ) {
|
|
1041
1561
|
const { docs } = await this.cms.db.find({
|
|
1042
1562
|
collection: AUDIT_LOGS,
|
|
@@ -3311,82 +3831,6 @@ import { tmpdir } from "os";
|
|
|
3311
3831
|
import { join as join2 } from "path";
|
|
3312
3832
|
import { promisify } from "util";
|
|
3313
3833
|
import { gzip } from "zlib";
|
|
3314
|
-
|
|
3315
|
-
// src/time-of-day.ts
|
|
3316
|
-
var DAY2 = 864e5;
|
|
3317
|
-
function isTimeZone(zone) {
|
|
3318
|
-
try {
|
|
3319
|
-
new Intl.DateTimeFormat("en-US", { timeZone: zone });
|
|
3320
|
-
return true;
|
|
3321
|
-
} catch {
|
|
3322
|
-
return false;
|
|
3323
|
-
}
|
|
3324
|
-
}
|
|
3325
|
-
var formatters = /* @__PURE__ */ new Map();
|
|
3326
|
-
function wallClock(instant, zone) {
|
|
3327
|
-
let format = formatters.get(zone);
|
|
3328
|
-
if (!format) {
|
|
3329
|
-
format = new Intl.DateTimeFormat("en-US", {
|
|
3330
|
-
timeZone: zone,
|
|
3331
|
-
hourCycle: "h23",
|
|
3332
|
-
year: "numeric",
|
|
3333
|
-
month: "numeric",
|
|
3334
|
-
day: "numeric",
|
|
3335
|
-
hour: "numeric",
|
|
3336
|
-
minute: "numeric",
|
|
3337
|
-
second: "numeric"
|
|
3338
|
-
});
|
|
3339
|
-
formatters.set(zone, format);
|
|
3340
|
-
}
|
|
3341
|
-
const parts = {};
|
|
3342
|
-
for (const part of format.formatToParts(instant))
|
|
3343
|
-
if (part.type !== "literal") parts[part.type] = Number(part.value);
|
|
3344
|
-
return Date.UTC(
|
|
3345
|
-
parts.year,
|
|
3346
|
-
parts.month - 1,
|
|
3347
|
-
parts.day,
|
|
3348
|
-
parts.hour,
|
|
3349
|
-
parts.minute,
|
|
3350
|
-
parts.second
|
|
3351
|
-
);
|
|
3352
|
-
}
|
|
3353
|
-
function instantOf(wall, zone) {
|
|
3354
|
-
let instant = wall - (wallClock(wall, zone) - wall);
|
|
3355
|
-
instant = wall - (wallClock(instant, zone) - instant);
|
|
3356
|
-
return instant;
|
|
3357
|
-
}
|
|
3358
|
-
var parse = (at) => at.split(":").map(Number);
|
|
3359
|
-
function lastSlot(now, at, zone) {
|
|
3360
|
-
const [hours2, minutes] = parse(at);
|
|
3361
|
-
if (!zone) {
|
|
3362
|
-
const slot2 = new Date(now);
|
|
3363
|
-
slot2.setHours(hours2, minutes, 0, 0);
|
|
3364
|
-
if (slot2.getTime() > now.getTime()) slot2.setDate(slot2.getDate() - 1);
|
|
3365
|
-
return slot2;
|
|
3366
|
-
}
|
|
3367
|
-
const today = wallClock(now.getTime(), zone);
|
|
3368
|
-
const midnight = today - today % DAY2;
|
|
3369
|
-
let slot = instantOf(midnight + hours2 * 36e5 + minutes * 6e4, zone);
|
|
3370
|
-
if (slot > now.getTime())
|
|
3371
|
-
slot = instantOf(midnight - DAY2 + hours2 * 36e5 + minutes * 6e4, zone);
|
|
3372
|
-
return new Date(slot);
|
|
3373
|
-
}
|
|
3374
|
-
function nextSlot(after, at, zone) {
|
|
3375
|
-
const last = lastSlot(after, at, zone);
|
|
3376
|
-
return lastSlot(new Date(last.getTime() + DAY2 + 2 * 36e5), at, zone);
|
|
3377
|
-
}
|
|
3378
|
-
function dueAt(now, at, every, lastRun, zone) {
|
|
3379
|
-
if (lastRun === void 0) return true;
|
|
3380
|
-
const last = Date.parse(lastRun);
|
|
3381
|
-
if (Number.isNaN(last)) return true;
|
|
3382
|
-
const slot = lastSlot(now, at, zone).getTime();
|
|
3383
|
-
return last < slot - (every * 1e3 - DAY2);
|
|
3384
|
-
}
|
|
3385
|
-
function nextAt(at, every, lastRun, zone) {
|
|
3386
|
-
return nextSlot(new Date(Date.parse(lastRun) + (every * 1e3 - DAY2)), at, zone);
|
|
3387
|
-
}
|
|
3388
|
-
|
|
3389
|
-
// src/backups.ts
|
|
3390
3834
|
var gzipAsync = promisify(gzip);
|
|
3391
3835
|
var MAGIC = Buffer.from("ECMSBAK1");
|
|
3392
3836
|
var IV_BYTES = 12;
|
|
@@ -4927,7 +5371,7 @@ var OFFICIAL_PLUGINS = [
|
|
|
4927
5371
|
en: "Claude, GPT and Gemini for alt text, SEO and translation",
|
|
4928
5372
|
th: "Claude, GPT \u0E41\u0E25\u0E30 Gemini \u0E2A\u0E33\u0E2B\u0E23\u0E31\u0E1A alt text, SEO \u0E41\u0E25\u0E30\u0E41\u0E1B\u0E25\u0E20\u0E32\u0E29\u0E32"
|
|
4929
5373
|
},
|
|
4930
|
-
docs: "https://easycms.dev/docs/
|
|
5374
|
+
docs: "https://easycms.dev/docs/ai"
|
|
4931
5375
|
},
|
|
4932
5376
|
{
|
|
4933
5377
|
name: "@easy-cms/plugin-ecommerce",
|
|
@@ -5066,6 +5510,8 @@ var Integrations = class {
|
|
|
5066
5510
|
// --- The admin's page ------------------------------------------------------------------------
|
|
5067
5511
|
async overview() {
|
|
5068
5512
|
const integrations = [];
|
|
5513
|
+
const usage = await this.cms.ai?.usageByConnection() ?? /* @__PURE__ */ new Map();
|
|
5514
|
+
const aiProviders = new Set(this.config.ai?.providers.map((p) => p.integration) ?? []);
|
|
5069
5515
|
const current = keyId(keyOf2(this.config.secret));
|
|
5070
5516
|
let stale = 0;
|
|
5071
5517
|
for (const def of this.definitions) {
|
|
@@ -5126,7 +5572,11 @@ var Integrations = class {
|
|
|
5126
5572
|
enabled: await this.enabled(def.id),
|
|
5127
5573
|
envFields: Object.keys(env),
|
|
5128
5574
|
testable: typeof def.test === "function",
|
|
5129
|
-
connections
|
|
5575
|
+
connections: aiProviders.has(def.id) ? connections.map((c) => ({
|
|
5576
|
+
...c,
|
|
5577
|
+
usage: usage.get(`${def.id}:${c.name}`) ?? { requests: 0, input: 0, output: 0 }
|
|
5578
|
+
})) : connections,
|
|
5579
|
+
...def.consent ? { consent: def.consent, consented: await this.consented(def.id) } : {}
|
|
5130
5580
|
});
|
|
5131
5581
|
}
|
|
5132
5582
|
const installed = new Set(this.config.installedPlugins.map((p) => p.name));
|
|
@@ -5204,11 +5654,12 @@ var Integrations = class {
|
|
|
5204
5654
|
async connect(id, body, user) {
|
|
5205
5655
|
const def = this.definition(id);
|
|
5206
5656
|
if (def.kind !== "fields") throw new QueryError(`${id} connects with its Connect button`);
|
|
5657
|
+
await this.checkConsent(def);
|
|
5207
5658
|
const input = body ?? {};
|
|
5208
5659
|
const name = await this.uniqueName(id, String(input.name ?? ""));
|
|
5209
5660
|
const values = this.cleanValues(def, input.values);
|
|
5210
5661
|
this.checkFields(def, values, false);
|
|
5211
|
-
const result = def.test ? await def.test(values, { cms: this.cms }) : null;
|
|
5662
|
+
const result = def.test ? await def.test(values, { cms: this.cms, name }) : null;
|
|
5212
5663
|
if (result && !result.ok)
|
|
5213
5664
|
throw new ValidationError(CONNECTIONS, [
|
|
5214
5665
|
{ field: "values", message: result.message ?? "the provider refused these values" }
|
|
@@ -5268,13 +5719,41 @@ var Integrations = class {
|
|
|
5268
5719
|
/** Turns the integration on or off; its connections are kept. */
|
|
5269
5720
|
async setEnabled(id, enabled, user) {
|
|
5270
5721
|
this.definition(id);
|
|
5722
|
+
await this.saveIntegration(id, { enabled });
|
|
5723
|
+
await this.audit(user, enabled ? "enable" : "disable", id);
|
|
5724
|
+
return { enabled };
|
|
5725
|
+
}
|
|
5726
|
+
/** Who accepted the integration's `consent`, and when; `null` before anyone did. */
|
|
5727
|
+
async consented(id) {
|
|
5728
|
+
const [row] = await this.rows(id, "integration");
|
|
5729
|
+
const consent = (row?.values ?? {}).consent;
|
|
5730
|
+
return consent ?? null;
|
|
5731
|
+
}
|
|
5732
|
+
async checkConsent(def) {
|
|
5733
|
+
if (def.consent && !await this.consented(def.id))
|
|
5734
|
+
throw new ValidationError(CONNECTIONS, [
|
|
5735
|
+
{ field: "consent", message: "must be accepted before connecting" }
|
|
5736
|
+
]);
|
|
5737
|
+
}
|
|
5738
|
+
/** An admin accepts the integration's `consent`; audit-logged. */
|
|
5739
|
+
async consent(id, user) {
|
|
5740
|
+
const def = this.definition(id);
|
|
5741
|
+
if (!def.consent) throw new QueryError(`${id} asks for no consent`);
|
|
5742
|
+
const [row] = await this.rows(id, "integration");
|
|
5743
|
+
const consent = { at: (/* @__PURE__ */ new Date()).toISOString(), by: String(user.email ?? user.id) };
|
|
5744
|
+
await this.saveIntegration(id, { values: { ...row?.values ?? {}, consent } });
|
|
5745
|
+
await this.audit(user, "consent", id);
|
|
5746
|
+
return { consented: consent };
|
|
5747
|
+
}
|
|
5748
|
+
/** The integration's own row (on or off, consent), made when first needed. */
|
|
5749
|
+
async saveIntegration(id, patch) {
|
|
5271
5750
|
const [row] = await this.rows(id, "integration");
|
|
5272
5751
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5273
5752
|
if (row)
|
|
5274
5753
|
await this.cms.db.update({
|
|
5275
5754
|
collection: CONNECTIONS,
|
|
5276
5755
|
id: row.id,
|
|
5277
|
-
data: { ...row,
|
|
5756
|
+
data: { ...row, ...patch, updatedAt: now }
|
|
5278
5757
|
});
|
|
5279
5758
|
else
|
|
5280
5759
|
await this.cms.db.create({
|
|
@@ -5289,22 +5768,22 @@ var Integrations = class {
|
|
|
5289
5768
|
account: null,
|
|
5290
5769
|
lastUsedAt: null,
|
|
5291
5770
|
lastError: null,
|
|
5292
|
-
enabled,
|
|
5771
|
+
enabled: null,
|
|
5293
5772
|
createdAt: now,
|
|
5294
|
-
updatedAt: now
|
|
5773
|
+
updatedAt: now,
|
|
5774
|
+
...patch
|
|
5295
5775
|
}
|
|
5296
5776
|
});
|
|
5297
|
-
await this.audit(user, enabled ? "enable" : "disable", id);
|
|
5298
|
-
return { enabled };
|
|
5299
5777
|
}
|
|
5300
5778
|
/** Checks a connection's values with the provider; the card shows the result. */
|
|
5301
5779
|
async test(id, cid) {
|
|
5302
5780
|
const def = this.definition(id);
|
|
5303
5781
|
if (!def.test) throw new QueryError(`${id} can't be tested`);
|
|
5304
5782
|
const values = cid === "env" ? this.envValues(def) : { ...this.decrypt(await this.row(id, cid)), ...this.envValues(def) };
|
|
5783
|
+
const name = cid === "env" ? ENV_NAME : String((await this.row(id, cid)).name);
|
|
5305
5784
|
let result;
|
|
5306
5785
|
try {
|
|
5307
|
-
result = await def.test(values, { cms: this.cms });
|
|
5786
|
+
result = await def.test(values, { cms: this.cms, name });
|
|
5308
5787
|
} catch (error) {
|
|
5309
5788
|
result = { ok: false, message: error.message };
|
|
5310
5789
|
}
|
|
@@ -5349,10 +5828,11 @@ var Integrations = class {
|
|
|
5349
5828
|
}
|
|
5350
5829
|
// --- OAuth -----------------------------------------------------------------------------------
|
|
5351
5830
|
/** The provider's sign-in page to send the admin to. */
|
|
5352
|
-
authorize(id, user, redirectUri, name) {
|
|
5831
|
+
async authorize(id, user, redirectUri, name) {
|
|
5353
5832
|
const def = this.definition(id);
|
|
5354
5833
|
if (def.kind !== "oauth" || !def.oauth)
|
|
5355
5834
|
throw new QueryError(`${id} connects with pasted values`);
|
|
5835
|
+
await this.checkConsent(def);
|
|
5356
5836
|
const state = signState(this.config.secret, {
|
|
5357
5837
|
i: id,
|
|
5358
5838
|
u: String(user.id),
|
|
@@ -6150,7 +6630,7 @@ function read(response, maxBytes, deadline) {
|
|
|
6150
6630
|
|
|
6151
6631
|
// src/config-keys.ts
|
|
6152
6632
|
var keys = (list) => new Set(list.split(/\s+/).filter(Boolean));
|
|
6153
|
-
var CONFIG = keys(`secret previousSecrets integrations listeners db serverURL cors cronSecret timeZone webhooks events jobs localization routes admin
|
|
6633
|
+
var CONFIG = keys(`secret previousSecrets integrations listeners ai aiFeatures db serverURL cors cronSecret timeZone webhooks events jobs localization routes admin
|
|
6154
6634
|
upload auth collections globals endpoints cliCommands onRequest fieldTypes apiKeys email backups
|
|
6155
6635
|
audit logs countCacheSeconds maxPage httpCache outbox rateLimit search telemetry plugins`);
|
|
6156
6636
|
var ADMIN = keys("path locale brand siteURL nav commands modules pages dashboard switcher");
|
|
@@ -6704,7 +7184,7 @@ var WEBHOOK_EVENTS = [
|
|
|
6704
7184
|
var RETRY_DELAYS = [1e3, 5e3];
|
|
6705
7185
|
var QUEUE_DELAYS2 = [6e4, 5 * 6e4, 30 * 6e4, 2 * 36e5, 6 * 36e5, 12 * 36e5];
|
|
6706
7186
|
var CLAIM2 = 5 * 6e4;
|
|
6707
|
-
var
|
|
7187
|
+
var TIMEOUT2 = 1e4;
|
|
6708
7188
|
var Webhooks = class {
|
|
6709
7189
|
constructor(hooks, logger, queue, fetcher = (...args) => fetch(...args), retryDelays = RETRY_DELAYS) {
|
|
6710
7190
|
this.hooks = hooks;
|
|
@@ -6872,7 +7352,7 @@ var Webhooks = class {
|
|
|
6872
7352
|
method: "POST",
|
|
6873
7353
|
headers,
|
|
6874
7354
|
body,
|
|
6875
|
-
signal: AbortSignal.timeout(
|
|
7355
|
+
signal: AbortSignal.timeout(TIMEOUT2)
|
|
6876
7356
|
});
|
|
6877
7357
|
if (response.ok || response.status < 500 && response.status !== 429) {
|
|
6878
7358
|
if (!response.ok)
|
|
@@ -6981,6 +7461,7 @@ function validateConfig(config) {
|
|
|
6981
7461
|
validateJobs(config, add);
|
|
6982
7462
|
validateIntegrations(config, add);
|
|
6983
7463
|
validateListeners(config, add);
|
|
7464
|
+
validateAi(config, add);
|
|
6984
7465
|
validateTimeZone(config, add);
|
|
6985
7466
|
validateEvents(config, add);
|
|
6986
7467
|
const apiKeys = config.apiKeys;
|
|
@@ -8626,6 +9107,22 @@ function validateIntegrations(config, add) {
|
|
|
8626
9107
|
} else add(`${at}.kind`, "must be 'fields' or 'oauth'");
|
|
8627
9108
|
if (item.test !== void 0 && typeof item.test !== "function")
|
|
8628
9109
|
add(`${at}.test`, "must be a function");
|
|
9110
|
+
if (item.icon !== void 0 && typeof item.icon !== "string") {
|
|
9111
|
+
const logo = item.icon ?? {};
|
|
9112
|
+
const hex = (v) => typeof v === "string" && /^#[0-9a-f]{6}$/i.test(v);
|
|
9113
|
+
const paths = Array.isArray(logo.paths) ? logo.paths : [];
|
|
9114
|
+
if (paths.length === 0 || paths.some((p) => {
|
|
9115
|
+
const path = p ?? {};
|
|
9116
|
+
return typeof path.d !== "string" || !/^[MmLlHhVvCcSsQqTtAaZzEe0-9.,\s+-]+$/.test(path.d) || path.fill !== void 0 && !hex(path.fill);
|
|
9117
|
+
}))
|
|
9118
|
+
add(
|
|
9119
|
+
`${at}.icon`,
|
|
9120
|
+
"must be an admin icon name, or { paths: [{ d, fill? }], color? } with SVG path data"
|
|
9121
|
+
);
|
|
9122
|
+
if (logo.color !== void 0 && !hex(logo.color)) add(`${at}.icon.color`, "must be #rrggbb");
|
|
9123
|
+
if (logo.viewBox !== void 0 && !(typeof logo.viewBox === "string" && /^-?[\d.]+( -?[\d.]+){3}$/.test(logo.viewBox)))
|
|
9124
|
+
add(`${at}.icon.viewBox`, "must be four numbers, e.g. '0 0 24 24'");
|
|
9125
|
+
}
|
|
8629
9126
|
for (const [f, field] of (Array.isArray(item.fields) ? item.fields : []).entries()) {
|
|
8630
9127
|
const fieldItem = field ?? {};
|
|
8631
9128
|
if (fieldItem.lookup !== void 0 && typeof fieldItem.lookup !== "function")
|
|
@@ -8648,6 +9145,45 @@ function validateListeners(config, add) {
|
|
|
8648
9145
|
if (typeof item.handler !== "function") add(`listeners[${i}].handler`, "must be a function");
|
|
8649
9146
|
}
|
|
8650
9147
|
}
|
|
9148
|
+
function validateAi(config, add) {
|
|
9149
|
+
const ai = config.ai;
|
|
9150
|
+
if (ai !== void 0) {
|
|
9151
|
+
const providers = ai?.providers;
|
|
9152
|
+
if (!Array.isArray(providers)) add("ai.providers", "must be an array");
|
|
9153
|
+
else {
|
|
9154
|
+
const ids = new Set((config.integrations ?? []).map((i) => i?.id));
|
|
9155
|
+
for (const [i, raw] of providers.entries()) {
|
|
9156
|
+
const provider = raw ?? {};
|
|
9157
|
+
if (typeof provider.integration !== "string" || !ids.has(provider.integration))
|
|
9158
|
+
add(`ai.providers[${i}].integration`, "must be the id of an integration");
|
|
9159
|
+
if (typeof provider.generate !== "function")
|
|
9160
|
+
add(`ai.providers[${i}].generate`, "must be a function");
|
|
9161
|
+
}
|
|
9162
|
+
}
|
|
9163
|
+
const limits = ai?.limits;
|
|
9164
|
+
const whole = (v) => Number.isInteger(v) && v >= 0;
|
|
9165
|
+
if (limits?.perUserPerDay !== void 0 && !whole(limits.perUserPerDay))
|
|
9166
|
+
add("ai.limits.perUserPerDay", "must be a whole number, 0 or more");
|
|
9167
|
+
for (const [role, value] of Object.entries(limits?.roles ?? {}))
|
|
9168
|
+
if (!whole(value)) add(`ai.limits.roles.${role}`, "must be a whole number, 0 or more");
|
|
9169
|
+
}
|
|
9170
|
+
const features = config.aiFeatures;
|
|
9171
|
+
if (features === void 0) return;
|
|
9172
|
+
if (!Array.isArray(features)) {
|
|
9173
|
+
add("aiFeatures", "must be an array");
|
|
9174
|
+
return;
|
|
9175
|
+
}
|
|
9176
|
+
const seen = /* @__PURE__ */ new Set();
|
|
9177
|
+
for (const [i, raw] of features.entries()) {
|
|
9178
|
+
const feature = raw ?? {};
|
|
9179
|
+
if (typeof feature.id !== "string" || !/^[a-z0-9][a-z0-9-]*$/.test(feature.id))
|
|
9180
|
+
add(`aiFeatures[${i}].id`, 'must be lowercase letters, digits and "-"', "e.g. id: 'alt-text'");
|
|
9181
|
+
else if (seen.has(feature.id) || feature.id === "test")
|
|
9182
|
+
add(`aiFeatures[${i}].id`, `"${feature.id}" is taken`);
|
|
9183
|
+
else seen.add(feature.id);
|
|
9184
|
+
if (!isLabel(feature.label)) add(`aiFeatures[${i}].label`, "must be a string or { en, th }");
|
|
9185
|
+
}
|
|
9186
|
+
}
|
|
8651
9187
|
|
|
8652
9188
|
// src/resolve-config.ts
|
|
8653
9189
|
var DEFAULT_ADMIN_PATH = "/admin";
|
|
@@ -8759,6 +9295,7 @@ async function resolveConfig(input) {
|
|
|
8759
9295
|
bulkOperationsCollection,
|
|
8760
9296
|
savedViewsCollection,
|
|
8761
9297
|
...resolvedIntegrations(config).length > 0 ? [connectionsCollection] : [],
|
|
9298
|
+
...config.ai ? [aiUsageCollection] : [],
|
|
8762
9299
|
...config.auth?.rbac ? [rolesCollection] : [],
|
|
8763
9300
|
...(config.auth?.providers?.length ?? 0) > 0 ? [userIdentitiesCollection] : [],
|
|
8764
9301
|
...config.audit ? [auditLogsCollection] : []
|
|
@@ -8766,6 +9303,7 @@ async function resolveConfig(input) {
|
|
|
8766
9303
|
globals: config.globals ?? [],
|
|
8767
9304
|
endpoints: config.endpoints ?? [],
|
|
8768
9305
|
integrations: resolvedIntegrations(config),
|
|
9306
|
+
aiFeatures: config.ai ? config.aiFeatures ?? [] : [],
|
|
8769
9307
|
cliCommands: config.cliCommands ?? [],
|
|
8770
9308
|
// Easy CMS's own events (#356), for webhooks and `cms.on`.
|
|
8771
9309
|
events: [.../* @__PURE__ */ new Set([...config.events ?? [], ...SYSTEM_EVENTS])],
|
|
@@ -8884,7 +9422,7 @@ function cacheableResponse(request, body, status, headers, times, tags) {
|
|
|
8884
9422
|
}
|
|
8885
9423
|
|
|
8886
9424
|
// src/version.ts
|
|
8887
|
-
var VERSION = true ? "1.
|
|
9425
|
+
var VERSION = true ? "1.33.0" : "0.0.0-dev";
|
|
8888
9426
|
|
|
8889
9427
|
// src/telemetry.ts
|
|
8890
9428
|
var DB_CALLS = /* @__PURE__ */ new Set([
|
|
@@ -9163,6 +9701,8 @@ var EasyCMS = class {
|
|
|
9163
9701
|
folders;
|
|
9164
9702
|
/** Accounts connected to plugins' integrations (`integrations`, Settings → Integrations). */
|
|
9165
9703
|
integrations;
|
|
9704
|
+
/** AI for the admin's features (#357): `null` without an AI plugin (`ai` in the config). */
|
|
9705
|
+
ai;
|
|
9166
9706
|
versions;
|
|
9167
9707
|
webhooks;
|
|
9168
9708
|
mailer;
|
|
@@ -9191,6 +9731,7 @@ var EasyCMS = class {
|
|
|
9191
9731
|
);
|
|
9192
9732
|
this.audit = new AuditLog(this);
|
|
9193
9733
|
this.integrations = new Integrations(this);
|
|
9734
|
+
this.ai = config.ai ? new Ai(this, config.ai) : null;
|
|
9194
9735
|
for (const listener of config.listeners ?? []) {
|
|
9195
9736
|
const run = (message2) => listener.handler(message2, { cms: this });
|
|
9196
9737
|
if (listener.events === "*") this.anyListeners.add(run);
|
|
@@ -10718,6 +11259,7 @@ var EasyCMS = class {
|
|
|
10718
11259
|
(error) => this.logger.error(`Backups: ${error.message}`, { error })
|
|
10719
11260
|
);
|
|
10720
11261
|
await this.audit.upkeep(now).catch((error) => this.logger.error(`Audit log: ${error.message}`, { error }));
|
|
11262
|
+
await this.ai?.upkeep(now).catch((error) => this.logger.error(`AI usage: ${error.message}`, { error }));
|
|
10721
11263
|
await this.rateLimiter?.clear(now).catch((error) => this.logger.error(`Rate limits: ${error.message}`, { error }));
|
|
10722
11264
|
const result = {
|
|
10723
11265
|
scheduled,
|
|
@@ -12049,51 +12591,51 @@ var EasyCMS = class {
|
|
|
12049
12591
|
const cms = this;
|
|
12050
12592
|
const { user, context } = guard;
|
|
12051
12593
|
const allowed2 = /* @__PURE__ */ new Map();
|
|
12052
|
-
for (const
|
|
12053
|
-
const filter =
|
|
12594
|
+
for (const ref2 of references2) {
|
|
12595
|
+
const filter = ref2.filterOptions;
|
|
12054
12596
|
if (!filter) continue;
|
|
12055
12597
|
const byCollection = allowed2.get(filter) ?? /* @__PURE__ */ new Map();
|
|
12056
12598
|
allowed2.set(filter, byCollection);
|
|
12057
|
-
if (!byCollection.has(
|
|
12599
|
+
if (!byCollection.has(ref2.collection))
|
|
12058
12600
|
byCollection.set(
|
|
12059
|
-
|
|
12060
|
-
await filter({ id: selfId, relationTo:
|
|
12601
|
+
ref2.collection,
|
|
12602
|
+
await filter({ id: selfId, relationTo: ref2.collection, user, cms, context })
|
|
12061
12603
|
);
|
|
12062
|
-
const where = byCollection.get(
|
|
12604
|
+
const where = byCollection.get(ref2.collection);
|
|
12063
12605
|
if (where === true) continue;
|
|
12064
|
-
const target = this.config.collections.find((c) => c.slug ===
|
|
12606
|
+
const target = this.config.collections.find((c) => c.slug === ref2.collection);
|
|
12065
12607
|
const localization = this.config.localization;
|
|
12066
12608
|
const scoped = target ? this.whereFor(target, where, localization ? { locale: localization.defaultLocale } : {}) : where;
|
|
12067
12609
|
const count = await this.db.count({
|
|
12068
|
-
collection:
|
|
12069
|
-
where: { and: [{ id: { equals:
|
|
12610
|
+
collection: ref2.collection,
|
|
12611
|
+
where: { and: [{ id: { equals: ref2.id } }, ...scoped ? [scoped] : []] }
|
|
12070
12612
|
});
|
|
12071
12613
|
if (count === 0)
|
|
12072
|
-
errors.push({ field:
|
|
12614
|
+
errors.push({ field: ref2.field, message: "is not one of the allowed choices" });
|
|
12073
12615
|
}
|
|
12074
12616
|
return errors;
|
|
12075
12617
|
}
|
|
12076
12618
|
async checkReferences(references2, guard) {
|
|
12077
12619
|
const byCollection = /* @__PURE__ */ new Map();
|
|
12078
12620
|
const errors = [];
|
|
12079
|
-
for (const
|
|
12080
|
-
const target = this.config.collections.find((c) => c.slug ===
|
|
12081
|
-
if (
|
|
12621
|
+
for (const ref2 of references2) {
|
|
12622
|
+
const target = this.config.collections.find((c) => c.slug === ref2.collection);
|
|
12623
|
+
if (ref2.link && (!target || INTERNAL_COLLECTIONS.has(ref2.collection))) {
|
|
12082
12624
|
errors.push({
|
|
12083
|
-
field:
|
|
12084
|
-
message: `links to "${
|
|
12625
|
+
field: ref2.field,
|
|
12626
|
+
message: `links to "${ref2.collection}", which is not a collection`
|
|
12085
12627
|
});
|
|
12086
12628
|
continue;
|
|
12087
12629
|
}
|
|
12088
|
-
if (
|
|
12630
|
+
if (ref2.link && typeof target?.preview !== "function") {
|
|
12089
12631
|
errors.push({
|
|
12090
|
-
field:
|
|
12091
|
-
message: `links to "${
|
|
12632
|
+
field: ref2.field,
|
|
12633
|
+
message: `links to "${ref2.collection}", whose documents have no URL (set its preview)`
|
|
12092
12634
|
});
|
|
12093
12635
|
continue;
|
|
12094
12636
|
}
|
|
12095
12637
|
if (!target) continue;
|
|
12096
|
-
byCollection.set(
|
|
12638
|
+
byCollection.set(ref2.collection, [...byCollection.get(ref2.collection) ?? [], ref2]);
|
|
12097
12639
|
}
|
|
12098
12640
|
for (const [collection2, refs] of byCollection) {
|
|
12099
12641
|
const found = await this.db.findByIds({
|
|
@@ -12101,28 +12643,28 @@ var EasyCMS = class {
|
|
|
12101
12643
|
ids: [...new Set(refs.map((r) => r.id))]
|
|
12102
12644
|
});
|
|
12103
12645
|
const byId = new Map(found.map((d) => [d.id, d]));
|
|
12104
|
-
for (const
|
|
12105
|
-
const doc = byId.get(
|
|
12646
|
+
for (const ref2 of refs) {
|
|
12647
|
+
const doc = byId.get(ref2.id);
|
|
12106
12648
|
if (!doc) {
|
|
12107
|
-
errors.push({ field:
|
|
12649
|
+
errors.push({ field: ref2.field, message: `${collection2} ${ref2.id} does not exist` });
|
|
12108
12650
|
continue;
|
|
12109
12651
|
}
|
|
12110
12652
|
const type = typeof doc.mimeType === "string" ? doc.mimeType : "";
|
|
12111
|
-
if (
|
|
12112
|
-
const images =
|
|
12653
|
+
if (ref2.mimeTypes && !mimeAllowedBy(type, ref2.mimeTypes)) {
|
|
12654
|
+
const images = ref2.mimeTypes.every((t) => t.toLowerCase() === "image/*");
|
|
12113
12655
|
errors.push({
|
|
12114
|
-
field:
|
|
12115
|
-
message: images ? `must be an image (${doc.filename ??
|
|
12656
|
+
field: ref2.field,
|
|
12657
|
+
message: images ? `must be an image (${doc.filename ?? ref2.id} is ${type || "not one"})` : `must be ${ref2.mimeTypes.join(", ")} (${doc.filename ?? ref2.id} is ${type || "unknown"})`
|
|
12116
12658
|
});
|
|
12117
12659
|
}
|
|
12118
|
-
if (
|
|
12119
|
-
|
|
12660
|
+
if (ref2.folderOnly && this.folders.enabled && !await this.folders.inKeyed(
|
|
12661
|
+
ref2.folderOnly,
|
|
12120
12662
|
doc.folder ?? null,
|
|
12121
12663
|
await this.folderScope(guard)
|
|
12122
12664
|
))
|
|
12123
12665
|
errors.push({
|
|
12124
|
-
field:
|
|
12125
|
-
message: `must be a file from the "${
|
|
12666
|
+
field: ref2.field,
|
|
12667
|
+
message: `must be a file from the "${ref2.folderOnly}" folder (${doc.filename ?? ref2.id} is not)`
|
|
12126
12668
|
});
|
|
12127
12669
|
}
|
|
12128
12670
|
}
|
|
@@ -12744,7 +13286,8 @@ var Roles = class {
|
|
|
12744
13286
|
new: !seen("globals", g.slug),
|
|
12745
13287
|
fields: adminFields(g.fields, void 0)
|
|
12746
13288
|
})),
|
|
12747
|
-
views: this.views()
|
|
13289
|
+
views: this.views(),
|
|
13290
|
+
ai: Boolean(this.config.ai)
|
|
12748
13291
|
};
|
|
12749
13292
|
}
|
|
12750
13293
|
async create(input, author) {
|
|
@@ -12923,7 +13466,8 @@ var Roles = class {
|
|
|
12923
13466
|
void 0
|
|
12924
13467
|
)
|
|
12925
13468
|
};
|
|
12926
|
-
|
|
13469
|
+
const aiPerDay = this.config.ai && Number.isInteger(input.aiPerDay) && input.aiPerDay >= 0 ? { aiPerDay: input.aiPerDay } : {};
|
|
13470
|
+
return { ...picked, admin, own, fields, ...aiPerDay };
|
|
12927
13471
|
}
|
|
12928
13472
|
/**
|
|
12929
13473
|
* Keeps rules for known top-level fields. A field that must be filled in (required, without a
|
|
@@ -12972,6 +13516,11 @@ var Roles = class {
|
|
|
12972
13516
|
]);
|
|
12973
13517
|
return value.trim();
|
|
12974
13518
|
}
|
|
13519
|
+
/** The role's daily AI limit set in Settings → Roles, if any (`auth.rbac`). */
|
|
13520
|
+
async aiLimit(key) {
|
|
13521
|
+
if (!this.enabled) return void 0;
|
|
13522
|
+
return (await this.permissionsOf(key)).aiPerDay;
|
|
13523
|
+
}
|
|
12975
13524
|
async permissionsOf(key) {
|
|
12976
13525
|
return (await this.load()).get(key) ?? {};
|
|
12977
13526
|
}
|
|
@@ -13628,10 +14177,10 @@ function folderIds(cms, user, context) {
|
|
|
13628
14177
|
return cms.folders.enabled ? (key) => cms.keyedFolder(key, { user, context }) : void 0;
|
|
13629
14178
|
}
|
|
13630
14179
|
function panelRef(panel) {
|
|
13631
|
-
const
|
|
14180
|
+
const ref2 = componentRef(panel);
|
|
13632
14181
|
if (typeof panel === "object" && "position" in panel && panel.position === "top")
|
|
13633
|
-
|
|
13634
|
-
return
|
|
14182
|
+
ref2.position = "top";
|
|
14183
|
+
return ref2;
|
|
13635
14184
|
}
|
|
13636
14185
|
function layoutOf(layout, fields) {
|
|
13637
14186
|
const shown2 = new Set(fields.filter((f) => f.position !== "sidebar").map((f) => f.name));
|
|
@@ -15097,6 +15646,15 @@ async function route(cms, ctx, method, path, options) {
|
|
|
15097
15646
|
}
|
|
15098
15647
|
throw methodNotAllowed(ctx, "PATCH, DELETE");
|
|
15099
15648
|
}
|
|
15649
|
+
if (first === "admin" && second === "ai" && segments.length === 2) {
|
|
15650
|
+
if (!ctx.user) throw new UnauthorizedError();
|
|
15651
|
+
if (!cms.ai) throw new HttpError("Not found", 404);
|
|
15652
|
+
if (!await cms.roles.canView(ctx.user, "integrations")) throw new ForbiddenError();
|
|
15653
|
+
if (method === "GET") return { body: await cms.ai.overview() };
|
|
15654
|
+
if (method === "PUT")
|
|
15655
|
+
return { body: await cms.ai.update(await readJson(ctx.request), ctx.user) };
|
|
15656
|
+
throw methodNotAllowed(ctx, "GET, PUT");
|
|
15657
|
+
}
|
|
15100
15658
|
if (first === "admin" && second === "integrations") {
|
|
15101
15659
|
if (!ctx.user) throw new UnauthorizedError();
|
|
15102
15660
|
if (cms.config.integrations.length === 0) throw new HttpError("Not found", 404);
|
|
@@ -15117,7 +15675,9 @@ async function route(cms, ctx, method, path, options) {
|
|
|
15117
15675
|
if (part === "authorize" && cid === void 0) {
|
|
15118
15676
|
if (method !== "POST") throw methodNotAllowed(ctx, "POST");
|
|
15119
15677
|
const body = await readJson(ctx.request);
|
|
15120
|
-
return {
|
|
15678
|
+
return {
|
|
15679
|
+
body: await integrations.authorize(id2, user, callbackUrl, String(body.name ?? ""))
|
|
15680
|
+
};
|
|
15121
15681
|
}
|
|
15122
15682
|
if (part === "callback" && cid === void 0) {
|
|
15123
15683
|
if (method !== "GET") throw methodNotAllowed(ctx, "GET");
|
|
@@ -15140,6 +15700,10 @@ async function route(cms, ctx, method, path, options) {
|
|
|
15140
15700
|
}
|
|
15141
15701
|
return { body: new Response(null, { status: 302, headers: { location: page.toString() } }) };
|
|
15142
15702
|
}
|
|
15703
|
+
if (part === "consent" && cid === void 0) {
|
|
15704
|
+
if (method !== "POST") throw methodNotAllowed(ctx, "POST");
|
|
15705
|
+
return { body: await integrations.consent(id2, user) };
|
|
15706
|
+
}
|
|
15143
15707
|
if (part === "lookup" && cid !== void 0 && action === void 0) {
|
|
15144
15708
|
if (method !== "POST") throw methodNotAllowed(ctx, "POST");
|
|
15145
15709
|
return { body: await integrations.lookup(id2, cid, await readJson(ctx.request)) };
|
|
@@ -16201,10 +16765,11 @@ export {
|
|
|
16201
16765
|
isStaff,
|
|
16202
16766
|
isSignedIn,
|
|
16203
16767
|
isAdmin,
|
|
16204
|
-
secretsOf,
|
|
16205
16768
|
MEDIA,
|
|
16206
16769
|
MEDIA_FOLDERS,
|
|
16207
16770
|
INTERNAL_COLLECTIONS,
|
|
16771
|
+
AiProviderError,
|
|
16772
|
+
secretsOf,
|
|
16208
16773
|
AuditLog,
|
|
16209
16774
|
API_KEYS,
|
|
16210
16775
|
API_KEY_OPERATIONS,
|