@adminforge/core 0.3.1

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.
Files changed (86) hide show
  1. package/.turbo/turbo-build.log +56 -0
  2. package/CHANGELOG.md +32 -0
  3. package/LICENSE +21 -0
  4. package/bin/adminforge.js +317 -0
  5. package/dist/auth-client.cjs +45 -0
  6. package/dist/auth-client.cjs.map +1 -0
  7. package/dist/auth-client.d.cts +17 -0
  8. package/dist/auth-client.d.ts +17 -0
  9. package/dist/auth-client.js +20 -0
  10. package/dist/auth-client.js.map +1 -0
  11. package/dist/auth.cjs +65 -0
  12. package/dist/auth.cjs.map +1 -0
  13. package/dist/auth.d.cts +21 -0
  14. package/dist/auth.d.ts +21 -0
  15. package/dist/auth.js +36 -0
  16. package/dist/auth.js.map +1 -0
  17. package/dist/client-D0cjJVsn.d.ts +20 -0
  18. package/dist/client-sRnmZ-Y9.d.cts +20 -0
  19. package/dist/index-CyzxaE7n.d.cts +124 -0
  20. package/dist/index-CyzxaE7n.d.ts +124 -0
  21. package/dist/index.cjs +453 -0
  22. package/dist/index.cjs.map +1 -0
  23. package/dist/index.d.cts +65 -0
  24. package/dist/index.d.ts +65 -0
  25. package/dist/index.js +410 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/next.cjs +839 -0
  28. package/dist/next.cjs.map +1 -0
  29. package/dist/next.d.cts +84 -0
  30. package/dist/next.d.ts +84 -0
  31. package/dist/next.js +800 -0
  32. package/dist/next.js.map +1 -0
  33. package/dist/styles.css +763 -0
  34. package/dist/styles.css.map +1 -0
  35. package/dist/styles.d.cts +2 -0
  36. package/dist/styles.d.ts +2 -0
  37. package/dist/ui.cjs +2500 -0
  38. package/dist/ui.cjs.map +1 -0
  39. package/dist/ui.d.cts +119 -0
  40. package/dist/ui.d.ts +119 -0
  41. package/dist/ui.js +2448 -0
  42. package/dist/ui.js.map +1 -0
  43. package/eslint.config.js +35 -0
  44. package/package.json +99 -0
  45. package/src/api/controller.ts +234 -0
  46. package/src/api/index.ts +4 -0
  47. package/src/api/next.ts +281 -0
  48. package/src/api/security/agent-auth.ts +134 -0
  49. package/src/auth/config.ts +20 -0
  50. package/src/auth/index.ts +3 -0
  51. package/src/auth/middleware.ts +15 -0
  52. package/src/auth/provider.tsx +28 -0
  53. package/src/core/fields/index.ts +119 -0
  54. package/src/core/hooks/index.ts +60 -0
  55. package/src/core/index.ts +43 -0
  56. package/src/core/registry/index.ts +22 -0
  57. package/src/core/schema/collection.ts +12 -0
  58. package/src/core/schema/config.ts +11 -0
  59. package/src/core/schema/normalize.ts +32 -0
  60. package/src/core/types/index.ts +114 -0
  61. package/src/db/client.ts +146 -0
  62. package/src/db/index.ts +3 -0
  63. package/src/db/schema-generator.ts +104 -0
  64. package/src/fields/index.ts +1 -0
  65. package/src/index.ts +4 -0
  66. package/src/next.ts +3 -0
  67. package/src/styles/adminforge.css +840 -0
  68. package/src/ui/AdminDashboard.tsx +176 -0
  69. package/src/ui/AdminForgeContext.tsx +64 -0
  70. package/src/ui/components/AdminLayout.tsx +107 -0
  71. package/src/ui/form-engine/FormEngine.tsx +250 -0
  72. package/src/ui/form-engine/ImageUpload.tsx +68 -0
  73. package/src/ui/form-engine/RelationInput.tsx +215 -0
  74. package/src/ui/form-engine/RichTextEditor.tsx +708 -0
  75. package/src/ui/index.ts +18 -0
  76. package/src/ui/screens/AdminPage.tsx +162 -0
  77. package/src/ui/screens/AgentTokenPage.tsx +232 -0
  78. package/src/ui/screens/CollectionFormPage.tsx +135 -0
  79. package/src/ui/screens/CollectionListPage.tsx +170 -0
  80. package/src/ui/screens/CollectionSchemaPage.tsx +180 -0
  81. package/src/ui/screens/RoleDetailPage.tsx +147 -0
  82. package/src/ui/screens/RolesListPage.tsx +57 -0
  83. package/src/ui/table-engine/TableEngine.tsx +157 -0
  84. package/src/ui.ts +3 -0
  85. package/tsconfig.json +10 -0
  86. package/tsup.config.ts +54 -0
