forked from
joebasser.com/atmosphere-account
this repo has no description
1/**
2 * Public JSON projection for registry read APIs (`/api/registry/profile/*`,
3 * `/api/registry/search`, `/api/registry/featured`).
4 *
5 * Strips AppView-only moderation and verification workflow fields
6 * (takedown columns, per-icon review, icon-access request metadata, etc.)
7 * so anonymous API consumers never see internal operational data.
8 *
9 * Includes a boolean `verified` when the project has the same public
10 * “verified” badge as Explore (`icon_access_status === 'granted'`), without
11 * exposing emails, timestamps, or admin DIDs.
12 */
13import type { LinkEntry, ScreenshotEntry } from "./lexicons.ts";
14import type { ProfileRow } from "./registry.ts";
15import { bskyCdnAvatarUrl } from "./avatar.ts";
16
17export interface PublicProfileJson {
18 did: string;
19 handle: string;
20 name: string;
21 description: string;
22 mainLink: string | null;
23 iosLink: string | null;
24 androidLink: string | null;
25 categories: string[];
26 subcategories: string[];
27 links: LinkEntry[];
28 screenshots: ScreenshotEntry[];
29 /** Fully-qualified URLs for lazily loaded detail-page screenshots. */
30 screenshotUrls: string[];
31 avatarCid: string | null;
32 avatarMime: string | null;
33 /** Fully-qualified URL for the profile avatar image, or null. */
34 avatarUrl: string | null;
35 /**
36 * True when the project shows the public verified badge on Explore
37 * (admin-approved verification). No other verification metadata is exposed.
38 */
39 verified: boolean;
40 /**
41 * Developer-facing SVG icon URL when the icon is approved and the project
42 * is verified; otherwise null. Raw `iconCid` / review state are not exposed.
43 */
44 iconUrl: string | null;
45 /**
46 * Optional black-and-white companion to `iconUrl`. Same gating
47 * (verified project + approved variant); null when not present.
48 */
49 iconBwUrl: string | null;
50 pdsUrl: string;
51 recordCid: string;
52 recordRev: string;
53 createdAt: number;
54 indexedAt: number;
55 /** Present when this profile appears in the featured join (search / featured lists). */
56 featured?: ProfileRow["featured"];
57}
58
59export function toPublicProfileJson(
60 profile: ProfileRow,
61 origin: string,
62): PublicProfileJson {
63 const avatarUrl = profile.avatarCid
64 ? bskyCdnAvatarUrl(profile.did, profile.avatarCid)
65 : null;
66 const verified = profile.iconAccessStatus === "granted";
67 const iconUrl = profile.iconCid &&
68 profile.iconStatus === "approved" &&
69 profile.iconAccessStatus === "granted"
70 ? `${origin}/api/registry/icon/${encodeURIComponent(profile.did)}?v=${
71 encodeURIComponent(profile.iconCid)
72 }`
73 : null;
74 const iconBwUrl = profile.iconBwCid &&
75 profile.iconBwStatus === "approved" &&
76 profile.iconAccessStatus === "granted"
77 ? `${origin}/api/registry/icon-bw/${encodeURIComponent(profile.did)}?v=${
78 encodeURIComponent(profile.iconBwCid)
79 }`
80 : null;
81 const screenshotUrls = profile.screenshots.map((_, i) =>
82 `${origin}/api/registry/screenshot/${encodeURIComponent(profile.did)}/${i}`
83 );
84
85 const out: PublicProfileJson = {
86 did: profile.did,
87 handle: profile.handle,
88 name: profile.name,
89 description: profile.description,
90 mainLink: profile.mainLink,
91 iosLink: profile.iosLink,
92 androidLink: profile.androidLink,
93 categories: profile.categories,
94 subcategories: profile.subcategories,
95 // `website` was the former Landing Page button. The current public
96 // API exposes the primary web destination via `mainLink` instead.
97 links: profile.links.filter((entry) => entry.kind !== "website"),
98 screenshots: profile.screenshots,
99 screenshotUrls,
100 avatarCid: profile.avatarCid,
101 avatarMime: profile.avatarMime,
102 avatarUrl,
103 verified,
104 iconUrl,
105 iconBwUrl,
106 pdsUrl: profile.pdsUrl,
107 recordCid: profile.recordCid,
108 recordRev: profile.recordRev,
109 createdAt: profile.createdAt,
110 indexedAt: profile.indexedAt,
111 };
112 if (profile.featured !== undefined) {
113 out.featured = profile.featured;
114 }
115 return out;
116}