WIP! A BB-style forum, on the ATmosphere! We're still working... we'll be back soon when we have something to show off!
node typescript hono htmx atproto

fix(appview): PUT /theme-policy accepts availableThemes without cid — looks up from DB (ATB-58)

+103 -19
+61 -8
apps/appview/src/routes/__tests__/admin.test.ts
··· 3236 3236 expect(body.error).toMatch(/availableThemes/i); 3237 3237 }); 3238 3238 3239 - it("returns 400 when availableThemes item is missing cid", async () => { 3239 + it("accepts availableThemes with just uri (no cid) by looking up cid from DB", async () => { 3240 + // Insert a theme so the DB lookup will find it 3241 + const themeUri = `at://${ctx.config.forumDid}/space.atbb.forum.theme/3lbltheme1aa`; 3242 + await ctx.db.insert(themes).values({ 3243 + did: ctx.config.forumDid, 3244 + rkey: "3lbltheme1aa", 3245 + cid: "bafytheme1", 3246 + name: "Neobrutal Light", 3247 + colorScheme: "light", 3248 + tokens: {}, 3249 + createdAt: new Date(), 3250 + indexedAt: new Date(), 3251 + }); 3252 + 3253 + const res = await app.request("/api/admin/theme-policy", { 3254 + method: "PUT", 3255 + headers: { "Content-Type": "application/json" }, 3256 + body: JSON.stringify({ 3257 + defaultLightThemeUri: themeUri, 3258 + defaultDarkThemeUri: themeUri, 3259 + allowUserChoice: true, 3260 + availableThemes: [{ uri: themeUri }], // no cid 3261 + }), 3262 + }); 3263 + 3264 + expect(res.status).toBe(200); 3265 + expect(mockPutRecord).toHaveBeenCalledOnce(); 3266 + const putCall = mockPutRecord.mock.calls[0][0]; 3267 + // The resolved entry should have the CID looked up from the DB 3268 + expect(putCall.record.availableThemes[0]).toEqual({ theme: { uri: themeUri, cid: "bafytheme1" } }); 3269 + expect(putCall.record.defaultLightTheme).toEqual({ theme: { uri: themeUri, cid: "bafytheme1" } }); 3270 + expect(putCall.record.defaultDarkTheme).toEqual({ theme: { uri: themeUri, cid: "bafytheme1" } }); 3271 + }); 3272 + 3273 + it("accepts availableThemes with uri only — uses empty string cid when uri not found in DB", async () => { 3274 + // No theme inserted in DB — URI won't be found 3275 + const unknownUri = `at://${ctx.config.forumDid}/space.atbb.forum.theme/3lblunknown1`; 3276 + 3240 3277 const res = await app.request("/api/admin/theme-policy", { 3241 3278 method: "PUT", 3242 3279 headers: { "Content-Type": "application/json" }, 3243 3280 body: JSON.stringify({ 3244 - ...validBody, 3245 - availableThemes: [{ uri: lightUri }], // missing cid 3246 - defaultLightThemeUri: lightUri, 3247 - defaultDarkThemeUri: lightUri, 3281 + defaultLightThemeUri: unknownUri, 3282 + defaultDarkThemeUri: unknownUri, 3283 + allowUserChoice: true, 3284 + availableThemes: [{ uri: unknownUri }], // no cid, not in DB 3248 3285 }), 3249 3286 }); 3250 - expect(res.status).toBe(400); 3251 - const body = await res.json(); 3252 - expect(body.error).toMatch(/uri.*cid|cid.*uri|uri and cid/i); 3287 + 3288 + expect(res.status).toBe(200); 3289 + expect(mockPutRecord).toHaveBeenCalledOnce(); 3290 + const putCall = mockPutRecord.mock.calls[0][0]; 3291 + // Falls back to empty string when URI not found in DB 3292 + expect(putCall.record.availableThemes[0]).toEqual({ theme: { uri: unknownUri, cid: "" } }); 3293 + }); 3294 + 3295 + it("uses provided cid when entry already includes one (no DB lookup needed)", async () => { 3296 + // The existing validBody has cid on each entry — should use those directly 3297 + await app.request("/api/admin/theme-policy", { 3298 + method: "PUT", 3299 + headers: { "Content-Type": "application/json" }, 3300 + body: JSON.stringify(validBody), 3301 + }); 3302 + const putCall = mockPutRecord.mock.calls[0][0]; 3303 + // Should use the cids from the request, not from DB 3304 + expect(putCall.record.availableThemes[0]).toEqual({ theme: { uri: lightUri, cid: "bafylight" } }); 3305 + expect(putCall.record.availableThemes[1]).toEqual({ theme: { uri: darkUri, cid: "bafydark" } }); 3253 3306 }); 3254 3307 3255 3308 it("returns 400 when defaultLightThemeUri is not in availableThemes", async () => {
+38 -8
apps/appview/src/routes/admin.ts
··· 1385 1385 if ( 1386 1386 typeof t !== "object" || 1387 1387 t === null || 1388 - typeof (t as Record<string, unknown>).uri !== "string" || 1389 - typeof (t as Record<string, unknown>).cid !== "string" 1388 + typeof (t as Record<string, unknown>).uri !== "string" 1390 1389 ) { 1391 - return c.json({ error: "Each availableThemes entry must have uri and cid string fields" }, 400); 1390 + return c.json({ error: "Each availableThemes entry must have a uri string field" }, 400); 1392 1391 } 1393 1392 } 1394 1393 ··· 1399 1398 return c.json({ error: "defaultDarkThemeUri is required and must be an AT-URI" }, 400); 1400 1399 } 1401 1400 1402 - const availableUris = (availableThemes as Array<{ uri: string; cid: string }>).map((t) => t.uri); 1401 + const typedAvailableThemes = availableThemes as Array<{ uri: string; cid?: string }>; 1402 + const availableUris = typedAvailableThemes.map((t) => t.uri); 1403 1403 if (!availableUris.includes(defaultLightThemeUri)) { 1404 1404 return c.json({ error: "defaultLightThemeUri must be present in availableThemes" }, 400); 1405 1405 } ··· 1409 1409 1410 1410 const resolvedAllowUserChoice = typeof allowUserChoice === "boolean" ? allowUserChoice : true; 1411 1411 1412 - const typedAvailableThemes = availableThemes as Array<{ uri: string; cid: string }>; 1413 - const lightTheme = typedAvailableThemes.find((t) => t.uri === defaultLightThemeUri)!; 1414 - const darkTheme = typedAvailableThemes.find((t) => t.uri === defaultDarkThemeUri)!; 1412 + // Build URI→CID map from DB for entries that don't supply a cid 1413 + let uriToCid = new Map<string, string>(); 1414 + const needsLookup = typedAvailableThemes.some((t) => typeof t.cid !== "string"); 1415 + if (needsLookup) { 1416 + try { 1417 + const allThemes = await ctx.db 1418 + .select({ did: themes.did, rkey: themes.rkey, cid: themes.cid }) 1419 + .from(themes) 1420 + .where(eq(themes.did, ctx.config.forumDid)); 1421 + uriToCid = new Map( 1422 + allThemes.map((t) => [ 1423 + `at://${t.did}/space.atbb.forum.theme/${t.rkey}`, 1424 + t.cid, 1425 + ]) 1426 + ); 1427 + } catch (error) { 1428 + if (isProgrammingError(error)) throw error; 1429 + ctx.logger.error("Failed to look up theme CIDs from DB", { 1430 + operation: "PUT /api/admin/theme-policy", 1431 + error: error instanceof Error ? error.message : String(error), 1432 + }); 1433 + return c.json({ error: "Failed to look up theme data. Please try again later." }, 500); 1434 + } 1435 + } 1436 + 1437 + // Resolve CIDs: use provided cid if present, otherwise look up from DB, fallback to "" 1438 + const resolvedThemes = typedAvailableThemes.map((t) => ({ 1439 + uri: t.uri, 1440 + cid: typeof t.cid === "string" ? t.cid : (uriToCid.get(t.uri) ?? ""), 1441 + })); 1442 + 1443 + const lightTheme = resolvedThemes.find((t) => t.uri === defaultLightThemeUri)!; 1444 + const darkTheme = resolvedThemes.find((t) => t.uri === defaultDarkThemeUri)!; 1415 1445 1416 1446 const { agent, error: agentError } = getForumAgentOrError(ctx, c, "PUT /api/admin/theme-policy"); 1417 1447 if (agentError) return agentError; ··· 1423 1453 rkey: "self", 1424 1454 record: { 1425 1455 $type: "space.atbb.forum.themePolicy", 1426 - availableThemes: typedAvailableThemes.map((t) => ({ 1456 + availableThemes: resolvedThemes.map((t) => ({ 1427 1457 theme: { uri: t.uri, cid: t.cid }, 1428 1458 })), 1429 1459 defaultLightTheme: { theme: { uri: lightTheme.uri, cid: lightTheme.cid } },
+4 -3
bruno/AppView API/Admin Themes/Update Theme Policy.bru
··· 11 11 body:json { 12 12 { 13 13 "availableThemes": [ 14 - { "uri": "at://did:plc:example/space.atbb.forum.theme/3lbllight1", "cid": "bafylight" }, 15 - { "uri": "at://did:plc:example/space.atbb.forum.theme/3lbldark11", "cid": "bafydark" } 14 + { "uri": "at://did:plc:example/space.atbb.forum.theme/3lbllight1" }, 15 + { "uri": "at://did:plc:example/space.atbb.forum.theme/3lbldark11" } 16 16 ], 17 17 "defaultLightThemeUri": "at://did:plc:example/space.atbb.forum.theme/3lbllight1", 18 18 "defaultDarkThemeUri": "at://did:plc:example/space.atbb.forum.theme/3lbldark11", ··· 33 33 **Requires:** space.atbb.permission.manageThemes 34 34 35 35 Body: 36 - - availableThemes (required): Non-empty array of { uri, cid } theme references. 36 + - availableThemes (required): Non-empty array of { uri, cid? } theme references. 37 + cid is optional — if omitted, the AppView looks it up from the themes table by URI. 37 38 Both defaultLightThemeUri and defaultDarkThemeUri must be present in this list. 38 39 - defaultLightThemeUri (required): AT-URI of the default light-mode theme. 39 40 Must be in availableThemes.