···5656 }
5757}
58585959+/** Known favicon URLs for domains that don't serve at standard paths. */
6060+const KNOWN_FAVICONS: Record<string, string> = {
6161+ "bsky.app": "https://web-cdn.bsky.app/static/favicon-32x32.png",
6262+};
6363+6464+async function fetchKnownFavicon(
6565+ domain: string,
6666+): Promise<{ data: Buffer; contentType: string } | null> {
6767+ const url = KNOWN_FAVICONS[domain];
6868+ if (!url) return null;
6969+ try {
7070+ const res = await fetch(url, {
7171+ signal: AbortSignal.timeout(FETCH_TIMEOUT),
7272+ redirect: "error",
7373+ });
7474+ if (!res.ok) return null;
7575+ const ct = res.headers.get("content-type") ?? "";
7676+ if (!ct.startsWith("image/")) return null;
7777+ const buf = Buffer.from(await res.arrayBuffer());
7878+ if (buf.length === 0 || buf.length > MAX_SIZE) return null;
7979+ return { data: buf, contentType: ct.split(";")[0]! };
8080+ } catch {
8181+ return null;
8282+ }
8383+}
8484+5985async function fetchFavicon(domain: string): Promise<{ data: Buffer; contentType: string } | null> {
8686+ const known = await fetchKnownFavicon(domain);
8787+ if (known) return known;
8888+6089 const ip = await resolveSafeIP(domain);
6190 if (!ip) return null;
6291
+5-5
lib/jetstream/handler.test.ts
···202202203203 await handleMatchedEvent(match);
204204205205- // Second action should receive fetchContext with action0 result
205205+ // Second action should receive fetchContext with action1 result
206206 expect(mockExecuteBskyPost).toHaveBeenCalledWith(
207207 match,
208208 1,
209209 expect.objectContaining({
210210- action0: {
210210+ action1: {
211211 uri: okWithUri.uri,
212212 cid: okWithUri.cid,
213213 rkey: "abc123",
···229229230230 expect(mockDispatch).toHaveBeenCalledOnce();
231231 expect(mockExecuteAction).toHaveBeenCalledOnce();
232232- // fetchContext should NOT contain action0 (webhook has no uri/cid)
233233- // but it does contain action1 from the record action (mutated after call)
232232+ // fetchContext should NOT contain action1 (webhook has no uri/cid)
233233+ // but it does contain action2 from the record action (mutated after call)
234234 // So we verify the record action result is present but webhook result is not
235235 const finalCtx = mockExecuteAction.mock.calls[0]![2]!;
236236- expect(finalCtx).not.toHaveProperty("action0");
236236+ expect(finalCtx).not.toHaveProperty("action1");
237237 });
238238});
+2-2
lib/jetstream/handler.ts
···3333 await logDryRun(match, i, action, fetchContext, fetchErrors);
3434 // Inject synthetic action result so subsequent dry-run actions can reference {{actionN.*}}
3535 if (isRecordProducingAction(action.$type)) {
3636- fetchContext[`action${i}`] = {
3636+ fetchContext[`action${i + 1}`] = {
3737 uri: `at://dry-run/${action.$type}/placeholder`,
3838 cid: "dry-run-cid",
3939 rkey: "placeholder",
···6161 // Accumulate result into fetchContext for downstream actions
6262 if (result.uri && result.cid) {
6363 const rkey = result.uri.split("/").pop() ?? "";
6464- fetchContext[`action${i}`] = { uri: result.uri, cid: result.cid, rkey, record: {} };
6464+ fetchContext[`action${i + 1}`] = { uri: result.uri, cid: result.cid, rkey, record: {} };
6565 }
66666767 // Fail-fast: stop chain on error