package/dist/next.js ADDED
@@ -0,0 +1,800 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res) => function __init() {
4
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
+ };
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+
11
+ // src/api/security/agent-auth.ts
12
+ var agent_auth_exports = {};
13
+ __export(agent_auth_exports, {
14
+ assertScope: () => assertScope,
15
+ generateAgentToken: () => generateAgentToken,
16
+ verifyAgentToken: () => verifyAgentToken
17
+ });
18
+ import jwt from "jsonwebtoken";
19
+ import crypto from "crypto";
20
+ function getSecret() {
21
+ const secret = process.env.ADMINFORGE_SECRET;
22
+ if (!secret) {
23
+ throw new Error("ADMINFORGE_SECRET env var is required. Generate one with: openssl rand -hex 32");
24
+ }
25
+ return secret;
26
+ }
27
+ function normalizeScope(scope) {
28
+ const normalized = scope.trim().toLowerCase();
29
+ if (!/^[a-z0-9_-]+:[a-z]+$/.test(normalized)) {
30
+ throw new Error(`Malformed scope format: ${scope}`);
31
+ }
32
+ return normalized;
33
+ }
34
+ function assertValidAction(action) {
35
+ const validActions = ["create", "read", "update", "delete"];
36
+ if (!validActions.includes(action)) {
37
+ throw new Error(`Invalid action: ${action}`);
38
+ }
39
+ return action;
40
+ }
41
+ function assertValidCollection(collection2) {
42
+ if (!/^[a-z0-9_-]+$/.test(collection2)) {
43
+ throw new Error(`Invalid collection name: ${collection2}`);
44
+ }
45
+ return collection2;
46
+ }
47
+ function generateAgentToken(userId, role, scopes, expiresInSeconds = 600) {
48
+ const normalizedScopes = scopes.map(normalizeScope);
49
+ return jwt.sign(
50
+ {
51
+ sub: userId,
52
+ role,
53
+ scope: normalizedScopes,
54
+ sessionId: crypto.randomUUID()
55
+ },
56
+ getSecret(),
57
+ {
58
+ expiresIn: expiresInSeconds,
59
+ issuer: "adminforge",
60
+ audience: "agent"
61
+ }
62
+ );
63
+ }
64
+ function isRevoked(sessionId) {
65
+ return false;
66
+ }
67
+ function verifyAgentToken(token) {
68
+ try {
69
+ const payload = jwt.verify(token, getSecret(), {
70
+ issuer: "adminforge",
71
+ audience: "agent"
72
+ });
73
+ if (!payload.sub) throw new Error("Missing userId (sub)");
74
+ if (!payload.role) throw new Error("Missing role");
75
+ if (!Array.isArray(payload.scope)) throw new Error("Invalid scope format");
76
+ if (isRevoked(payload.sessionId)) {
77
+ throw new Error("Session revoked");
78
+ }
79
+ payload.scope = payload.scope.map(normalizeScope);
80
+ return payload;
81
+ } catch (error) {
82
+ throw new Error(`Unauthorized: ${error.message}`);
83
+ }
84
+ }
85
+ function assertScope(agent, collection2, action) {
86
+ const validColl = assertValidCollection(collection2);
87
+ const validAction = assertValidAction(action);
88
+ const key = `${validColl}:${validAction}`.toLowerCase();
89
+ if (!agent.scope.includes(key)) {
90
+ throw new Error(`Forbidden: Missing scope ${key}`);
91
+ }
92
+ }
93
+ var init_agent_auth = __esm({
94
+ "src/api/security/agent-auth.ts"() {
95
+ "use strict";
96
+ }
97
+ });
98
+
99
+ // src/core/schema/config.ts
100
+ function defineConfig(config) {
101
+ return {
102
+ collections: config.collections,
103
+ auth: config.auth ?? { enabled: false }
104
+ };
105
+ }
106
+ var init_config = __esm({
107
+ "src/core/schema/config.ts"() {
108
+ "use strict";
109
+ }
110
+ });
111
+
112
+ // src/core/schema/collection.ts
113
+ function collection(input) {
114
+ return {
115
+ name: input.name,
116
+ label: input.label ?? input.name.charAt(0).toUpperCase() + input.name.slice(1),
117
+ icon: input.icon,
118
+ fields: input.fields,
119
+ hooks: input.hooks,
120
+ access: input.access
121
+ };
122
+ }
123
+ var init_collection = __esm({
124
+ "src/core/schema/collection.ts"() {
125
+ "use strict";
126
+ }
127
+ });
128
+
129
+ // src/core/schema/normalize.ts
130
+ function normalize(config) {
131
+ return {
132
+ collections: config.collections.map((c) => ({
133
+ ...c,
134
+ label: c.label ?? c.name.charAt(0).toUpperCase() + c.name.slice(1)
135
+ })),
136
+ auth: config.auth ?? { enabled: false }
137
+ };
138
+ }
139
+ function serializeConfig(config) {
140
+ return {
141
+ ...config,
142
+ collections: config.collections.map((c) => ({
143
+ ...c,
144
+ fields: Object.fromEntries(
145
+ Object.entries(c.fields).map(([name, field]) => {
146
+ const { validation, ...rest } = field;
147
+ return [name, rest];
148
+ })
149
+ )
150
+ }))
151
+ };
152
+ }
153
+ var init_normalize = __esm({
154
+ "src/core/schema/normalize.ts"() {
155
+ "use strict";
156
+ }
157
+ });
158
+
159
+ // src/core/fields/index.ts
160
+ import { z as z2 } from "zod";
161
+ function fieldMeta(options = {}) {
162
+ return {
163
+ required: Boolean(options.required),
164
+ unique: Boolean(options.unique),
165
+ default: options.default,
166
+ label: options.label,
167
+ hidden: options.hidden,
168
+ readOnly: options.readOnly,
169
+ description: options.description
170
+ };
171
+ }
172
+ function text(options = {}) {
173
+ return {
174
+ type: "text",
175
+ db: { type: "String", nullable: !options.required, unique: options.unique, default: options.default },
176
+ access: options.access,
177
+ ui: { component: "text", props: { label: options.label, hidden: options.hidden, readOnly: options.readOnly } },
178
+ validation: options.required ? z2.string().min(1) : z2.string().optional(),
179
+ meta: fieldMeta(options)
180
+ };
181
+ }
182
+ function boolean(options = {}) {
183
+ return {
184
+ type: "boolean",
185
+ db: { type: "Boolean", nullable: !options.required, default: options.default ?? false },
186
+ access: options.access,
187
+ ui: { component: "boolean", props: { label: options.label, hidden: options.hidden, readOnly: options.readOnly } },
188
+ validation: options.required ? z2.boolean() : z2.boolean().optional(),
189
+ meta: fieldMeta(options)
190
+ };
191
+ }
192
+ function richText(options = {}) {
193
+ return {
194
+ type: "richText",
195
+ db: { type: "String", nullable: !options.required },
196
+ access: options.access,
197
+ ui: { component: "richText", props: { label: options.label, hidden: options.hidden, readOnly: options.readOnly } },
198
+ validation: options.required ? z2.string().min(1) : z2.string().optional(),
199
+ meta: fieldMeta(options)
200
+ };
201
+ }
202
+ function slug(options) {
203
+ return {
204
+ type: "slug",
205
+ db: { type: "String", nullable: !options.required, unique: options.unique ?? true },
206
+ access: options.access,
207
+ ui: { component: "slug", props: { from: options.from, label: options.label, hidden: options.hidden, readOnly: options.readOnly } },
208
+ validation: options.required ? z2.string().regex(/^[a-z0-9-]+$/) : z2.string().regex(/^[a-z0-9-]+$/).optional(),
209
+ hooks: { beforeSave: (value) => {
210
+ if (typeof value === "string") return value.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
211
+ return value;
212
+ } },
213
+ meta: { ...fieldMeta(options), unique: options.unique ?? true }
214
+ };
215
+ }
216
+ function relation(options) {
217
+ const isMulti = options.type === "many-to-many" || options.type === "one-to-many";
218
+ return {
219
+ type: "relation",
220
+ db: { type: "String", nullable: !options.required, references: { model: options.to, field: "id" }, relationType: options.type },
221
+ access: options.access,
222
+ ui: { component: "relation", props: { to: options.to, relationType: options.type, label: options.label, hidden: options.hidden, readOnly: options.readOnly } },
223
+ validation: isMulti ? options.required ? z2.array(z2.string()).min(1) : z2.array(z2.string()).optional() : options.required ? z2.string().min(1) : z2.string().optional(),
224
+ meta: fieldMeta(options)
225
+ };
226
+ }
227
+ function date(options = {}) {
228
+ return {
229
+ type: "date",
230
+ db: { type: "DateTime", nullable: !options.required },
231
+ access: options.access,
232
+ ui: { component: "date", props: { label: options.label, hidden: options.hidden, readOnly: options.readOnly } },
233
+ validation: options.required ? z2.string().datetime() : z2.string().datetime().optional(),
234
+ hooks: { beforeSave: (value) => {
235
+ if (options.autoCreate && !value) return (/* @__PURE__ */ new Date()).toISOString();
236
+ return value;
237
+ } },
238
+ meta: fieldMeta(options)
239
+ };
240
+ }
241
+ function image(options = {}) {
242
+ return {
243
+ type: "image",
244
+ db: { type: "String", nullable: true },
245
+ access: options.access,
246
+ ui: { component: "image", props: { label: options.label, hidden: options.hidden, readOnly: options.readOnly } },
247
+ validation: z2.string().optional(),
248
+ meta: fieldMeta(options)
249
+ };
250
+ }
251
+ var fields;
252
+ var init_fields = __esm({
253
+ "src/core/fields/index.ts"() {
254
+ "use strict";
255
+ fields = {
256
+ text,
257
+ boolean,
258
+ richText,
259
+ slug,
260
+ relation,
261
+ date,
262
+ image
263
+ };
264
+ }
265
+ });
266
+
267
+ // src/core/registry/index.ts
268
+ function registerField(name, definition) {
269
+ if (fieldRegistry.has(name)) {
270
+ throw new Error(`Field "${name}" is already registered`);
271
+ }
272
+ fieldRegistry.set(name, definition);
273
+ }
274
+ function getField(name) {
275
+ return fieldRegistry.get(name);
276
+ }
277
+ function getRegisteredFields() {
278
+ return new Map(fieldRegistry);
279
+ }
280
+ function clearRegistry() {
281
+ fieldRegistry.clear();
282
+ }
283
+ var fieldRegistry;
284
+ var init_registry = __esm({
285
+ "src/core/registry/index.ts"() {
286
+ "use strict";
287
+ fieldRegistry = /* @__PURE__ */ new Map();
288
+ }
289
+ });
290
+
291
+ // src/core/hooks/index.ts
292
+ async function executeBeforeCreate(hooks, data) {
293
+ if (hooks?.beforeCreate) {
294
+ return await hooks.beforeCreate({ data });
295
+ }
296
+ return data;
297
+ }
298
+ async function executeAfterCreate(hooks, data, id) {
299
+ if (hooks?.afterCreate) {
300
+ await hooks.afterCreate({ data, id });
301
+ }
302
+ }
303
+ async function executeBeforeUpdate(hooks, data, id) {
304
+ if (hooks?.beforeUpdate) {
305
+ return await hooks.beforeUpdate({ data, id });
306
+ }
307
+ return data;
308
+ }
309
+ async function executeAfterUpdate(hooks, data, id) {
310
+ if (hooks?.afterUpdate) {
311
+ await hooks.afterUpdate({ data, id });
312
+ }
313
+ }
314
+ async function executeBeforeDelete(hooks, id) {
315
+ if (hooks?.beforeDelete) {
316
+ await hooks.beforeDelete({ id });
317
+ }
318
+ }
319
+ async function executeAfterDelete(hooks, id) {
320
+ if (hooks?.afterDelete) {
321
+ await hooks.afterDelete({ id });
322
+ }
323
+ }
324
+ var init_hooks = __esm({
325
+ "src/core/hooks/index.ts"() {
326
+ "use strict";
327
+ }
328
+ });
329
+
330
+ // src/core/index.ts
331
+ var core_exports = {};
332
+ __export(core_exports, {
333
+ clearRegistry: () => clearRegistry,
334
+ collection: () => collection,
335
+ defineConfig: () => defineConfig,
336
+ executeAfterCreate: () => executeAfterCreate,
337
+ executeAfterDelete: () => executeAfterDelete,
338
+ executeAfterUpdate: () => executeAfterUpdate,
339
+ executeBeforeCreate: () => executeBeforeCreate,
340
+ executeBeforeDelete: () => executeBeforeDelete,
341
+ executeBeforeUpdate: () => executeBeforeUpdate,
342
+ fields: () => fields,
343
+ getField: () => getField,
344
+ getRegisteredFields: () => getRegisteredFields,
345
+ normalize: () => normalize,
346
+ registerField: () => registerField,
347
+ serializeConfig: () => serializeConfig
348
+ });
349
+ var init_core = __esm({
350
+ "src/core/index.ts"() {
351
+ "use strict";
352
+ init_config();
353
+ init_collection();
354
+ init_normalize();
355
+ init_fields();
356
+ init_registry();
357
+ init_hooks();
358
+ }
359
+ });
360
+
361
+ // src/api/controller.ts
362
+ init_agent_auth();
363
+ import { z } from "zod";
364
+ function buildValidationSchema(collection2) {
365
+ const shape = {};
366
+ for (const [name, field] of Object.entries(collection2.fields)) {
367
+ shape[name] = field.validation;
368
+ }
369
+ return z.object(shape);
370
+ }
371
+ function hasAccess(access, operation, role) {
372
+ if (!access) return true;
373
+ const allowed = access[operation];
374
+ if (!allowed || !Array.isArray(allowed)) return true;
375
+ if (!role) return false;
376
+ return allowed.includes(role);
377
+ }
378
+ function createController(collection2, db, session) {
379
+ const validationSchema = buildValidationSchema(collection2);
380
+ const role = session?.user?.role || session?.agent?.role;
381
+ const actorId = session?.user?.id || session?.agent?.sub;
382
+ function requireAccess(operation) {
383
+ console.error(`[ACL] Operation: ${operation}, Collection: ${collection2.name}, Role: ${role}, Source: ${session?.source}`);
384
+ if (session?.agent) {
385
+ assertScope(session.agent, collection2.name, operation);
386
+ }
387
+ if (!hasAccess(collection2.access, operation, role)) {
388
+ console.warn(`[ACL] DENIED: ${role} not allowed to ${operation} on ${collection2.name}. Allowed:`, collection2.access?.[operation]);
389
+ throw new Error(`Access denied: ${operation} on ${collection2.name}`);
390
+ }
391
+ console.error(JSON.stringify({
392
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
393
+ type: "mutation_attempt",
394
+ source: session?.source || "unknown",
395
+ userId: actorId || "anonymous",
396
+ role: role || "none",
397
+ collection: collection2.name,
398
+ action: operation,
399
+ sessionId: session?.agent?.sessionId
400
+ }));
401
+ }
402
+ function buildSearchWhere(search) {
403
+ if (!search) return void 0;
404
+ const searchFields = Object.entries(collection2.fields).filter(([_, field]) => field.type === "text" || field.type === "slug" || field.type === "richText").map(([name]) => name);
405
+ if (searchFields.length === 0) return void 0;
406
+ return {
407
+ OR: searchFields.map((field) => ({
408
+ [field]: { contains: search, mode: "insensitive" }
409
+ }))
410
+ };
411
+ }
412
+ function filterFields(fields2) {
413
+ const filtered = {};
414
+ for (const [key, value] of Object.entries(fields2)) {
415
+ const field = collection2.fields[key];
416
+ if (hasAccess(field?.access, "read", role)) {
417
+ filtered[key] = value;
418
+ }
419
+ }
420
+ return filtered;
421
+ }
422
+ function filterWritableFields(fields2, operation) {
423
+ const filtered = {};
424
+ for (const [key, value] of Object.entries(fields2)) {
425
+ const field = collection2.fields[key];
426
+ if (field && hasAccess(field.access, operation, role)) {
427
+ filtered[key] = value;
428
+ }
429
+ }
430
+ return filtered;
431
+ }
432
+ function applyFieldHooks(fields2, hookName) {
433
+ const next = { ...fields2 };
434
+ for (const [key, value] of Object.entries(next)) {
435
+ const hook = collection2.fields[key]?.hooks?.[hookName];
436
+ if (hook) {
437
+ next[key] = hook(value);
438
+ }
439
+ }
440
+ return next;
441
+ }
442
+ function applyDerivedFields(fields2) {
443
+ const next = { ...fields2 };
444
+ for (const [name, field] of Object.entries(collection2.fields)) {
445
+ if (field.type !== "slug") continue;
446
+ const current = next[name];
447
+ if (typeof current === "string" && current.trim().length > 0) continue;
448
+ const from = field.ui.props?.from;
449
+ if (typeof from !== "string") continue;
450
+ const source = next[from];
451
+ if (typeof source !== "string" || source.trim().length === 0) continue;
452
+ const slugified = source.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
453
+ next[name] = slugified;
454
+ }
455
+ return next;
456
+ }
457
+ return {
458
+ async list(args = {}) {
459
+ requireAccess("read");
460
+ const { where, orderBy, page = 1, pageSize = 50, search } = args;
461
+ const searchWhere = buildSearchWhere(search);
462
+ const combinedWhere = searchWhere ? { ...where, ...searchWhere } : where;
463
+ const skip = (page - 1) * pageSize;
464
+ const effectiveOrderBy = orderBy || (collection2.fields.createdAt ? { createdAt: "desc" } : { id: "desc" });
465
+ const [raw, total] = await Promise.all([
466
+ db.findMany(collection2.name, { where: combinedWhere, orderBy: effectiveOrderBy, skip, take: pageSize }),
467
+ db.count(collection2.name, { where: combinedWhere })
468
+ ]);
469
+ const data = raw.map(filterFields);
470
+ return { data, total, page, pageSize };
471
+ },
472
+ async get(id) {
473
+ requireAccess("read");
474
+ const raw = await db.findUnique(collection2.name, id);
475
+ if (!raw) return null;
476
+ return filterFields(raw);
477
+ },
478
+ async create(data) {
479
+ requireAccess("create");
480
+ const allowed = filterWritableFields(data, "create");
481
+ const withDerived = applyDerivedFields(allowed);
482
+ const beforeValidate = applyFieldHooks(withDerived, "beforeValidate");
483
+ const parsed = validationSchema.parse(beforeValidate);
484
+ const beforeSave = applyFieldHooks(parsed, "beforeSave");
485
+ const transformed = transformRelations(beforeSave, false);
486
+ return db.create(collection2.name, transformed);
487
+ },
488
+ async update(id, data) {
489
+ requireAccess("update");
490
+ const allowed = filterWritableFields(data, "update");
491
+ const beforeValidate = applyFieldHooks(allowed, "beforeValidate");
492
+ const parsed = validationSchema.partial().parse(beforeValidate);
493
+ const beforeSave = applyFieldHooks(parsed, "beforeSave");
494
+ const transformed = transformRelations(beforeSave, true);
495
+ return db.update(collection2.name, id, transformed);
496
+ },
497
+ async delete(id) {
498
+ requireAccess("delete");
499
+ return db.delete(collection2.name, id);
500
+ }
501
+ };
502
+ function transformRelations(data, isUpdate) {
503
+ const transformed = { ...data };
504
+ for (const [key, value] of Object.entries(transformed)) {
505
+ const field = collection2.fields[key];
506
+ if (field?.type === "relation") {
507
+ const relationType = field.db.relationType ?? "many-to-one";
508
+ const isMulti = relationType === "many-to-many" || relationType === "one-to-many";
509
+ if (Array.isArray(value)) {
510
+ const ids = value.filter((id) => typeof id === "string" && id.length > 0);
511
+ transformed[key] = isUpdate ? { set: ids.map((id) => ({ id })) } : { connect: ids.map((id) => ({ id })) };
512
+ } else if (!isMulti && typeof value === "string" && value.length > 0) {
513
+ transformed[key] = { connect: { id: value } };
514
+ } else if ((value === null || value === "") && isUpdate) {
515
+ transformed[key] = { disconnect: true };
516
+ } else if (value === null || value === "") {
517
+ delete transformed[key];
518
+ }
519
+ }
520
+ }
521
+ return transformed;
522
+ }
523
+ }
524
+
525
+ // src/api/index.ts
526
+ init_agent_auth();
527
+
528
+ // src/api/next.ts
529
+ init_agent_auth();
530
+ function jsonResponse(data, status = 200) {
531
+ return new Response(JSON.stringify(data), {
532
+ status,
533
+ headers: { "Content-Type": "application/json" }
534
+ });
535
+ }
536
+ function getBody(request) {
537
+ return request.json().catch(() => ({}));
538
+ }
539
+ function createRouteHandlers({ config, db, auth: auth2 }) {
540
+ const getSecurity = async (request) => {
541
+ const authHeader = request.headers.get("authorization");
542
+ if (authHeader?.startsWith("Bearer ")) {
543
+ const token = authHeader.split(" ")[1];
544
+ try {
545
+ const agent = verifyAgentToken(token);
546
+ return {
547
+ source: "agent",
548
+ agent,
549
+ user: { id: agent.sub, role: agent.role }
550
+ };
551
+ } catch (e) {
552
+ console.error(`[Auth] Agent Verification Failed: ${e.message}`);
553
+ }
554
+ }
555
+ if (auth2) {
556
+ try {
557
+ const session = await auth2();
558
+ if (session?.user) {
559
+ return {
560
+ source: "user",
561
+ user: {
562
+ id: session.user.id || session.user.email,
563
+ role: session.role || "user"
564
+ }
565
+ };
566
+ }
567
+ } catch (e) {
568
+ console.error(`[Auth] Session Retrieval Failed: ${e.message}`);
569
+ }
570
+ }
571
+ return { source: "user" };
572
+ };
573
+ function generateHandlers(collectionName) {
574
+ const collection2 = config.collections.find((c) => c.name === collectionName);
575
+ if (!collection2) {
576
+ throw new Error(`Collection "${collectionName}" not found in config`);
577
+ }
578
+ return {
579
+ GET: async (request, context) => {
580
+ const security = await getSecurity(request);
581
+ const controller = createController(collection2, db, security);
582
+ const params = await context.params;
583
+ if (params.id) {
584
+ const result2 = await controller.get(params.id);
585
+ if (!result2) return jsonResponse({ error: "Not found" }, 404);
586
+ return jsonResponse(result2);
587
+ }
588
+ const url = new URL(request.url);
589
+ const page = parseInt(url.searchParams.get("page") ?? "1");
590
+ const pageSize = parseInt(url.searchParams.get("pageSize") ?? "10");
591
+ const search = url.searchParams.get("search") ?? void 0;
592
+ const result = await controller.list({ page, pageSize, search });
593
+ return jsonResponse(result);
594
+ },
595
+ POST: async (request) => {
596
+ const security = await getSecurity(request);
597
+ const controller = createController(collection2, db, security);
598
+ const body = await getBody(request);
599
+ try {
600
+ const result = await controller.create(body);
601
+ return jsonResponse(result, 201);
602
+ } catch (err) {
603
+ const error = err;
604
+ if (error.name === "ZodError") {
605
+ const issues = error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join(", ");
606
+ return jsonResponse({ error: `Validation failed: ${issues}` }, 400);
607
+ }
608
+ return jsonResponse({ error: error.message }, 400);
609
+ }
610
+ },
611
+ PATCH: async (request, context) => {
612
+ const security = await getSecurity(request);
613
+ const controller = createController(collection2, db, security);
614
+ const params = await context.params;
615
+ if (!params.id) return jsonResponse({ error: "ID required" }, 400);
616
+ const body = await getBody(request);
617
+ try {
618
+ const result = await controller.update(params.id, body);
619
+ return jsonResponse(result);
620
+ } catch (err) {
621
+ const error = err;
622
+ if (error.name === "ZodError") {
623
+ const issues = error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join(", ");
624
+ return jsonResponse({ error: `Validation failed: ${issues}` }, 400);
625
+ }
626
+ return jsonResponse({ error: error.message }, 400);
627
+ }
628
+ },
629
+ DELETE: async (request, context) => {
630
+ const security = await getSecurity(request);
631
+ const controller = createController(collection2, db, security);
632
+ const params = await context.params;
633
+ if (!params.id) return jsonResponse({ error: "ID required" }, 400);
634
+ try {
635
+ const result = await controller.delete(params.id);
636
+ return jsonResponse(result);
637
+ } catch (err) {
638
+ const error = err;
639
+ return jsonResponse({ error: error.message }, 400);
640
+ }
641
+ }
642
+ };
643
+ }
644
+ return { generateHandlers };
645
+ }
646
+ function createAdminForgeApi({ config, db, auth: auth2 }) {
647
+ const { generateHandlers } = createRouteHandlers({ config, db, auth: auth2 });
648
+ const getCollectionAndId = (slug2) => {
649
+ const [collectionName, id] = slug2;
650
+ const handlers = generateHandlers(collectionName);
651
+ return { handlers, id };
652
+ };
653
+ return {
654
+ async GET(request, { params }) {
655
+ try {
656
+ const resolvedParams = await params;
657
+ const slug2 = resolvedParams.slug || resolvedParams.admin || Object.values(resolvedParams)[0];
658
+ if (slug2.includes("_config")) {
659
+ if (config.auth?.enabled && auth2) {
660
+ try {
661
+ const session = await auth2();
662
+ if (!session?.user) {
663
+ return jsonResponse({ error: "Unauthorized" }, 401);
664
+ }
665
+ } catch {
666
+ return jsonResponse({ error: "Unauthorized" }, 401);
667
+ }
668
+ }
669
+ const { serializeConfig: serializeConfig2 } = await Promise.resolve().then(() => (init_core(), core_exports));
670
+ return jsonResponse(serializeConfig2(config));
671
+ }
672
+ const { handlers, id } = getCollectionAndId(slug2);
673
+ return handlers.GET(request, { params: Promise.resolve({ id: id || "" }) });
674
+ } catch (err) {
675
+ return jsonResponse({ error: err.message }, 404);
676
+ }
677
+ },
678
+ async POST(request, { params }) {
679
+ try {
680
+ const resolvedParams = await params;
681
+ const slug2 = resolvedParams.slug || resolvedParams.admin || Object.values(resolvedParams)[0];
682
+ if (slug2[0] === "_media") {
683
+ const formData = await request.formData();
684
+ const file = formData.get("file");
685
+ if (!file) return jsonResponse({ error: "No file uploaded" }, 400);
686
+ const bytes = await file.arrayBuffer();
687
+ const buffer = Buffer.from(bytes);
688
+ const path = await import("path");
689
+ const fs = await import("fs/promises");
690
+ const uploadDir = path.join(process.cwd(), "public", "uploads");
691
+ try {
692
+ await fs.mkdir(uploadDir, { recursive: true });
693
+ } catch {
694
+ }
695
+ const filename = `${Date.now()}-${file.name.replace(/\s+/g, "-")}`;
696
+ const filePath = path.join(uploadDir, filename);
697
+ await fs.writeFile(filePath, buffer);
698
+ return jsonResponse({
699
+ url: `/uploads/${filename}`,
700
+ filename
701
+ }, 201);
702
+ }
703
+ if (slug2[0] === "_tokens") {
704
+ const { generateAgentToken: generateAgentToken2 } = await Promise.resolve().then(() => (init_agent_auth(), agent_auth_exports));
705
+ const body = await request.json();
706
+ const { scope, expiresIn = 600 } = body;
707
+ if (!Array.isArray(scope)) {
708
+ return jsonResponse({ error: "Scope must be an array" }, 400);
709
+ }
710
+ for (const s of scope) {
711
+ const [collection2, action] = s.split(":");
712
+ const exists = config.collections.find((c) => c.name === collection2);
713
+ if (!exists) return jsonResponse({ error: `Invalid collection: ${collection2}` }, 400);
714
+ if (!["create", "read", "update", "delete"].includes(action)) {
715
+ return jsonResponse({ error: `Invalid action: ${action}` }, 400);
716
+ }
717
+ }
718
+ let userId = "admin";
719
+ let role = "admin";
720
+ if (auth2) {
721
+ const session = await auth2();
722
+ if (session?.user) {
723
+ userId = session.user.id || session.user.email;
724
+ role = session.role || "admin";
725
+ }
726
+ }
727
+ const token = generateAgentToken2(userId, role, scope, expiresIn);
728
+ return jsonResponse({ token });
729
+ }
730
+ const { handlers } = getCollectionAndId(slug2);
731
+ return handlers.POST(request);
732
+ } catch (err) {
733
+ return jsonResponse({ error: err.message }, 404);
734
+ }
735
+ },
736
+ async PATCH(request, { params }) {
737
+ try {
738
+ const resolvedParams = await params;
739
+ const slug2 = resolvedParams.slug || resolvedParams.admin || Object.values(resolvedParams)[0];
740
+ const { handlers, id } = getCollectionAndId(slug2);
741
+ return handlers.PATCH(request, { params: Promise.resolve({ id: id || "" }) });
742
+ } catch (err) {
743
+ return jsonResponse({ error: err.message }, 404);
744
+ }
745
+ },
746
+ async DELETE(request, { params }) {
747
+ try {
748
+ const resolvedParams = await params;
749
+ const slug2 = resolvedParams.slug || resolvedParams.admin || Object.values(resolvedParams)[0];
750
+ const { handlers, id } = getCollectionAndId(slug2);
751
+ return handlers.DELETE(request, { params: Promise.resolve({ id: id || "" }) });
752
+ } catch (err) {
753
+ return jsonResponse({ error: err.message }, 404);
754
+ }
755
+ }
756
+ };
757
+ }
758
+
759
+ // src/auth/config.ts
760
+ function createAuthConfig(options) {
761
+ return {
762
+ ...options,
763
+ providers: ["credentials"]
764
+ };
765
+ }
766
+ var auth = {
767
+ providers: {
768
+ credentials: {
769
+ id: "credentials",
770
+ name: "Credentials"
771
+ }
772
+ }
773
+ };
774
+
775
+ // src/auth/middleware.ts
776
+ function adminMiddleware(handler) {
777
+ return async (request) => {
778
+ const sessionCookie = request.headers.get("cookie") ?? "";
779
+ const hasSession = sessionCookie.includes("next-auth.session-token");
780
+ if (!hasSession) {
781
+ return new Response(JSON.stringify({ error: "Unauthorized" }), {
782
+ status: 401,
783
+ headers: { "Content-Type": "application/json" }
784
+ });
785
+ }
786
+ return handler(request);
787
+ };
788
+ }
789
+ export {
790
+ adminMiddleware,
791
+ assertScope,
792
+ auth,
793
+ createAdminForgeApi,
794
+ createAuthConfig,
795
+ createController,
796
+ createRouteHandlers,
797
+ generateAgentToken,
798
+ verifyAgentToken
799
+ };
800
+ //# sourceMappingURL=next.js.map