@farcaster/snap-hono 1.3.2 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/og-image.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { SnapHandlerResult } from "@farcaster/snap";
1
+ import type { SnapHandlerResult, SnapSpec } from "@farcaster/snap";
2
2
  import { DEFAULT_THEME_ACCENT, PALETTE_LIGHT_HEX } from "@farcaster/snap";
3
3
  import satori from "satori";
4
4
  import { Resvg, initWasm } from "@resvg/resvg-wasm";
@@ -542,7 +542,7 @@ function mapElement(
542
542
  return mapProgress(el, accent);
543
543
  case "list":
544
544
  return mapList(el);
545
- case "button_group":
545
+ case "toggle_group":
546
546
  return mapButtonGroup(el, accent);
547
547
  case "bar_chart":
548
548
  return mapBarChart(el, accent);
@@ -696,7 +696,7 @@ function estimateElementHeight(el: El, imageMap: Map<string, string>): number {
696
696
  return estimateProgressHeight(el);
697
697
  case "list":
698
698
  return estimateListHeight(el);
699
- case "button_group":
699
+ case "toggle_group":
700
700
  return estimateButtonGroupHeight(el);
701
701
  case "bar_chart":
702
702
  return 100;
@@ -765,17 +765,41 @@ function estimateDefaultOgHeight(
765
765
  );
766
766
  }
767
767
 
768
+ // ─── Spec helpers ─────────────────────────────────────
769
+
770
+ /** Walk the flat spec from root and collect top-level children as El objects for the OG renderer. */
771
+ function specToElementList(spec: SnapSpec): El[] {
772
+ const rootEl = spec.elements[spec.root];
773
+ if (!rootEl?.children) return [];
774
+ return rootEl.children
775
+ .map((key) => {
776
+ const el = spec.elements[key];
777
+ if (!el) return null;
778
+ return { type: el.type, ...el.props } as El;
779
+ })
780
+ .filter((el): el is El => el != null);
781
+ }
782
+
783
+ /** Extract button elements (type: "button") from the spec. */
784
+ function specToButtons(spec: SnapSpec): El[] {
785
+ return Object.values(spec.elements)
786
+ .filter((el) => el.type === "button")
787
+ .map((el) => ({ type: "button", ...el.props }) as El);
788
+ }
789
+
768
790
  // ─── Main PNG renderer ─────────────────────────────────────
769
791
 
770
792
  export async function renderSnapPageToPng(
771
793
  snap: SnapHandlerResult,
772
794
  options?: OgOptions,
773
795
  ): Promise<Uint8Array> {
774
- const page = snap.page;
775
- const accent = accentHex(page.theme?.accent as string | undefined);
796
+ const accent = accentHex(snap.theme?.accent as string | undefined);
797
+ const spec = snap.ui as unknown as SnapSpec;
798
+ const elements = specToElementList(spec);
799
+ const pageButtons = specToButtons(spec);
776
800
 
777
801
  // Pre-fetch all image URLs (SSRF-safe)
778
- const imageUrls = (page.elements.children as El[])
802
+ const imageUrls = elements
779
803
  .filter((el) => el.type === "image")
780
804
  .map((el) => el.url as string);
781
805
  const unique = [...new Set(imageUrls)];
@@ -786,20 +810,19 @@ export async function renderSnapPageToPng(
786
810
  fetched.filter(([, v]) => v != null) as [string, string][],
787
811
  );
788
812
 
789
- const buttonLayout = page.button_layout as string | undefined;
790
- const pageButtons = (page.buttons as El[] | undefined) ?? [];
791
813
  const W = options?.width ?? DEFAULT_OG_WIDTH_PX;
792
814
  const H =
793
815
  options?.height ??
794
816
  estimateDefaultOgHeight(
795
- page.elements.children as El[],
817
+ elements,
796
818
  imageMap,
797
819
  pageButtons,
798
- buttonLayout,
820
+ "column",
799
821
  );
800
822
 
801
- // Build element VNodes
802
- const elementNodes = (page.elements.children as El[])
823
+ // Build element VNodes (skip buttons — handled separately)
824
+ const elementNodes = elements
825
+ .filter((el) => el.type !== "button")
803
826
  .map((el) => mapElement(el, accent, imageMap))
804
827
  .filter((n): n is VNode => n != null);
805
828
 
@@ -824,7 +847,7 @@ export async function renderSnapPageToPng(
824
847
  "div",
825
848
  {
826
849
  display: "flex",
827
- flexDirection: buttonLayout === "row" ? "row" : "column",
850
+ flexDirection: "column" as const,
828
851
  gap: 8,
829
852
  marginTop: 12,
830
853
  width: OG_CARD_INNER_WIDTH_PX,
@@ -4,6 +4,7 @@ import {
4
4
  validateSnapResponse,
5
5
  snapResponseSchema,
6
6
  } from "@farcaster/snap";
7
+ import { snapJsonRenderCatalog } from "@farcaster/snap/ui";
7
8
 
8
9
  type PayloadToResponseOptions = {
9
10
  resourcePath: string;
@@ -19,20 +20,17 @@ export function payloadToResponse(
19
20
  const resourcePath = options.resourcePath ?? "/";
20
21
  const mediaTypes = options.mediaTypes ?? [...DEFAULT_LINK_MEDIA_TYPES];
21
22
 
23
+ // Validate snap envelope (version, theme, effects, ui shape)
22
24
  const validation = validateSnapResponse(payload);
23
25
  if (!validation.valid) {
24
- return new Response(
25
- JSON.stringify({
26
- error: "invalid snap page",
27
- issues: validation.issues,
28
- }),
29
- {
30
- status: 400,
31
- headers: {
32
- "Content-Type": "application/json; charset=utf-8",
33
- },
34
- },
35
- );
26
+ return errorResponse("invalid snap page", validation.issues);
27
+ }
28
+
29
+ // Validate ui against catalog (element types, props, actions)
30
+ const catalogResult = snapJsonRenderCatalog.validate(payload.ui);
31
+ if (!catalogResult.success) {
32
+ const issues = catalogResult.error?.issues ?? [];
33
+ return errorResponse("invalid snap ui", issues);
36
34
  }
37
35
 
38
36
  const finalized = snapResponseSchema.parse(payload);
@@ -44,6 +42,16 @@ export function payloadToResponse(
44
42
  });
45
43
  }
46
44
 
45
+ function errorResponse(error: string, issues: unknown[]): Response {
46
+ return new Response(
47
+ JSON.stringify({ error, issues }),
48
+ {
49
+ status: 400,
50
+ headers: { "Content-Type": "application/json; charset=utf-8" },
51
+ },
52
+ );
53
+ }
54
+
47
55
  export function snapHeaders(
48
56
  resourcePath: string,
49
57
  currentMediaType: string,