@actuate-media/cms-core 0.35.1 → 0.36.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.
Files changed (40) hide show
  1. package/dist/__tests__/api/appearance-routes.test.d.ts +2 -0
  2. package/dist/__tests__/api/appearance-routes.test.d.ts.map +1 -0
  3. package/dist/__tests__/api/appearance-routes.test.js +134 -0
  4. package/dist/__tests__/api/appearance-routes.test.js.map +1 -0
  5. package/dist/__tests__/appearance/appearance.test.d.ts +2 -0
  6. package/dist/__tests__/appearance/appearance.test.d.ts.map +1 -0
  7. package/dist/__tests__/appearance/appearance.test.js +273 -0
  8. package/dist/__tests__/appearance/appearance.test.js.map +1 -0
  9. package/dist/api/handlers.d.ts.map +1 -1
  10. package/dist/api/handlers.js +61 -0
  11. package/dist/api/handlers.js.map +1 -1
  12. package/dist/appearance/color.d.ts +24 -0
  13. package/dist/appearance/color.d.ts.map +1 -0
  14. package/dist/appearance/color.js +90 -0
  15. package/dist/appearance/color.js.map +1 -0
  16. package/dist/appearance/index.d.ts +13 -0
  17. package/dist/appearance/index.d.ts.map +1 -0
  18. package/dist/appearance/index.js +5 -0
  19. package/dist/appearance/index.js.map +1 -0
  20. package/dist/appearance/logo-shape.d.ts +26 -0
  21. package/dist/appearance/logo-shape.d.ts.map +1 -0
  22. package/dist/appearance/logo-shape.js +62 -0
  23. package/dist/appearance/logo-shape.js.map +1 -0
  24. package/dist/appearance/resolve.d.ts +46 -0
  25. package/dist/appearance/resolve.d.ts.map +1 -0
  26. package/dist/appearance/resolve.js +208 -0
  27. package/dist/appearance/resolve.js.map +1 -0
  28. package/dist/appearance/theme-tokens.d.ts +50 -0
  29. package/dist/appearance/theme-tokens.d.ts.map +1 -0
  30. package/dist/appearance/theme-tokens.js +198 -0
  31. package/dist/appearance/theme-tokens.js.map +1 -0
  32. package/dist/appearance/types.d.ts +110 -0
  33. package/dist/appearance/types.d.ts.map +1 -0
  34. package/dist/appearance/types.js +16 -0
  35. package/dist/appearance/types.js.map +1 -0
  36. package/dist/index.d.ts +2 -0
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +2 -0
  39. package/dist/index.js.map +1 -1
  40. package/package.json +6 -1
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=appearance-routes.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"appearance-routes.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/api/appearance-routes.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,134 @@
1
+ import { beforeEach, describe, expect, it } from 'vitest';
2
+ import { handleActuateAPI } from '../../api/index.js';
3
+ import { initDB } from '../../db.js';
4
+ import { generateApiKey } from '../../security/api-key-enhanced.js';
5
+ /**
6
+ * Contract tests for the appearance/brand endpoints:
7
+ * - GET /public/brand → public, exposes ONLY the public brand + favicon.
8
+ * - GET /appearance → authed, exposes stored settings + resolved views.
9
+ *
10
+ * Pins the response shapes the public frontend and admin Appearance tab depend
11
+ * on, and proves the public route never leaks the admin-only brand.
12
+ */
13
+ const VALID_SECRET = 'a'.repeat(48);
14
+ const SETTINGS_BLOB = {
15
+ siteTitle: 'Actuate Media',
16
+ brand: {
17
+ mode: 'customAdminBrand',
18
+ publicPrimaryLogoAssetId: 'pub-logo',
19
+ adminPrimaryLogoAssetId: 'admin-logo',
20
+ faviconSourceAssetId: 'fav',
21
+ updatedAt: '2026-02-03T00:00:00.000Z',
22
+ },
23
+ appearance: { adminAccentColor: 'teal', sidebarDensity: 'compact', reduceMotion: true },
24
+ };
25
+ const MEDIA_ROWS = [
26
+ {
27
+ id: 'pub-logo',
28
+ publicUrl: 'https://cdn.example.com/pub.svg',
29
+ mimeType: 'image/svg+xml',
30
+ width: 400,
31
+ height: 100,
32
+ },
33
+ {
34
+ id: 'admin-logo',
35
+ publicUrl: 'https://cdn.example.com/admin.svg',
36
+ mimeType: 'image/svg+xml',
37
+ width: 380,
38
+ height: 90,
39
+ },
40
+ {
41
+ id: 'fav',
42
+ publicUrl: 'https://cdn.example.com/fav.png',
43
+ mimeType: 'image/png',
44
+ width: 32,
45
+ height: 32,
46
+ },
47
+ ];
48
+ function createMockDB(opts = {}) {
49
+ const apiKey = opts.apiKey;
50
+ return {
51
+ apiKey: {
52
+ findMany: async () => (apiKey ? [apiKey] : []),
53
+ findUnique: async ({ where }) => apiKey && apiKey.keyHash === where.keyHash ? apiKey : null,
54
+ update: async () => apiKey,
55
+ },
56
+ document: {
57
+ findFirst: async () => ({ data: SETTINGS_BLOB }),
58
+ findMany: async () => [],
59
+ count: async () => 0,
60
+ },
61
+ media: {
62
+ findMany: async () => MEDIA_ROWS,
63
+ },
64
+ user: {},
65
+ session: {},
66
+ };
67
+ }
68
+ async function makeKey(scopes) {
69
+ const { key, keyHash, keyPrefix } = await generateApiKey({ prefix: 'act_sk', scopes });
70
+ return {
71
+ key,
72
+ record: {
73
+ id: 'apikey-1',
74
+ keyHash,
75
+ keyPrefix,
76
+ userId: 'user-1',
77
+ scopes,
78
+ ipRestrictions: null,
79
+ expiresAt: null,
80
+ lastUsedAt: null,
81
+ revokedAt: null,
82
+ },
83
+ };
84
+ }
85
+ describe('GET /public/brand', () => {
86
+ beforeEach(() => {
87
+ delete globalThis.__actuateConfig;
88
+ process.env.CMS_SECRET = VALID_SECRET;
89
+ });
90
+ it('returns the public brand + favicon without auth and hides the admin brand', async () => {
91
+ const db = createMockDB();
92
+ initDB(db);
93
+ const handler = handleActuateAPI({ prismaClient: db, config: { collections: {} } });
94
+ const res = await handler(new Request('https://example.com/api/cms/public/brand'));
95
+ expect(res.status).toBe(200);
96
+ const body = (await res.json());
97
+ expect(body.data.public.primary.url).toBe('https://cdn.example.com/pub.svg');
98
+ expect(body.data.favicon.icon).toContain('https://cdn.example.com/fav.png');
99
+ // The admin-only brand must NOT be exposed publicly.
100
+ expect(body.data.admin).toBeUndefined();
101
+ expect(JSON.stringify(body)).not.toContain('admin.svg');
102
+ });
103
+ });
104
+ describe('GET /appearance', () => {
105
+ beforeEach(() => {
106
+ delete globalThis.__actuateConfig;
107
+ process.env.CMS_SECRET = VALID_SECRET;
108
+ });
109
+ it('requires authentication', async () => {
110
+ const db = createMockDB();
111
+ initDB(db);
112
+ const handler = handleActuateAPI({ prismaClient: db, config: { collections: {} } });
113
+ const res = await handler(new Request('https://example.com/api/cms/appearance'));
114
+ expect(res.status).toBe(401);
115
+ });
116
+ it('returns stored appearance + brand + resolved admin/public views when authed', async () => {
117
+ const { key, record } = await makeKey({ admin: true });
118
+ const db = createMockDB({ apiKey: record });
119
+ initDB(db);
120
+ const handler = handleActuateAPI({ prismaClient: db, config: { collections: {} } });
121
+ const res = await handler(new Request('https://example.com/api/cms/appearance', {
122
+ headers: { Authorization: `Bearer ${key}` },
123
+ }));
124
+ expect(res.status).toBe(200);
125
+ const body = (await res.json());
126
+ expect(body.data.appearance.adminAccentColor).toBe('teal');
127
+ expect(body.data.appearance.sidebarDensity).toBe('compact');
128
+ expect(body.data.brand.mode).toBe('customAdminBrand');
129
+ // customAdminBrand → admin primary resolves to the admin asset.
130
+ expect(body.data.resolved.admin.primary.url).toBe('https://cdn.example.com/admin.svg');
131
+ expect(body.data.resolved.public.primary.url).toBe('https://cdn.example.com/pub.svg');
132
+ });
133
+ });
134
+ //# sourceMappingURL=appearance-routes.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"appearance-routes.test.js","sourceRoot":"","sources":["../../../src/__tests__/api/appearance-routes.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=appearance.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"appearance.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/appearance/appearance.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,273 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { classifyLogoShape, logoShapeWarning, contrastRatio, isParseableColor, validateAccentColor, generateAdminThemeTokens, accentTokensToCssVars, ACCENT_PRESETS, siteInitials, resolveAdminBrand, resolvePublicBrand, getFaviconMetadata, parseAppearance, parseBrand, brandAssetIds, resolveBrand, DEFAULT_APPEARANCE, } from '../../appearance/index.js';
3
+ describe('classifyLogoShape', () => {
4
+ it('classifies by aspect ratio', () => {
5
+ expect(classifyLogoShape(100, 100)).toBe('square');
6
+ expect(classifyLogoShape(120, 100)).toBe('square'); // ratio 1.2
7
+ expect(classifyLogoShape(400, 100)).toBe('horizontal'); // ratio 4
8
+ expect(classifyLogoShape(100, 400)).toBe('vertical'); // ratio 0.25
9
+ expect(classifyLogoShape(160, 100)).toBe('neutral'); // ratio 1.6
10
+ });
11
+ it('returns unknown for missing/invalid dimensions', () => {
12
+ expect(classifyLogoShape(undefined, undefined)).toBe('unknown');
13
+ expect(classifyLogoShape(0, 100)).toBe('unknown');
14
+ expect(classifyLogoShape(100, null)).toBe('unknown');
15
+ });
16
+ });
17
+ describe('logoShapeWarning', () => {
18
+ it('warns when a square is used as a primary logo', () => {
19
+ expect(logoShapeWarning('publicPrimaryLogo', 'square')).toMatch(/square mark/i);
20
+ });
21
+ it('warns when a horizontal is used as a logo mark', () => {
22
+ expect(logoShapeWarning('adminLogoMark', 'horizontal')).toMatch(/square mark works better/i);
23
+ });
24
+ it('warns when a non-square favicon source is uploaded', () => {
25
+ expect(logoShapeWarning('faviconSource', 'horizontal')).toMatch(/not square/i);
26
+ });
27
+ it('warns about vertical logos in wide slots', () => {
28
+ expect(logoShapeWarning('publicPrimaryLogo', 'vertical')).toMatch(/vertical/i);
29
+ });
30
+ it('is silent for good fits and unknown shapes', () => {
31
+ expect(logoShapeWarning('publicPrimaryLogo', 'horizontal')).toBeNull();
32
+ expect(logoShapeWarning('publicLogoMark', 'square')).toBeNull();
33
+ expect(logoShapeWarning('publicPrimaryLogo', 'unknown')).toBeNull();
34
+ });
35
+ });
36
+ describe('color + contrast', () => {
37
+ it('computes the canonical black/white contrast as 21:1', () => {
38
+ expect(contrastRatio('#000000', '#ffffff')).toBeCloseTo(21, 0);
39
+ });
40
+ it('returns 1 for identical colors', () => {
41
+ expect(contrastRatio('#ffffff', '#ffffff')).toBeCloseTo(1, 5);
42
+ });
43
+ it('parses oklch and hex', () => {
44
+ expect(isParseableColor('oklch(0.54 0.23 293)')).toBe(true);
45
+ expect(isParseableColor('#abc')).toBe(true);
46
+ expect(isParseableColor('rebeccapurple')).toBe(false);
47
+ expect(isParseableColor('')).toBe(false);
48
+ });
49
+ it('ranks a dark oklch accent as high contrast against white', () => {
50
+ // The purple preset on white should clear the 3:1 UI bar comfortably.
51
+ expect(contrastRatio('oklch(0.54 0.23 293)', '#ffffff')).toBeGreaterThan(3);
52
+ });
53
+ });
54
+ describe('validateAccentColor', () => {
55
+ it('accepts every preset', () => {
56
+ for (const name of Object.keys(ACCENT_PRESETS)) {
57
+ expect(validateAccentColor(name).valid).toBe(true);
58
+ }
59
+ });
60
+ it('rejects an unparseable color', () => {
61
+ const r = validateAccentColor('not-a-color');
62
+ expect(r.valid).toBe(false);
63
+ expect(r.reason).toMatch(/valid hex or oklch/i);
64
+ });
65
+ it('rejects a custom color that fails contrast against white', () => {
66
+ const r = validateAccentColor('#fff8dc'); // near-white, low contrast
67
+ expect(r.valid).toBe(false);
68
+ expect(r.reason).toMatch(/below the 3:1/i);
69
+ });
70
+ it('accepts a sufficiently dark custom color', () => {
71
+ expect(validateAccentColor('#0a3d62').valid).toBe(true);
72
+ });
73
+ });
74
+ describe('generateAdminThemeTokens', () => {
75
+ it('returns preset tokens for light and dark', () => {
76
+ expect(generateAdminThemeTokens({ adminAccentColor: 'blue' }, 'light')).toEqual(ACCENT_PRESETS.blue.light);
77
+ expect(generateAdminThemeTokens({ adminAccentColor: 'blue' }, 'dark')).toEqual(ACCENT_PRESETS.blue.dark);
78
+ });
79
+ it('drives tokens from a custom color', () => {
80
+ const tokens = generateAdminThemeTokens({ adminAccentColor: '#0a3d62' }, 'light');
81
+ expect(tokens.brand).toBe('#0a3d62');
82
+ expect(tokens.primary).toBe('#0a3d62');
83
+ });
84
+ it('falls back to purple for an unknown name', () => {
85
+ expect(generateAdminThemeTokens({ adminAccentColor: 'chartreuse' }, 'light')).toEqual(ACCENT_PRESETS.purple.light);
86
+ });
87
+ it('maps tokens to the expected CSS variables', () => {
88
+ const vars = accentTokensToCssVars(ACCENT_PRESETS.purple.light);
89
+ expect(Object.keys(vars).sort()).toEqual([
90
+ '--brand',
91
+ '--brand-foreground',
92
+ '--primary',
93
+ '--primary-foreground',
94
+ '--ring',
95
+ '--sidebar-primary',
96
+ '--sidebar-primary-foreground',
97
+ ].sort());
98
+ });
99
+ });
100
+ describe('siteInitials', () => {
101
+ it('derives up to two initials', () => {
102
+ expect(siteInitials('Actuate Media')).toBe('AM');
103
+ expect(siteInitials('Actuate')).toBe('AC');
104
+ expect(siteInitials('')).toBe('A');
105
+ expect(siteInitials(undefined)).toBe('A');
106
+ });
107
+ });
108
+ function assetMap(...assets) {
109
+ return new Map(assets.map((a) => [a.id, a]));
110
+ }
111
+ const HORIZONTAL = {
112
+ id: 'primary-1',
113
+ publicUrl: 'https://cdn.example.com/logo.svg',
114
+ mimeType: 'image/svg+xml',
115
+ width: 400,
116
+ height: 100,
117
+ };
118
+ const MARK = {
119
+ id: 'mark-1',
120
+ publicUrl: 'https://cdn.example.com/mark.svg',
121
+ mimeType: 'image/svg+xml',
122
+ width: 64,
123
+ height: 64,
124
+ };
125
+ const FAVICON = {
126
+ id: 'fav-1',
127
+ publicUrl: 'https://cdn.example.com/favicon.png',
128
+ mimeType: 'image/png',
129
+ width: 32,
130
+ height: 32,
131
+ };
132
+ describe('resolveAdminBrand', () => {
133
+ it('inherits the public brand by default', () => {
134
+ const brand = {
135
+ mode: 'inheritPublicBrand',
136
+ publicPrimaryLogoAssetId: 'primary-1',
137
+ publicLogoMarkAssetId: 'mark-1',
138
+ };
139
+ const r = resolveAdminBrand(brand, assetMap(HORIZONTAL, MARK), 'Actuate Media');
140
+ expect(r.inherited).toBe(true);
141
+ expect(r.primary?.url).toBe(HORIZONTAL.publicUrl);
142
+ expect(r.mark?.url).toBe(MARK.publicUrl);
143
+ expect(r.primary?.shape).toBe('horizontal');
144
+ expect(r.initials).toBe('AM');
145
+ });
146
+ it('prefers admin assets in customAdminBrand mode, falling back to public', () => {
147
+ const adminPrimary = { ...HORIZONTAL, id: 'admin-primary' };
148
+ const brand = {
149
+ mode: 'customAdminBrand',
150
+ adminPrimaryLogoAssetId: 'admin-primary',
151
+ publicLogoMarkAssetId: 'mark-1',
152
+ };
153
+ const r = resolveAdminBrand(brand, assetMap(adminPrimary, MARK), 'Actuate Media');
154
+ expect(r.inherited).toBe(false);
155
+ expect(r.primary?.url).toBe(adminPrimary.publicUrl);
156
+ // No admin mark → falls back to the public mark.
157
+ expect(r.mark?.url).toBe(MARK.publicUrl);
158
+ });
159
+ it('falls back the collapsed mark to the favicon source', () => {
160
+ const brand = {
161
+ mode: 'inheritPublicBrand',
162
+ publicPrimaryLogoAssetId: 'primary-1',
163
+ faviconSourceAssetId: 'fav-1',
164
+ };
165
+ const r = resolveAdminBrand(brand, assetMap(HORIZONTAL, FAVICON), 'Actuate Media');
166
+ expect(r.mark?.url).toBe(FAVICON.publicUrl);
167
+ });
168
+ it('returns null logos and initials when nothing is configured', () => {
169
+ const r = resolveAdminBrand({ mode: 'inheritPublicBrand' }, assetMap(), 'Globex Corp');
170
+ expect(r.primary).toBeNull();
171
+ expect(r.mark).toBeNull();
172
+ expect(r.initials).toBe('GC');
173
+ });
174
+ });
175
+ describe('resolvePublicBrand', () => {
176
+ it('resolves public assets and mark fallback', () => {
177
+ const brand = {
178
+ mode: 'customAdminBrand', // irrelevant to public resolution
179
+ publicPrimaryLogoAssetId: 'primary-1',
180
+ faviconSourceAssetId: 'fav-1',
181
+ };
182
+ const r = resolvePublicBrand(brand, assetMap(HORIZONTAL, FAVICON), 'Actuate Media');
183
+ expect(r.primary?.url).toBe(HORIZONTAL.publicUrl);
184
+ expect(r.mark?.url).toBe(FAVICON.publicUrl);
185
+ });
186
+ });
187
+ describe('getFaviconMetadata', () => {
188
+ it('returns null icons when no favicon source is set', () => {
189
+ const r = getFaviconMetadata({ mode: 'inheritPublicBrand' }, assetMap());
190
+ expect(r.icon).toBeNull();
191
+ expect(r.apple).toBeNull();
192
+ });
193
+ it('cache-busts the icon URL from updatedAt', () => {
194
+ const brand = {
195
+ mode: 'inheritPublicBrand',
196
+ faviconSourceAssetId: 'fav-1',
197
+ updatedAt: '2026-02-03T00:00:00.000Z',
198
+ };
199
+ const r = getFaviconMetadata(brand, assetMap(FAVICON));
200
+ expect(r.icon).toBe(`${FAVICON.publicUrl}?v=${Date.parse(brand.updatedAt)}`);
201
+ expect(r.apple).toBe(r.icon);
202
+ expect(r.type).toBe('image/png');
203
+ });
204
+ });
205
+ describe('parse + ids', () => {
206
+ it('parses appearance with defaults', () => {
207
+ expect(parseAppearance({})).toEqual(DEFAULT_APPEARANCE);
208
+ expect(parseAppearance({ appearance: { adminAccentColor: 'teal' } }).adminAccentColor).toBe('teal');
209
+ });
210
+ it('parses brand with defaults', () => {
211
+ expect(parseBrand({}).mode).toBe('inheritPublicBrand');
212
+ });
213
+ it('collects unique asset ids', () => {
214
+ const brand = {
215
+ mode: 'customAdminBrand',
216
+ publicPrimaryLogoAssetId: 'a',
217
+ adminPrimaryLogoAssetId: 'a', // duplicate
218
+ faviconSourceAssetId: 'b',
219
+ generatedFaviconAssetIds: ['c', 'b'],
220
+ };
221
+ expect(brandAssetIds(brand).sort()).toEqual(['a', 'b', 'c']);
222
+ });
223
+ });
224
+ describe('resolveBrand (async)', () => {
225
+ it('reads the settings global and resolves every view', async () => {
226
+ const db = {
227
+ document: {
228
+ findFirst: async () => ({
229
+ data: {
230
+ siteTitle: 'Actuate Media',
231
+ brand: {
232
+ mode: 'inheritPublicBrand',
233
+ publicPrimaryLogoAssetId: 'primary-1',
234
+ faviconSourceAssetId: 'fav-1',
235
+ updatedAt: '2026-02-03T00:00:00.000Z',
236
+ },
237
+ },
238
+ }),
239
+ },
240
+ media: {
241
+ findMany: async () => [
242
+ {
243
+ id: 'primary-1',
244
+ publicUrl: HORIZONTAL.publicUrl,
245
+ mimeType: 'image/svg+xml',
246
+ width: 400,
247
+ height: 100,
248
+ },
249
+ {
250
+ id: 'fav-1',
251
+ publicUrl: FAVICON.publicUrl,
252
+ mimeType: 'image/png',
253
+ width: 32,
254
+ height: 32,
255
+ },
256
+ ],
257
+ },
258
+ };
259
+ const r = await resolveBrand(db, null);
260
+ expect(r.admin.primary?.url).toBe(HORIZONTAL.publicUrl);
261
+ expect(r.public.primary?.url).toBe(HORIZONTAL.publicUrl);
262
+ expect(r.favicon.icon).toContain(FAVICON.publicUrl);
263
+ expect(r.admin.initials).toBe('AM');
264
+ });
265
+ it('degrades gracefully with no media table', async () => {
266
+ const db = { document: { findFirst: async () => ({ data: {} }) } };
267
+ const r = await resolveBrand(db, 'Globex');
268
+ expect(r.admin.primary).toBeNull();
269
+ expect(r.favicon.icon).toBeNull();
270
+ expect(r.public.initials).toBe('GL');
271
+ });
272
+ });
273
+ //# sourceMappingURL=appearance.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"appearance.test.js","sourceRoot":"","sources":["../../../src/__tests__/appearance/appearance.test.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"handlers.d.ts","sourceRoot":"","sources":["../../src/api/handlers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AA+/B5C,iFAAiF;AACjF,wBAAgB,wBAAwB,IAAI,IAAI,CAE/C;AA8JD,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAS9E;AAyRD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CA0tUzD"}
1
+ {"version":3,"file":"handlers.d.ts","sourceRoot":"","sources":["../../src/api/handlers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AA+/B5C,iFAAiF;AACjF,wBAAgB,wBAAwB,IAAI,IAAI,CAE/C;AA8JD,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAS9E;AAyRD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAwxUzD"}
@@ -8897,6 +8897,58 @@ export function registerCMSRoutes(router) {
8897
8897
  return internalError(err);
8898
8898
  }
8899
8899
  });
