this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: Tangled default domain + public-only registry API JSON

- Use tangled.org for default Tangled links and README (was tangled.sh)
- Add lib/public-profile.ts (toPublicProfileJson) for read APIs
- Strip moderation, verification workflow, and raw icon CIDs from
GET /api/registry/profile, /search, /featured; add verified boolean
- Update developer-resources API copy

Made-with: Cursor

+112 -44
+2 -2
README.md
··· 5 5 6 6 Open source under the [MIT License](./LICENSE). Contributions welcome — fork the 7 7 repo on either [GitHub](https://github.com/jobiwanken0bi/atmosphere-account) or 8 - [tangled](https://tangled.sh/@joebasser.com/atmosphere-account) and open a PR. 8 + [tangled](https://tangled.org/@joebasser.com/atmosphere-account) and open a PR. 9 9 10 10 ## Prerequisites 11 11 ··· 61 61 PRs and forks welcome on either forge: 62 62 63 63 - **GitHub:** https://github.com/jobiwanken0bi/atmosphere-account 64 - - **tangled:** https://tangled.sh/@joebasser.com/atmosphere-account 64 + - **tangled:** https://tangled.org/@joebasser.com/atmosphere-account 65 65 66 66 Both forges mirror the same `main` branch. 67 67
+4 -4
i18n/messages/en.tsx
··· 331 331 method: "GET", 332 332 path: "/api/registry/profile/:handleOrDid", 333 333 summary: 334 - "Single profile by handle or DID. Returns the same shape as /explore/<handle>, plus a fully-qualified avatarUrl convenience field.", 334 + "Single profile by handle or DID. Public fields only: identity, listing content, avatarUrl, optional iconUrl, verified boolean, and indexing metadata. Does not include moderation or verification workflow fields.", 335 335 cache: "public, max-age=30, s-maxage=120", 336 336 }, 337 337 search: { 338 338 method: "GET", 339 339 path: "/api/registry/search", 340 340 summary: 341 - "Paginated profile search. Filter by free-text query, category, and subcategory. Returns { profiles, total, page, pageSize }.", 341 + "Paginated profile search. Filter by free-text query, category, and subcategory. Returns { profiles, total, page, pageSize } with the same public profile shape as the single-profile endpoint.", 342 342 params: [ 343 343 { name: "q", desc: "Free-text search query." }, 344 344 { ··· 363 363 method: "GET", 364 364 path: "/api/registry/featured", 365 365 summary: 366 - "Curated featured list, ordered by position. Returns { profiles }.", 366 + "Curated featured list, ordered by position. Returns { profiles } using the same public profile shape as the other read endpoints.", 367 367 params: [ 368 368 { 369 369 name: "limit", ··· 390 390 }, 391 391 schemaHeading: "Schema", 392 392 schemaBody: 393 - "Profiles are AT Protocol records. The canonical schema is the lexicon below — use it to validate records, generate types, or browse the full field set.", 393 + "Profiles originate as AT Protocol records; the lexicon below is the canonical on-repo schema. Public HTTP responses add derived fields (avatarUrl, iconUrl, verified) and omit AppView-only moderation and verification workflow data — use the live JSON from the playground as the reference for what the API returns.", 394 394 downloadLexicon: "Download lexicon (JSON)", 395 395 }, 396 396 },
+1 -1
lib/atmosphere-links.ts
··· 60 60 } 61 61 62 62 /** Default Tangled domain for the user-profile URL pattern. */ 63 - const TANGLED_DOMAIN = "tangled.sh"; 63 + const TANGLED_DOMAIN = "tangled.org"; 64 64 /** Default Supper domain. */ 65 65 const SUPPER_DOMAIN = "supper.support"; 66 66
+1 -1
lib/lexicons.ts
··· 68 68 * 69 69 * bsky — a Bluesky-style profile button. Requires `clientId`; URL is 70 70 * derived from clientId + the user's current handle. 71 - * tangled — a Tangled profile button. URL defaults to tangled.sh/@handle 71 + * tangled — a Tangled profile button. URL defaults to tangled.org/<handle> 72 72 * but the user may override (`url`) to point at a project repo. 73 73 * supper — a Supper (supper.support/@handle) button. URL is derived; 74 74 * `url` override is allowed.
+86
lib/public-profile.ts
··· 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 + */ 13 + import type { LinkEntry } from "./lexicons.ts"; 14 + import type { ProfileRow } from "./registry.ts"; 15 + 16 + export interface PublicProfileJson { 17 + did: string; 18 + handle: string; 19 + name: string; 20 + description: string; 21 + mainLink: string | null; 22 + categories: string[]; 23 + subcategories: string[]; 24 + links: LinkEntry[]; 25 + avatarCid: string | null; 26 + avatarMime: string | null; 27 + /** Fully-qualified URL for the profile avatar image proxy, or null. */ 28 + avatarUrl: string | null; 29 + /** 30 + * True when the project shows the public verified badge on Explore 31 + * (admin-approved verification). No other verification metadata is exposed. 32 + */ 33 + verified: boolean; 34 + /** 35 + * Developer-facing SVG icon URL when the icon is approved and the project 36 + * is verified; otherwise null. Raw `iconCid` / review state are not exposed. 37 + */ 38 + iconUrl: string | null; 39 + pdsUrl: string; 40 + recordCid: string; 41 + recordRev: string; 42 + createdAt: number; 43 + indexedAt: number; 44 + /** Present when this profile appears in the featured join (search / featured lists). */ 45 + featured?: ProfileRow["featured"]; 46 + } 47 + 48 + export function toPublicProfileJson( 49 + profile: ProfileRow, 50 + origin: string, 51 + ): PublicProfileJson { 52 + const avatarUrl = profile.avatarCid 53 + ? `${origin}/api/registry/avatar/${encodeURIComponent(profile.did)}` 54 + : null; 55 + const verified = profile.iconAccessStatus === "granted"; 56 + const iconUrl = profile.iconCid && 57 + profile.iconStatus === "approved" && 58 + profile.iconAccessStatus === "granted" 59 + ? `${origin}/api/registry/icon/${encodeURIComponent(profile.did)}` 60 + : null; 61 + 62 + const out: PublicProfileJson = { 63 + did: profile.did, 64 + handle: profile.handle, 65 + name: profile.name, 66 + description: profile.description, 67 + mainLink: profile.mainLink, 68 + categories: profile.categories, 69 + subcategories: profile.subcategories, 70 + links: profile.links, 71 + avatarCid: profile.avatarCid, 72 + avatarMime: profile.avatarMime, 73 + avatarUrl, 74 + verified, 75 + iconUrl, 76 + pdsUrl: profile.pdsUrl, 77 + recordCid: profile.recordCid, 78 + recordRev: profile.recordRev, 79 + createdAt: profile.createdAt, 80 + indexedAt: profile.indexedAt, 81 + }; 82 + if (profile.featured !== undefined) { 83 + out.featured = profile.featured; 84 + } 85 + return out; 86 + }
+4 -1
routes/api/registry/featured.ts
··· 1 1 import { define } from "../../../utils.ts"; 2 2 import { listFeaturedProfiles } from "../../../lib/registry.ts"; 3 + import { toPublicProfileJson } from "../../../lib/public-profile.ts"; 3 4 import { withRateLimit } from "../../../lib/rate-limit.ts"; 4 5 5 6 export const handler = define.handlers({ 6 7 GET: withRateLimit(async (ctx) => { 7 8 const limit = Number(ctx.url.searchParams.get("limit") ?? "12") || 12; 8 - const profiles = await listFeaturedProfiles( 9 + const origin = new URL(ctx.req.url).origin; 10 + const rows = await listFeaturedProfiles( 9 11 Math.min(48, Math.max(1, limit)), 10 12 ); 13 + const profiles = rows.map((p) => toPublicProfileJson(p, origin)); 11 14 return new Response(JSON.stringify({ profiles }), { 12 15 headers: { 13 16 "content-type": "application/json; charset=utf-8",
+7 -34
routes/api/registry/profile/[id].ts
··· 4 4 * GET /api/registry/profile/alice.bsky.social 5 5 * GET /api/registry/profile/did:plc:abc123... 6 6 * 7 - * Returns the same `ProfileRow` shape that powers the SSR 8 - * `/explore/[handle]` page so the public response stays in sync with 9 - * the rendered profile view. 7 + * Returns a **public** projection (`PublicProfileJson`) — lexicon-shaped 8 + * fields plus identity, avatar URLs, and indexing metadata. Does not 9 + * include AppView moderation, SVG review, or verification-request fields. 10 10 * 11 - * Adds one synthesised convenience field — `avatarUrl` — derived from 12 - * the request origin so callers don't have to know about the 13 - * `/api/registry/avatar/<did>` proxy route. `null` when the profile 14 - * has no avatar set. 11 + * Adds synthesised convenience fields — `avatarUrl`, optional `iconUrl`, 12 + * and `verified` — aligned with what anonymous clients should see. 15 13 */ 16 14 import { define } from "../../../../utils.ts"; 17 15 import { 18 16 getProfileByDid, 19 17 getProfileByHandle, 20 - type ProfileRow, 21 18 } from "../../../../lib/registry.ts"; 19 + import { toPublicProfileJson } from "../../../../lib/public-profile.ts"; 22 20 import { withRateLimit } from "../../../../lib/rate-limit.ts"; 23 21 24 - interface PublicProfileResponse extends ProfileRow { 25 - /** Fully-qualified URL for the profile's avatar, or null if unset. */ 26 - avatarUrl: string | null; 27 - /** 28 - * Fully-qualified URL for the profile's developer-facing SVG icon, 29 - * or null if unset / pending review / rejected. Served as 30 - * `image/svg+xml` with strict CSP + `nosniff`; safe for `<img src>` 31 - * embedding. 32 - * 33 - * SDK consumers that want to hint at pending/rejected state should 34 - * read `iconStatus` directly. 35 - */ 36 - iconUrl: string | null; 37 - } 38 - 39 22 export const handler = define.handlers({ 40 23 GET: withRateLimit(async (ctx) => { 41 24 const raw = decodeURIComponent(ctx.params.id ?? "").trim(); ··· 57 40 } 58 41 59 42 const origin = new URL(ctx.req.url).origin; 60 - const body: PublicProfileResponse = { 61 - ...profile, 62 - avatarUrl: profile.avatarCid 63 - ? `${origin}/api/registry/avatar/${encodeURIComponent(profile.did)}` 64 - : null, 65 - iconUrl: profile.iconCid && 66 - profile.iconStatus === "approved" && 67 - profile.iconAccessStatus === "granted" 68 - ? `${origin}/api/registry/icon/${encodeURIComponent(profile.did)}` 69 - : null, 70 - }; 43 + const body = toPublicProfileJson(profile, origin); 71 44 72 45 return new Response(JSON.stringify(body), { 73 46 headers: {
+7 -1
routes/api/registry/search.ts
··· 1 1 import { define } from "../../../utils.ts"; 2 2 import { CATEGORIES } from "../../../lib/lexicons.ts"; 3 3 import { searchProfiles } from "../../../lib/registry.ts"; 4 + import { toPublicProfileJson } from "../../../lib/public-profile.ts"; 4 5 import { withRateLimit } from "../../../lib/rate-limit.ts"; 5 6 6 7 export const handler = define.handlers({ ··· 16 17 const page = Number(url.searchParams.get("page") ?? "1") || 1; 17 18 const pageSize = Number(url.searchParams.get("pageSize") ?? "24") || 24; 18 19 20 + const origin = new URL(ctx.req.url).origin; 19 21 const result = await searchProfiles({ 20 22 query: q, 21 23 category, ··· 23 25 page, 24 26 pageSize, 25 27 }); 26 - return new Response(JSON.stringify(result), { 28 + const publicResult = { 29 + ...result, 30 + profiles: result.profiles.map((p) => toPublicProfileJson(p, origin)), 31 + }; 32 + return new Response(JSON.stringify(publicResult), { 27 33 headers: { 28 34 "content-type": "application/json; charset=utf-8", 29 35 "cache-control": "public, max-age=10, s-maxage=30",