this repo has no description
1/**
2 * scripts/publish-featured.ts
3 *
4 * Publishes (or overwrites) the curated featured directory record on the
5 * Atmosphere account's PDS, at:
6 * com.atmosphereaccount.registry.featured/self
7 *
8 * Input: a JSON file describing the directory. Entries may use either
9 * `did` or `handle` (handles are resolved to DIDs before publish):
10 *
11 * {
12 * "entries": [
13 * { "did": "did:plc:...", "badges": ["official"], "position": 0 },
14 * { "handle": "tangled.org", "badges": ["verified"] }
15 * ]
16 * }
17 *
18 * Usage:
19 * deno task publish:featured # uses ./featured.json
20 * deno task publish:featured ./mylist.json # custom path
21 *
22 * Required env vars:
23 * ATMOSPHERE_DID — DID of the curator account
24 * TURSO_DATABASE_URL — must contain a valid OAuth session for
25 * ATMOSPHERE_DID (sign in once via
26 * `/oauth/login` to seed it)
27 * OAUTH_PRIVATE_JWK,
28 * OAUTH_PUBLIC_JWK,
29 * OAUTH_KID,
30 * SESSION_SECRET — same OAuth env used by the web app
31 *
32 * The indexer ignores featured writes from any account other than the
33 * one configured via ATMOSPHERE_DID, so this script must be run for that
34 * account.
35 */
36import { FEATURED_NSID, validateFeatured } from "../lib/lexicons.ts";
37import { resolveIdentity } from "../lib/identity.ts";
38import { putRecord } from "../lib/pds.ts";
39import { getValidSession } from "../lib/oauth.ts";
40
41interface RawEntry {
42 did?: string;
43 handle?: string;
44 badges?: string[];
45 position?: number;
46}
47
48interface RawFile {
49 entries: RawEntry[];
50}
51
52async function loadFile(path: string): Promise<RawFile> {
53 const text = await Deno.readTextFile(path);
54 const json = JSON.parse(text) as Record<string, unknown>;
55 if (!Array.isArray(json.entries)) {
56 throw new Error(`${path}: missing "entries" array`);
57 }
58 return { entries: json.entries as RawEntry[] };
59}
60
61async function resolveEntries(raw: RawFile): Promise<{
62 did: string;
63 badges?: string[];
64 position?: number;
65}[]> {
66 const out: { did: string; badges?: string[]; position?: number }[] = [];
67 for (const [i, e] of raw.entries.entries()) {
68 let did = e.did;
69 if (!did) {
70 if (!e.handle) throw new Error(`entry ${i}: must have "did" or "handle"`);
71 const id = await resolveIdentity(e.handle);
72 did = id.did;
73 console.log(`[publish-featured] resolved ${e.handle} -> ${did}`);
74 }
75 out.push({ did, badges: e.badges, position: e.position ?? i });
76 }
77 return out;
78}
79
80async function main() {
81 const path = Deno.args[0] ?? "./featured.json";
82 const did = Deno.env.get("ATMOSPHERE_DID");
83 if (!did) {
84 console.error("ATMOSPHERE_DID env var is required.");
85 Deno.exit(1);
86 }
87
88 const raw = await loadFile(path);
89 const entries = await resolveEntries(raw);
90 const record = { entries };
91
92 const validation = validateFeatured(record);
93 if (!validation.ok || !validation.value) {
94 console.error(`Invalid featured record: ${validation.error}`);
95 Deno.exit(1);
96 }
97
98 const session = await getValidSession(did);
99 if (!session) {
100 console.error(
101 `No active OAuth session for ${did}. Sign in once via /oauth/login as ` +
102 `the Atmosphere account, then re-run this script.`,
103 );
104 Deno.exit(1);
105 }
106
107 const result = await putRecord(
108 did,
109 session.pdsUrl,
110 FEATURED_NSID,
111 "self",
112 record as unknown as Record<string, unknown>,
113 );
114 console.log(`[publish-featured] put ${result.uri} cid=${result.cid}`);
115 console.log(
116 `[publish-featured] indexer will pick up the change via Jetstream within seconds.`,
117 );
118}
119
120if (import.meta.main) {
121 await main();
122}