@farcaster/snap-hono 1.3.2 → 1.3.3
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/dist/og-image.js +33 -11
- package/dist/payloadToResponse.js +15 -9
- package/dist/renderSnapPage.d.ts +2 -6
- package/dist/renderSnapPage.js +166 -223
- package/package.json +2 -2
- package/src/og-image.ts +36 -13
- package/src/payloadToResponse.ts +20 -12
- package/src/renderSnapPage.ts +175 -325
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 "
|
|
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 "
|
|
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
|
|
775
|
-
const
|
|
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 =
|
|
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
|
-
|
|
817
|
+
elements,
|
|
796
818
|
imageMap,
|
|
797
819
|
pageButtons,
|
|
798
|
-
|
|
820
|
+
"column",
|
|
799
821
|
);
|
|
800
822
|
|
|
801
|
-
// Build element VNodes
|
|
802
|
-
const elementNodes =
|
|
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:
|
|
850
|
+
flexDirection: "column" as const,
|
|
828
851
|
gap: 8,
|
|
829
852
|
marginTop: 12,
|
|
830
853
|
width: OG_CARD_INNER_WIDTH_PX,
|
package/src/payloadToResponse.ts
CHANGED
|
@@ -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
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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,
|