8900
+ // Public brand: resolved logos + favicon for the public frontend. No auth —
8901
+ // brand assets are public site data. Only the PUBLIC brand view + favicon are
8902
+ // exposed here (never the admin-only brand) so a white-labeled admin doesn't
8903
+ // leak through the public API.
8904
+ router.get('/public/brand', async (request) => {
8905
+ try {
8906
+ const { resolveBrand } = await import('../appearance/index.js');
8907
+ const resolved = await resolveBrand(db(), null);
8908
+ return cachedRead(request, {
8909
+ cache: getActuateConfig()?.cache,
8910
+ tags: [ACTUATE_TAG_ROOT, actuateTag('global', 'settings')],
8911
+ wrap: jsonWithCache,
8912
+ build: async () => ({
8913
+ data: { public: resolved.public, favicon: resolved.favicon },
8914
+ }),
8915
+ });
8916
+ }
8917
+ catch (err) {
8918
+ return internalError(err, 'public/brand GET');
8919
+ }
8920
+ });
8921
+ // Authenticated appearance read: stored appearance + brand settings plus the
8922
+ // fully resolved admin/public/favicon views (with asset URLs) so the admin
8923
+ // Appearance tab can render live previews. View-only auth — writes go through
8924
+ // PUT /globals/settings (ADMIN_ROLES), so editors can view but not save.
8925
+ router.get('/appearance', async (request) => {
8926
+ try {
8927
+ const auth = await requireAuth(request);
8928
+ if (auth.error)
8929
+ return auth.error;
8930
+ const { getAppearanceSettings, getBrandSettings, resolveBrand } = await import('../appearance/index.js');
8931
+ const [appearance, brand, resolved] = await Promise.all([
8932
+ getAppearanceSettings(db()),
8933
+ getBrandSettings(db()),
8934
+ resolveBrand(db(), null),
8935
+ ]);
8936
+ return json({
8937
+ data: {
8938
+ appearance,
8939
+ brand,
8940
+ resolved: { admin: resolved.admin, public: resolved.public, favicon: resolved.favicon },
8941
+ // Direct per-asset URLs so the editor shows each field's actual
8942
+ // asset (not the resolved fallback chain).
8943
+ assets: resolved.assets,
8944
+ siteTitle: resolved.siteTitle,
8945
+ },
8946
+ });
8947
+ }
8948
+ catch (err) {
8949
+ return internalError(err, 'appearance GET');
8950
+ }
8951
+ });
8900
8952
  router.get('/globals/:slug', async (request, params) => {
8901
8953
  try {
8902
8954
  const auth = await requireAuth(request);
@@ -8946,8 +8998,17 @@ export function registerCMSRoutes(router) {
8946
8998
  // Audit settings changes (title/tagline/URL/locale/timezone/etc.) with a
8947
8999
  // per-key before/after diff so the audit trail records what changed and
8948
9000
  // who changed it. Scalar values only — we never log nested secret blobs.
9001
+ // Nested config blobs (`appearance`, `brand`) are logged as a changed
9002
+ // key (not their contents) so appearance/branding edits are auditable
9003
+ // without dumping asset IDs or theme objects into the log.
8949
9004
  try {
8950
9005
  const changes = diffScalarKeys(previous ?? {}, body);
9006
+ const prev = (previous ?? {});
9007
+ for (const key of ['appearance', 'brand']) {
9008
+ if (key in body && JSON.stringify(prev[key]) !== JSON.stringify(body[key])) {
9009
+ changes.push({ key, from: '[changed]', to: '[changed]' });
9010
+ }
9011
+ }
8951
9012
  if (changes.length > 0) {
8952
9013
  void logEvent({
8953
9014
  event: 'settings_changed',