Skip to content

Commit 17d5376

Browse files
fullstackjamclaude
andcommitted
fix(api): preserve macos_prefs[].host in config GET responses
The three config GET endpoints mapped each pref to a fixed shape {domain, key, type, value, desc}, silently dropping the `host` field the CLI now publishes for ByHost prefs (e.g. Control Center menu-bar dropdowns under defaults -currentHost). The snapshot blob in D1 had the field — it was lost only on the read path. Preserve host through the three GETs (alias, public config, owner read), mirror Go's `omitempty` by only emitting when non-empty, and do the same in the editor's hydrate path so a save-after-edit can't re-strip it. Add an optional host enum {"", "currentHost"} to validateMacOSPrefs to lock down the allowed values. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 291b917 commit 17d5376

7 files changed

Lines changed: 101 additions & 29 deletions

File tree

src/lib/components/ConfigEditor.svelte

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
type: string;
6363
value: string;
6464
desc: string;
65+
host?: string;
6566
}
6667
let macosPrefs = $state<MacOSPref[]>([]);
6768
let expandedPrefCats = $state<Set<string>>(new Set());
@@ -220,13 +221,15 @@
220221
if (data.snapshot?.macos_prefs) {
221222
macosPrefs = data.snapshot.macos_prefs.map((p: any) => {
222223
const type = p.type || '';
223-
return {
224+
const out: MacOSPref = {
224225
domain: p.domain || '',
225226
key: p.key || '',
226227
type,
227228
value: normalizePrefValue(type, String(p.value ?? '')),
228229
desc: p.desc || '',
229230
};
231+
if (typeof p.host === 'string' && p.host !== '') out.host = p.host;
232+
return out;
230233
});
231234
}
232235
initExpandedCats();
@@ -255,13 +258,15 @@
255258
macosPrefs = Array.isArray(config.snapshot?.macos_prefs)
256259
? config.snapshot.macos_prefs.map((p: any) => {
257260
const type = p.type || '';
258-
return {
261+
const out: MacOSPref = {
259262
domain: p.domain || '',
260263
key: p.key || '',
261264
type,
262265
value: normalizePrefValue(type, String(p.value ?? '')),
263266
desc: p.desc || '',
264267
};
268+
if (typeof p.host === 'string' && p.host !== '') out.host = p.host;
269+
return out;
265270
})
266271
: [];
267272
initExpandedCats();
@@ -336,13 +341,15 @@
336341
macosPrefs = Array.isArray(parsed.snapshot?.macos_prefs)
337342
? parsed.snapshot.macos_prefs.map((p: any) => {
338343
const type = p.type || '';
339-
return {
344+
const out: MacOSPref = {
340345
domain: p.domain || '',
341346
key: p.key || '',
342347
type,
343348
value: normalizePrefValue(type, String(p.value ?? '')),
344349
desc: p.desc || '',
345350
};
351+
if (typeof p.host === 'string' && p.host !== '') out.host = p.host;
352+
return out;
346353
})
347354
: [];
348355
initExpandedCats();

src/lib/components/MacOSPreferencesEditor.svelte

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
type: string;
88
value: string;
99
desc: string;
10+
host?: string;
1011
}
1112
1213
let {

src/lib/server/validation.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,13 +102,16 @@ export function validateMacOSPrefs(prefs: unknown): ValidationResult {
102102
if (prefs.length > 100) return { valid: false, error: 'Maximum 100 macOS preferences allowed' };
103103

104104
const validTypes = new Set(['', 'string', 'int', 'bool', 'float']);
105+
// `host` selects the defaults scope: "" = main domain, "currentHost" = ByHost (defaults -currentHost).
106+
// Required for keys stored under ~/Library/Preferences/ByHost (e.g. Control Center menu-bar dropdowns).
107+
const validHosts = new Set(['', 'currentHost']);
105108

106109
for (let i = 0; i < prefs.length; i++) {
107110
const p = prefs[i];
108111
if (typeof p !== 'object' || p === null) {
109112
return { valid: false, error: `macos_prefs[${i}] must be an object` };
110113
}
111-
const { domain, key, value, type } = p as Record<string, unknown>;
114+
const { domain, key, value, type, host } = p as Record<string, unknown>;
112115
if (!domain || typeof domain !== 'string') {
113116
return { valid: false, error: `macos_prefs[${i}] missing required field: domain` };
114117
}
@@ -121,6 +124,9 @@ export function validateMacOSPrefs(prefs: unknown): ValidationResult {
121124
if (type !== undefined && (typeof type !== 'string' || !validTypes.has(type as string))) {
122125
return { valid: false, error: `macos_prefs[${i}] invalid type "${type}" (allowed: string, int, bool, float)` };
123126
}
127+
if (host !== undefined && (typeof host !== 'string' || !validHosts.has(host as string))) {
128+
return { valid: false, error: `macos_prefs[${i}] invalid host "${host}" (allowed: "", "currentHost")` };
129+
}
124130
}
125131

126132
return { valid: true };

src/routes/[username]/[slug]/config/+server.ts

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ export const GET: RequestHandler = async ({ platform, params, request }) => {
3535

3636
const tapsSet = new Set<string>();
3737
const snapshotCasks = new Set<string>();
38-
let macosPrefs: { domain: string; key: string; type: string; value: string; desc: string }[] | null = null;
38+
type MacOSPrefOut = { domain: string; key: string; type: string; value: string; desc: string; host?: string };
39+
let macosPrefs: MacOSPrefOut[] | null = null;
3940

4041
if (config.snapshot) {
4142
try {
@@ -48,19 +49,25 @@ export const GET: RequestHandler = async ({ platform, params, request }) => {
4849
}
4950
if (Array.isArray(snapshot.macos_prefs) && snapshot.macos_prefs.length > 0) {
5051
const filtered = snapshot.macos_prefs.filter(
51-
(p: unknown): p is { domain: string; key: string; type: string; value: string; desc: string } =>
52+
(p: unknown): p is Record<string, unknown> =>
5253
typeof p === 'object' &&
5354
p !== null &&
5455
typeof (p as Record<string, unknown>).domain === 'string' &&
5556
typeof (p as Record<string, unknown>).key === 'string' &&
5657
typeof (p as Record<string, unknown>).value === 'string'
57-
).map((p: Record<string, unknown>) => ({
58-
domain: p.domain as string,
59-
key: p.key as string,
60-
type: typeof p.type === 'string' ? p.type : '',
61-
value: p.value as string,
62-
desc: typeof p.desc === 'string' ? p.desc : ''
63-
}));
58+
).map((p: Record<string, unknown>) => {
59+
const out: MacOSPrefOut = {
60+
domain: p.domain as string,
61+
key: p.key as string,
62+
type: typeof p.type === 'string' ? p.type : '',
63+
value: p.value as string,
64+
desc: typeof p.desc === 'string' ? p.desc : ''
65+
};
66+
// `host` selects the ByHost scope (defaults -currentHost). Mirrors Go `omitempty`:
67+
// only emit when non-empty so payloads stay clean for the common main-domain case.
68+
if (typeof p.host === 'string' && p.host !== '') out.host = p.host;
69+
return out;
70+
});
6471
if (filtered.length > 0) macosPrefs = filtered;
6572
}
6673
} catch (err) {

src/routes/[username]/[slug]/config/server.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,43 @@ describe('[username]/[slug]/config GET - Visibility Auth', () => {
343343
expect(json.macos_prefs[1].key).toBe('autohide');
344344
});
345345

346+
it('should preserve host="currentHost" for ByHost prefs and omit it when empty', async () => {
347+
const config = {
348+
...mockPublicConfig,
349+
snapshot: JSON.stringify({
350+
packages: { taps: [], casks: [] },
351+
macos_prefs: [
352+
// ByHost pref — must round-trip `host` so the CLI uses `defaults -currentHost`.
353+
{ domain: 'com.apple.controlcenter', key: 'Sound', type: 'int', value: '18', desc: 'Sound dropdown', host: 'currentHost' },
354+
// Main-domain pref — no `host` field in input, must not gain one in output.
355+
{ domain: 'NSGlobalDomain', key: 'AppleShowAllExtensions', type: 'bool', value: 'true', desc: 'Show extensions' }
356+
]
357+
})
358+
};
359+
360+
const db = createMockDB({ users: [mockUser], configs: [config] });
361+
const platform = createMockPlatform(db);
362+
363+
const response = await GET({
364+
request: createMockRequest({ url: baseUrl }),
365+
platform,
366+
params: { username: 'testuser', slug: 'public-config' },
367+
url: new URL(baseUrl),
368+
route: { id: '/[username]/[slug]/config' },
369+
locals: {},
370+
isDataRequest: false,
371+
isSubRequest: false,
372+
cookies: {} as any,
373+
getClientAddress: () => '',
374+
fetch: globalThis.fetch
375+
});
376+
377+
const json = await getJSON(response);
378+
expect(json.macos_prefs).toHaveLength(2);
379+
expect(json.macos_prefs[0].host).toBe('currentHost');
380+
expect(json.macos_prefs[1]).not.toHaveProperty('host');
381+
});
382+
346383
it('should filter out invalid macos_prefs entries', async () => {
347384
const config = {
348385
...mockPublicConfig,

src/routes/api/configs/[slug]/+server.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@ export const GET: RequestHandler = async ({ platform, cookies, params, request }
5454
return p;
5555
});
5656

57-
let macosPrefs: { domain: string; key: string; type?: string; value: string; desc?: string }[] | null = null;
57+
type MacOSPrefOut = { domain: string; key: string; type?: string; value: string; desc?: string; host?: string };
58+
let macosPrefs: MacOSPrefOut[] | null = null;
5859
if (parsedSnapshot) {
5960
const rawPrefs = (parsedSnapshot as any).macos_prefs;
6061
if (Array.isArray(rawPrefs) && rawPrefs.length > 0) {
@@ -67,13 +68,19 @@ export const GET: RequestHandler = async ({ platform, cookies, params, request }
6768
typeof (p as Record<string, unknown>).key === 'string' &&
6869
typeof (p as Record<string, unknown>).value === 'string'
6970
)
70-
.map((p: Record<string, unknown>) => ({
71-
domain: p.domain as string,
72-
key: p.key as string,
73-
type: typeof p.type === 'string' ? p.type : '',
74-
value: p.value as string,
75-
desc: typeof p.desc === 'string' ? p.desc : ''
76-
}));
71+
.map((p: Record<string, unknown>) => {
72+
const out: MacOSPrefOut = {
73+
domain: p.domain as string,
74+
key: p.key as string,
75+
type: typeof p.type === 'string' ? p.type : '',
76+
value: p.value as string,
77+
desc: typeof p.desc === 'string' ? p.desc : ''
78+
};
79+
// `host` selects the ByHost scope (defaults -currentHost). Mirrors Go `omitempty`:
80+
// only emit when non-empty so payloads stay clean for the common main-domain case.
81+
if (typeof p.host === 'string' && p.host !== '') out.host = p.host;
82+
return out;
83+
});
7784
if (filtered.length > 0) macosPrefs = filtered;
7885
}
7986
}

src/routes/api/configs/alias/[alias]/+server.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ export const GET: RequestHandler = async ({ platform, params }) => {
2020

2121
const tapsSet = new Set<string>();
2222
const snapshotCasks = new Set<string>();
23-
let macosPrefs: { domain: string; key: string; type: string; value: string; desc: string }[] | null = null;
23+
type MacOSPrefOut = { domain: string; key: string; type: string; value: string; desc: string; host?: string };
24+
let macosPrefs: MacOSPrefOut[] | null = null;
2425

2526
if (config.snapshot) {
2627
try {
@@ -39,13 +40,19 @@ export const GET: RequestHandler = async ({ platform, params }) => {
3940
typeof (p as Record<string, unknown>).domain === 'string' &&
4041
typeof (p as Record<string, unknown>).key === 'string' &&
4142
typeof (p as Record<string, unknown>).value === 'string'
42-
).map((p: Record<string, unknown>) => ({
43-
domain: p.domain as string,
44-
key: p.key as string,
45-
type: typeof p.type === 'string' ? p.type : '',
46-
value: p.value as string,
47-
desc: typeof p.desc === 'string' ? p.desc : ''
48-
}));
43+
).map((p: Record<string, unknown>) => {
44+
const out: MacOSPrefOut = {
45+
domain: p.domain as string,
46+
key: p.key as string,
47+
type: typeof p.type === 'string' ? p.type : '',
48+
value: p.value as string,
49+
desc: typeof p.desc === 'string' ? p.desc : ''
50+
};
51+
// `host` selects the ByHost scope (defaults -currentHost). Mirrors Go `omitempty`:
52+
// only emit when non-empty so payloads stay clean for the common main-domain case.
53+
if (typeof p.host === 'string' && p.host !== '') out.host = p.host;
54+
return out;
55+
});
4956
if (filtered.length > 0) macosPrefs = filtered;
5057
}
5158
} catch (err) {

0 commit comments

Comments
 (0)