kernelcms 0.1.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,1181 @@
1
+ // ../core/src/config.ts
2
+ function withAuthFields(collection) {
3
+ const fields = [...collection.fields];
4
+ if (!fields.some((f) => f.name === "email")) {
5
+ fields.unshift({ name: "email", type: "email", required: true, unique: true, index: true });
6
+ }
7
+ if (!fields.some((f) => f.name === "hash")) {
8
+ fields.push({ name: "hash", type: "text", admin: { hidden: true } });
9
+ }
10
+ return { ...collection, fields };
11
+ }
12
+ var IDENT_RE = /^[a-z][a-z0-9_]*$/;
13
+ function defineConfig(config) {
14
+ return config;
15
+ }
16
+ function assert(condition, message) {
17
+ if (!condition) throw new Error(`KernelCMS config error: ${message}`);
18
+ }
19
+ function validateCollection(collection) {
20
+ assert(IDENT_RE.test(collection.slug), `collection slug "${collection.slug}" must be snake_case starting with a letter`);
21
+ const seen = /* @__PURE__ */ new Set();
22
+ for (const field of collection.fields) {
23
+ assert(IDENT_RE.test(field.name), `field "${field.name}" in "${collection.slug}" must be snake_case`);
24
+ assert(!seen.has(field.name), `duplicate field "${field.name}" in collection "${collection.slug}"`);
25
+ seen.add(field.name);
26
+ }
27
+ }
28
+ function validateGlobal(global) {
29
+ assert(IDENT_RE.test(global.slug), `global slug "${global.slug}" must be snake_case starting with a letter`);
30
+ }
31
+ function sanitizeConfig(config) {
32
+ assert(config.db, "a database adapter is required (config.db)");
33
+ assert(Array.isArray(config.collections), "config.collections must be an array");
34
+ const collections = config.collections.map((c) => c.auth ? withAuthFields(c) : c);
35
+ const globals = config.globals ?? [];
36
+ const collectionsBySlug = {};
37
+ for (const collection of collections) {
38
+ validateCollection(collection);
39
+ assert(!collectionsBySlug[collection.slug], `duplicate collection slug "${collection.slug}"`);
40
+ collectionsBySlug[collection.slug] = collection;
41
+ }
42
+ const globalsBySlug = {};
43
+ for (const global of globals) {
44
+ validateGlobal(global);
45
+ assert(!globalsBySlug[global.slug], `duplicate global slug "${global.slug}"`);
46
+ assert(!collectionsBySlug[global.slug], `global "${global.slug}" collides with a collection slug`);
47
+ globalsBySlug[global.slug] = global;
48
+ }
49
+ let localization = false;
50
+ if (config.localization) {
51
+ const { locales, defaultLocale, fallback } = config.localization;
52
+ assert(locales.length > 0, "localization.locales must not be empty");
53
+ assert(locales.includes(defaultLocale), `localization.defaultLocale "${defaultLocale}" must be in locales`);
54
+ localization = { locales, defaultLocale, fallback: fallback ?? true };
55
+ }
56
+ const secret = config.secret ?? process.env.KERNEL_SECRET ?? devSecret();
57
+ const adminUser = config.admin?.user ?? collections.find((c) => c.auth)?.slug ?? "users";
58
+ return {
59
+ serverURL: config.serverURL ?? "http://localhost:3000",
60
+ db: config.db,
61
+ collections,
62
+ globals,
63
+ localization,
64
+ routes: { api: config.routes?.api ?? "/api" },
65
+ admin: { user: adminUser },
66
+ secret,
67
+ collectionsBySlug,
68
+ globalsBySlug
69
+ };
70
+ }
71
+ var warned = false;
72
+ function devSecret() {
73
+ if (!warned) {
74
+ warned = true;
75
+ console.warn(
76
+ "[KernelCMS] No `secret` or KERNEL_SECRET set \u2014 using an insecure development secret. Set KERNEL_SECRET in production."
77
+ );
78
+ }
79
+ return "kernel-dev-insecure-secret-do-not-use-in-production";
80
+ }
81
+ function defaultLocaleOf(config) {
82
+ return config.localization ? config.localization.defaultLocale : "en";
83
+ }
84
+
85
+ // ../core/src/fields.ts
86
+ var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
87
+ var SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
88
+ function humanize(name) {
89
+ return name.replace(/[_-]+/g, " ").replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/^\w/, (c) => c.toUpperCase()).trim();
90
+ }
91
+ function fieldLabel(field) {
92
+ return field.label ?? humanize(field.name);
93
+ }
94
+ function optionValue(opt) {
95
+ return typeof opt === "string" ? opt : opt.value;
96
+ }
97
+ function optionLabel(opt) {
98
+ return typeof opt === "string" ? humanize(opt) : opt.label;
99
+ }
100
+ function storageTypeForField(field) {
101
+ if (field.localized) return "json";
102
+ switch (field.type) {
103
+ case "number":
104
+ return field.integer ? "integer" : "real";
105
+ case "boolean":
106
+ case "checkbox":
107
+ return "boolean";
108
+ case "date":
109
+ return "timestamp";
110
+ case "json":
111
+ case "richText":
112
+ case "point":
113
+ case "array":
114
+ case "group":
115
+ return "json";
116
+ case "select":
117
+ case "radio":
118
+ return field.hasMany ? "json" : "text";
119
+ case "relationship":
120
+ case "upload":
121
+ return field.hasMany ? "json" : "text";
122
+ default:
123
+ return "text";
124
+ }
125
+ }
126
+ function columnForField(field) {
127
+ const column = {
128
+ name: field.name,
129
+ type: storageTypeForField(field),
130
+ required: Boolean(field.required),
131
+ unique: Boolean(field.unique),
132
+ indexed: Boolean(field.index ?? field.unique),
133
+ localized: Boolean(field.localized)
134
+ };
135
+ if ((field.type === "relationship" || field.type === "upload") && !field.hasMany) {
136
+ column.relationTo = field.relationTo;
137
+ }
138
+ return column;
139
+ }
140
+ function defaultForField(field) {
141
+ const dv = field.defaultValue;
142
+ if (dv === void 0) return void 0;
143
+ if (typeof dv === "function") return dv();
144
+ return dv;
145
+ }
146
+ function applyDefaults(fields, data) {
147
+ const out = { ...data };
148
+ for (const field of fields) {
149
+ if (out[field.name] === void 0) {
150
+ const dv = defaultForField(field);
151
+ if (dv !== void 0) out[field.name] = dv;
152
+ }
153
+ }
154
+ return out;
155
+ }
156
+ function isEmpty(value) {
157
+ return value === void 0 || value === null || value === "" || Array.isArray(value) && value.length === 0;
158
+ }
159
+ async function validateFields(fields, data, ctx, prefix = "") {
160
+ const errors = [];
161
+ for (const field of fields) {
162
+ const path = prefix ? `${prefix}.${field.name}` : field.name;
163
+ const value = data?.[field.name];
164
+ const label = fieldLabel(field);
165
+ if (isEmpty(value)) {
166
+ if (field.required) errors.push({ path, message: `${label} is required.` });
167
+ continue;
168
+ }
169
+ const typeError = validateFieldType(field, value, label);
170
+ if (typeError) {
171
+ errors.push({ path, message: typeError });
172
+ continue;
173
+ }
174
+ if (field.type === "array" && Array.isArray(value)) {
175
+ if (field.minRows !== void 0 && value.length < field.minRows) {
176
+ errors.push({ path, message: `${label} requires at least ${field.minRows} row(s).` });
177
+ }
178
+ if (field.maxRows !== void 0 && value.length > field.maxRows) {
179
+ errors.push({ path, message: `${label} allows at most ${field.maxRows} row(s).` });
180
+ }
181
+ for (let i = 0; i < value.length; i++) {
182
+ const row = value[i];
183
+ if (row && typeof row === "object") {
184
+ errors.push(...await validateFields(field.fields, row, ctx, `${path}.${i}`));
185
+ } else {
186
+ errors.push({ path: `${path}.${i}`, message: "Each row must be an object." });
187
+ }
188
+ }
189
+ } else if (field.type === "group" && value && typeof value === "object") {
190
+ errors.push(...await validateFields(field.fields, value, ctx, path));
191
+ }
192
+ if (field.validate) {
193
+ const result = await field.validate({
194
+ value,
195
+ data,
196
+ siblingData: data,
197
+ req: ctx.req,
198
+ operation: ctx.operation
199
+ });
200
+ if (result !== true) errors.push({ path, message: result });
201
+ }
202
+ }
203
+ return errors;
204
+ }
205
+ function validateFieldType(field, value, label) {
206
+ switch (field.type) {
207
+ case "text":
208
+ case "textarea":
209
+ case "code":
210
+ case "email":
211
+ case "slug": {
212
+ if (typeof value !== "string") return `${label} must be text.`;
213
+ if (field.minLength !== void 0 && value.length < field.minLength)
214
+ return `${label} must be at least ${field.minLength} characters.`;
215
+ if (field.maxLength !== void 0 && value.length > field.maxLength)
216
+ return `${label} must be at most ${field.maxLength} characters.`;
217
+ if (field.type === "email" && !EMAIL_RE.test(value)) return `${label} must be a valid email address.`;
218
+ if (field.type === "slug" && !SLUG_RE.test(value)) return `${label} must be a lowercase, hyphenated slug.`;
219
+ if (field.pattern && !new RegExp(field.pattern).test(value)) return `${label} has an invalid format.`;
220
+ return null;
221
+ }
222
+ case "number": {
223
+ if (typeof value !== "number" || Number.isNaN(value)) return `${label} must be a number.`;
224
+ if (field.integer && !Number.isInteger(value)) return `${label} must be an integer.`;
225
+ if (field.min !== void 0 && value < field.min) return `${label} must be at least ${field.min}.`;
226
+ if (field.max !== void 0 && value > field.max) return `${label} must be at most ${field.max}.`;
227
+ return null;
228
+ }
229
+ case "boolean":
230
+ case "checkbox":
231
+ return typeof value === "boolean" ? null : `${label} must be true or false.`;
232
+ case "date":
233
+ return Number.isNaN(Date.parse(String(value))) ? `${label} must be a valid date.` : null;
234
+ case "select":
235
+ case "radio": {
236
+ const allowed = field.options.map(optionValue);
237
+ if (field.hasMany) {
238
+ if (!Array.isArray(value)) return `${label} must be a list.`;
239
+ for (const v of value) if (!allowed.includes(String(v))) return `${label} contains an invalid option.`;
240
+ return null;
241
+ }
242
+ return allowed.includes(String(value)) ? null : `${label} must be one of: ${allowed.join(", ")}.`;
243
+ }
244
+ case "relationship":
245
+ case "upload": {
246
+ if (field.hasMany) return Array.isArray(value) ? null : `${label} must be a list of references.`;
247
+ return typeof value === "string" || typeof value === "number" ? null : `${label} must be a reference id.`;
248
+ }
249
+ case "array":
250
+ return Array.isArray(value) ? null : `${label} must be a list.`;
251
+ case "group":
252
+ return value && typeof value === "object" && !Array.isArray(value) ? null : `${label} must be an object.`;
253
+ case "point":
254
+ return Array.isArray(value) && value.length === 2 ? null : `${label} must be a [lng, lat] pair.`;
255
+ case "json":
256
+ case "richText":
257
+ return null;
258
+ default:
259
+ return null;
260
+ }
261
+ }
262
+ function normalizeWrite(field, value) {
263
+ if (value === void 0) return null;
264
+ if (field.type === "date") {
265
+ if (value instanceof Date) return value.toISOString();
266
+ if (typeof value === "number") return new Date(value).toISOString();
267
+ }
268
+ return value;
269
+ }
270
+ function serializeDoc(fields, data, opts) {
271
+ const row = {};
272
+ for (const field of fields) {
273
+ const has = Object.prototype.hasOwnProperty.call(data, field.name);
274
+ if (field.localized) {
275
+ const existing = opts.existingRow?.[field.name];
276
+ const map = existing && typeof existing === "object" && !Array.isArray(existing) ? { ...existing } : {};
277
+ if (has) map[opts.locale] = normalizeWrite(field, data[field.name]);
278
+ row[field.name] = map;
279
+ } else if (has) {
280
+ row[field.name] = normalizeWrite(field, data[field.name]);
281
+ } else if (opts.existingRow && Object.prototype.hasOwnProperty.call(opts.existingRow, field.name)) {
282
+ row[field.name] = opts.existingRow[field.name];
283
+ }
284
+ }
285
+ return row;
286
+ }
287
+ function resolveLocale(raw, locale, fallback) {
288
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) {
289
+ const map = raw;
290
+ if (locale in map) return map[locale];
291
+ if (fallback && fallback in map) return map[fallback];
292
+ return null;
293
+ }
294
+ return raw ?? null;
295
+ }
296
+ function deserializeDoc(fields, row, opts) {
297
+ const doc = {};
298
+ for (const field of fields) {
299
+ const raw = row[field.name];
300
+ doc[field.name] = field.localized ? resolveLocale(raw, opts.locale, opts.fallbackLocale) : raw === void 0 ? null : raw;
301
+ }
302
+ return doc;
303
+ }
304
+ function relationshipFields(fields) {
305
+ const out = [];
306
+ for (const field of fields) {
307
+ if (field.type === "relationship" || field.type === "upload") {
308
+ out.push({ name: field.name, relationTo: field.relationTo, hasMany: Boolean(field.hasMany) });
309
+ }
310
+ }
311
+ return out;
312
+ }
313
+
314
+ // ../core/src/schema.ts
315
+ function tableForCollection(slug) {
316
+ return slug;
317
+ }
318
+ function tableForGlobal(slug) {
319
+ return `__global_${slug}`;
320
+ }
321
+ var GLOBAL_ROW_ID = "singleton";
322
+ function compileSchema(config) {
323
+ const tables = [];
324
+ for (const collection of config.collections) {
325
+ tables.push({
326
+ table: tableForCollection(collection.slug),
327
+ slug: collection.slug,
328
+ columns: collection.fields.map(columnForField),
329
+ timestamps: collection.timestamps ?? true,
330
+ singleton: false
331
+ });
332
+ }
333
+ for (const global of config.globals) {
334
+ tables.push({
335
+ table: tableForGlobal(global.slug),
336
+ slug: global.slug,
337
+ columns: global.fields.map(columnForField),
338
+ timestamps: true,
339
+ singleton: true
340
+ });
341
+ }
342
+ return { tables };
343
+ }
344
+
345
+ // ../core/src/errors.ts
346
+ var KernelError = class extends Error {
347
+ code;
348
+ status;
349
+ details;
350
+ constructor(message, code, status, details) {
351
+ super(message);
352
+ this.name = "KernelError";
353
+ this.code = code;
354
+ this.status = status;
355
+ this.details = details;
356
+ Object.setPrototypeOf(this, new.target.prototype);
357
+ }
358
+ toJSON() {
359
+ return {
360
+ error: {
361
+ code: this.code,
362
+ message: this.message,
363
+ ...this.details === void 0 ? {} : { details: this.details }
364
+ }
365
+ };
366
+ }
367
+ };
368
+ var ValidationError = class extends KernelError {
369
+ errors;
370
+ constructor(errors) {
371
+ super(
372
+ `Validation failed: ${errors.map((e) => e.path).join(", ")}`,
373
+ "VALIDATION",
374
+ 400,
375
+ errors
376
+ );
377
+ this.name = "ValidationError";
378
+ this.errors = errors;
379
+ }
380
+ };
381
+ var NotFoundError = class extends KernelError {
382
+ constructor(message = "The requested resource was not found.") {
383
+ super(message, "NOT_FOUND", 404);
384
+ this.name = "NotFoundError";
385
+ }
386
+ };
387
+ var ForbiddenError = class extends KernelError {
388
+ constructor(message = "You are not allowed to perform this action.") {
389
+ super(message, "FORBIDDEN", 403);
390
+ this.name = "ForbiddenError";
391
+ }
392
+ };
393
+ var UnauthorizedError = class extends KernelError {
394
+ constructor(message = "Authentication is required.") {
395
+ super(message, "UNAUTHORIZED", 401);
396
+ this.name = "UnauthorizedError";
397
+ }
398
+ };
399
+ var BadRequestError = class extends KernelError {
400
+ constructor(message = "The request was malformed.", details) {
401
+ super(message, "BAD_REQUEST", 400, details);
402
+ this.name = "BadRequestError";
403
+ }
404
+ };
405
+ var ConflictError = class extends KernelError {
406
+ constructor(message = "The resource already exists.", details) {
407
+ super(message, "CONFLICT", 409, details);
408
+ this.name = "ConflictError";
409
+ }
410
+ };
411
+ var TooManyRequestsError = class extends KernelError {
412
+ /** Seconds the client should wait before retrying, surfaced as Retry-After. */
413
+ retryAfter;
414
+ constructor(message = "Too many requests. Please try again later.", retryAfter) {
415
+ super(message, "TOO_MANY_REQUESTS", 429, retryAfter === void 0 ? void 0 : { retryAfter });
416
+ this.name = "TooManyRequestsError";
417
+ this.retryAfter = retryAfter;
418
+ }
419
+ };
420
+ function isKernelError(err) {
421
+ return err instanceof KernelError;
422
+ }
423
+
424
+ // ../core/src/auth.ts
425
+ import { createHmac, randomBytes, scrypt as scryptCb, timingSafeEqual } from "crypto";
426
+ import { promisify } from "util";
427
+ var SCRYPT_KEYLEN = 64;
428
+ var scrypt = promisify(scryptCb);
429
+ async function hashPassword(password) {
430
+ const salt = randomBytes(16);
431
+ const derived = await scrypt(password, salt, SCRYPT_KEYLEN);
432
+ return `scrypt$${salt.toString("hex")}$${derived.toString("hex")}`;
433
+ }
434
+ async function verifyPassword(password, stored) {
435
+ const parts = stored.split("$");
436
+ if (parts.length !== 3 || parts[0] !== "scrypt") return false;
437
+ const salt = Buffer.from(parts[1], "hex");
438
+ const expected = Buffer.from(parts[2], "hex");
439
+ const derived = await scrypt(password, salt, expected.length);
440
+ return expected.length === derived.length && timingSafeEqual(expected, derived);
441
+ }
442
+ function nowSeconds() {
443
+ return Math.floor(Date.now() / 1e3);
444
+ }
445
+ function signToken(payload, secret, expiresInSec = 3600) {
446
+ const now = nowSeconds();
447
+ const header = Buffer.from(JSON.stringify({ alg: "HS256", typ: "JWT" })).toString("base64url");
448
+ const body = Buffer.from(JSON.stringify({ ...payload, iat: now, exp: now + expiresInSec })).toString("base64url");
449
+ const sig = createHmac("sha256", secret).update(`${header}.${body}`).digest("base64url");
450
+ return `${header}.${body}.${sig}`;
451
+ }
452
+ function verifyToken(token, secret) {
453
+ const parts = token.split(".");
454
+ if (parts.length !== 3) return null;
455
+ const [header, body, sig] = parts;
456
+ const expected = createHmac("sha256", secret).update(`${header}.${body}`).digest("base64url");
457
+ const a = Buffer.from(sig);
458
+ const b = Buffer.from(expected);
459
+ if (a.length !== b.length || !timingSafeEqual(a, b)) return null;
460
+ try {
461
+ const payload = JSON.parse(Buffer.from(body, "base64url").toString("utf8"));
462
+ if (typeof payload.exp === "number" && payload.exp < nowSeconds()) return null;
463
+ return payload;
464
+ } catch {
465
+ return null;
466
+ }
467
+ }
468
+
469
+ // ../core/src/access.ts
470
+ async function evalAccess(fn, args) {
471
+ if (!fn) return Boolean(args.req.user);
472
+ return fn(args);
473
+ }
474
+ function isAllowed(result) {
475
+ return result !== false;
476
+ }
477
+ function asWhere(result) {
478
+ return result === true || result === false ? void 0 : result;
479
+ }
480
+
481
+ // ../core/src/query.ts
482
+ function parseSort(sort) {
483
+ if (!sort) return [];
484
+ const tokens = (Array.isArray(sort) ? sort : [sort]).flatMap((s) => s.split(",")).map((s) => s.trim()).filter(Boolean);
485
+ return tokens.map(
486
+ (token) => token.startsWith("-") ? { field: token.slice(1), direction: "desc" } : { field: token, direction: "asc" }
487
+ );
488
+ }
489
+ function mergeWhere(a, b) {
490
+ if (a && b) return { and: [a, b] };
491
+ return a ?? b;
492
+ }
493
+ function toArray(v) {
494
+ return Array.isArray(v) ? v : [v];
495
+ }
496
+ function looseEq(a, b) {
497
+ if (a === b) return true;
498
+ if (a === null || a === void 0 || b === null || b === void 0) return false;
499
+ if (typeof a === "number" || typeof b === "number") return Number(a) === Number(b);
500
+ return String(a) === String(b);
501
+ }
502
+ function compare(a, b) {
503
+ const an = Number(a);
504
+ const bn = Number(b);
505
+ if (!Number.isNaN(an) && !Number.isNaN(bn)) return an < bn ? -1 : an > bn ? 1 : 0;
506
+ return String(a).localeCompare(String(b));
507
+ }
508
+ function matchesCondition(value, cond) {
509
+ for (const [op, operand] of Object.entries(cond)) {
510
+ switch (op) {
511
+ case "equals":
512
+ if (!looseEq(value, operand)) return false;
513
+ break;
514
+ case "not_equals":
515
+ if (looseEq(value, operand)) return false;
516
+ break;
517
+ case "in":
518
+ if (!toArray(operand).some((o) => looseEq(value, o))) return false;
519
+ break;
520
+ case "not_in":
521
+ if (toArray(operand).some((o) => looseEq(value, o))) return false;
522
+ break;
523
+ case "greater_than":
524
+ if (!(compare(value, operand) > 0)) return false;
525
+ break;
526
+ case "greater_than_equal":
527
+ if (!(compare(value, operand) >= 0)) return false;
528
+ break;
529
+ case "less_than":
530
+ if (!(compare(value, operand) < 0)) return false;
531
+ break;
532
+ case "less_than_equal":
533
+ if (!(compare(value, operand) <= 0)) return false;
534
+ break;
535
+ case "like":
536
+ case "contains":
537
+ if (!String(value ?? "").toLowerCase().includes(String(operand).toLowerCase())) return false;
538
+ break;
539
+ case "exists": {
540
+ const exists = value !== null && value !== void 0;
541
+ if (Boolean(operand) !== exists) return false;
542
+ break;
543
+ }
544
+ default:
545
+ return false;
546
+ }
547
+ }
548
+ return true;
549
+ }
550
+ function matchesWhere(row, where) {
551
+ if (!where) return true;
552
+ if (where.and && !where.and.every((w) => matchesWhere(row, w))) return false;
553
+ if (where.or && where.or.length > 0 && !where.or.some((w) => matchesWhere(row, w))) return false;
554
+ for (const [key, cond] of Object.entries(where)) {
555
+ if (key === "and" || key === "or" || cond === void 0) continue;
556
+ if (!matchesCondition(row[key], cond)) return false;
557
+ }
558
+ return true;
559
+ }
560
+
561
+ // ../core/src/operations.ts
562
+ import { randomUUID } from "crypto";
563
+ var MAX_LIMIT = 1e3;
564
+ var DEFAULT_LIMIT = 25;
565
+ var LOGIN_MAX_FAILURES = 10;
566
+ var LOGIN_WINDOW_MS = 15 * 60 * 1e3;
567
+ function createOperations(ctx) {
568
+ const { config, db } = ctx;
569
+ const loginFailures = /* @__PURE__ */ new Map();
570
+ function loginKey(slug, email) {
571
+ return `${slug}:${email.trim().toLowerCase()}`;
572
+ }
573
+ function assertLoginAllowed(key) {
574
+ const rec = loginFailures.get(key);
575
+ if (!rec) return;
576
+ const elapsed = Date.now() - rec.windowStart;
577
+ if (elapsed > LOGIN_WINDOW_MS) {
578
+ loginFailures.delete(key);
579
+ return;
580
+ }
581
+ if (rec.count >= LOGIN_MAX_FAILURES) {
582
+ const retryAfter = Math.ceil((LOGIN_WINDOW_MS - elapsed) / 1e3);
583
+ throw new TooManyRequestsError("Too many failed login attempts. Please try again later.", retryAfter);
584
+ }
585
+ }
586
+ function recordLoginFailure(key) {
587
+ const now = Date.now();
588
+ const rec = loginFailures.get(key);
589
+ if (!rec || now - rec.windowStart > LOGIN_WINDOW_MS) {
590
+ loginFailures.set(key, { count: 1, windowStart: now });
591
+ } else {
592
+ rec.count += 1;
593
+ }
594
+ }
595
+ async function applyFieldAccess(fields, data, operation, req, id) {
596
+ for (const field of fields) {
597
+ if (!Object.prototype.hasOwnProperty.call(data, field.name)) continue;
598
+ const rule = field.access?.[operation];
599
+ if (rule) {
600
+ const decision = await evalAccess(rule, { req, id, data });
601
+ if (!isAllowed(decision)) {
602
+ delete data[field.name];
603
+ continue;
604
+ }
605
+ }
606
+ const value = data[field.name];
607
+ if (field.type === "group" && value && typeof value === "object" && !Array.isArray(value)) {
608
+ await applyFieldAccess(field.fields, value, operation, req, id);
609
+ } else if (field.type === "array" && Array.isArray(value)) {
610
+ for (const row of value) {
611
+ if (row && typeof row === "object") {
612
+ await applyFieldAccess(field.fields, row, operation, req, id);
613
+ }
614
+ }
615
+ }
616
+ }
617
+ }
618
+ function buildReq(partial) {
619
+ const defaultLocale = config.localization ? config.localization.defaultLocale : "en";
620
+ const fallback = config.localization ? config.localization.fallback ? config.localization.defaultLocale : false : false;
621
+ return {
622
+ user: partial?.user ?? null,
623
+ locale: partial?.locale ?? defaultLocale,
624
+ fallbackLocale: partial?.fallbackLocale ?? fallback,
625
+ context: partial?.context ?? {}
626
+ };
627
+ }
628
+ function collectionOrThrow(slug) {
629
+ const collection = config.collectionsBySlug[slug];
630
+ if (!collection) throw new BadRequestError(`Unknown collection "${slug}".`);
631
+ return collection;
632
+ }
633
+ function globalOrThrow(slug) {
634
+ const global = config.globalsBySlug[slug];
635
+ if (!global) throw new BadRequestError(`Unknown global "${slug}".`);
636
+ return global;
637
+ }
638
+ function rowToDoc(collection, row, req) {
639
+ const body = deserializeDoc(collection.fields, row, {
640
+ locale: req.locale,
641
+ fallbackLocale: req.fallbackLocale
642
+ });
643
+ const doc = { id: String(row.id), ...body };
644
+ if (row.createdAt !== void 0) doc.createdAt = row.createdAt;
645
+ if (row.updatedAt !== void 0) doc.updatedAt = row.updatedAt;
646
+ if (collection.auth) delete doc.hash;
647
+ return doc;
648
+ }
649
+ function authTtl(collection) {
650
+ const auth = collection.auth;
651
+ return typeof auth === "object" && auth.tokenExpiration ? auth.tokenExpiration : 3600;
652
+ }
653
+ async function prepareAuthInput(collection, data, operation) {
654
+ if (!collection.auth) return data;
655
+ const next = { ...data };
656
+ delete next.hash;
657
+ const password = next.password;
658
+ delete next.password;
659
+ if (typeof password === "string" && password.length > 0) {
660
+ if (password.length < 8) {
661
+ throw new ValidationError([{ path: "password", message: "Password must be at least 8 characters." }]);
662
+ }
663
+ next.hash = await hashPassword(password);
664
+ } else if (operation === "create") {
665
+ throw new ValidationError([{ path: "password", message: "Password is required." }]);
666
+ }
667
+ return next;
668
+ }
669
+ async function runHooks(hooks, args, key) {
670
+ let current = args[key];
671
+ if (!hooks) return current;
672
+ for (const hook of hooks) {
673
+ current = await hook({ ...args, [key]: current });
674
+ }
675
+ return current;
676
+ }
677
+ async function populate(collection, doc, depth, req) {
678
+ if (depth <= 0) return doc;
679
+ for (const rel of relationshipFields(collection.fields)) {
680
+ if (!config.collectionsBySlug[rel.relationTo]) continue;
681
+ const value = doc[rel.name];
682
+ if (rel.hasMany && Array.isArray(value)) {
683
+ const out = [];
684
+ for (const id of value) {
685
+ const related = await safeFindByID(rel.relationTo, String(id), depth - 1, req);
686
+ out.push(related ?? id);
687
+ }
688
+ doc[rel.name] = out;
689
+ } else if (!rel.hasMany && value != null) {
690
+ const related = await safeFindByID(rel.relationTo, String(value), depth - 1, req);
691
+ doc[rel.name] = related ?? value;
692
+ }
693
+ }
694
+ return doc;
695
+ }
696
+ async function safeFindByID(slug, id, depth, req) {
697
+ try {
698
+ return await findByID({ collection: slug, id, depth, req });
699
+ } catch (err) {
700
+ if (isKernelError(err)) return null;
701
+ throw err;
702
+ }
703
+ }
704
+ async function create(opts) {
705
+ const collection = collectionOrThrow(opts.collection);
706
+ const req = buildReq(opts.req);
707
+ const override = opts.overrideAccess ?? false;
708
+ if (!override) {
709
+ const access = await evalAccess(collection.access?.create, { req, data: opts.data });
710
+ if (!isAllowed(access)) throw new ForbiddenError();
711
+ }
712
+ const incoming = { ...opts.data };
713
+ if (!override) await applyFieldAccess(collection.fields, incoming, "create", req);
714
+ let data = applyDefaults(collection.fields, incoming);
715
+ data = await prepareAuthInput(collection, data, "create");
716
+ data = await runHooks(collection.hooks?.beforeChange, { req, operation: "create", data }, "data");
717
+ const errors = await validateFields(collection.fields, data, { req, operation: "create" });
718
+ if (errors.length) throw new ValidationError(errors);
719
+ const row = serializeDoc(collection.fields, data, { locale: req.locale });
720
+ row.id = randomUUID();
721
+ const created = await db.create({ collection: collection.slug, data: row });
722
+ let doc = rowToDoc(collection, created, req);
723
+ doc = await runHooks(collection.hooks?.afterChange, { req, operation: "create", doc }, "doc");
724
+ doc = await runHooks(collection.hooks?.afterRead, { req, operation: "read", doc }, "doc");
725
+ doc = await populate(collection, doc, opts.depth ?? 0, req);
726
+ return doc;
727
+ }
728
+ async function find(opts) {
729
+ const collection = collectionOrThrow(opts.collection);
730
+ const req = buildReq(opts.req);
731
+ const override = opts.overrideAccess ?? false;
732
+ let where = opts.where;
733
+ if (!override) {
734
+ const access = await evalAccess(collection.access?.read, { req });
735
+ if (!isAllowed(access)) throw new ForbiddenError();
736
+ where = mergeWhere(where, asWhere(access));
737
+ }
738
+ const timestamps = collection.timestamps ?? true;
739
+ const sort = parseSort(opts.sort);
740
+ if (sort.length === 0) {
741
+ sort.push(timestamps ? { field: "createdAt", direction: "desc" } : { field: "id", direction: "asc" });
742
+ }
743
+ const limit = Math.min(Math.max(opts.limit ?? DEFAULT_LIMIT, 1), MAX_LIMIT);
744
+ const page = Math.max(opts.page ?? 1, 1);
745
+ const result = await db.find({ collection: collection.slug, where, sort, limit, page });
746
+ const docs = [];
747
+ for (const row of result.docs) {
748
+ let doc = rowToDoc(collection, row, req);
749
+ doc = await runHooks(collection.hooks?.afterRead, { req, operation: "read", doc }, "doc");
750
+ doc = await populate(collection, doc, opts.depth ?? 0, req);
751
+ docs.push(doc);
752
+ }
753
+ return { ...result, docs };
754
+ }
755
+ async function findByID(opts) {
756
+ const collection = collectionOrThrow(opts.collection);
757
+ const req = buildReq(opts.req);
758
+ const override = opts.overrideAccess ?? false;
759
+ const row = await db.findByID({ collection: collection.slug, id: opts.id });
760
+ if (!row) return null;
761
+ if (!override) {
762
+ const access = await evalAccess(collection.access?.read, { req, id: opts.id });
763
+ if (!isAllowed(access)) throw new ForbiddenError();
764
+ const scope = asWhere(access);
765
+ if (scope && !matchesWhere(row, scope)) throw new ForbiddenError();
766
+ }
767
+ let doc = rowToDoc(collection, row, req);
768
+ doc = await runHooks(collection.hooks?.afterRead, { req, operation: "read", doc }, "doc");
769
+ doc = await populate(collection, doc, opts.depth ?? 0, req);
770
+ return doc;
771
+ }
772
+ async function update(opts) {
773
+ const collection = collectionOrThrow(opts.collection);
774
+ const req = buildReq(opts.req);
775
+ const override = opts.overrideAccess ?? false;
776
+ const existing = await db.findByID({ collection: collection.slug, id: opts.id });
777
+ if (!existing) throw new NotFoundError();
778
+ if (!override) {
779
+ const access = await evalAccess(collection.access?.update, { req, id: opts.id, data: opts.data });
780
+ if (!isAllowed(access)) throw new ForbiddenError();
781
+ const scope = asWhere(access);
782
+ if (scope && !matchesWhere(existing, scope)) throw new ForbiddenError();
783
+ }
784
+ const filtered = { ...opts.data };
785
+ if (!override) await applyFieldAccess(collection.fields, filtered, "update", req, opts.id);
786
+ const input = await prepareAuthInput(collection, filtered, "update");
787
+ const existingDoc = rowToDoc(collection, existing, req);
788
+ let merged = { ...existingDoc, ...input };
789
+ merged = await runHooks(
790
+ collection.hooks?.beforeChange,
791
+ { req, operation: "update", data: merged, originalDoc: existingDoc },
792
+ "data"
793
+ );
794
+ const errors = await validateFields(collection.fields, merged, { req, operation: "update" });
795
+ if (errors.length) throw new ValidationError(errors);
796
+ const row = serializeDoc(collection.fields, input, { locale: req.locale, existingRow: existing });
797
+ const updated = await db.update({ collection: collection.slug, id: opts.id, data: row });
798
+ if (!updated) return null;
799
+ let doc = rowToDoc(collection, updated, req);
800
+ doc = await runHooks(collection.hooks?.afterChange, { req, operation: "update", doc }, "doc");
801
+ doc = await runHooks(collection.hooks?.afterRead, { req, operation: "read", doc }, "doc");
802
+ doc = await populate(collection, doc, opts.depth ?? 0, req);
803
+ return doc;
804
+ }
805
+ async function deleteOne(opts) {
806
+ const collection = collectionOrThrow(opts.collection);
807
+ const req = buildReq(opts.req);
808
+ const override = opts.overrideAccess ?? false;
809
+ const existing = await db.findByID({ collection: collection.slug, id: opts.id });
810
+ if (!existing) throw new NotFoundError();
811
+ if (!override) {
812
+ const access = await evalAccess(collection.access?.delete, { req, id: opts.id });
813
+ if (!isAllowed(access)) throw new ForbiddenError();
814
+ const scope = asWhere(access);
815
+ if (scope && !matchesWhere(existing, scope)) throw new ForbiddenError();
816
+ }
817
+ for (const hook of collection.hooks?.beforeDelete ?? []) await hook({ req, id: opts.id });
818
+ const removed = await db.delete({ collection: collection.slug, id: opts.id });
819
+ const doc = removed ? rowToDoc(collection, removed, req) : null;
820
+ if (doc) for (const hook of collection.hooks?.afterDelete ?? []) await hook({ req, id: opts.id, doc });
821
+ return doc;
822
+ }
823
+ async function count(opts) {
824
+ const collection = collectionOrThrow(opts.collection);
825
+ const req = buildReq(opts.req);
826
+ let where = opts.where;
827
+ if (!(opts.overrideAccess ?? false)) {
828
+ const access = await evalAccess(collection.access?.read, { req });
829
+ if (!isAllowed(access)) throw new ForbiddenError();
830
+ where = mergeWhere(where, asWhere(access));
831
+ }
832
+ return db.count({ collection: collection.slug, where });
833
+ }
834
+ async function login(opts) {
835
+ const collection = collectionOrThrow(opts.collection);
836
+ if (!collection.auth) throw new BadRequestError(`Collection "${opts.collection}" is not an auth collection.`);
837
+ const key = loginKey(collection.slug, opts.email);
838
+ assertLoginAllowed(key);
839
+ const result = await db.find({
840
+ collection: collection.slug,
841
+ where: { email: { equals: opts.email } },
842
+ sort: [{ field: "id", direction: "asc" }],
843
+ limit: 1,
844
+ page: 1
845
+ });
846
+ const row = result.docs[0];
847
+ const passwordOk = !!row && typeof row.hash === "string" && await verifyPassword(opts.password, row.hash);
848
+ if (!row || !passwordOk) {
849
+ recordLoginFailure(key);
850
+ throw new UnauthorizedError("Invalid email or password.");
851
+ }
852
+ loginFailures.delete(key);
853
+ const user = rowToDoc(collection, row, buildReq());
854
+ user.collection = collection.slug;
855
+ const ttl = authTtl(collection);
856
+ const token = signToken({ sub: user.id, collection: collection.slug }, config.secret, ttl);
857
+ return { user, token, exp: Math.floor(Date.now() / 1e3) + ttl };
858
+ }
859
+ async function authenticate(token) {
860
+ const payload = verifyToken(token, config.secret);
861
+ if (!payload) return null;
862
+ const collection = config.collectionsBySlug[payload.collection];
863
+ if (!collection?.auth) return null;
864
+ const row = await db.findByID({ collection: collection.slug, id: payload.sub });
865
+ if (!row) return null;
866
+ const user = rowToDoc(collection, row, buildReq());
867
+ user.collection = collection.slug;
868
+ return user;
869
+ }
870
+ function globalDoc(global, row, req) {
871
+ if (!row) {
872
+ const defaults = applyDefaults(global.fields, {});
873
+ return deserializeDoc(global.fields, defaults, {
874
+ locale: req.locale,
875
+ fallbackLocale: req.fallbackLocale
876
+ });
877
+ }
878
+ const body = deserializeDoc(global.fields, row, {
879
+ locale: req.locale,
880
+ fallbackLocale: req.fallbackLocale
881
+ });
882
+ if (row.updatedAt !== void 0) body.updatedAt = row.updatedAt;
883
+ return body;
884
+ }
885
+ async function findGlobal(opts) {
886
+ const global = globalOrThrow(opts.slug);
887
+ const req = buildReq(opts.req);
888
+ if (!(opts.overrideAccess ?? false)) {
889
+ const access = await evalAccess(global.access?.read, { req });
890
+ if (!isAllowed(access)) throw new ForbiddenError();
891
+ }
892
+ const table = tableForGlobal(global.slug);
893
+ const row = await db.findByID({ collection: table, id: GLOBAL_ROW_ID });
894
+ let doc = globalDoc(global, row, req);
895
+ doc = await runHooks(global.hooks?.afterRead, { req, operation: "read", doc }, "doc");
896
+ return doc;
897
+ }
898
+ async function updateGlobal(opts) {
899
+ const global = globalOrThrow(opts.slug);
900
+ const req = buildReq(opts.req);
901
+ if (!(opts.overrideAccess ?? false)) {
902
+ const access = await evalAccess(global.access?.update, { req, data: opts.data });
903
+ if (!isAllowed(access)) throw new ForbiddenError();
904
+ }
905
+ const table = tableForGlobal(global.slug);
906
+ const existing = await db.findByID({ collection: table, id: GLOBAL_ROW_ID });
907
+ const existingDoc = globalDoc(global, existing, req);
908
+ const incoming = { ...opts.data };
909
+ if (!(opts.overrideAccess ?? false)) await applyFieldAccess(global.fields, incoming, "update", req);
910
+ let merged = { ...existingDoc, ...incoming };
911
+ merged = await runHooks(global.hooks?.beforeChange, { req, operation: "update", data: merged }, "data");
912
+ const errors = await validateFields(global.fields, merged, { req, operation: "update" });
913
+ if (errors.length) throw new ValidationError(errors);
914
+ const row = serializeDoc(global.fields, incoming, { locale: req.locale, existingRow: existing });
915
+ let saved;
916
+ if (existing) {
917
+ saved = await db.update({ collection: table, id: GLOBAL_ROW_ID, data: row });
918
+ } else {
919
+ row.id = GLOBAL_ROW_ID;
920
+ saved = await db.create({ collection: table, data: row });
921
+ }
922
+ let doc = globalDoc(global, saved, req);
923
+ doc = await runHooks(global.hooks?.afterChange, { req, operation: "update", doc }, "doc");
924
+ return doc;
925
+ }
926
+ return { create, find, findByID, update, delete: deleteOne, count, login, authenticate, findGlobal, updateGlobal };
927
+ }
928
+
929
+ // ../core/src/kernel.ts
930
+ var LEVELS = { debug: 10, info: 20, warn: 30, error: 40 };
931
+ function createLogger(level = "info") {
932
+ const min = LEVELS[level];
933
+ const emit = (lvl, stream) => (msg, meta) => {
934
+ if (LEVELS[lvl] < min) return;
935
+ const line = `[kernel] ${lvl.toUpperCase()} ${msg}`;
936
+ if (meta === void 0) console[stream](line);
937
+ else console[stream](line, meta);
938
+ };
939
+ return {
940
+ debug: emit("debug", "log"),
941
+ info: emit("info", "log"),
942
+ warn: emit("warn", "warn"),
943
+ error: emit("error", "error")
944
+ };
945
+ }
946
+ async function initKernel(config, options = {}) {
947
+ const sanitized = sanitizeConfig(config);
948
+ const schema = compileSchema(sanitized);
949
+ const logger = createLogger(options.logLevel ?? process.env.KERNEL_LOG_LEVEL ?? "info");
950
+ await sanitized.db.init({ logger });
951
+ if (options.autoMigrate) await sanitized.db.migrate(schema);
952
+ const ops = createOperations({ config: sanitized, db: sanitized.db });
953
+ return {
954
+ config: sanitized,
955
+ db: sanitized.db,
956
+ schema,
957
+ find: ops.find,
958
+ findByID: ops.findByID,
959
+ create: ops.create,
960
+ update: ops.update,
961
+ delete: ops.delete,
962
+ count: ops.count,
963
+ login: ops.login,
964
+ authenticate: ops.authenticate,
965
+ findGlobal: ops.findGlobal,
966
+ updateGlobal: ops.updateGlobal,
967
+ async migrate() {
968
+ await sanitized.db.migrate(schema);
969
+ },
970
+ async destroy() {
971
+ await sanitized.db.destroy();
972
+ }
973
+ };
974
+ }
975
+
976
+ // ../core/src/codegen.ts
977
+ function pascal(slug) {
978
+ return slug.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((p) => p[0].toUpperCase() + p.slice(1)).join("");
979
+ }
980
+ function tsType(field, indent) {
981
+ switch (field.type) {
982
+ case "text":
983
+ case "textarea":
984
+ case "email":
985
+ case "code":
986
+ case "slug":
987
+ return "string";
988
+ case "number":
989
+ return "number";
990
+ case "boolean":
991
+ case "checkbox":
992
+ return "boolean";
993
+ case "date":
994
+ return "string";
995
+ case "json":
996
+ case "richText":
997
+ return "unknown";
998
+ case "point":
999
+ return "[number, number]";
1000
+ case "select":
1001
+ case "radio": {
1002
+ const union = field.options.map((o) => JSON.stringify(optionValue(o))).join(" | ") || "string";
1003
+ return field.hasMany ? `Array<${union}>` : union;
1004
+ }
1005
+ case "relationship":
1006
+ case "upload":
1007
+ return field.hasMany ? "string[]" : "string";
1008
+ case "array":
1009
+ return `Array<${renderObject(field.fields, indent)}>`;
1010
+ case "group":
1011
+ return renderObject(field.fields, indent);
1012
+ default:
1013
+ return "unknown";
1014
+ }
1015
+ }
1016
+ function renderObject(fields, indent) {
1017
+ const inner = `${indent} `;
1018
+ const lines = fields.map(
1019
+ (f) => `${inner}${f.name}${f.required ? "" : "?"}: ${tsType(f, inner)}${f.required ? "" : " | null"}`
1020
+ );
1021
+ return `{
1022
+ ${lines.join("\n")}
1023
+ ${indent}}`;
1024
+ }
1025
+ function renderInterface(name, fields, timestamps) {
1026
+ const lines = fields.map(
1027
+ (f) => ` ${f.name}${f.required ? "" : "?"}: ${tsType(f, " ")}${f.required ? "" : " | null"}`
1028
+ );
1029
+ const ts = timestamps ? " createdAt?: string\n updatedAt?: string\n" : "";
1030
+ return `export interface ${name} {
1031
+ id: string
1032
+ ${lines.join("\n")}
1033
+ ${ts}}`;
1034
+ }
1035
+ function generateTypes(input) {
1036
+ const header = `/**
1037
+ * This file is auto-generated by KernelCMS. Do not edit by hand.
1038
+ * Run \`kernel generate:types\` to refresh it.
1039
+ */
1040
+ /* eslint-disable */
1041
+ `;
1042
+ const collectionTypes = input.collections.map((c) => renderInterface(pascal(c.slug), c.fields, c.timestamps ?? true)).join("\n\n");
1043
+ const globalTypes = input.globals.map((g) => renderInterface(pascal(g.slug), g.fields, true)).join("\n\n");
1044
+ const collectionMap = input.collections.map((c) => ` ${c.slug}: ${pascal(c.slug)}`).join("\n");
1045
+ const globalMap = input.globals.map((g) => ` ${g.slug}: ${pascal(g.slug)}`).join("\n");
1046
+ const registry = `export interface KernelTypes {
1047
+ collections: {
1048
+ ${collectionMap || " [slug: string]: { id: string }"}
1049
+ }
1050
+ globals: {
1051
+ ${globalMap || " [slug: string]: Record<string, unknown>"}
1052
+ }
1053
+ }`;
1054
+ return [header, collectionTypes, globalTypes, registry].filter(Boolean).join("\n\n") + "\n";
1055
+ }
1056
+
1057
+ // ../core/src/describe.ts
1058
+ function describeField(field) {
1059
+ const out = {
1060
+ name: field.name,
1061
+ type: field.type,
1062
+ label: fieldLabel(field),
1063
+ required: Boolean(field.required),
1064
+ unique: Boolean(field.unique),
1065
+ localized: Boolean(field.localized)
1066
+ };
1067
+ if (field.admin) {
1068
+ const { condition, ...rest } = field.admin;
1069
+ void condition;
1070
+ out.admin = rest;
1071
+ }
1072
+ if (field.type === "select" || field.type === "radio") {
1073
+ out.options = field.options.map((o) => ({ label: optionLabel(o), value: optionValue(o) }));
1074
+ out.hasMany = Boolean(field.hasMany);
1075
+ }
1076
+ if (field.type === "relationship" || field.type === "upload") {
1077
+ out.relationTo = field.relationTo;
1078
+ out.hasMany = Boolean(field.hasMany);
1079
+ }
1080
+ if (field.type === "array" || field.type === "group") {
1081
+ out.fields = field.fields.map(describeField);
1082
+ }
1083
+ if (field.type === "number") {
1084
+ if (field.min !== void 0) out.min = field.min;
1085
+ if (field.max !== void 0) out.max = field.max;
1086
+ }
1087
+ if (field.type === "text" || field.type === "textarea" || field.type === "email" || field.type === "code") {
1088
+ if (field.minLength !== void 0) out.minLength = field.minLength;
1089
+ if (field.maxLength !== void 0) out.maxLength = field.maxLength;
1090
+ }
1091
+ return out;
1092
+ }
1093
+ function describeCollection(collection) {
1094
+ const fields = collection.fields.filter((f) => f.name !== "hash").map(describeField);
1095
+ if (collection.auth) {
1096
+ fields.push({
1097
+ name: "password",
1098
+ type: "password",
1099
+ label: "Password",
1100
+ required: false,
1101
+ unique: false,
1102
+ localized: false,
1103
+ admin: { description: "Leave blank to keep the current password." }
1104
+ });
1105
+ }
1106
+ return {
1107
+ slug: collection.slug,
1108
+ labels: {
1109
+ singular: collection.labels?.singular ?? collection.slug,
1110
+ plural: collection.labels?.plural ?? collection.slug
1111
+ },
1112
+ useAsTitle: collection.admin?.useAsTitle ?? collection.fields[0]?.name ?? "id",
1113
+ defaultColumns: collection.admin?.defaultColumns,
1114
+ group: collection.admin?.group,
1115
+ description: collection.admin?.description,
1116
+ auth: Boolean(collection.auth),
1117
+ hidden: Boolean(collection.admin?.hidden),
1118
+ fields
1119
+ };
1120
+ }
1121
+ function describeGlobal(global) {
1122
+ return {
1123
+ slug: global.slug,
1124
+ label: global.label ?? global.slug,
1125
+ fields: global.fields.map(describeField)
1126
+ };
1127
+ }
1128
+ function describeConfig(config) {
1129
+ return {
1130
+ collections: config.collections.map(describeCollection),
1131
+ globals: config.globals.map(describeGlobal),
1132
+ localization: config.localization ? { locales: config.localization.locales, defaultLocale: config.localization.defaultLocale } : null,
1133
+ routes: { api: config.routes.api }
1134
+ };
1135
+ }
1136
+
1137
+ export {
1138
+ defineConfig,
1139
+ sanitizeConfig,
1140
+ defaultLocaleOf,
1141
+ humanize,
1142
+ fieldLabel,
1143
+ optionValue,
1144
+ optionLabel,
1145
+ storageTypeForField,
1146
+ columnForField,
1147
+ defaultForField,
1148
+ applyDefaults,
1149
+ validateFields,
1150
+ serializeDoc,
1151
+ deserializeDoc,
1152
+ relationshipFields,
1153
+ tableForCollection,
1154
+ tableForGlobal,
1155
+ GLOBAL_ROW_ID,
1156
+ compileSchema,
1157
+ KernelError,
1158
+ ValidationError,
1159
+ NotFoundError,
1160
+ ForbiddenError,
1161
+ UnauthorizedError,
1162
+ BadRequestError,
1163
+ ConflictError,
1164
+ TooManyRequestsError,
1165
+ isKernelError,
1166
+ hashPassword,
1167
+ verifyPassword,
1168
+ signToken,
1169
+ verifyToken,
1170
+ evalAccess,
1171
+ isAllowed,
1172
+ asWhere,
1173
+ parseSort,
1174
+ mergeWhere,
1175
+ matchesWhere,
1176
+ createOperations,
1177
+ createLogger,
1178
+ initKernel,
1179
+ generateTypes,
1180
+ describeConfig
1181
+ };