@dyrected/core 2.24.3 → 2.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{app-Bg34iyUz.d.cts → app-7SuKzu5w.d.cts} +128 -3
- package/dist/{app-Bg34iyUz.d.ts → app-7SuKzu5w.d.ts} +128 -3
- package/dist/index.cjs +130 -21
- package/dist/index.d.cts +64 -4
- package/dist/index.d.ts +64 -4
- package/dist/index.js +123 -21
- package/dist/server.cjs +215 -51
- package/dist/server.d.cts +2 -2
- package/dist/server.d.ts +2 -2
- package/dist/server.js +215 -51
- package/package.json +1 -1
package/dist/server.cjs
CHANGED
|
@@ -1961,10 +1961,11 @@ var init_readonly_fix = __esm({
|
|
|
1961
1961
|
});
|
|
1962
1962
|
|
|
1963
1963
|
// src/types/navigation.ts
|
|
1964
|
-
var CURRENT_NAV_PREFERENCES_VERSION, DEFAULT_USER_NAV_PREFERENCES;
|
|
1964
|
+
var NAVIGATION_POLICY_KEY, CURRENT_NAV_PREFERENCES_VERSION, DEFAULT_USER_NAV_PREFERENCES;
|
|
1965
1965
|
var init_navigation = __esm({
|
|
1966
1966
|
"src/types/navigation.ts"() {
|
|
1967
1967
|
"use strict";
|
|
1968
|
+
NAVIGATION_POLICY_KEY = "admin:navigation-policy";
|
|
1968
1969
|
CURRENT_NAV_PREFERENCES_VERSION = 1;
|
|
1969
1970
|
DEFAULT_USER_NAV_PREFERENCES = {
|
|
1970
1971
|
_version: CURRENT_NAV_PREFERENCES_VERSION,
|
|
@@ -7183,6 +7184,7 @@ function reconcileNavigation(baseTree, prefs, schemas, options) {
|
|
|
7183
7184
|
};
|
|
7184
7185
|
}
|
|
7185
7186
|
const includeHidden = options?.includeHidden ?? false;
|
|
7187
|
+
const allowUserItems = options?.allowUserItems ?? true;
|
|
7186
7188
|
const effectivePrefs = prefs || DEFAULT_USER_NAV_PREFERENCES;
|
|
7187
7189
|
const hiddenSet = new Set(effectivePrefs.hidden || []);
|
|
7188
7190
|
const knownCollectionSlugs = new Set((schemas?.collections || []).map((c) => c.slug));
|
|
@@ -7214,8 +7216,14 @@ function reconcileNavigation(baseTree, prefs, schemas, options) {
|
|
|
7214
7216
|
});
|
|
7215
7217
|
}
|
|
7216
7218
|
}
|
|
7219
|
+
const ungroupedKeys = /* @__PURE__ */ new Set();
|
|
7220
|
+
for (const item of baseTree.ungrouped || []) {
|
|
7221
|
+
ungroupedKeys.add(item.id);
|
|
7222
|
+
if (item.slug) ungroupedKeys.add(item.slug);
|
|
7223
|
+
}
|
|
7217
7224
|
for (const itemOpt of effectivePrefs.items || []) {
|
|
7218
7225
|
const itemId = itemOpt.slug ? `workspace_${itemOpt.slug}` : itemOpt.collection ? `collection_${itemOpt.collection}` : `custom_${Math.random()}`;
|
|
7226
|
+
if (ungroupedKeys.has(itemId) || itemOpt.slug && ungroupedKeys.has(itemOpt.slug)) continue;
|
|
7219
7227
|
let existingItem;
|
|
7220
7228
|
let existingParentGroup;
|
|
7221
7229
|
for (const g of groupsList) {
|
|
@@ -7226,6 +7234,7 @@ function reconcileNavigation(baseTree, prefs, schemas, options) {
|
|
|
7226
7234
|
break;
|
|
7227
7235
|
}
|
|
7228
7236
|
}
|
|
7237
|
+
if (!existingItem && !allowUserItems) continue;
|
|
7229
7238
|
const targetGroup = typeof itemOpt.group === "object" ? itemOpt.group?.name : itemOpt.group || existingParentGroup?.name || "Custom";
|
|
7230
7239
|
const groupKey = targetGroup;
|
|
7231
7240
|
let group = findGroup(groupKey);
|
|
@@ -7255,7 +7264,7 @@ function reconcileNavigation(baseTree, prefs, schemas, options) {
|
|
|
7255
7264
|
const baseViews = existingItem?.views || [];
|
|
7256
7265
|
const userViews = (itemOpt.views || []).map((uv) => {
|
|
7257
7266
|
const bv = baseViews.find((b) => b.slug === uv.slug);
|
|
7258
|
-
if (!bv) return uv;
|
|
7267
|
+
if (!bv) return { ...uv, access: void 0 };
|
|
7259
7268
|
const metricsScope = uv.metricsScope ?? bv.metricsScope;
|
|
7260
7269
|
let metrics = uv.metrics;
|
|
7261
7270
|
if (Array.isArray(uv.metrics) && Array.isArray(bv.metrics)) {
|
|
@@ -7276,6 +7285,7 @@ function reconcileNavigation(baseTree, prefs, schemas, options) {
|
|
|
7276
7285
|
return {
|
|
7277
7286
|
...bv,
|
|
7278
7287
|
...uv,
|
|
7288
|
+
access: bv.access,
|
|
7279
7289
|
metricsScope,
|
|
7280
7290
|
metrics
|
|
7281
7291
|
};
|
|
@@ -7312,10 +7322,12 @@ function reconcileNavigation(baseTree, prefs, schemas, options) {
|
|
|
7312
7322
|
group: group.name,
|
|
7313
7323
|
order: itemOpt.order ?? existingItem?.order ?? 100,
|
|
7314
7324
|
views: mergedViews,
|
|
7315
|
-
collection:
|
|
7316
|
-
global:
|
|
7317
|
-
|
|
7318
|
-
|
|
7325
|
+
collection: existingItem?.collection || itemOpt.collection,
|
|
7326
|
+
global: existingItem?.global || itemOpt.global,
|
|
7327
|
+
// Access and badges are policy/server-computed: only the server tree may set them.
|
|
7328
|
+
badge: existingItem?.badge,
|
|
7329
|
+
access: existingItem?.access,
|
|
7330
|
+
...existingItem ? existingItem.userDefined ? { userDefined: true } : {} : { userDefined: true }
|
|
7319
7331
|
};
|
|
7320
7332
|
if (existingIndex >= 0) {
|
|
7321
7333
|
group.items[existingIndex] = compiledItem;
|
|
@@ -7414,7 +7426,7 @@ function reconcileNavigation(baseTree, prefs, schemas, options) {
|
|
|
7414
7426
|
label: pin.label || match.label,
|
|
7415
7427
|
icon: pin.icon || match.icon
|
|
7416
7428
|
});
|
|
7417
|
-
} else {
|
|
7429
|
+
} else if (allowUserItems) {
|
|
7418
7430
|
pinnedItems.push({
|
|
7419
7431
|
id: `pin_${pin.slug}`,
|
|
7420
7432
|
type: pin.type,
|
|
@@ -7433,6 +7445,62 @@ function reconcileNavigation(baseTree, prefs, schemas, options) {
|
|
|
7433
7445
|
};
|
|
7434
7446
|
}
|
|
7435
7447
|
|
|
7448
|
+
// src/utils/navigation-policy.ts
|
|
7449
|
+
init_navigation();
|
|
7450
|
+
var isStringArray = (value) => Array.isArray(value) && value.every((v) => typeof v === "string");
|
|
7451
|
+
var itemKeys = (item) => [item.id, ...item.slug ? [item.slug] : []];
|
|
7452
|
+
var allTreeItems = (tree) => [
|
|
7453
|
+
...tree.groups.flatMap((g) => g.items),
|
|
7454
|
+
...tree.ungrouped
|
|
7455
|
+
];
|
|
7456
|
+
function sanitizeNavigationLayout(prefs, tree) {
|
|
7457
|
+
const known = new Set(allTreeItems(tree).flatMap(itemKeys));
|
|
7458
|
+
return {
|
|
7459
|
+
_version: CURRENT_NAV_PREFERENCES_VERSION,
|
|
7460
|
+
hidden: isStringArray(prefs?.hidden) ? prefs.hidden : [],
|
|
7461
|
+
groupOrder: isStringArray(prefs?.groupOrder) ? prefs.groupOrder : [],
|
|
7462
|
+
itemOrder: Object.fromEntries(
|
|
7463
|
+
Object.entries(prefs?.itemOrder ?? {}).filter(([, order]) => isStringArray(order))
|
|
7464
|
+
),
|
|
7465
|
+
groups: Array.isArray(prefs?.groups) ? prefs.groups : [],
|
|
7466
|
+
items: (Array.isArray(prefs?.items) ? prefs.items : []).filter((item) => {
|
|
7467
|
+
if (item.slug) return known.has(item.slug) || known.has(`workspace_${item.slug}`);
|
|
7468
|
+
if (item.collection) return known.has(`collection_${item.collection}`);
|
|
7469
|
+
if (item.global) return known.has(`global_${item.global}`);
|
|
7470
|
+
return false;
|
|
7471
|
+
})
|
|
7472
|
+
};
|
|
7473
|
+
}
|
|
7474
|
+
function parseNavigationRolePolicy(value, tree) {
|
|
7475
|
+
if (!value || typeof value !== "object") return null;
|
|
7476
|
+
const raw = value;
|
|
7477
|
+
if (typeof raw.enforced !== "boolean" || !isStringArray(raw.allowed)) return null;
|
|
7478
|
+
const layout2 = sanitizeNavigationLayout(raw.layout, tree);
|
|
7479
|
+
return {
|
|
7480
|
+
_version: CURRENT_NAV_PREFERENCES_VERSION,
|
|
7481
|
+
enforced: raw.enforced,
|
|
7482
|
+
allowed: raw.allowed,
|
|
7483
|
+
layout: {
|
|
7484
|
+
_version: layout2._version,
|
|
7485
|
+
hidden: layout2.hidden,
|
|
7486
|
+
groupOrder: layout2.groupOrder,
|
|
7487
|
+
itemOrder: layout2.itemOrder,
|
|
7488
|
+
groups: layout2.groups,
|
|
7489
|
+
items: layout2.items
|
|
7490
|
+
}
|
|
7491
|
+
};
|
|
7492
|
+
}
|
|
7493
|
+
function applyNavigationRolePolicies(tree, policies) {
|
|
7494
|
+
const enforced = policies.filter((p) => !!p?.enforced);
|
|
7495
|
+
if (policies.length === 0 || enforced.length !== policies.length) return tree;
|
|
7496
|
+
const allowed = new Set(enforced.flatMap((p) => p.allowed));
|
|
7497
|
+
const isAllowed = (item) => itemKeys(item).some((key) => allowed.has(key));
|
|
7498
|
+
const groups = tree.groups.map((g) => ({ ...g, items: g.items.filter(isAllowed) })).filter((g) => g.items.length > 0);
|
|
7499
|
+
const filtered = { groups, ungrouped: tree.ungrouped.filter(isAllowed) };
|
|
7500
|
+
const reconciled = reconcileNavigation(filtered, enforced[0].layout, void 0, { allowUserItems: false });
|
|
7501
|
+
return { groups: reconciled.groups, ungrouped: reconciled.ungrouped, locked: true };
|
|
7502
|
+
}
|
|
7503
|
+
|
|
7436
7504
|
// src/utils/navigation.ts
|
|
7437
7505
|
var RESERVED_NAVIGATION_SLUGS = /* @__PURE__ */ new Set([
|
|
7438
7506
|
"collections",
|
|
@@ -12052,7 +12120,7 @@ function layout({ preheader, title, content, footer, siteName }) {
|
|
|
12052
12120
|
${table(
|
|
12053
12121
|
row(
|
|
12054
12122
|
table(
|
|
12055
|
-
row(" ", `height:5px;background:${emailTokens.colors.accent};font-size:0;line-height:0`) + row(`${sectionLabel(siteName
|
|
12123
|
+
row(" ", `height:5px;background:${emailTokens.colors.accent};font-size:0;line-height:0`) + row(`${siteName?.trim() ? sectionLabel(siteName) : ""}${heading(title)}`, "padding:30px 32px 24px") + row(resolvedContent, "padding:0 32px 32px") + row(
|
|
12056
12124
|
`${divider()}${paragraph(resolvedFooter, "20px 0 6px")}${paragraph("Privacy: this message contains account-related information; please avoid forwarding it.", "10px 0px 0px 0px", "12px")}`,
|
|
12057
12125
|
"padding:0 32px 28px"
|
|
12058
12126
|
),
|
|
@@ -12067,14 +12135,15 @@ function layout({ preheader, title, content, footer, siteName }) {
|
|
|
12067
12135
|
</html>`;
|
|
12068
12136
|
}
|
|
12069
12137
|
function getDefaultEmailTemplate(purpose, options) {
|
|
12070
|
-
const siteName = options?.siteName ?? "
|
|
12138
|
+
const siteName = options?.siteName?.trim() ?? "";
|
|
12139
|
+
const account = siteName ? `${siteName} account` : "account";
|
|
12071
12140
|
switch (purpose) {
|
|
12072
12141
|
case "invite":
|
|
12073
12142
|
return {
|
|
12074
|
-
subject: `You've been invited to ${siteName}
|
|
12143
|
+
subject: siteName ? `You've been invited to ${siteName}` : "You've been invited",
|
|
12075
12144
|
rawHtml: layout({
|
|
12076
12145
|
siteName,
|
|
12077
|
-
preheader: `You've been invited to join ${siteName}
|
|
12146
|
+
preheader: siteName ? `You've been invited to join ${siteName}.` : "You've been invited to join.",
|
|
12078
12147
|
title: "You've been invited",
|
|
12079
12148
|
content: [
|
|
12080
12149
|
paragraph("Use the invitation link below to create your account. The link expires in 7 days."),
|
|
@@ -12091,7 +12160,7 @@ function getDefaultEmailTemplate(purpose, options) {
|
|
|
12091
12160
|
subject: "Reset your password",
|
|
12092
12161
|
rawHtml: layout({
|
|
12093
12162
|
siteName,
|
|
12094
|
-
preheader: `Reset your ${siteName} password
|
|
12163
|
+
preheader: siteName ? `Reset your ${siteName} password.` : "Reset your password.",
|
|
12095
12164
|
title: "Reset your password",
|
|
12096
12165
|
content: [
|
|
12097
12166
|
paragraph("We received a request to reset your password. The reset link expires in 1 hour."),
|
|
@@ -12105,10 +12174,10 @@ function getDefaultEmailTemplate(purpose, options) {
|
|
|
12105
12174
|
};
|
|
12106
12175
|
case "welcome":
|
|
12107
12176
|
return {
|
|
12108
|
-
subject: `Welcome to ${siteName} \u2014 your account is ready
|
|
12177
|
+
subject: siteName ? `Welcome to ${siteName} \u2014 your account is ready` : "Welcome \u2014 your account is ready",
|
|
12109
12178
|
rawHtml: layout({
|
|
12110
12179
|
siteName,
|
|
12111
|
-
preheader: `Your ${
|
|
12180
|
+
preheader: `Your ${account} is ready.`,
|
|
12112
12181
|
title: "Welcome \u2014 your account is ready",
|
|
12113
12182
|
content: [paragraph("Your account has been created. You can now log in with:"), detailBox("{{email}}")],
|
|
12114
12183
|
footer: "If you didn't create this account, you can safely ignore this email."
|
|
@@ -12119,7 +12188,7 @@ function getDefaultEmailTemplate(purpose, options) {
|
|
|
12119
12188
|
subject: "Your password has been changed",
|
|
12120
12189
|
rawHtml: layout({
|
|
12121
12190
|
siteName,
|
|
12122
|
-
preheader: `Your ${siteName} password was changed
|
|
12191
|
+
preheader: siteName ? `Your ${siteName} password was changed.` : "Your password was changed.",
|
|
12123
12192
|
title: "Password changed",
|
|
12124
12193
|
content: [
|
|
12125
12194
|
paragraph("The password for your account has been changed."),
|
|
@@ -12130,18 +12199,33 @@ function getDefaultEmailTemplate(purpose, options) {
|
|
|
12130
12199
|
};
|
|
12131
12200
|
default:
|
|
12132
12201
|
return {
|
|
12133
|
-
subject: `Notification from ${siteName}
|
|
12202
|
+
subject: siteName ? `Notification from ${siteName}` : "Account notification",
|
|
12134
12203
|
rawHtml: layout({
|
|
12135
12204
|
siteName,
|
|
12136
|
-
preheader: `Account notification from ${siteName}
|
|
12205
|
+
preheader: siteName ? `Account notification from ${siteName}.` : "Account notification.",
|
|
12137
12206
|
title: "Account Notification",
|
|
12138
|
-
content: [paragraph(`This is an account notification from ${siteName}.`)],
|
|
12207
|
+
content: [paragraph(siteName ? `This is an account notification from ${siteName}.` : "This is an account notification.")],
|
|
12139
12208
|
footer: "If you have any questions, please reach out to our team."
|
|
12140
12209
|
})
|
|
12141
12210
|
};
|
|
12142
12211
|
}
|
|
12143
12212
|
}
|
|
12144
12213
|
|
|
12214
|
+
// src/utils/app-name.ts
|
|
12215
|
+
var SUFFIX_SEPARATOR = /^[\s\-\u2013\u2014|\u00b7:]+/;
|
|
12216
|
+
function resolveAppName(admin) {
|
|
12217
|
+
const candidates = [
|
|
12218
|
+
admin?.branding?.appName,
|
|
12219
|
+
admin?.branding?.logoText,
|
|
12220
|
+
admin?.meta?.titleSuffix?.replace(SUFFIX_SEPARATOR, "")
|
|
12221
|
+
];
|
|
12222
|
+
for (const candidate of candidates) {
|
|
12223
|
+
const value = typeof candidate === "string" ? candidate.trim() : "";
|
|
12224
|
+
if (value) return value;
|
|
12225
|
+
}
|
|
12226
|
+
return "";
|
|
12227
|
+
}
|
|
12228
|
+
|
|
12145
12229
|
// src/services/email.service.ts
|
|
12146
12230
|
var _devSend = null;
|
|
12147
12231
|
var _devSendPromise = null;
|
|
@@ -12325,7 +12409,7 @@ function enrichArgs(config, collection, args) {
|
|
|
12325
12409
|
collection: slug,
|
|
12326
12410
|
collectionLabel: collection?.labels?.singular || collection?.labels?.plural || fallbackLabel,
|
|
12327
12411
|
collectionLabels: collection?.labels,
|
|
12328
|
-
siteName: config.admin
|
|
12412
|
+
siteName: resolveAppName(config.admin)
|
|
12329
12413
|
};
|
|
12330
12414
|
}
|
|
12331
12415
|
function toOutboundEmail(to, result, metadata) {
|
|
@@ -12361,7 +12445,7 @@ function buildWelcomeEmail(config, args, collection, dbOverride) {
|
|
|
12361
12445
|
subject: "Welcome \u2014 your account is ready",
|
|
12362
12446
|
html: layout({
|
|
12363
12447
|
siteName: enriched.siteName,
|
|
12364
|
-
preheader: `Your ${enriched.siteName} account is ready
|
|
12448
|
+
preheader: enriched.siteName ? `Your ${enriched.siteName} account is ready.` : "Your account is ready.",
|
|
12365
12449
|
title: "Welcome \u2014 your account is ready",
|
|
12366
12450
|
content: [
|
|
12367
12451
|
paragraph("Your account has been created. You can now log in with:"),
|
|
@@ -12386,7 +12470,7 @@ function buildInviteEmail(config, args, collection, dbOverride) {
|
|
|
12386
12470
|
subject: "You've been invited",
|
|
12387
12471
|
html: layout({
|
|
12388
12472
|
siteName: enriched.siteName,
|
|
12389
|
-
preheader: `You've been invited to join ${enriched.siteName}
|
|
12473
|
+
preheader: enriched.siteName ? `You've been invited to join ${enriched.siteName}.` : "You've been invited to join.",
|
|
12390
12474
|
title: "You've been invited",
|
|
12391
12475
|
content: [
|
|
12392
12476
|
args.invitedByEmail && paragraph(`You were invited by ${args.invitedByEmail}.`),
|
|
@@ -12421,7 +12505,7 @@ function buildResetPasswordEmail(config, args, collection, dbOverride) {
|
|
|
12421
12505
|
subject: "Reset your password",
|
|
12422
12506
|
html: layout({
|
|
12423
12507
|
siteName: enriched.siteName,
|
|
12424
|
-
preheader: `Reset your ${enriched.siteName} password
|
|
12508
|
+
preheader: enriched.siteName ? `Reset your ${enriched.siteName} password.` : "Reset your password.",
|
|
12425
12509
|
title: "Reset your password",
|
|
12426
12510
|
content: [
|
|
12427
12511
|
resetLink ? [
|
|
@@ -12453,7 +12537,7 @@ function buildPasswordChangedEmail(config, args, collection, dbOverride) {
|
|
|
12453
12537
|
subject: "Your password has been changed",
|
|
12454
12538
|
html: layout({
|
|
12455
12539
|
siteName: enriched.siteName,
|
|
12456
|
-
preheader: `Your ${enriched.siteName} password was changed
|
|
12540
|
+
preheader: enriched.siteName ? `Your ${enriched.siteName} password was changed.` : "Your password was changed.",
|
|
12457
12541
|
title: "Password changed",
|
|
12458
12542
|
content: [
|
|
12459
12543
|
paragraph("The password for this account was just changed:"),
|
|
@@ -14315,10 +14399,10 @@ function buildDyrectedSystemPrompt(context) {
|
|
|
14315
14399
|
return summary;
|
|
14316
14400
|
}).join("\n");
|
|
14317
14401
|
return `
|
|
14318
|
-
You are the
|
|
14402
|
+
You are the ${context.project?.name ? `${context.project.name} ` : ""}AI Assistant \u2014 an elite conversion copywriter, content strategist, and proactive editorial partner embedded natively inside this headless CMS.
|
|
14319
14403
|
|
|
14320
14404
|
### 1. CMS CONTEXT & PROJECT
|
|
14321
|
-
- Project: "${project.name}" (ID: ${project.id})
|
|
14405
|
+
- Project: "${project.name || project.id}" (ID: ${project.id})
|
|
14322
14406
|
- User: ${user.name || "Editor"} (Role: ${user.role || "editor"})
|
|
14323
14407
|
${globalPrompt ? `
|
|
14324
14408
|
### BRAND VOICE & DIRECTIVES
|
|
@@ -14413,7 +14497,7 @@ var AIAgent = class {
|
|
|
14413
14497
|
prompt: g.ai?.prompt
|
|
14414
14498
|
}));
|
|
14415
14499
|
const project = {
|
|
14416
|
-
name: this.config.admin
|
|
14500
|
+
name: resolveAppName(this.config.admin),
|
|
14417
14501
|
id: this.projectId
|
|
14418
14502
|
};
|
|
14419
14503
|
this.contextCache = {
|
|
@@ -15577,12 +15661,13 @@ function getGlobalTag(global) {
|
|
|
15577
15661
|
return `Global: ${global.label || global.slug}`;
|
|
15578
15662
|
}
|
|
15579
15663
|
function generateOpenApi(config) {
|
|
15664
|
+
const appName = resolveAppName(config.admin);
|
|
15580
15665
|
const spec = {
|
|
15581
15666
|
openapi: "3.0.0",
|
|
15582
15667
|
info: {
|
|
15583
|
-
title:
|
|
15668
|
+
title: appName ? `${appName} API` : "API",
|
|
15584
15669
|
version: "1.0.0",
|
|
15585
|
-
description: "Automatically generated OpenAPI specification for
|
|
15670
|
+
description: appName ? `Automatically generated OpenAPI specification for ${appName}.` : "Automatically generated OpenAPI specification for this project."
|
|
15586
15671
|
},
|
|
15587
15672
|
components: {
|
|
15588
15673
|
schemas: {
|
|
@@ -15693,7 +15778,7 @@ function generateOpenApi(config) {
|
|
|
15693
15778
|
spec.paths["/api/schemas"] = {
|
|
15694
15779
|
get: {
|
|
15695
15780
|
tags: ["System"],
|
|
15696
|
-
summary: "Get the serialized
|
|
15781
|
+
summary: "Get the serialized schema",
|
|
15697
15782
|
security: [],
|
|
15698
15783
|
responses: {
|
|
15699
15784
|
200: { description: "Collection and global schema definitions" }
|
|
@@ -16885,7 +16970,9 @@ function fieldToSchema(field) {
|
|
|
16885
16970
|
}
|
|
16886
16971
|
|
|
16887
16972
|
// src/utils/swagger.ts
|
|
16888
|
-
|
|
16973
|
+
var escapeTitle = (value) => value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
16974
|
+
function getSwaggerHtml(specUrl, options) {
|
|
16975
|
+
const title = [options?.appName?.trim(), "API Documentation"].filter(Boolean).join(" ");
|
|
16889
16976
|
return `
|
|
16890
16977
|
<!DOCTYPE html>
|
|
16891
16978
|
<html lang="en">
|
|
@@ -16893,7 +16980,7 @@ function getSwaggerHtml(specUrl) {
|
|
|
16893
16980
|
<meta charset="utf-8" />
|
|
16894
16981
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
16895
16982
|
<meta name="description" content="SwaggerUI" />
|
|
16896
|
-
<title
|
|
16983
|
+
<title>${escapeTitle(title)}</title>
|
|
16897
16984
|
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui.css" />
|
|
16898
16985
|
</head>
|
|
16899
16986
|
<body>
|
|
@@ -16939,6 +17026,7 @@ function getSwaggerHtml(specUrl) {
|
|
|
16939
17026
|
}
|
|
16940
17027
|
|
|
16941
17028
|
// src/router.ts
|
|
17029
|
+
init_navigation();
|
|
16942
17030
|
init_access2();
|
|
16943
17031
|
init_access_control();
|
|
16944
17032
|
init_trash();
|
|
@@ -17493,7 +17581,7 @@ function registerRoutes(app, config) {
|
|
|
17493
17581
|
return c.json(generateOpenApi(config));
|
|
17494
17582
|
});
|
|
17495
17583
|
app.get("/api/docs", (c) => {
|
|
17496
|
-
return c.html(getSwaggerHtml());
|
|
17584
|
+
return c.html(getSwaggerHtml(void 0, { appName: resolveAppName(config.admin) }));
|
|
17497
17585
|
});
|
|
17498
17586
|
app.get("/api/preferences/:key", optionalAuth(config), async (c) => {
|
|
17499
17587
|
const db = config.db;
|
|
@@ -17569,6 +17657,24 @@ function registerRoutes(app, config) {
|
|
|
17569
17657
|
if (!user?.collection || !user.sub) return c.json({ error: true, message: "Authentication required." }, 401);
|
|
17570
17658
|
if (!key) return c.json({ error: true, message: "Preference key is required." }, 400);
|
|
17571
17659
|
const body = await c.req.json().catch(() => ({}));
|
|
17660
|
+
if (key === NAVIGATION_POLICY_KEY) {
|
|
17661
|
+
if (scope !== "role") {
|
|
17662
|
+
return c.json(
|
|
17663
|
+
{ error: true, message: "Navigation policies are stored per role (scope=role)." },
|
|
17664
|
+
400
|
|
17665
|
+
);
|
|
17666
|
+
}
|
|
17667
|
+
const policy = parseNavigationRolePolicy(body.value, compileNavigation(config));
|
|
17668
|
+
if (!policy) {
|
|
17669
|
+
return c.json(
|
|
17670
|
+
{ error: true, message: "Invalid navigation policy: expected { enforced: boolean, allowed: string[] }." },
|
|
17671
|
+
400
|
|
17672
|
+
);
|
|
17673
|
+
}
|
|
17674
|
+
body.value = policy;
|
|
17675
|
+
} else if (key === "admin:navigation" && (scope === "role" || scope === "global")) {
|
|
17676
|
+
body.value = sanitizeNavigationLayout(body.value, compileNavigation(config));
|
|
17677
|
+
}
|
|
17572
17678
|
if (scope === "global") {
|
|
17573
17679
|
const userCol = config.collections?.find((col) => col.slug === user?.collection);
|
|
17574
17680
|
const isAdminUser = isUserAdmin(user, userCol);
|
|
@@ -17709,8 +17815,8 @@ function registerRoutes(app, config) {
|
|
|
17709
17815
|
});
|
|
17710
17816
|
return c.json({ success: true });
|
|
17711
17817
|
});
|
|
17712
|
-
const createNavAccessChecker = (c) => {
|
|
17713
|
-
const user = c.get("user");
|
|
17818
|
+
const createNavAccessChecker = (c, userOverride) => {
|
|
17819
|
+
const user = userOverride ?? c.get("user");
|
|
17714
17820
|
const roles = /* @__PURE__ */ new Set([...user?.role ? [user.role] : [], ...Array.isArray(user?.roles) ? user.roles : []]);
|
|
17715
17821
|
const args = { user, req: toHookRequestContext(c.req) };
|
|
17716
17822
|
const allows = async (rule) => {
|
|
@@ -17728,11 +17834,47 @@ function registerRoutes(app, config) {
|
|
|
17728
17834
|
canAccessView: (view) => allows(view.access?.read)
|
|
17729
17835
|
};
|
|
17730
17836
|
};
|
|
17731
|
-
|
|
17837
|
+
const loadNavigationPolicies = async (user, compiled) => {
|
|
17838
|
+
const db = config.db;
|
|
17839
|
+
const roles = [
|
|
17840
|
+
.../* @__PURE__ */ new Set([
|
|
17841
|
+
...typeof user?.role === "string" ? [user.role] : [],
|
|
17842
|
+
...Array.isArray(user?.roles) ? user.roles : []
|
|
17843
|
+
])
|
|
17844
|
+
];
|
|
17845
|
+
if (!db) return [];
|
|
17846
|
+
return Promise.all(
|
|
17847
|
+
roles.map(async (role) => {
|
|
17848
|
+
try {
|
|
17849
|
+
const doc = await db.findOne({
|
|
17850
|
+
collection: "__role_preferences",
|
|
17851
|
+
id: `${role}_${NAVIGATION_POLICY_KEY}`
|
|
17852
|
+
});
|
|
17853
|
+
return doc ? parseNavigationRolePolicy(doc.value, compiled) : null;
|
|
17854
|
+
} catch {
|
|
17855
|
+
return null;
|
|
17856
|
+
}
|
|
17857
|
+
})
|
|
17858
|
+
);
|
|
17859
|
+
};
|
|
17860
|
+
const resolveNavigationForUser = async (c, user, options) => {
|
|
17732
17861
|
const compiled = compileNavigation(config);
|
|
17733
|
-
const { canAccess, canAccessView } = createNavAccessChecker(c);
|
|
17862
|
+
const { canAccess, canAccessView } = createNavAccessChecker(c, user);
|
|
17734
17863
|
const pruned = await pruneNavigationByAccess(compiled, canAccess, canAccessView);
|
|
17735
|
-
return
|
|
17864
|
+
if (options?.ignorePolicy) return pruned;
|
|
17865
|
+
return applyNavigationRolePolicies(pruned, await loadNavigationPolicies(user ?? c.get("user"), compiled));
|
|
17866
|
+
};
|
|
17867
|
+
app.get("/api/admin/navigation", optionalAuth(config), async (c) => {
|
|
17868
|
+
const asRole = c.req.query("asRole");
|
|
17869
|
+
if (!asRole) return c.json(await resolveNavigationForUser(c));
|
|
17870
|
+
const user = c.get("user");
|
|
17871
|
+
if (!user) return c.json({ error: true, message: "Authentication required." }, 401);
|
|
17872
|
+
const userCol = config.collections?.find((col) => col.slug === user.collection);
|
|
17873
|
+
if (!isUserAdmin(user, userCol)) {
|
|
17874
|
+
return c.json({ error: true, message: "Only administrators can view navigation as another role." }, 403);
|
|
17875
|
+
}
|
|
17876
|
+
const asUser = { ...user, role: asRole, roles: [asRole] };
|
|
17877
|
+
return c.json(await resolveNavigationForUser(c, asUser, { ignorePolicy: c.req.query("raw") === "true" }));
|
|
17736
17878
|
});
|
|
17737
17879
|
app.on(["GET", "POST"], "/api/admin/navigation/badges", optionalAuth(config), async (c) => {
|
|
17738
17880
|
const db = config.db;
|
|
@@ -17763,6 +17905,25 @@ function registerRoutes(app, config) {
|
|
|
17763
17905
|
} catch {
|
|
17764
17906
|
}
|
|
17765
17907
|
}
|
|
17908
|
+
if (!userNavPrefs) {
|
|
17909
|
+
const userRoles = [
|
|
17910
|
+
...typeof user?.role === "string" ? [user.role] : [],
|
|
17911
|
+
...Array.isArray(user?.roles) ? user.roles : []
|
|
17912
|
+
];
|
|
17913
|
+
for (const role of userRoles) {
|
|
17914
|
+
try {
|
|
17915
|
+
const roleDoc = await db.findOne({
|
|
17916
|
+
collection: "__role_preferences",
|
|
17917
|
+
id: `${role}_admin:navigation`
|
|
17918
|
+
});
|
|
17919
|
+
if (roleDoc?.value) {
|
|
17920
|
+
userNavPrefs = roleDoc.value;
|
|
17921
|
+
break;
|
|
17922
|
+
}
|
|
17923
|
+
} catch {
|
|
17924
|
+
}
|
|
17925
|
+
}
|
|
17926
|
+
}
|
|
17766
17927
|
if (!userNavPrefs) {
|
|
17767
17928
|
try {
|
|
17768
17929
|
const globalDoc = await db.findOne({
|
|
@@ -17775,28 +17936,31 @@ function registerRoutes(app, config) {
|
|
|
17775
17936
|
} catch {
|
|
17776
17937
|
}
|
|
17777
17938
|
}
|
|
17939
|
+
const baseTree = await resolveNavigationForUser(c);
|
|
17778
17940
|
let allItems;
|
|
17779
17941
|
if (userNavPrefs) {
|
|
17780
|
-
const reconciled = reconcileNavigation(
|
|
17781
|
-
|
|
17782
|
-
|
|
17783
|
-
|
|
17942
|
+
const reconciled = reconcileNavigation(
|
|
17943
|
+
baseTree,
|
|
17944
|
+
userNavPrefs,
|
|
17945
|
+
{
|
|
17946
|
+
collections: config.collections,
|
|
17947
|
+
globals: config.globals
|
|
17948
|
+
},
|
|
17949
|
+
{ allowUserItems: !baseTree.locked }
|
|
17950
|
+
);
|
|
17784
17951
|
allItems = [
|
|
17785
17952
|
...reconciled.ungrouped || [],
|
|
17786
17953
|
...reconciled.pinnedItems || [],
|
|
17787
17954
|
...(reconciled.groups || []).flatMap((g) => g.items || [])
|
|
17788
17955
|
];
|
|
17789
|
-
} else {
|
|
17790
|
-
const pruned = await pruneNavigationByAccess(compiled, createNavAccessChecker(c).canAccess);
|
|
17791
|
-
allItems = [
|
|
17792
|
-
...pruned.ungrouped,
|
|
17793
|
-
...pruned.groups.flatMap((g) => g.items)
|
|
17794
|
-
];
|
|
17795
|
-
}
|
|
17796
|
-
if (userNavPrefs) {
|
|
17797
17956
|
const { canAccess } = createNavAccessChecker(c);
|
|
17798
17957
|
const allowed = await Promise.all(allItems.map((item) => canAccess(item)));
|
|
17799
17958
|
allItems = allItems.filter((_, i) => allowed[i]);
|
|
17959
|
+
} else {
|
|
17960
|
+
allItems = [
|
|
17961
|
+
...baseTree.ungrouped,
|
|
17962
|
+
...baseTree.groups.flatMap((g) => g.items)
|
|
17963
|
+
];
|
|
17800
17964
|
}
|
|
17801
17965
|
const badges = {};
|
|
17802
17966
|
const requestsByCollection = /* @__PURE__ */ new Map();
|
|
@@ -18081,7 +18245,7 @@ function registerRoutes(app, config) {
|
|
|
18081
18245
|
if (!to || typeof to !== "string" || !to.includes("@")) {
|
|
18082
18246
|
return c.json({ error: true, message: "A valid recipient email address is required" }, 400);
|
|
18083
18247
|
}
|
|
18084
|
-
const siteName = config.admin
|
|
18248
|
+
const siteName = resolveAppName(config.admin);
|
|
18085
18249
|
let appUrl = config.admin?.siteUrl;
|
|
18086
18250
|
if (!appUrl) {
|
|
18087
18251
|
try {
|
|
@@ -18708,7 +18872,7 @@ var EMAIL_TEMPLATES_COLLECTION_CONFIG = {
|
|
|
18708
18872
|
beforeChange: [
|
|
18709
18873
|
async ({ data, req }) => {
|
|
18710
18874
|
if (data && data.format === "html") {
|
|
18711
|
-
const siteName = req?.config?.admin
|
|
18875
|
+
const siteName = resolveAppName(req?.config?.admin);
|
|
18712
18876
|
const defaults = getDefaultEmailTemplate(data.purpose || "invite", { siteName });
|
|
18713
18877
|
if (!data.subject) {
|
|
18714
18878
|
data.subject = defaults.subject;
|
package/dist/server.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { O as DyrectedContext, D as DyrectedConfig, C as CollectionConfig, G as GlobalConfig,
|
|
2
|
-
export {
|
|
1
|
+
import { O as DyrectedContext, D as DyrectedConfig, C as CollectionConfig, G as GlobalConfig, cd as EmailTemplateArgs, ce as EmailTemplateResult, d9 as OutboundEmail, e as DatabaseAdapter, b as AuthenticatedUser, c9 as DyrectedAIContext, aA as AIThread, aw as AIMessage, dh as RAGSearchResult, az as AIRateLimitStore, ay as AIRateLimitResult } from './app-7SuKzu5w.cjs';
|
|
2
|
+
export { ea as CollectionTokenPayload, eb as DEFAULT_INVITE_EXPIRY, ec as DEFAULT_RESET_PASSWORD_EXPIRY, ed as DEFAULT_SESSION_EXPIRY, ee as DyrectedObservabilityRuntime, ef as RequestBodyCapture, eg as RequestTraceContext, eh as ResolvedObservabilityConfig, ei as attachRequestMetrics, ej as bindObservabilityRuntime, ek as buildRequestLogger, el as captureRequestBody, em as createDyrectedApp, en as createObservabilityRuntime, eo as createRequestSpan, ep as decodeCollectionToken, eq as endRequestSpan, er as getConfigLogger, es as getObservabilityRuntime, et as getRequestLogger, eu as redactBody, ev as redactHeaders, ew as renderPrometheusMetrics, ex as resolveInviteTokenExpiry, ey as resolveObservabilityConfig, ez as resolveResetPasswordTokenExpiry, eA as resolveSessionTokenExpiry, eB as shouldSampleRequest, eC as signCollectionToken, eD as verifyCollectionToken } from './app-7SuKzu5w.cjs';
|
|
3
3
|
import * as hono from 'hono';
|
|
4
4
|
import { Hono, Context, Next } from 'hono';
|
|
5
5
|
import * as hono_utils_http_status from 'hono/utils/http-status';
|
package/dist/server.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { O as DyrectedContext, D as DyrectedConfig, C as CollectionConfig, G as GlobalConfig,
|
|
2
|
-
export {
|
|
1
|
+
import { O as DyrectedContext, D as DyrectedConfig, C as CollectionConfig, G as GlobalConfig, cd as EmailTemplateArgs, ce as EmailTemplateResult, d9 as OutboundEmail, e as DatabaseAdapter, b as AuthenticatedUser, c9 as DyrectedAIContext, aA as AIThread, aw as AIMessage, dh as RAGSearchResult, az as AIRateLimitStore, ay as AIRateLimitResult } from './app-7SuKzu5w.js';
|
|
2
|
+
export { ea as CollectionTokenPayload, eb as DEFAULT_INVITE_EXPIRY, ec as DEFAULT_RESET_PASSWORD_EXPIRY, ed as DEFAULT_SESSION_EXPIRY, ee as DyrectedObservabilityRuntime, ef as RequestBodyCapture, eg as RequestTraceContext, eh as ResolvedObservabilityConfig, ei as attachRequestMetrics, ej as bindObservabilityRuntime, ek as buildRequestLogger, el as captureRequestBody, em as createDyrectedApp, en as createObservabilityRuntime, eo as createRequestSpan, ep as decodeCollectionToken, eq as endRequestSpan, er as getConfigLogger, es as getObservabilityRuntime, et as getRequestLogger, eu as redactBody, ev as redactHeaders, ew as renderPrometheusMetrics, ex as resolveInviteTokenExpiry, ey as resolveObservabilityConfig, ez as resolveResetPasswordTokenExpiry, eA as resolveSessionTokenExpiry, eB as shouldSampleRequest, eC as signCollectionToken, eD as verifyCollectionToken } from './app-7SuKzu5w.js';
|
|
3
3
|
import * as hono from 'hono';
|
|
4
4
|
import { Hono, Context, Next } from 'hono';
|
|
5
5
|
import * as hono_utils_http_status from 'hono/utils/http-status';
|