@easy-cms/core 0.47.4 → 0.49.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.
@@ -0,0 +1,136 @@
1
+ import { ab as Config, al as Database, bl as NavGroup, a6 as CollectionConfig, I as ID, bE as RawDocument, aD as Field, bI as ResolvedConfig, ad as ConfigIssue } from './config-jzFGsNL6.js';
2
+ export { cH as API_KEYS, cI as API_KEY_GLOBAL_OPERATIONS, cJ as API_KEY_OPERATIONS, cK as DEFAULT_MAX_VERSIONS, cL as DEFAULT_PASSWORD_EMAILS, cM as FIELD_TYPE_BASES, cN as FOLDER_LEVELS, cO as MediaFolders, cP as SSO_COOKIE, cQ as codeOfStatus, cx as hasRows, cz as isHasMany, cR as keyAllows, cD as mimeAllowedBy, cE as rowFields, cG as uniqueWithinOf } from './config-jzFGsNL6.js';
3
+ export { C as CONFIG_FILE_NAMES, g as CSRF_COOKIE, h as CSRF_HEADER, D as DEFAULT_ADMIN_PATH, i as DEFAULT_API_PATH, j as DEFAULT_MAX_FILE_SIZE, k as DEFAULT_TOKEN_EXPIRATION, m as applyDefaults, n as applyPlugins, o as createRootEndpointHandler, p as fillMissing, q as findConfigFile, t as forwardedClientIp, u as generateSlugs, v as importConfig, w as mergeForUpdate, x as parseId, y as platformClientIp, z as readCookie, E as validateFields, F as writeBackupFile } from './handler-CMA1LIq7.js';
4
+ export { conditionIssues } from './conditions.js';
5
+
6
+ /** The session cookie the REST API sets at login. */
7
+ declare const SESSION_COOKIE = "ecms-session";
8
+
9
+ declare const MIN_PASSWORD_LENGTH = 8;
10
+ /** Hashes a password as `scrypt$N$r$p$salt$hash` (base64url). */
11
+ declare function hashPassword(password: string): Promise<string>;
12
+ /** Checks a password against a hash in constant time. Malformed hashes never match. */
13
+ declare function verifyPassword(password: string, stored: string): Promise<boolean>;
14
+
15
+ declare const MEDIA = "media";
16
+ declare const MEDIA_FOLDERS = "media-folders";
17
+ /** Collections Easy CMS uses internally. Not exposed over REST or in the admin UI. */
18
+ declare const INTERNAL_COLLECTIONS: ReadonlySet<string>;
19
+
20
+ /**
21
+ * Throws a ConfigError for options of a plugin or adapter whose name changed, so an upgrade
22
+ * doesn't quietly drop them. `renamed` maps the old name to the new one, maybe followed by a note.
23
+ */
24
+ declare function checkRenamedOptions(where: string, options: object | undefined, renamed: Readonly<Record<string, string>>): void;
25
+
26
+ /**
27
+ * A string that is the same for configs with the same structure: collections, fields, options
28
+ * and every other plain value. Functions count only by where they are, not by their code, since
29
+ * bundlers rename variables differently each time they compile the same source.
30
+ *
31
+ * Frameworks may load the config module more than once (Next.js bundles it into each server
32
+ * layer), which gives different objects for the same config; this tells those apart from edits.
33
+ */
34
+ declare function configSignature(config: Config): string;
35
+
36
+ interface CopyProgress {
37
+ readonly table: string;
38
+ readonly rows: number;
39
+ }
40
+ interface CopyResult {
41
+ readonly tables: number;
42
+ readonly rows: number;
43
+ }
44
+ /**
45
+ * Copies every stored row from `source` into `target` (another kind of database is fine, e.g.
46
+ * SQLite to Postgres), keeping ids, so relationships, versions and users stay as they are.
47
+ * Both must come from the same collections and fields, and the target must be empty.
48
+ * Uploaded files are not copied: they stay in the storage they are in.
49
+ */
50
+ declare function copyDatabase(source: Database, target: Database, options?: {
51
+ onTable?: (progress: CopyProgress) => void;
52
+ }): Promise<CopyResult>;
53
+
54
+ /** File types Easy CMS recognizes from their contents (FR-UPL-03). */
55
+ declare const EXTENSIONS: Readonly<Record<string, string>>;
56
+ /**
57
+ * Names for several types at once, usable in `upload.mimeTypes` and an upload field's
58
+ * `mimeTypes`: `documents` (PDF, Word, Excel, PowerPoint, OpenDocument, CSV, text), `office`
59
+ * (docx, xlsx, pptx) and `archives` (zip).
60
+ */
61
+ declare const MIME_GROUPS: Readonly<Record<string, readonly string[]>>;
62
+ /** Patterns with group names (`documents`…) replaced by their types. */
63
+ declare function expandMimeTypes(patterns: readonly string[]): string[];
64
+ /**
65
+ * Detects the MIME type from file contents, ignoring the type the client sent. The name only
66
+ * tells CSV from plain text, which have the same contents.
67
+ */
68
+ declare function sniffMimeType(data: Uint8Array, name?: string): string | undefined;
69
+ /**
70
+ * `image/*` style matching; group names (`documents`…) stand for their types. A wildcard leaves
71
+ * out SVG, which can carry scripts: it is allowed only when listed as `image/svg+xml`.
72
+ */
73
+ declare function mimeAllowed(type: string, allowed: readonly string[]): boolean;
74
+ /** Width and height of PNG, GIF, JPEG and WebP images without decoding them. */
75
+ declare function imageDimensions(data: Uint8Array, type: string): {
76
+ width: number;
77
+ height: number;
78
+ } | undefined;
79
+
80
+ /** The menu's groups that are always there. Declaring their ids changes labels, icons, order. */
81
+ declare const BUILTIN_NAV: readonly NavGroup[];
82
+
83
+ declare const DEFAULT_DEPTH = 1;
84
+ declare const MAX_DEPTH = 3;
85
+ /** Loads the documents of one collection that the caller may see. */
86
+ type Loader = (collection: CollectionConfig, ids: readonly ID[]) => Promise<RawDocument[]>;
87
+ /**
88
+ * Replaces relationship and upload ids with documents, `depth` levels deep.
89
+ * Missing (or unreadable) documents become `null` (single) or are dropped (hasMany).
90
+ * Returns new objects; the input is not modified.
91
+ */
92
+ declare function populate(load: Loader, collections: readonly CollectionConfig[], fields: readonly Field[], docs: RawDocument[], depth: number): Promise<RawDocument[]>;
93
+
94
+ /**
95
+ * Finds the file of an admin module (`admin.modules`): a package export or a path relative to
96
+ * the project root. Looks from the project root first, then from the server's entry file
97
+ * (a Nuxt `.output/server` build keeps traced packages next to it). `undefined` when not found.
98
+ */
99
+ declare function resolveAdminModule(specifier: string, cwd?: string): string | undefined;
100
+
101
+ /** A token that proves the form `purpose` names was loaded at `now`: `<time>.<signature>`. */
102
+ declare function formToken(secret: string, purpose: string, now?: number): string;
103
+ /**
104
+ * Checks a token: `ok` when the form was loaded at least `minTime` ms ago (people take a few
105
+ * seconds; bots don't) and less than a day ago.
106
+ */
107
+ declare function checkFormToken(secret: string, purpose: string, token: unknown, minTime: number, now?: number): 'ok' | 'invalid' | 'too-fast' | 'expired';
108
+ /**
109
+ * Rate-limit keys for `subject` (e.g. an IP and a form) in the current and previous window of
110
+ * `window` seconds. A hash with the window in it: no IP is kept, and keys from different
111
+ * windows can't be linked.
112
+ */
113
+ declare function rateKeys(secret: string, subject: string, window: number, now?: number): {
114
+ current: string;
115
+ previous: string;
116
+ };
117
+ /** The hidden field bots fill in: a common name the form doesn't use. */
118
+ declare function honeypotName(names: readonly string[]): string;
119
+ /** Checks a Cloudflare Turnstile response with Cloudflare. */
120
+ declare function verifyTurnstile(secretKey: string, response: unknown, ip: string | undefined, fetcher?: typeof fetch): Promise<boolean>;
121
+
122
+ /** Best-effort English singular: "categories" → "category". */
123
+ declare function singularize(word: string): string;
124
+ /**
125
+ * TypeScript declarations for every collection and global, as the REST API returns them.
126
+ * The output has no imports, so a frontend in another repository can use it as is.
127
+ */
128
+ declare function generateTypes(config: ResolvedConfig): string;
129
+
130
+ declare const MIN_SECRET_LENGTH = 32;
131
+ /** Collections that Easy CMS provides. They can be relationship targets. */
132
+ declare const BUILTIN_COLLECTIONS: readonly ["users", "media"];
133
+ /** Checks a config and returns every problem found. An empty array means it is valid. */
134
+ declare function validateConfig(config: Config): ConfigIssue[];
135
+
136
+ export { BUILTIN_COLLECTIONS, BUILTIN_NAV, type CopyProgress, type CopyResult, DEFAULT_DEPTH, EXTENSIONS, INTERNAL_COLLECTIONS, MAX_DEPTH, MEDIA, MEDIA_FOLDERS, MIME_GROUPS, MIN_PASSWORD_LENGTH, MIN_SECRET_LENGTH, SESSION_COOKIE, checkFormToken, checkRenamedOptions, configSignature, copyDatabase, expandMimeTypes, formToken, generateTypes, hashPassword, honeypotName, imageDimensions, mimeAllowed, populate, rateKeys, resolveAdminModule, singularize, sniffMimeType, validateConfig, verifyPassword, verifyTurnstile };
@@ -0,0 +1,310 @@
1
+ import {
2
+ API_KEYS,
3
+ API_KEY_GLOBAL_OPERATIONS,
4
+ API_KEY_OPERATIONS,
5
+ BUILTIN_COLLECTIONS,
6
+ BUILTIN_NAV,
7
+ CONFIG_FILE_NAMES,
8
+ CSRF_COOKIE,
9
+ CSRF_HEADER,
10
+ DEFAULT_ADMIN_PATH,
11
+ DEFAULT_API_PATH,
12
+ DEFAULT_DEPTH,
13
+ DEFAULT_MAX_FILE_SIZE,
14
+ DEFAULT_MAX_VERSIONS,
15
+ DEFAULT_PASSWORD_EMAILS,
16
+ DEFAULT_TOKEN_EXPIRATION,
17
+ FOLDER_LEVELS,
18
+ INTERNAL_COLLECTIONS,
19
+ MAX_DEPTH,
20
+ MEDIA,
21
+ MEDIA_FOLDERS,
22
+ MIN_PASSWORD_LENGTH,
23
+ MIN_SECRET_LENGTH,
24
+ MediaFolders,
25
+ SESSION_COOKIE,
26
+ SSO_COOKIE,
27
+ applyDefaults,
28
+ applyPlugins,
29
+ checkFormToken,
30
+ checkRenamedOptions,
31
+ configSignature,
32
+ copyDatabase,
33
+ createRootEndpointHandler,
34
+ fillMissing,
35
+ findConfigFile,
36
+ formToken,
37
+ forwardedClientIp,
38
+ generateSlugs,
39
+ hashPassword,
40
+ honeypotName,
41
+ importConfig,
42
+ keyAllows,
43
+ mergeForUpdate,
44
+ parseId,
45
+ platformClientIp,
46
+ populate,
47
+ rateKeys,
48
+ readCookie,
49
+ resolveAdminModule,
50
+ validateConfig,
51
+ validateFields,
52
+ verifyPassword,
53
+ verifyTurnstile,
54
+ writeBackupFile
55
+ } from "./chunk-4PGEDNBY.js";
56
+ import {
57
+ conditionIssues
58
+ } from "./chunk-U266LV4M.js";
59
+ import {
60
+ EXTENSIONS,
61
+ FIELD_TYPE_BASES,
62
+ MIME_GROUPS,
63
+ codeOfStatus,
64
+ expandMimeTypes,
65
+ hasRows,
66
+ imageDimensions,
67
+ isHasMany,
68
+ mimeAllowed,
69
+ mimeAllowedBy,
70
+ rowFields,
71
+ sniffMimeType,
72
+ uniqueWithinOf
73
+ } from "./chunk-CGVNZUW4.js";
74
+
75
+ // src/typegen.ts
76
+ var pascal = (name) => name.split(/[^A-Za-z0-9]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
77
+ function singularize(word) {
78
+ if (/ies$/i.test(word)) return word.replace(/ies$/i, "y");
79
+ if (/(ss|us|ia)$/i.test(word)) return word;
80
+ if (/(x|ch|sh)es$/i.test(word)) return word.replace(/es$/i, "");
81
+ return word.replace(/s$/i, "");
82
+ }
83
+ var quote = (value) => JSON.stringify(value);
84
+ var key = (name) => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : quote(name);
85
+ function names(config) {
86
+ const taken = /* @__PURE__ */ new Set(["ID", "RichTextDocument", "MediaSize", "Collections", "Globals"]);
87
+ const claim = (base, suffix) => {
88
+ let name = base || suffix;
89
+ if (taken.has(name)) name = `${base}${suffix}`;
90
+ for (let n = 2; taken.has(name); n++) name = `${base}${suffix}${n}`;
91
+ taken.add(name);
92
+ return name;
93
+ };
94
+ const collections = /* @__PURE__ */ new Map();
95
+ for (const c of config.collections) {
96
+ if (!INTERNAL_COLLECTIONS.has(c.slug))
97
+ collections.set(c.slug, claim(pascal(singularize(c.slug)), "Document"));
98
+ }
99
+ const globals = /* @__PURE__ */ new Map();
100
+ for (const g of config.globals) globals.set(g.slug, claim(pascal(g.slug), "Global"));
101
+ const custom = new Map(
102
+ (config.fieldTypes ?? []).filter((t) => t.typescript).map((t) => [t.name, t.typescript])
103
+ );
104
+ return { collections, globals, custom };
105
+ }
106
+ function fieldType(field, n, indent) {
107
+ const custom = field.customType ? n.custom.get(field.customType) : void 0;
108
+ if (custom) return custom;
109
+ switch (field.type) {
110
+ case "text":
111
+ case "textarea":
112
+ case "email":
113
+ case "slug":
114
+ case "date":
115
+ return "string";
116
+ case "number":
117
+ return "number";
118
+ case "boolean":
119
+ return "boolean";
120
+ case "json":
121
+ return "unknown";
122
+ case "richText":
123
+ return "RichTextDocument";
124
+ case "select": {
125
+ const union = field.options.map((o) => quote(typeof o === "string" ? o : o.value)).join(" | ") || "string";
126
+ return field.hasMany ? `(${union})[]` : union;
127
+ }
128
+ case "upload": {
129
+ const one = `ID | ${n.collections.get(MEDIA) ?? "Record<string, unknown>"}`;
130
+ return field.hasMany ? `(${one})[]` : one;
131
+ }
132
+ case "relationship": {
133
+ const target = n.collections.get(field.to);
134
+ const one = target ? `ID | ${target}` : "ID";
135
+ return field.hasMany ? `(${one})[]` : one;
136
+ }
137
+ case "group":
138
+ return `{
139
+ ${fieldLines(field.fields, n, `${indent} `).join("\n")}
140
+ ${indent}}`;
141
+ case "array":
142
+ return `{
143
+ ${indent} id: string
144
+ ${fieldLines(field.fields, n, `${indent} `).join("\n")}
145
+ ${indent}}[]`;
146
+ case "blocks": {
147
+ const kinds = field.blocks.map(
148
+ (block) => `{
149
+ ${indent} id: string
150
+ ${indent} blockType: ${quote(block.slug)}
151
+ ${fieldLines(block.fields, n, `${indent} `).join("\n")}
152
+ ${indent}}`
153
+ );
154
+ return `(${kinds.join(" | ")})[]`;
155
+ }
156
+ }
157
+ }
158
+ function fieldLines(fields, n, indent) {
159
+ const lines = [];
160
+ for (const field of fields) {
161
+ if (field.hidden) continue;
162
+ const type = fieldType(field, n, indent);
163
+ const alwaysPresent = field.type === "array" || field.type === "blocks" || field.type === "group" || isHasMany(field);
164
+ if (field.required || alwaysPresent) lines.push(`${indent}${key(field.name)}: ${type}`);
165
+ else lines.push(`${indent}${key(field.name)}?: ${type} | null`);
166
+ }
167
+ return lines;
168
+ }
169
+ function collectionInterface(collection, name, n) {
170
+ const lines = [" id: ID"];
171
+ if (collection.slug === MEDIA) {
172
+ lines.push(" url: string", " sizes: Record<string, MediaSize>");
173
+ }
174
+ const fields = collection.slug === MEDIA ? collection.fields.filter((f) => f.name !== "sizes") : collection.fields;
175
+ lines.push(...fieldLines(fields, n, " "));
176
+ if (collection.drafts) lines.push(` status: 'draft' | 'published'`);
177
+ lines.push(" createdAt: string", " updatedAt: string");
178
+ return `export interface ${name} {
179
+ ${lines.join("\n")}
180
+ }`;
181
+ }
182
+ function globalInterface(global, name, n) {
183
+ const lines = [...fieldLines(global.fields, n, " ")];
184
+ if (global.drafts) lines.push(` status: 'draft' | 'published'`);
185
+ lines.push(
186
+ " /** `null` until the global is saved for the first time. */",
187
+ " updatedAt: string | null"
188
+ );
189
+ return `export interface ${name} {
190
+ ${lines.join("\n")}
191
+ }`;
192
+ }
193
+ function generateTypes(config) {
194
+ const n = names(config);
195
+ const parts = [
196
+ "// Generated by `easy-cms generate:types`. Do not edit; run the command again after changing the config.",
197
+ "",
198
+ "/** Document ids are integers. */",
199
+ "export type ID = number",
200
+ "",
201
+ "/** Rich text as stored: a Tiptap / ProseMirror JSON document. */",
202
+ "export interface RichTextDocument {",
203
+ ` type: 'doc'`,
204
+ " content?: unknown[]",
205
+ "}",
206
+ "",
207
+ "export interface MediaSize {",
208
+ " filename: string",
209
+ " width: number",
210
+ " height: number",
211
+ " filesize: number",
212
+ " url: string",
213
+ "}"
214
+ ];
215
+ for (const collection of config.collections) {
216
+ const name = n.collections.get(collection.slug);
217
+ if (name) parts.push("", collectionInterface(collection, name, n));
218
+ }
219
+ for (const global of config.globals) {
220
+ parts.push("", globalInterface(global, n.globals.get(global.slug), n));
221
+ }
222
+ const map = (entries) => [...entries].map(([slug, name]) => ` ${key(slug)}: ${name}`).join("\n");
223
+ parts.push(
224
+ "",
225
+ "/** Document type by collection slug. */",
226
+ `export interface Collections {
227
+ ${map(n.collections)}
228
+ }`
229
+ );
230
+ parts.push(
231
+ "",
232
+ "/** Data type by global slug. */",
233
+ `export interface Globals {
234
+ ${map(n.globals)}
235
+ }`,
236
+ ""
237
+ );
238
+ return parts.join("\n");
239
+ }
240
+ export {
241
+ API_KEYS,
242
+ API_KEY_GLOBAL_OPERATIONS,
243
+ API_KEY_OPERATIONS,
244
+ BUILTIN_COLLECTIONS,
245
+ BUILTIN_NAV,
246
+ CONFIG_FILE_NAMES,
247
+ CSRF_COOKIE,
248
+ CSRF_HEADER,
249
+ DEFAULT_ADMIN_PATH,
250
+ DEFAULT_API_PATH,
251
+ DEFAULT_DEPTH,
252
+ DEFAULT_MAX_FILE_SIZE,
253
+ DEFAULT_MAX_VERSIONS,
254
+ DEFAULT_PASSWORD_EMAILS,
255
+ DEFAULT_TOKEN_EXPIRATION,
256
+ EXTENSIONS,
257
+ FIELD_TYPE_BASES,
258
+ FOLDER_LEVELS,
259
+ INTERNAL_COLLECTIONS,
260
+ MAX_DEPTH,
261
+ MEDIA,
262
+ MEDIA_FOLDERS,
263
+ MIME_GROUPS,
264
+ MIN_PASSWORD_LENGTH,
265
+ MIN_SECRET_LENGTH,
266
+ MediaFolders,
267
+ SESSION_COOKIE,
268
+ SSO_COOKIE,
269
+ applyDefaults,
270
+ applyPlugins,
271
+ checkFormToken,
272
+ checkRenamedOptions,
273
+ codeOfStatus,
274
+ conditionIssues,
275
+ configSignature,
276
+ copyDatabase,
277
+ createRootEndpointHandler,
278
+ expandMimeTypes,
279
+ fillMissing,
280
+ findConfigFile,
281
+ formToken,
282
+ forwardedClientIp,
283
+ generateSlugs,
284
+ generateTypes,
285
+ hasRows,
286
+ hashPassword,
287
+ honeypotName,
288
+ imageDimensions,
289
+ importConfig,
290
+ isHasMany,
291
+ keyAllows,
292
+ mergeForUpdate,
293
+ mimeAllowed,
294
+ mimeAllowedBy,
295
+ parseId,
296
+ platformClientIp,
297
+ populate,
298
+ rateKeys,
299
+ readCookie,
300
+ resolveAdminModule,
301
+ rowFields,
302
+ singularize,
303
+ sniffMimeType,
304
+ uniqueWithinOf,
305
+ validateConfig,
306
+ validateFields,
307
+ verifyPassword,
308
+ verifyTurnstile,
309
+ writeBackupFile
310
+ };