@aotter/mantle-cloudflare 0.0.11-alpha.73 → 0.1.0-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -5
- package/dist/auth/createAuth.d.ts +22 -10
- package/dist/auth/createAuth.d.ts.map +1 -1
- package/dist/auth/createAuth.js +69 -1
- package/dist/auth/createAuth.js.map +1 -1
- package/dist/auth/rejectCrossOriginMutation.d.ts +2 -0
- package/dist/auth/rejectCrossOriginMutation.d.ts.map +1 -0
- package/dist/auth/rejectCrossOriginMutation.js +15 -0
- package/dist/auth/rejectCrossOriginMutation.js.map +1 -0
- package/dist/bindings/D1DatabaseDriver.js +21 -13
- package/dist/bindings/D1DatabaseDriver.js.map +1 -1
- package/dist/bindings/conventionalBindings.d.ts +0 -1
- package/dist/bindings/conventionalBindings.d.ts.map +1 -1
- package/dist/bindings/conventionalBindings.js +0 -4
- package/dist/bindings/conventionalBindings.js.map +1 -1
- package/dist/bindings/index.d.ts +0 -1
- package/dist/bindings/index.d.ts.map +1 -1
- package/dist/bindings/index.js +0 -1
- package/dist/bindings/index.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/mount/bootRuntimeOnce.d.ts +1 -1
- package/dist/mount/bootRuntimeOnce.d.ts.map +1 -1
- package/dist/mount/bootRuntimeOnce.js +49 -5
- package/dist/mount/bootRuntimeOnce.js.map +1 -1
- package/dist/mount/cmsConfig.d.ts +3 -3
- package/dist/mount/cmsConfig.d.ts.map +1 -1
- package/dist/mount/mountMcp.d.ts +4 -4
- package/dist/mount/mountMcp.d.ts.map +1 -1
- package/dist/mount/mountMcp.js +34 -20
- package/dist/mount/mountMcp.js.map +1 -1
- package/dist/mount/mountPublicRoutes.d.ts +13 -18
- package/dist/mount/mountPublicRoutes.d.ts.map +1 -1
- package/dist/mount/mountPublicRoutes.js +39 -97
- package/dist/mount/mountPublicRoutes.js.map +1 -1
- package/dist/mount/mountServerEndpoints.d.ts +3 -4
- package/dist/mount/mountServerEndpoints.d.ts.map +1 -1
- package/dist/mount/mountServerEndpoints.js +234 -140
- package/dist/mount/mountServerEndpoints.js.map +1 -1
- package/dist/mount/resolveCaller.d.ts +2 -1
- package/dist/mount/resolveCaller.d.ts.map +1 -1
- package/dist/mount/resolveCaller.js +43 -7
- package/dist/mount/resolveCaller.js.map +1 -1
- package/dist/oauth/consentHtml.d.ts +1 -1
- package/dist/oauth/consentHtml.d.ts.map +1 -1
- package/dist/oauth/consentHtml.js +15 -13
- package/dist/oauth/consentHtml.js.map +1 -1
- package/dist/oauth/index.d.ts +1 -1
- package/dist/oauth/index.d.ts.map +1 -1
- package/dist/oauth/index.js.map +1 -1
- package/dist/oauth/mountOAuth.d.ts +0 -7
- package/dist/oauth/mountOAuth.d.ts.map +1 -1
- package/dist/oauth/mountOAuth.js +7 -15
- package/dist/oauth/mountOAuth.js.map +1 -1
- package/dist/oauth/oauthSingleton.d.ts +2 -3
- package/dist/oauth/oauthSingleton.d.ts.map +1 -1
- package/dist/oauth/oauthSingleton.js +6 -0
- package/dist/oauth/oauthSingleton.js.map +1 -1
- package/dist/worker/createMantleWorker.d.ts +4 -2
- package/dist/worker/createMantleWorker.d.ts.map +1 -1
- package/dist/worker/createMantleWorker.js +16 -2
- package/dist/worker/createMantleWorker.js.map +1 -1
- package/package.json +13 -13
- package/dist/bindings/KvCacheBinding.d.ts +0 -14
- package/dist/bindings/KvCacheBinding.d.ts.map +0 -1
- package/dist/bindings/KvCacheBinding.js +0 -27
- package/dist/bindings/KvCacheBinding.js.map +0 -1
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import { DiagnosticError, HTTP_STATUS_BY_CODE, MANTLE_REF_KEYWORD, MCP_HINT_KEYWORD, VIEW_PARAMS_RESERVED, isMediaMcpHint, redactForWire, runtimeDiagnostic, } from "@aotter/mantle-spec";
|
|
1
|
+
import { DiagnosticError, HTTP_STATUS_BY_CODE, MANTLE_REF_KEYWORD, MCP_HINT_KEYWORD, VIEW_PARAMS_RESERVED, isMediaMcpHint, meetsRole, redactForWire, runtimeDiagnostic, checkSchemaListFilter, schemaSortableFields, } from "@aotter/mantle-spec";
|
|
2
2
|
import { ViewParamCoercionError, coerceViewParams, compilePathMatcher, evaluateAuthAll, } from "@aotter/mantle-runtime";
|
|
3
3
|
import { indexHtml } from "@aotter/mantle-admin-ui";
|
|
4
4
|
import { resolveCaller } from "./resolveCaller.js";
|
|
5
5
|
import { runMantleUseCase } from "./runMantleUseCase.js";
|
|
6
|
-
import { STAFF_ROLE_SET } from "../auth/createAuth.js";
|
|
6
|
+
import { decodeMemberCursor, STAFF_ROLE_SET, } from "../auth/createAuth.js";
|
|
7
|
+
import { rejectCrossOriginMutation } from "../auth/rejectCrossOriginMutation.js";
|
|
7
8
|
import { AOTTER_FAVICON_SVG } from "../assets/aotterFavicon.js";
|
|
8
9
|
const [PAGE_PARAM, SHOW_PARAM] = VIEW_PARAMS_RESERVED;
|
|
9
10
|
/** Mount HTTP Triggers + Views + the Better Auth admin surface.
|
|
10
|
-
* HTTP
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* catch-all. */
|
|
11
|
+
* HTTP Triggers resolve configured credentials and sessions through
|
|
12
|
+
* `resolveCaller`; the target Procedure's `requires.auth` owns
|
|
13
|
+
* authorization. */
|
|
14
14
|
export function mountServerEndpoints(app, ref) {
|
|
15
15
|
for (const t of ref.manifests) {
|
|
16
16
|
if (t.kind !== "Trigger")
|
|
@@ -89,10 +89,10 @@ function mountAdminBetterAuth(app, ref, auth) {
|
|
|
89
89
|
"/admin/c/:collection",
|
|
90
90
|
"/admin/c/:collection/:id",
|
|
91
91
|
"/admin/media",
|
|
92
|
-
"/admin/approvals",
|
|
93
92
|
"/admin/preferences",
|
|
94
93
|
"/admin/settings",
|
|
95
94
|
"/admin/staff",
|
|
95
|
+
"/admin/members",
|
|
96
96
|
"/admin/ops",
|
|
97
97
|
"/admin/views/:name",
|
|
98
98
|
]) {
|
|
@@ -146,19 +146,47 @@ function mountAdminBetterAuth(app, ref, auth) {
|
|
|
146
146
|
return body(c, gate);
|
|
147
147
|
});
|
|
148
148
|
};
|
|
149
|
-
|
|
150
|
-
// staff). Role semantics follow StaffRoleHierarchy: only `owner`
|
|
151
|
-
// "manages staff".
|
|
152
|
-
const ownerGuarded = (method, path, body) => {
|
|
149
|
+
const roleGuarded = (method, path, minimumRole, body) => {
|
|
153
150
|
guarded(method, path, (c, gate) => {
|
|
154
|
-
if (gate.role
|
|
155
|
-
return
|
|
151
|
+
if (!meetsRole(gate.role, minimumRole)) {
|
|
152
|
+
return adminInsufficientRole(c, path, minimumRole);
|
|
153
|
+
}
|
|
156
154
|
return body(c, gate);
|
|
157
155
|
});
|
|
158
156
|
};
|
|
159
|
-
guarded("get", "/admin/api/me", (_c, gate) => Response.json({ login: gate.login, role: gate.role, userId: gate.userId }));
|
|
160
|
-
|
|
161
|
-
|
|
157
|
+
guarded("get", "/admin/api/me", (_c, gate) => Response.json({ login: gate.login, role: gate.role, userId: gate.userId, image: gate.image }));
|
|
158
|
+
roleGuarded("get", "/admin/api/staff", "owner", async () => Response.json({ users: await auth.listUsers() }));
|
|
159
|
+
roleGuarded("get", "/admin/api/members", "editor", async (c) => {
|
|
160
|
+
const rawLimit = c.req.query("limit");
|
|
161
|
+
const limit = rawLimit === undefined ? 50 : Number(rawLimit);
|
|
162
|
+
const cursor = c.req.query("cursor") || undefined;
|
|
163
|
+
const search = c.req.query("search")?.trim() || undefined;
|
|
164
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 100 ||
|
|
165
|
+
(cursor && !decodeMemberCursor(cursor)) || (search?.length ?? 0) > 200) {
|
|
166
|
+
return Response.json({
|
|
167
|
+
ok: false,
|
|
168
|
+
diagnostic: runtimeDiagnostic({
|
|
169
|
+
code: "INPUT_VALIDATION_FAILED",
|
|
170
|
+
severity: "error",
|
|
171
|
+
path: "GET /admin/api/members",
|
|
172
|
+
expected: "limit 1..100, a valid cursor, and search up to 200 characters",
|
|
173
|
+
message: "Member list parameters are invalid.",
|
|
174
|
+
}),
|
|
175
|
+
}, { status: 400 });
|
|
176
|
+
}
|
|
177
|
+
const result = await auth.listMembers({
|
|
178
|
+
limit,
|
|
179
|
+
search,
|
|
180
|
+
cursor,
|
|
181
|
+
cursorDirection: c.req.query("cursor_direction") === "backward" ? "backward" : "forward",
|
|
182
|
+
});
|
|
183
|
+
return Response.json({
|
|
184
|
+
items: result.items,
|
|
185
|
+
previous_cursor: result.previousCursor,
|
|
186
|
+
next_cursor: result.nextCursor,
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
roleGuarded("patch", "/admin/api/staff/:id/role", "owner", async (c, gate) => {
|
|
162
190
|
const userId = c.req.param("id") ?? "";
|
|
163
191
|
const body = (await c.req.raw.json().catch(() => ({})));
|
|
164
192
|
const role = body.role === null ? null : typeof body.role === "string" ? body.role : undefined;
|
|
@@ -204,7 +232,7 @@ function mountAdminBetterAuth(app, ref, auth) {
|
|
|
204
232
|
}
|
|
205
233
|
return Response.json({ ok: true });
|
|
206
234
|
});
|
|
207
|
-
|
|
235
|
+
roleGuarded("post", "/admin/api/staff/invitations", "owner", async (c, gate) => {
|
|
208
236
|
const body = (await c.req.raw.json().catch(() => ({})));
|
|
209
237
|
const email = typeof body.email === "string" ? body.email.trim().toLowerCase() : "";
|
|
210
238
|
const role = typeof body.role === "string" ? body.role : "";
|
|
@@ -222,21 +250,23 @@ function mountAdminBetterAuth(app, ref, auth) {
|
|
|
222
250
|
}
|
|
223
251
|
const result = await auth.inviteUser(email, role);
|
|
224
252
|
if (result.kind === "exists") {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
253
|
+
if (result.id === gate.userId) {
|
|
254
|
+
return Response.json({
|
|
255
|
+
ok: false,
|
|
256
|
+
diagnostic: runtimeDiagnostic({
|
|
257
|
+
code: "AUTH_DENIED",
|
|
258
|
+
severity: "error",
|
|
259
|
+
path: "POST /admin/api/staff/invitations",
|
|
260
|
+
expected: "an email other than the caller's",
|
|
261
|
+
message: "You cannot change your own role.",
|
|
262
|
+
}),
|
|
263
|
+
}, { status: 403 });
|
|
264
|
+
}
|
|
265
|
+
await auth.setUserRole(result.id, role);
|
|
236
266
|
}
|
|
237
267
|
return Response.json({ ok: true, userId: result.id });
|
|
238
268
|
});
|
|
239
|
-
|
|
269
|
+
roleGuarded("delete", "/admin/api/staff/invitations/:id", "owner", async (c) => {
|
|
240
270
|
const revoked = await auth.revokeInvite(c.req.param("id") ?? "");
|
|
241
271
|
if (!revoked) {
|
|
242
272
|
return Response.json({
|
|
@@ -267,8 +297,8 @@ function mountAdminBetterAuth(app, ref, auth) {
|
|
|
267
297
|
return handleViewRequest(c.req.raw, runtime, viewName, ref, c.env, waitUntil, "/admin/api/views");
|
|
268
298
|
});
|
|
269
299
|
}
|
|
270
|
-
guarded("get", "/admin/api/operations", () => Response.json({
|
|
271
|
-
operations: operations.map((op) => ({
|
|
300
|
+
guarded("get", "/admin/api/operations", (c, gate) => Response.json({
|
|
301
|
+
operations: operations.filter((op) => evaluateAuthAll(op.procedure.spec.requires, adminHandlerContext(c, gate), `GET /admin/api/operations/${op.name}`, "runtime") === null).map((op) => ({
|
|
272
302
|
name: op.name,
|
|
273
303
|
title: op.title,
|
|
274
304
|
description: op.description,
|
|
@@ -315,19 +345,19 @@ function mountAdminBetterAuth(app, ref, auth) {
|
|
|
315
345
|
});
|
|
316
346
|
guarded("get", "/admin/api/site", async (c) => {
|
|
317
347
|
const runtime = await ref.get();
|
|
318
|
-
const site = await runtime.siteConfig.load();
|
|
319
|
-
const
|
|
348
|
+
const { origin, ...site } = await runtime.siteConfig.load();
|
|
349
|
+
const publicUrl = origin || new URL(c.req.url).origin;
|
|
320
350
|
return Response.json({
|
|
321
351
|
...site,
|
|
322
|
-
publicUrl
|
|
323
|
-
mcpUrl: `${
|
|
352
|
+
publicUrl,
|
|
353
|
+
mcpUrl: `${publicUrl}/mcp/staff`,
|
|
324
354
|
});
|
|
325
355
|
});
|
|
326
|
-
|
|
356
|
+
roleGuarded("get", "/admin/api/site-settings", "owner", async () => runMantleUseCase("GET /admin/api/site-settings", async () => {
|
|
327
357
|
const runtime = await ref.get();
|
|
328
358
|
return adminSiteSettings(await runtime.siteConfig.load());
|
|
329
359
|
}));
|
|
330
|
-
|
|
360
|
+
roleGuarded("patch", "/admin/api/site-settings", "owner", async (c) => runMantleUseCase("PATCH /admin/api/site-settings", async () => {
|
|
331
361
|
const runtime = await ref.get();
|
|
332
362
|
const body = (await c.req.raw.json().catch(() => ({})));
|
|
333
363
|
const site = await runtime.updateSiteSettings.execute({
|
|
@@ -357,6 +387,21 @@ function mountAdminBetterAuth(app, ref, auth) {
|
|
|
357
387
|
const rawLimit = c.req.query("limit");
|
|
358
388
|
const parsedLimit = rawLimit ? Number.parseInt(rawLimit, 10) : NaN;
|
|
359
389
|
const statusQuery = c.req.query("status");
|
|
390
|
+
const sortDirection = c.req.query("direction") === "asc" ? "asc" : "desc";
|
|
391
|
+
const filterField = c.req.query("filter_field");
|
|
392
|
+
const filterValue = c.req.query("filter_value");
|
|
393
|
+
if (Boolean(filterField) !== Boolean(filterValue)) {
|
|
394
|
+
return Response.json({
|
|
395
|
+
ok: false,
|
|
396
|
+
diagnostic: runtimeDiagnostic({
|
|
397
|
+
code: "INPUT_VALIDATION_FAILED",
|
|
398
|
+
severity: "error",
|
|
399
|
+
path: "GET /admin/api/entries",
|
|
400
|
+
expected: "filter_field and filter_value together",
|
|
401
|
+
message: "List filters require both `filter_field` and `filter_value`.",
|
|
402
|
+
}),
|
|
403
|
+
}, { status: 400 });
|
|
404
|
+
}
|
|
360
405
|
// Admin pagination needs the cursored shape — `executePage` returns
|
|
361
406
|
// `{ rows, nextCursor? }`. `execute()` is the flat-array variant
|
|
362
407
|
// for app code.
|
|
@@ -365,10 +410,57 @@ function mountAdminBetterAuth(app, ref, auth) {
|
|
|
365
410
|
status: statusQuery && statusQuery !== "all" ? statusQuery : undefined,
|
|
366
411
|
limit: Number.isFinite(parsedLimit) ? parsedLimit : 99,
|
|
367
412
|
cursor: c.req.query("cursor") ?? undefined,
|
|
413
|
+
cursorDirection: c.req.query("cursor_direction") === "backward" ? "backward" : "forward",
|
|
368
414
|
search: c.req.query("search") || undefined,
|
|
415
|
+
filter: filterField && filterValue
|
|
416
|
+
? { field: filterField, value: filterValue }
|
|
417
|
+
: undefined,
|
|
418
|
+
sort: {
|
|
419
|
+
field: c.req.query("sort") || "updatedAt",
|
|
420
|
+
direction: sortDirection,
|
|
421
|
+
},
|
|
422
|
+
});
|
|
423
|
+
const translationLocales = new Map();
|
|
424
|
+
const translationSchemas = schemas.filter((schema) => schema.spec.translates?.parent === collection);
|
|
425
|
+
for (const schema of translationSchemas) {
|
|
426
|
+
const field = schema.spec.translates.on;
|
|
427
|
+
const parentValues = new Map();
|
|
428
|
+
for (const row of result.rows) {
|
|
429
|
+
const value = primitiveJoinValue(row.data[field]);
|
|
430
|
+
if (value !== null)
|
|
431
|
+
parentValues.set(joinValueKey(value), value);
|
|
432
|
+
}
|
|
433
|
+
const translations = await runtime.entryReader.readByDataFieldIn({
|
|
434
|
+
collection: schema.metadata.name,
|
|
435
|
+
field,
|
|
436
|
+
values: [...parentValues.values()],
|
|
437
|
+
});
|
|
438
|
+
const localesByValue = new Map();
|
|
439
|
+
for (const entry of translations) {
|
|
440
|
+
const value = primitiveJoinValue(entry.data[field]);
|
|
441
|
+
if (value === null || !entry.locale)
|
|
442
|
+
continue;
|
|
443
|
+
const key = joinValueKey(value);
|
|
444
|
+
const locales = localesByValue.get(key) ?? new Set();
|
|
445
|
+
locales.add(entry.locale);
|
|
446
|
+
localesByValue.set(key, locales);
|
|
447
|
+
}
|
|
448
|
+
for (const row of result.rows) {
|
|
449
|
+
const value = primitiveJoinValue(row.data[field]);
|
|
450
|
+
if (value === null)
|
|
451
|
+
continue;
|
|
452
|
+
const locales = translationLocales.get(row.id) ?? new Set();
|
|
453
|
+
for (const locale of localesByValue.get(joinValueKey(value)) ?? [])
|
|
454
|
+
locales.add(locale);
|
|
455
|
+
translationLocales.set(row.id, locales);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
const items = result.rows.map((row) => adminListItem(row, schemasByName, [...(translationLocales.get(row.id) ?? [])]));
|
|
459
|
+
return Response.json({
|
|
460
|
+
items,
|
|
461
|
+
previous_cursor: result.previousCursor ?? null,
|
|
462
|
+
next_cursor: result.nextCursor ?? null,
|
|
369
463
|
});
|
|
370
|
-
const items = result.rows.map((row) => adminListItem(row, schemasByName));
|
|
371
|
-
return Response.json({ items, next_cursor: result.nextCursor ?? null });
|
|
372
464
|
});
|
|
373
465
|
guarded("get", "/admin/api/entries/export", async (c) => {
|
|
374
466
|
const collection = c.req.query("collection");
|
|
@@ -426,6 +518,10 @@ function mountAdminBetterAuth(app, ref, auth) {
|
|
|
426
518
|
message: "A non-empty `collection` is required.",
|
|
427
519
|
}));
|
|
428
520
|
}
|
|
521
|
+
if (gate.role === "contributor" &&
|
|
522
|
+
(schemasByName.get(body.collection)?.spec.lifecycle ?? "publishing") === "operational") {
|
|
523
|
+
throw new DiagnosticError(adminRoleDiagnostic("POST /admin/api/entries", "editor", "Contributors can create drafts, not operational records."));
|
|
524
|
+
}
|
|
429
525
|
const row = await runtime.createDraft.execute({
|
|
430
526
|
collection: body.collection,
|
|
431
527
|
data: objectField(body.data),
|
|
@@ -438,6 +534,13 @@ function mountAdminBetterAuth(app, ref, auth) {
|
|
|
438
534
|
guarded("patch", "/admin/api/entries/:id", async (c, gate) => runMantleUseCase(`PATCH /admin/api/entries/${c.req.param("id")}`, async () => {
|
|
439
535
|
const runtime = await ref.get();
|
|
440
536
|
const id = c.req.param("id");
|
|
537
|
+
if (gate.role === "contributor") {
|
|
538
|
+
const current = await runtime.getEntry.execute({ id });
|
|
539
|
+
const lifecycle = schemasByName.get(current.collection)?.spec.lifecycle ?? "publishing";
|
|
540
|
+
if (lifecycle === "operational" || current.status !== "draft") {
|
|
541
|
+
throw new DiagnosticError(adminRoleDiagnostic(`PATCH /admin/api/entries/${id}`, "editor", "Contributors can edit drafts only."));
|
|
542
|
+
}
|
|
543
|
+
}
|
|
441
544
|
const body = (await c.req.raw.json().catch(() => ({})));
|
|
442
545
|
const updated = await runtime.updateDraft.execute({
|
|
443
546
|
id,
|
|
@@ -448,7 +551,7 @@ function mountAdminBetterAuth(app, ref, auth) {
|
|
|
448
551
|
});
|
|
449
552
|
return entryEditorPayload(runtime, updated, schemas);
|
|
450
553
|
}));
|
|
451
|
-
|
|
554
|
+
roleGuarded("post", "/admin/api/entries/:id/publish", "editor", async (c, gate) => runMantleUseCase(`POST /admin/api/entries/${c.req.param("id")}/publish`, async () => {
|
|
452
555
|
const runtime = await ref.get();
|
|
453
556
|
const id = c.req.param("id");
|
|
454
557
|
const body = await c.req.raw.json().catch(() => ({}));
|
|
@@ -459,7 +562,7 @@ function mountAdminBetterAuth(app, ref, auth) {
|
|
|
459
562
|
});
|
|
460
563
|
return entryEditorPayload(runtime, row, schemas);
|
|
461
564
|
}));
|
|
462
|
-
|
|
565
|
+
roleGuarded("post", "/admin/api/entries/:id/unpublish", "editor", async (c, gate) => runMantleUseCase(`POST /admin/api/entries/${c.req.param("id")}/unpublish`, async () => {
|
|
463
566
|
const runtime = await ref.get();
|
|
464
567
|
const id = c.req.param("id");
|
|
465
568
|
const body = await c.req.raw.json().catch(() => ({}));
|
|
@@ -470,7 +573,7 @@ function mountAdminBetterAuth(app, ref, auth) {
|
|
|
470
573
|
});
|
|
471
574
|
return entryEditorPayload(runtime, row, schemas);
|
|
472
575
|
}));
|
|
473
|
-
|
|
576
|
+
roleGuarded("delete", "/admin/api/entries/:id", "editor", async (c, gate) => runMantleUseCase(`DELETE /admin/api/entries/${c.req.param("id")}`, async () => {
|
|
474
577
|
const runtime = await ref.get();
|
|
475
578
|
const id = c.req.param("id");
|
|
476
579
|
const body = await c.req.raw.json().catch(() => ({}));
|
|
@@ -485,7 +588,7 @@ function mountAdminBetterAuth(app, ref, auth) {
|
|
|
485
588
|
// directly to R2 S3 (Worker bypassed) → POST /uploads/:groupId/commit.
|
|
486
589
|
const MEDIA_UPLOADS_PATH = "/admin/api/media/uploads";
|
|
487
590
|
const MEDIA_COMMIT_PATH = "/admin/api/media/uploads/:uploadGroupId/commit";
|
|
488
|
-
|
|
591
|
+
roleGuarded("post", MEDIA_UPLOADS_PATH, "editor", async (c) => {
|
|
489
592
|
const runtime = await ref.get();
|
|
490
593
|
const media = runtime.media;
|
|
491
594
|
if (!media)
|
|
@@ -547,7 +650,7 @@ function mountAdminBetterAuth(app, ref, auth) {
|
|
|
547
650
|
caption: typeof body.caption === "string" ? body.caption : undefined,
|
|
548
651
|
}));
|
|
549
652
|
});
|
|
550
|
-
|
|
653
|
+
roleGuarded("post", MEDIA_COMMIT_PATH, "editor", async (c) => {
|
|
551
654
|
const runtime = await ref.get();
|
|
552
655
|
const media = runtime.media;
|
|
553
656
|
if (!media)
|
|
@@ -567,7 +670,7 @@ function mountAdminBetterAuth(app, ref, auth) {
|
|
|
567
670
|
// `mediaStorage` is bound (mirrors the upload handlers above).
|
|
568
671
|
const MEDIA_LIST_PATH = "/admin/api/media";
|
|
569
672
|
const MEDIA_ASSET_PATH = "/admin/api/media/:id";
|
|
570
|
-
|
|
673
|
+
roleGuarded("get", MEDIA_LIST_PATH, "editor", async (c) => {
|
|
571
674
|
const runtime = await ref.get();
|
|
572
675
|
const media = runtime.media;
|
|
573
676
|
if (!media)
|
|
@@ -586,7 +689,7 @@ function mountAdminBetterAuth(app, ref, auth) {
|
|
|
586
689
|
};
|
|
587
690
|
});
|
|
588
691
|
});
|
|
589
|
-
|
|
692
|
+
roleGuarded("get", MEDIA_ASSET_PATH, "editor", async (c) => {
|
|
590
693
|
const runtime = await ref.get();
|
|
591
694
|
const media = runtime.media;
|
|
592
695
|
if (!media)
|
|
@@ -594,7 +697,7 @@ function mountAdminBetterAuth(app, ref, auth) {
|
|
|
594
697
|
const id = c.req.param("id");
|
|
595
698
|
return runMantleUseCase(`GET ${MEDIA_ASSET_PATH}`, async () => adminMediaItem(await media.getAsset.execute(id)));
|
|
596
699
|
});
|
|
597
|
-
|
|
700
|
+
roleGuarded("patch", MEDIA_ASSET_PATH, "editor", async (c) => {
|
|
598
701
|
const runtime = await ref.get();
|
|
599
702
|
const media = runtime.media;
|
|
600
703
|
if (!media)
|
|
@@ -620,7 +723,7 @@ function mountAdminBetterAuth(app, ref, auth) {
|
|
|
620
723
|
caption: typeof body.caption === "string" ? body.caption : undefined,
|
|
621
724
|
})));
|
|
622
725
|
});
|
|
623
|
-
|
|
726
|
+
roleGuarded("delete", MEDIA_ASSET_PATH, "editor", async (c) => {
|
|
624
727
|
const runtime = await ref.get();
|
|
625
728
|
const media = runtime.media;
|
|
626
729
|
if (!media)
|
|
@@ -827,7 +930,7 @@ function titleFieldKey(data, schema) {
|
|
|
827
930
|
* next to the system column (#443). NAME-based only, on purpose: no
|
|
828
931
|
* fuzzy/semantic matching, just these two well-known reserved names. */
|
|
829
932
|
const DATA_PREVIEW_SYSTEM_COLUMN_NAMES = new Set(["updatedAt", "createdAt"]);
|
|
830
|
-
/** Operational
|
|
933
|
+
/** Operational collections have no title/status
|
|
831
934
|
* workflow worth a dedicated column — instead the admin list shows up
|
|
832
935
|
* to 3 raw data columns. Mirrors the client-side column-picking rule
|
|
833
936
|
* in `collection-view.tsx`: first 3 `required` properties, skipping
|
|
@@ -835,7 +938,7 @@ const DATA_PREVIEW_SYSTEM_COLUMN_NAMES = new Set(["updatedAt", "createdAt"]);
|
|
|
835
938
|
* above (#443). Kept small on the wire on purpose — this is a
|
|
836
939
|
* preview, not the full entry. */
|
|
837
940
|
function adminDataPreview(data, manifest) {
|
|
838
|
-
if (!manifest || manifest.spec.lifecycle !== "
|
|
941
|
+
if (!manifest || manifest.spec.lifecycle !== "operational")
|
|
839
942
|
return undefined;
|
|
840
943
|
const schema = manifest.spec.schema;
|
|
841
944
|
// Schema-stable skip (not per-row): rows with a blank title field
|
|
@@ -874,7 +977,7 @@ function isStringTypedSchema(schema) {
|
|
|
874
977
|
const types = Array.isArray(rawType) ? rawType : rawType ? [rawType] : [];
|
|
875
978
|
return types.includes("string");
|
|
876
979
|
}
|
|
877
|
-
function adminListItem(row, schemasByName) {
|
|
980
|
+
function adminListItem(row, schemasByName, translationLocales = []) {
|
|
878
981
|
const manifest = schemasByName.get(row.collection);
|
|
879
982
|
return {
|
|
880
983
|
id: row.id,
|
|
@@ -884,6 +987,7 @@ function adminListItem(row, schemasByName) {
|
|
|
884
987
|
version: row.version,
|
|
885
988
|
title: adminEntryTitle(row.data, manifest?.spec.schema),
|
|
886
989
|
updated_at: row.updatedAt,
|
|
990
|
+
translation_locales: translationLocales,
|
|
887
991
|
data_preview: adminDataPreview(row.data, manifest),
|
|
888
992
|
};
|
|
889
993
|
}
|
|
@@ -937,6 +1041,7 @@ async function entryEditorPayload(runtime, row, schemas) {
|
|
|
937
1041
|
return {
|
|
938
1042
|
collection: adminEditorCollection(schema, schemas),
|
|
939
1043
|
entry: adminEditorEntry(row),
|
|
1044
|
+
parentEntryId: await parentEntryId(runtime, schema, row, schemas),
|
|
940
1045
|
related,
|
|
941
1046
|
};
|
|
942
1047
|
}
|
|
@@ -945,7 +1050,7 @@ function adminEditorCollection(schema, schemas) {
|
|
|
945
1050
|
name: schema.metadata.name,
|
|
946
1051
|
title: schema.spec.title,
|
|
947
1052
|
description: schema.spec.description ?? null,
|
|
948
|
-
lifecycle: schema.spec.lifecycle ?? "
|
|
1053
|
+
lifecycle: schema.spec.lifecycle ?? "publishing",
|
|
949
1054
|
parent: collectionParentFor(schema, schemas),
|
|
950
1055
|
hasTranslations: schemas.some((candidate) => candidate.spec.translates?.parent === schema.metadata.name),
|
|
951
1056
|
localized: schema.spec.localized ?? Boolean(schema.spec.translates),
|
|
@@ -953,6 +1058,8 @@ function adminEditorCollection(schema, schemas) {
|
|
|
953
1058
|
schema: schema.spec.schema,
|
|
954
1059
|
uiSchema: schema.spec.uiSchema ?? null,
|
|
955
1060
|
mediaFields: mediaFieldsForCollection(schema, schemas),
|
|
1061
|
+
sortableFields: schemaSortableFields(schema),
|
|
1062
|
+
filter: checkSchemaListFilter(schema).filter,
|
|
956
1063
|
};
|
|
957
1064
|
}
|
|
958
1065
|
function adminEditorEntry(row) {
|
|
@@ -973,7 +1080,9 @@ async function relatedEntrySections(runtime, parentSchema, parentRow, schemas) {
|
|
|
973
1080
|
const childSchema = schemas.find((schema) => schema.metadata.name === relationship.collection);
|
|
974
1081
|
if (!childSchema)
|
|
975
1082
|
continue;
|
|
976
|
-
const entries =
|
|
1083
|
+
const entries = relationship.parentValue === null
|
|
1084
|
+
? []
|
|
1085
|
+
: await entriesByDataValue(runtime, relationship.collection, relationship.childField, relationship.parentValue);
|
|
977
1086
|
sections.push({
|
|
978
1087
|
collection: adminEditorCollection(childSchema, schemas),
|
|
979
1088
|
relationship: {
|
|
@@ -989,12 +1098,11 @@ async function relatedEntrySections(runtime, parentSchema, parentRow, schemas) {
|
|
|
989
1098
|
}
|
|
990
1099
|
function discoverChildRelationships(parentSchema, parentRow, schemas) {
|
|
991
1100
|
const parentName = parentSchema.metadata.name;
|
|
992
|
-
const parentProps = parentSchema.spec.schema.properties ?? {};
|
|
993
1101
|
const relationships = [];
|
|
994
1102
|
const seen = new Set();
|
|
995
|
-
const add = (childSchema, kind, parentField, childField) => {
|
|
996
|
-
const parentValue = primitiveJoinValue(
|
|
997
|
-
if (parentValue == null)
|
|
1103
|
+
const add = (childSchema, kind, parentField, childField, rawParentValue) => {
|
|
1104
|
+
const parentValue = primitiveJoinValue(rawParentValue);
|
|
1105
|
+
if (parentValue == null && kind !== "translation")
|
|
998
1106
|
return;
|
|
999
1107
|
const key = `${childSchema.metadata.name}:${kind}:${parentField}:${childField}:${String(parentValue)}`;
|
|
1000
1108
|
if (seen.has(key))
|
|
@@ -1008,45 +1116,27 @@ function discoverChildRelationships(parentSchema, parentRow, schemas) {
|
|
|
1008
1116
|
parentValue,
|
|
1009
1117
|
});
|
|
1010
1118
|
};
|
|
1119
|
+
const translates = parentSchema.spec.translates;
|
|
1120
|
+
if (translates) {
|
|
1121
|
+
add(parentSchema, "translation", translates.on, translates.on, parentRow.data[translates.on]);
|
|
1122
|
+
}
|
|
1011
1123
|
for (const childSchema of schemas) {
|
|
1012
1124
|
if (childSchema.metadata.name === parentName)
|
|
1013
1125
|
continue;
|
|
1014
1126
|
const childProps = childSchema.spec.schema.properties ?? {};
|
|
1015
1127
|
const translates = childSchema.spec.translates;
|
|
1016
1128
|
if (translates?.parent === parentName) {
|
|
1017
|
-
add(childSchema, "translation", translates.on, translates.on);
|
|
1129
|
+
add(childSchema, "translation", translates.on, translates.on, parentRow.data[translates.on]);
|
|
1018
1130
|
continue;
|
|
1019
1131
|
}
|
|
1020
|
-
for (const [childField] of Object.entries(childProps)) {
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1132
|
+
for (const [childField, childProperty] of Object.entries(childProps)) {
|
|
1133
|
+
if (childProperty[MANTLE_REF_KEYWORD] === parentName) {
|
|
1134
|
+
add(childSchema, "field", "id", childField, parentRow.id);
|
|
1135
|
+
}
|
|
1024
1136
|
}
|
|
1025
1137
|
}
|
|
1026
1138
|
return relationships;
|
|
1027
1139
|
}
|
|
1028
|
-
function conventionalParentField(parentCollectionName, childField, parentProps) {
|
|
1029
|
-
const bases = new Set([
|
|
1030
|
-
camelCaseIdentifier(parentCollectionName),
|
|
1031
|
-
singularizeIdentifier(camelCaseIdentifier(parentCollectionName)),
|
|
1032
|
-
]);
|
|
1033
|
-
for (const base of bases) {
|
|
1034
|
-
for (const [parentField, parentSchema] of Object.entries(parentProps)) {
|
|
1035
|
-
if (!isPrimitiveJoinSchema(parentSchema))
|
|
1036
|
-
continue;
|
|
1037
|
-
if (childField === `${base}${capitalizeIdentifier(parentField)}`)
|
|
1038
|
-
return parentField;
|
|
1039
|
-
}
|
|
1040
|
-
}
|
|
1041
|
-
return null;
|
|
1042
|
-
}
|
|
1043
|
-
function isPrimitiveJoinSchema(schema) {
|
|
1044
|
-
const rawType = schema.type;
|
|
1045
|
-
const types = Array.isArray(rawType) ? rawType : rawType ? [rawType] : [];
|
|
1046
|
-
if (types.length === 0 && schema.enum)
|
|
1047
|
-
return true;
|
|
1048
|
-
return types.some((type) => type === "string" || type === "number" || type === "integer" || type === "boolean");
|
|
1049
|
-
}
|
|
1050
1140
|
function collectionParentFor(childSchema, schemas) {
|
|
1051
1141
|
if (childSchema.spec.translates) {
|
|
1052
1142
|
return {
|
|
@@ -1060,27 +1150,30 @@ function collectionParentFor(childSchema, schemas) {
|
|
|
1060
1150
|
// parent, so it's buried under the parent in the sidebar). Optional
|
|
1061
1151
|
// ref = weak reference — the collection stays top-level.
|
|
1062
1152
|
const childRequired = new Set(childSchema.spec.schema.required ?? []);
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1153
|
+
const schemaNames = new Set(schemas
|
|
1154
|
+
.filter((schema) => !schema.spec.translates)
|
|
1155
|
+
.map((schema) => schema.metadata.name));
|
|
1156
|
+
for (const [childField, childProperty] of Object.entries(childProps)) {
|
|
1157
|
+
if (!childRequired.has(childField))
|
|
1067
1158
|
continue;
|
|
1068
|
-
const
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
continue;
|
|
1072
|
-
const parentField = conventionalParentField(parentSchema.metadata.name, childField, parentProps);
|
|
1073
|
-
if (parentField) {
|
|
1074
|
-
return {
|
|
1075
|
-
collection: parentSchema.metadata.name,
|
|
1076
|
-
parentField,
|
|
1077
|
-
childField,
|
|
1078
|
-
};
|
|
1079
|
-
}
|
|
1159
|
+
const parent = childProperty[MANTLE_REF_KEYWORD];
|
|
1160
|
+
if (typeof parent === "string" && schemaNames.has(parent)) {
|
|
1161
|
+
return { collection: parent, parentField: "id", childField };
|
|
1080
1162
|
}
|
|
1081
1163
|
}
|
|
1082
1164
|
return null;
|
|
1083
1165
|
}
|
|
1166
|
+
async function parentEntryId(runtime, childSchema, childRow, schemas) {
|
|
1167
|
+
const parent = collectionParentFor(childSchema, schemas);
|
|
1168
|
+
if (!parent)
|
|
1169
|
+
return null;
|
|
1170
|
+
const value = primitiveJoinValue(childRow.data[parent.childField]);
|
|
1171
|
+
if (value === null)
|
|
1172
|
+
return null;
|
|
1173
|
+
if (parent.parentField === "id" && typeof value === "string")
|
|
1174
|
+
return value;
|
|
1175
|
+
return (await entriesByDataValue(runtime, parent.collection, parent.parentField, value))[0]?.id ?? null;
|
|
1176
|
+
}
|
|
1084
1177
|
function primitiveJoinValue(value) {
|
|
1085
1178
|
if (typeof value === "string" && value !== "")
|
|
1086
1179
|
return value;
|
|
@@ -1090,27 +1183,8 @@ function primitiveJoinValue(value) {
|
|
|
1090
1183
|
return value;
|
|
1091
1184
|
return null;
|
|
1092
1185
|
}
|
|
1093
|
-
function
|
|
1094
|
-
return value
|
|
1095
|
-
.split(/[^a-zA-Z0-9]+/)
|
|
1096
|
-
.filter(Boolean)
|
|
1097
|
-
.map((part, index) => {
|
|
1098
|
-
const lower = part.charAt(0).toLowerCase() + part.slice(1);
|
|
1099
|
-
return index === 0 ? lower : capitalizeIdentifier(lower);
|
|
1100
|
-
})
|
|
1101
|
-
.join("");
|
|
1102
|
-
}
|
|
1103
|
-
function singularizeIdentifier(value) {
|
|
1104
|
-
if (value.endsWith("ies"))
|
|
1105
|
-
return `${value.slice(0, -3)}y`;
|
|
1106
|
-
if (value.endsWith("ses"))
|
|
1107
|
-
return value.slice(0, -2);
|
|
1108
|
-
if (value.endsWith("s") && value.length > 1)
|
|
1109
|
-
return value.slice(0, -1);
|
|
1110
|
-
return value;
|
|
1111
|
-
}
|
|
1112
|
-
function capitalizeIdentifier(value) {
|
|
1113
|
-
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
1186
|
+
function joinValueKey(value) {
|
|
1187
|
+
return `${typeof value}:${String(value)}`;
|
|
1114
1188
|
}
|
|
1115
1189
|
async function entriesByDataValue(runtime, collection, field, value) {
|
|
1116
1190
|
return runtime.entryReader.findManyByDataField({
|
|
@@ -1122,7 +1196,9 @@ async function entriesByDataValue(runtime, collection, field, value) {
|
|
|
1122
1196
|
}
|
|
1123
1197
|
function adminSiteSettings(site) {
|
|
1124
1198
|
return {
|
|
1125
|
-
|
|
1199
|
+
brand: site.brand,
|
|
1200
|
+
title: site.title,
|
|
1201
|
+
description: site.description,
|
|
1126
1202
|
ga4MeasurementId: site.ga4MeasurementId ?? "",
|
|
1127
1203
|
facebookPixelId: site.facebookPixelId ?? "",
|
|
1128
1204
|
};
|
|
@@ -1174,6 +1250,7 @@ async function readStaffGate(c, auth) {
|
|
|
1174
1250
|
kind: "ok",
|
|
1175
1251
|
userId: session.user.id,
|
|
1176
1252
|
login,
|
|
1253
|
+
image: session.user.image ?? null,
|
|
1177
1254
|
role: role,
|
|
1178
1255
|
sessionId: session.session.id,
|
|
1179
1256
|
};
|
|
@@ -1192,14 +1269,32 @@ async function handleHttpTrigger(req, runtime, ref, triggerName, triggerPath, pa
|
|
|
1192
1269
|
const caller = await resolveCaller(req, {
|
|
1193
1270
|
auth: ref.auth,
|
|
1194
1271
|
credentialResolver: ref.credentialResolver,
|
|
1195
|
-
|
|
1272
|
+
jwtBearer: ref.jwtBearer,
|
|
1196
1273
|
env,
|
|
1197
1274
|
waitUntil,
|
|
1198
1275
|
});
|
|
1199
1276
|
if (caller.kind === "invalid") {
|
|
1200
1277
|
return Response.json({ ok: false, diagnostic: caller.diagnostic }, { status: caller.status });
|
|
1201
1278
|
}
|
|
1202
|
-
|
|
1279
|
+
if (caller.context.auth?.credential === "session") {
|
|
1280
|
+
const rejected = rejectCrossOriginMutation(req);
|
|
1281
|
+
if (rejected)
|
|
1282
|
+
return rejected;
|
|
1283
|
+
}
|
|
1284
|
+
let body;
|
|
1285
|
+
try {
|
|
1286
|
+
body = await readBody(req);
|
|
1287
|
+
}
|
|
1288
|
+
catch {
|
|
1289
|
+
const diagnostic = runtimeDiagnostic({
|
|
1290
|
+
code: "INPUT_VALIDATION_FAILED",
|
|
1291
|
+
severity: "error",
|
|
1292
|
+
path: `${triggerPathPrefix}#/body`,
|
|
1293
|
+
expected: "valid JSON",
|
|
1294
|
+
message: "HTTP Trigger request body is not valid JSON.",
|
|
1295
|
+
});
|
|
1296
|
+
return Response.json({ ok: false, diagnostic: redactForWire(diagnostic) }, { status: 400 });
|
|
1297
|
+
}
|
|
1203
1298
|
// Spread order matters: URL path params are authoritative for the
|
|
1204
1299
|
// resource identifier (a `DELETE /entries/{id}` body MUST NOT spoof
|
|
1205
1300
|
// `id`). Body fields fill in non-path inputs only.
|
|
@@ -1237,7 +1332,7 @@ mountPath) {
|
|
|
1237
1332
|
const caller = await resolveCaller(req, {
|
|
1238
1333
|
auth: ref.auth,
|
|
1239
1334
|
credentialResolver: ref.credentialResolver,
|
|
1240
|
-
|
|
1335
|
+
jwtBearer: ref.jwtBearer,
|
|
1241
1336
|
env,
|
|
1242
1337
|
waitUntil,
|
|
1243
1338
|
});
|
|
@@ -1299,13 +1394,8 @@ async function readBody(req) {
|
|
|
1299
1394
|
const ct = req.headers.get("content-type") ?? "";
|
|
1300
1395
|
if (!ct.includes("json"))
|
|
1301
1396
|
return {};
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
return parsed ?? {};
|
|
1305
|
-
}
|
|
1306
|
-
catch {
|
|
1307
|
-
return {};
|
|
1308
|
-
}
|
|
1397
|
+
const parsed = await req.json();
|
|
1398
|
+
return parsed ?? {};
|
|
1309
1399
|
}
|
|
1310
1400
|
function openApiToHono(path) {
|
|
1311
1401
|
return path.replace(/\{([^}]+)\}/g, ":$1");
|
|
@@ -1366,18 +1456,22 @@ function adminUnauthenticated(c, path) {
|
|
|
1366
1456
|
// instead of bouncing them back to /admin/sign-in (which the OAuth
|
|
1367
1457
|
// re-auth then silently fast-forwards through, producing a visible
|
|
1368
1458
|
// 5-step redirect chain that looks like an infinite loop).
|
|
1369
|
-
function
|
|
1459
|
+
function adminInsufficientRole(c, path, minimumRole) {
|
|
1460
|
+
const diagnostic = adminRoleDiagnostic(`${c.req.method} ${path}`, minimumRole, `This action requires the ${minimumRole} role.`);
|
|
1370
1461
|
return Response.json({
|
|
1371
1462
|
ok: false,
|
|
1372
|
-
diagnostic
|
|
1373
|
-
code: "AUTH_DENIED",
|
|
1374
|
-
severity: "error",
|
|
1375
|
-
path: `${c.req.method} ${path}`,
|
|
1376
|
-
expected: "owner role for the signed-in user",
|
|
1377
|
-
message: "Staff management is owner-only.",
|
|
1378
|
-
}),
|
|
1463
|
+
diagnostic,
|
|
1379
1464
|
}, { status: 403 });
|
|
1380
1465
|
}
|
|
1466
|
+
function adminRoleDiagnostic(path, minimumRole, message) {
|
|
1467
|
+
return runtimeDiagnostic({
|
|
1468
|
+
code: "AUTH_DENIED",
|
|
1469
|
+
severity: "error",
|
|
1470
|
+
path,
|
|
1471
|
+
expected: `${minimumRole} role or higher for the signed-in user`,
|
|
1472
|
+
message,
|
|
1473
|
+
});
|
|
1474
|
+
}
|
|
1381
1475
|
function adminNotStaff(c, path, login) {
|
|
1382
1476
|
return Response.json({
|
|
1383
1477
|
ok: false,
|