@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/package.json ADDED
@@ -0,0 +1,99 @@
1
+ {
2
+ "name": "@adminforge/core",
3
+ "version": "0.3.1",
4
+ "license": "MIT",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/YuZaGa/AdminForge.git",
11
+ "directory": "packages/adminforge"
12
+ },
13
+ "type": "module",
14
+ "bin": {
15
+ "adminforge": "./bin/adminforge.js"
16
+ },
17
+ "main": "./dist/index.js",
18
+ "types": "./dist/index.d.ts",
19
+ "exports": {
20
+ ".": {
21
+ "types": "./dist/index.d.ts",
22
+ "import": "./dist/index.js",
23
+ "require": "./dist/index.cjs"
24
+ },
25
+ "./ui": {
26
+ "types": "./dist/ui.d.ts",
27
+ "import": "./dist/ui.js",
28
+ "require": "./dist/ui.cjs"
29
+ },
30
+ "./next": {
31
+ "types": "./dist/next.d.ts",
32
+ "import": "./dist/next.js",
33
+ "require": "./dist/next.cjs"
34
+ },
35
+ "./auth": {
36
+ "types": "./dist/auth.d.ts",
37
+ "import": "./dist/auth.js",
38
+ "require": "./dist/auth.cjs"
39
+ },
40
+ "./auth-client": {
41
+ "types": "./dist/auth-client.d.ts",
42
+ "import": "./dist/auth-client.js",
43
+ "require": "./dist/auth-client.cjs"
44
+ },
45
+ "./styles": "./dist/styles.css"
46
+ },
47
+ "dependencies": {
48
+ "@prisma/client": "^6.0.0",
49
+ "@tanstack/react-table": "^8.20.0",
50
+ "@tiptap/core": "3.23.1",
51
+ "@tiptap/extension-bubble-menu": "3.23.1",
52
+ "@tiptap/extension-floating-menu": "3.23.1",
53
+ "@tiptap/extension-highlight": "3.23.1",
54
+ "@tiptap/extension-horizontal-rule": "3.23.1",
55
+ "@tiptap/extension-image": "3.23.1",
56
+ "@tiptap/extension-link": "3.23.1",
57
+ "@tiptap/extension-placeholder": "3.23.1",
58
+ "@tiptap/extension-subscript": "3.23.1",
59
+ "@tiptap/extension-superscript": "3.23.1",
60
+ "@tiptap/extension-task-item": "3.23.1",
61
+ "@tiptap/extension-task-list": "3.23.1",
62
+ "@tiptap/extension-text-align": "3.23.1",
63
+ "@tiptap/extension-typography": "3.23.1",
64
+ "@tiptap/extension-underline": "3.23.1",
65
+ "@tiptap/pm": "3.23.1",
66
+ "@tiptap/react": "3.23.1",
67
+ "@tiptap/starter-kit": "3.23.1",
68
+ "jsonwebtoken": "^9.0.3",
69
+ "next": "^15.0.0",
70
+ "next-auth": "^5.0.0-beta.25",
71
+ "react": "^19.0.0",
72
+ "react-dom": "^19.0.0",
73
+ "zod": "^3.24.0"
74
+ },
75
+ "devDependencies": {
76
+ "@eslint/js": "^10.0.1",
77
+ "@types/jsonwebtoken": "^9.0.10",
78
+ "@types/node": "^22.10.1",
79
+ "@types/react": "^19.0.0",
80
+ "@types/react-dom": "^19.0.0",
81
+ "@typescript-eslint/eslint-plugin": "^8.59.3",
82
+ "@typescript-eslint/parser": "^8.59.3",
83
+ "eslint": "^10.3.0",
84
+ "eslint-plugin-react": "^7.37.5",
85
+ "eslint-plugin-react-hooks": "^7.1.1",
86
+ "globals": "^17.6.0",
87
+ "prisma": "^6.0.0",
88
+ "tsup": "^8.3.0",
89
+ "typescript": "^5.7.0",
90
+ "typescript-eslint": "^8.59.3"
91
+ },
92
+ "scripts": {
93
+ "build": "tsup",
94
+ "dev": "DEV=true tsup --watch",
95
+ "lint": "eslint .",
96
+ "typecheck": "tsc --noEmit",
97
+ "clean": "rm -rf dist"
98
+ }
99
+ }
@@ -0,0 +1,234 @@
1
+ import type { AdminForgeConfig, CollectionDefinition, AccessConfig } from "../core";
2
+ import type { DbClient } from "../db";
3
+ import { z } from "zod";
4
+ import { assertScope, type SecurityContext, type Action } from "./security/agent-auth";
5
+
6
+ function buildValidationSchema(collection: CollectionDefinition): z.ZodObject<Record<string, z.ZodTypeAny>> {
7
+ const shape: Record<string, z.ZodTypeAny> = {};
8
+ for (const [name, field] of Object.entries(collection.fields)) {
9
+ shape[name] = field.validation;
10
+ }
11
+ return z.object(shape);
12
+ }
13
+
14
+ interface AdminSession {
15
+ user: { id: string; email: string; role?: string } | null;
16
+ role?: string; // Support for root-level role
17
+ }
18
+
19
+ function hasAccess(access: AccessConfig | undefined, operation: string, role?: string): boolean {
20
+ if (!access) return true;
21
+ const allowed = access[operation as keyof AccessConfig];
22
+ if (!allowed || !Array.isArray(allowed)) return true;
23
+ if (!role) return false;
24
+ return allowed.includes(role);
25
+ }
26
+
27
+ export type AccessInfo = SecurityContext;
28
+
29
+ export interface Controller {
30
+ list: (args?: { where?: Record<string, unknown>; orderBy?: Record<string, string>; page?: number; pageSize?: number; search?: string }) => Promise<{ data: unknown[]; total: number; page: number; pageSize: number }>;
31
+ get: (id: string) => Promise<unknown | null>;
32
+ create: (data: Record<string, unknown>) => Promise<unknown>;
33
+ update: (id: string, data: Record<string, unknown>) => Promise<unknown>;
34
+ delete: (id: string) => Promise<unknown>;
35
+ }
36
+
37
+ export function createController(
38
+ collection: CollectionDefinition,
39
+ db: DbClient,
40
+ session?: SecurityContext,
41
+ ): Controller {
42
+ const validationSchema = buildValidationSchema(collection);
43
+ const role = session?.user?.role || session?.agent?.role;
44
+ const actorId = session?.user?.id || session?.agent?.sub;
45
+
46
+ function requireAccess(operation: Action) {
47
+ console.error(`[ACL] Operation: ${operation}, Collection: ${collection.name}, Role: ${role}, Source: ${session?.source}`);
48
+
49
+ // 1. Scope Enforcement (limiter)
50
+ if (session?.agent) {
51
+ assertScope(session.agent, collection.name, operation);
52
+ }
53
+
54
+ // 2. RBAC Enforcement (authority)
55
+ if (!hasAccess(collection.access, operation, role)) {
56
+ console.warn(`[ACL] DENIED: ${role} not allowed to ${operation} on ${collection.name}. Allowed:`, collection.access?.[operation]);
57
+ throw new Error(`Access denied: ${operation} on ${collection.name}`);
58
+ }
59
+
60
+ // 3. Audit Logging
61
+ console.error(JSON.stringify({
62
+ timestamp: new Date().toISOString(),
63
+ type: "mutation_attempt",
64
+ source: session?.source || "unknown",
65
+ userId: actorId || "anonymous",
66
+ role: role || "none",
67
+ collection: collection.name,
68
+ action: operation,
69
+ sessionId: session?.agent?.sessionId,
70
+ }));
71
+ }
72
+
73
+ function buildSearchWhere(search?: string): Record<string, unknown> | undefined {
74
+ if (!search) return undefined;
75
+
76
+ // Find all text-based fields to search in
77
+ const searchFields = Object.entries(collection.fields)
78
+ .filter(([_, field]) => field.type === "text" || field.type === "slug" || field.type === "richText")
79
+ .map(([name]) => name);
80
+
81
+ if (searchFields.length === 0) return undefined;
82
+
83
+ return {
84
+ OR: searchFields.map(field => ({
85
+ [field]: { contains: search, mode: 'insensitive' }
86
+ }))
87
+ };
88
+ }
89
+
90
+ function filterFields(fields: Record<string, unknown>): Record<string, unknown> {
91
+ const filtered: Record<string, unknown> = {};
92
+ for (const [key, value] of Object.entries(fields)) {
93
+ const field = collection.fields[key];
94
+ if (hasAccess(field?.access, "read", role)) {
95
+ filtered[key] = value;
96
+ }
97
+ }
98
+ return filtered;
99
+ }
100
+
101
+ function filterWritableFields(
102
+ fields: Record<string, unknown>,
103
+ operation: "create" | "update"
104
+ ): Record<string, unknown> {
105
+ const filtered: Record<string, unknown> = {};
106
+ for (const [key, value] of Object.entries(fields)) {
107
+ const field = collection.fields[key];
108
+ if (field && hasAccess(field.access, operation, role)) {
109
+ filtered[key] = value;
110
+ }
111
+ }
112
+ return filtered;
113
+ }
114
+
115
+ function applyFieldHooks(
116
+ fields: Record<string, unknown>,
117
+ hookName: "beforeValidate" | "beforeSave"
118
+ ): Record<string, unknown> {
119
+ const next = { ...fields };
120
+ for (const [key, value] of Object.entries(next)) {
121
+ const hook = collection.fields[key]?.hooks?.[hookName];
122
+ if (hook) {
123
+ next[key] = hook(value);
124
+ }
125
+ }
126
+ return next;
127
+ }
128
+
129
+ function applyDerivedFields(fields: Record<string, unknown>): Record<string, unknown> {
130
+ const next = { ...fields };
131
+ for (const [name, field] of Object.entries(collection.fields)) {
132
+ if (field.type !== "slug") continue;
133
+ const current = next[name];
134
+ if (typeof current === "string" && current.trim().length > 0) continue;
135
+ const from = field.ui.props?.from;
136
+ if (typeof from !== "string") continue;
137
+ const source = next[from];
138
+ if (typeof source !== "string" || source.trim().length === 0) continue;
139
+ const slugified = source
140
+ .toLowerCase()
141
+ .normalize("NFD")
142
+ .replace(/[\u0300-\u036f]/g, "") // Remove accents
143
+ .replace(/\s+/g, "-") // Replace spaces with -
144
+ .replace(/[^a-z0-9-]/g, "") // Remove non-alphanumeric
145
+ .replace(/-+/g, "-") // Remove double dashes
146
+ .replace(/^-+|-+$/g, ""); // Trim dashes
147
+
148
+ next[name] = slugified;
149
+ }
150
+ return next;
151
+ }
152
+
153
+ return {
154
+ async list(args = {}) {
155
+ requireAccess("read");
156
+ const { where, orderBy, page = 1, pageSize = 50, search } = args;
157
+
158
+ const searchWhere = buildSearchWhere(search);
159
+ const combinedWhere = searchWhere ? { ...where, ...searchWhere } : where;
160
+
161
+ const skip = (page - 1) * pageSize;
162
+
163
+ // Default sorting to newest first.
164
+ // We check for 'createdAt' but default to 'id' desc as a safe fallback for all DBs.
165
+ const effectiveOrderBy = orderBy || (collection.fields.createdAt ? { createdAt: 'desc' } : { id: 'desc' });
166
+
167
+ const [raw, total] = await Promise.all([
168
+ db.findMany(collection.name, { where: combinedWhere, orderBy: effectiveOrderBy, skip, take: pageSize }),
169
+ db.count(collection.name, { where: combinedWhere })
170
+ ]);
171
+
172
+ const data = (raw as Record<string, unknown>[]).map(filterFields);
173
+ return { data, total, page, pageSize };
174
+ },
175
+
176
+ async get(id: string) {
177
+ requireAccess("read");
178
+ const raw = await db.findUnique(collection.name, id);
179
+ if (!raw) return null;
180
+ return filterFields(raw as Record<string, unknown>);
181
+ },
182
+
183
+ async create(data: Record<string, unknown>) {
184
+ requireAccess("create");
185
+ const allowed = filterWritableFields(data, "create");
186
+ const withDerived = applyDerivedFields(allowed);
187
+ const beforeValidate = applyFieldHooks(withDerived, "beforeValidate");
188
+ const parsed = validationSchema.parse(beforeValidate);
189
+ const beforeSave = applyFieldHooks(parsed, "beforeSave");
190
+ const transformed = transformRelations(beforeSave, false);
191
+ return db.create(collection.name, transformed);
192
+ },
193
+
194
+ async update(id: string, data: Record<string, unknown>) {
195
+ requireAccess("update");
196
+ const allowed = filterWritableFields(data, "update");
197
+ const beforeValidate = applyFieldHooks(allowed, "beforeValidate");
198
+ const parsed = validationSchema.partial().parse(beforeValidate);
199
+ const beforeSave = applyFieldHooks(parsed, "beforeSave");
200
+ const transformed = transformRelations(beforeSave, true);
201
+ return db.update(collection.name, id, transformed);
202
+ },
203
+
204
+ async delete(id: string) {
205
+ requireAccess("delete");
206
+ return db.delete(collection.name, id);
207
+ },
208
+ };
209
+
210
+ function transformRelations(data: Record<string, unknown>, isUpdate: boolean) {
211
+ const transformed = { ...data };
212
+ for (const [key, value] of Object.entries(transformed)) {
213
+ const field = collection.fields[key];
214
+ if (field?.type === "relation") {
215
+ const relationType = field.db.relationType ?? "many-to-one";
216
+ const isMulti = relationType === "many-to-many" || relationType === "one-to-many";
217
+
218
+ if (Array.isArray(value)) {
219
+ const ids = value.filter((id): id is string => typeof id === "string" && id.length > 0);
220
+ transformed[key] = isUpdate
221
+ ? { set: ids.map((id) => ({ id })) }
222
+ : { connect: ids.map((id) => ({ id })) };
223
+ } else if (!isMulti && typeof value === "string" && value.length > 0) {
224
+ transformed[key] = { connect: { id: value } };
225
+ } else if ((value === null || value === "") && isUpdate) {
226
+ transformed[key] = { disconnect: true };
227
+ } else if (value === null || value === "") {
228
+ delete transformed[key];
229
+ }
230
+ }
231
+ }
232
+ return transformed;
233
+ }
234
+ }
@@ -0,0 +1,4 @@
1
+ export { createController } from "./controller.js";
2
+ export type { Controller } from "./controller.js";
3
+ export * from "./security/agent-auth.js";
4
+
@@ -0,0 +1,281 @@
1
+ import type { AdminForgeConfig, CollectionDefinition } from "../core";
2
+ import type { DbClient } from "../db";
3
+ import { createController } from "./controller.js";
4
+ import { verifyAgentToken, type SecurityContext } from "./security/agent-auth.js";
5
+
6
+ interface RouteContext {
7
+ params: Promise<Record<string, string>>;
8
+ }
9
+
10
+ function jsonResponse(data: unknown, status = 200): Response {
11
+ return new Response(JSON.stringify(data), {
12
+ status,
13
+ headers: { "Content-Type": "application/json" },
14
+ });
15
+ }
16
+
17
+ function getBody(request: Request): Promise<Record<string, unknown>> {
18
+ return request.json().catch(() => ({}));
19
+ }
20
+
21
+ interface RouteParams {
22
+ config: AdminForgeConfig;
23
+ db: DbClient;
24
+ auth?: any; // The NextAuth auth() function
25
+ }
26
+
27
+ export function createRouteHandlers({ config, db, auth }: RouteParams) {
28
+ // We'll create controllers per request to inject the correct security context
29
+ const getSecurity = async (request: Request): Promise<SecurityContext> => {
30
+ const authHeader = request.headers.get("authorization");
31
+
32
+ if (authHeader?.startsWith("Bearer ")) {
33
+ const token = authHeader.split(" ")[1];
34
+ try {
35
+ const agent = verifyAgentToken(token);
36
+ return {
37
+ source: "agent",
38
+ agent,
39
+ user: { id: agent.sub, role: agent.role }
40
+ };
41
+ } catch (e: any) {
42
+ console.error(`[Auth] Agent Verification Failed: ${e.message}`);
43
+ }
44
+ }
45
+
46
+ // Try to get session via NextAuth if provided
47
+ if (auth) {
48
+ try {
49
+ const session = await auth();
50
+ if (session?.user) {
51
+ return {
52
+ source: "user",
53
+ user: {
54
+ id: session.user.id || session.user.email,
55
+ role: (session as any).role || "user"
56
+ }
57
+ };
58
+ }
59
+ } catch (e: any) {
60
+ console.error(`[Auth] Session Retrieval Failed: ${e.message}`);
61
+ }
62
+ }
63
+
64
+ return { source: "user" };
65
+ };
66
+
67
+ function generateHandlers(collectionName: string) {
68
+ const collection = config.collections.find((c) => c.name === collectionName);
69
+ if (!collection) {
70
+ throw new Error(`Collection "${collectionName}" not found in config`);
71
+ }
72
+
73
+ return {
74
+ GET: async (request: Request, context: RouteContext) => {
75
+ const security = await getSecurity(request);
76
+ const controller = createController(collection, db, security);
77
+ const params = await context.params;
78
+ if (params.id) {
79
+ const result = await controller.get(params.id);
80
+ if (!result) return jsonResponse({ error: "Not found" }, 404);
81
+ return jsonResponse(result);
82
+ }
83
+ const url = new URL(request.url);
84
+ const page = parseInt(url.searchParams.get("page") ?? "1");
85
+ const pageSize = parseInt(url.searchParams.get("pageSize") ?? "10");
86
+ const search = url.searchParams.get("search") ?? undefined;
87
+ const result = await controller.list({ page, pageSize, search });
88
+ return jsonResponse(result);
89
+ },
90
+
91
+ POST: async (request: Request) => {
92
+ const security = await getSecurity(request);
93
+ const controller = createController(collection, db, security);
94
+ const body = await getBody(request);
95
+ try {
96
+ const result = await controller.create(body);
97
+ return jsonResponse(result, 201);
98
+ } catch (err) {
99
+ const error = err as any;
100
+ if (error.name === "ZodError") {
101
+ const issues = error.issues.map((i: any) => `${i.path.join(".")}: ${i.message}`).join(", ");
102
+ return jsonResponse({ error: `Validation failed: ${issues}` }, 400);
103
+ }
104
+ return jsonResponse({ error: error.message }, 400);
105
+ }
106
+ },
107
+
108
+ PATCH: async (request: Request, context: RouteContext) => {
109
+ const security = await getSecurity(request);
110
+ const controller = createController(collection, db, security);
111
+ const params = await context.params;
112
+ if (!params.id) return jsonResponse({ error: "ID required" }, 400);
113
+ const body = await getBody(request);
114
+ try {
115
+ const result = await controller.update(params.id, body);
116
+ return jsonResponse(result);
117
+ } catch (err) {
118
+ const error = err as any;
119
+ if (error.name === "ZodError") {
120
+ const issues = error.issues.map((i: any) => `${i.path.join(".")}: ${i.message}`).join(", ");
121
+ return jsonResponse({ error: `Validation failed: ${issues}` }, 400);
122
+ }
123
+ return jsonResponse({ error: error.message }, 400);
124
+ }
125
+ },
126
+
127
+ DELETE: async (request: Request, context: RouteContext) => {
128
+ const security = await getSecurity(request);
129
+ const controller = createController(collection, db, security);
130
+ const params = await context.params;
131
+ if (!params.id) return jsonResponse({ error: "ID required" }, 400);
132
+ try {
133
+ const result = await controller.delete(params.id);
134
+ return jsonResponse(result);
135
+ } catch (err) {
136
+ const error = err as Error;
137
+ return jsonResponse({ error: error.message }, 400);
138
+ }
139
+ },
140
+ };
141
+ }
142
+
143
+ return { generateHandlers };
144
+ }
145
+
146
+ export function createAdminForgeApi({ config, db, auth }: RouteParams) {
147
+ const { generateHandlers } = createRouteHandlers({ config, db, auth });
148
+
149
+ const getCollectionAndId = (slug: string[]) => {
150
+ const [collectionName, id] = slug;
151
+ const handlers = generateHandlers(collectionName);
152
+ return { handlers, id };
153
+ };
154
+
155
+ return {
156
+ async GET(request: Request, { params }: { params: Promise<any> }) {
157
+ try {
158
+ const resolvedParams = await params;
159
+ const slug = resolvedParams.slug || resolvedParams.admin || Object.values(resolvedParams)[0] as string[];
160
+
161
+ // Return serialized config for the UI
162
+ // Detect _config anywhere in the slug to support various mounting points
163
+ if (slug.includes("_config")) {
164
+ if (config.auth?.enabled && auth) {
165
+ try {
166
+ const session = await auth();
167
+ if (!session?.user) {
168
+ return jsonResponse({ error: "Unauthorized" }, 401);
169
+ }
170
+ } catch {
171
+ return jsonResponse({ error: "Unauthorized" }, 401);
172
+ }
173
+ }
174
+ const { serializeConfig } = await import("../core/index.js");
175
+ return jsonResponse(serializeConfig(config));
176
+ }
177
+
178
+ const { handlers, id } = getCollectionAndId(slug);
179
+ return handlers.GET(request, { params: Promise.resolve({ id: id || "" }) });
180
+ } catch (err) {
181
+ return jsonResponse({ error: (err as Error).message }, 404);
182
+ }
183
+ },
184
+
185
+ async POST(request: Request, { params }: { params: Promise<any> }) {
186
+ try {
187
+ const resolvedParams = await params;
188
+ const slug = resolvedParams.slug || resolvedParams.admin || Object.values(resolvedParams)[0] as string[];
189
+
190
+ // Handle Media Uploads
191
+ if (slug[0] === "_media") {
192
+ const formData = await request.formData();
193
+ const file = formData.get("file") as File;
194
+ if (!file) return jsonResponse({ error: "No file uploaded" }, 400);
195
+
196
+ const bytes = await file.arrayBuffer();
197
+ const buffer = Buffer.from(bytes);
198
+
199
+ const path = await import("path");
200
+ const fs = await import("fs/promises");
201
+ const uploadDir = path.join(process.cwd(), "public", "uploads");
202
+
203
+ try {
204
+ await fs.mkdir(uploadDir, { recursive: true });
205
+ } catch {}
206
+
207
+ const filename = `${Date.now()}-${file.name.replace(/\s+/g, "-")}`;
208
+ const filePath = path.join(uploadDir, filename);
209
+ await fs.writeFile(filePath, buffer);
210
+
211
+ return jsonResponse({
212
+ url: `/uploads/${filename}`,
213
+ filename
214
+ }, 201);
215
+ }
216
+
217
+ // Handle Agent Token Generation
218
+ if (slug[0] === "_tokens") {
219
+ const { generateAgentToken } = await import("./security/agent-auth.js");
220
+ const body = await request.json();
221
+ const { scope, expiresIn = 600 } = body;
222
+
223
+ if (!Array.isArray(scope)) {
224
+ return jsonResponse({ error: "Scope must be an array" }, 400);
225
+ }
226
+
227
+ // Validation: Ensure collections exist
228
+ for (const s of scope) {
229
+ const [collection, action] = s.split(":");
230
+ const exists = config.collections.find(c => c.name === collection);
231
+ if (!exists) return jsonResponse({ error: `Invalid collection: ${collection}` }, 400);
232
+ if (!["create", "read", "update", "delete"].includes(action)) {
233
+ return jsonResponse({ error: `Invalid action: ${action}` }, 400);
234
+ }
235
+ }
236
+
237
+ // Auth: Get session
238
+ let userId = "admin";
239
+ let role = "admin";
240
+ if (auth) {
241
+ const session = await auth();
242
+ if (session?.user) {
243
+ userId = session.user.id || session.user.email;
244
+ role = (session as any).role || "admin";
245
+ }
246
+ }
247
+
248
+ const token = generateAgentToken(userId, role, scope, expiresIn);
249
+ return jsonResponse({ token });
250
+ }
251
+
252
+ const { handlers } = getCollectionAndId(slug);
253
+ return handlers.POST(request);
254
+ } catch (err) {
255
+ return jsonResponse({ error: (err as Error).message }, 404);
256
+ }
257
+ },
258
+
259
+ async PATCH(request: Request, { params }: { params: Promise<any> }) {
260
+ try {
261
+ const resolvedParams = await params;
262
+ const slug = resolvedParams.slug || resolvedParams.admin || Object.values(resolvedParams)[0] as string[];
263
+ const { handlers, id } = getCollectionAndId(slug);
264
+ return handlers.PATCH(request, { params: Promise.resolve({ id: id || "" }) });
265
+ } catch (err) {
266
+ return jsonResponse({ error: (err as Error).message }, 404);
267
+ }
268
+ },
269
+
270
+ async DELETE(request: Request, { params }: { params: Promise<any> }) {
271
+ try {
272
+ const resolvedParams = await params;
273
+ const slug = resolvedParams.slug || resolvedParams.admin || Object.values(resolvedParams)[0] as string[];
274
+ const { handlers, id } = getCollectionAndId(slug);
275
+ return handlers.DELETE(request, { params: Promise.resolve({ id: id || "" }) });
276
+ } catch (err) {
277
+ return jsonResponse({ error: (err as Error).message }, 404);
278
+ }
279
+ },
280
+ };
281
+ }