@farcaster/snap 2.0.0 → 2.0.2

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.
Files changed (194) hide show
  1. package/dist/colors.d.ts +4 -4
  2. package/dist/colors.js +20 -20
  3. package/dist/constants.d.ts +17 -1
  4. package/dist/constants.js +19 -1
  5. package/dist/index.d.ts +4 -6
  6. package/dist/index.js +2 -4
  7. package/dist/react/accent-context.d.ts +3 -1
  8. package/dist/react/accent-context.js +7 -4
  9. package/dist/react/catalog-renderer.js +4 -0
  10. package/dist/react/components/action-button.d.ts +2 -1
  11. package/dist/react/components/action-button.js +35 -13
  12. package/dist/react/components/badge.js +8 -8
  13. package/dist/react/components/bar-chart.d.ts +5 -0
  14. package/dist/react/components/bar-chart.js +26 -0
  15. package/dist/react/components/cell-grid.d.ts +5 -0
  16. package/dist/react/components/cell-grid.js +87 -0
  17. package/dist/react/components/icon.js +4 -10
  18. package/dist/react/components/input.js +12 -6
  19. package/dist/react/components/item-group.js +3 -1
  20. package/dist/react/components/item.d.ts +3 -3
  21. package/dist/react/components/item.js +4 -3
  22. package/dist/react/components/progress.js +3 -3
  23. package/dist/react/components/separator.js +3 -1
  24. package/dist/react/components/slider.js +15 -10
  25. package/dist/react/components/switch.js +10 -12
  26. package/dist/react/components/text.js +6 -14
  27. package/dist/react/components/toggle-group.js +20 -6
  28. package/dist/react/hooks/use-snap-colors.d.ts +38 -0
  29. package/dist/react/hooks/use-snap-colors.js +81 -0
  30. package/dist/react/index.d.ts +13 -1
  31. package/dist/react/index.js +9 -188
  32. package/dist/react/snap-view-core.d.ts +11 -0
  33. package/dist/react/snap-view-core.js +227 -0
  34. package/dist/react/v1/snap-view.d.ts +16 -0
  35. package/dist/react/v1/snap-view.js +90 -0
  36. package/dist/react/v2/snap-view.d.ts +23 -0
  37. package/dist/react/v2/snap-view.js +91 -0
  38. package/dist/react-native/catalog-renderer.d.ts +5 -0
  39. package/dist/react-native/catalog-renderer.js +40 -0
  40. package/dist/react-native/components/snap-action-button.d.ts +2 -0
  41. package/dist/react-native/components/snap-action-button.js +69 -0
  42. package/dist/react-native/components/snap-badge.d.ts +2 -0
  43. package/dist/react-native/components/snap-badge.js +41 -0
  44. package/dist/react-native/components/snap-bar-chart.d.ts +2 -0
  45. package/dist/react-native/components/snap-bar-chart.js +39 -0
  46. package/dist/react-native/components/snap-cell-grid.d.ts +2 -0
  47. package/dist/react-native/components/snap-cell-grid.js +94 -0
  48. package/dist/react-native/components/snap-icon.d.ts +5 -0
  49. package/dist/react-native/components/snap-icon.js +56 -0
  50. package/dist/react-native/components/snap-image.d.ts +2 -0
  51. package/dist/react-native/components/snap-image.js +23 -0
  52. package/dist/react-native/components/snap-input.d.ts +2 -0
  53. package/dist/react-native/components/snap-input.js +37 -0
  54. package/dist/react-native/components/snap-item-group.d.ts +5 -0
  55. package/dist/react-native/components/snap-item-group.js +23 -0
  56. package/dist/react-native/components/snap-item.d.ts +5 -0
  57. package/dist/react-native/components/snap-item.js +42 -0
  58. package/dist/react-native/components/snap-progress.d.ts +2 -0
  59. package/dist/react-native/components/snap-progress.js +26 -0
  60. package/dist/react-native/components/snap-separator.d.ts +2 -0
  61. package/dist/react-native/components/snap-separator.js +23 -0
  62. package/dist/react-native/components/snap-slider.d.ts +2 -0
  63. package/dist/react-native/components/snap-slider.js +43 -0
  64. package/dist/react-native/components/snap-stack.d.ts +5 -0
  65. package/dist/react-native/components/snap-stack.js +49 -0
  66. package/dist/react-native/components/snap-switch.d.ts +2 -0
  67. package/dist/react-native/components/snap-switch.js +31 -0
  68. package/dist/react-native/components/snap-text.d.ts +2 -0
  69. package/dist/react-native/components/snap-text.js +35 -0
  70. package/dist/react-native/components/snap-toggle-group.d.ts +2 -0
  71. package/dist/react-native/components/snap-toggle-group.js +99 -0
  72. package/dist/react-native/confetti-overlay.d.ts +1 -0
  73. package/dist/react-native/confetti-overlay.js +106 -0
  74. package/dist/react-native/index.d.ts +28 -0
  75. package/dist/react-native/index.js +15 -0
  76. package/dist/react-native/snap-view-core.d.ts +11 -0
  77. package/dist/react-native/snap-view-core.js +156 -0
  78. package/dist/react-native/theme.d.ts +27 -0
  79. package/dist/react-native/theme.js +43 -0
  80. package/dist/react-native/types.d.ts +42 -0
  81. package/dist/react-native/types.js +1 -0
  82. package/dist/react-native/use-snap-palette.d.ts +13 -0
  83. package/dist/react-native/use-snap-palette.js +48 -0
  84. package/dist/react-native/v1/snap-view.d.ts +24 -0
  85. package/dist/react-native/v1/snap-view.js +96 -0
  86. package/dist/react-native/v2/snap-view.d.ts +33 -0
  87. package/dist/react-native/v2/snap-view.js +114 -0
  88. package/dist/schemas.d.ts +100 -13
  89. package/dist/schemas.js +28 -10
  90. package/dist/server/parseRequest.d.ts +10 -0
  91. package/dist/server/parseRequest.js +48 -7
  92. package/dist/server/verify.d.ts +1 -0
  93. package/dist/server/verify.js +1 -0
  94. package/dist/ui/badge.d.ts +7 -2
  95. package/dist/ui/badge.js +2 -0
  96. package/dist/ui/bar-chart.d.ts +30 -0
  97. package/dist/ui/bar-chart.js +30 -0
  98. package/dist/ui/button.d.ts +4 -6
  99. package/dist/ui/button.js +1 -1
  100. package/dist/ui/catalog.d.ts +90 -16
  101. package/dist/ui/catalog.js +17 -3
  102. package/dist/ui/cell-grid.d.ts +34 -0
  103. package/dist/ui/cell-grid.js +39 -0
  104. package/dist/ui/icon.d.ts +2 -2
  105. package/dist/ui/image.d.ts +1 -2
  106. package/dist/ui/image.js +1 -1
  107. package/dist/ui/index.d.ts +4 -0
  108. package/dist/ui/index.js +2 -0
  109. package/dist/ui/item.d.ts +1 -3
  110. package/dist/ui/item.js +1 -1
  111. package/dist/ui/schema.d.ts +6 -2
  112. package/dist/ui/schema.js +2 -2
  113. package/dist/ui/slider.d.ts +1 -0
  114. package/dist/ui/slider.js +2 -0
  115. package/dist/ui/text.d.ts +2 -4
  116. package/dist/ui/text.js +2 -2
  117. package/dist/validator.d.ts +3 -2
  118. package/dist/validator.js +203 -2
  119. package/llms.txt +199 -0
  120. package/package.json +9 -3
  121. package/src/colors.ts +20 -20
  122. package/src/constants.ts +23 -1
  123. package/src/index.ts +16 -13
  124. package/src/react/accent-context.tsx +13 -6
  125. package/src/react/catalog-renderer.tsx +4 -0
  126. package/src/react/components/action-button.tsx +50 -20
  127. package/src/react/components/badge.tsx +14 -18
  128. package/src/react/components/bar-chart.tsx +69 -0
  129. package/src/react/components/cell-grid.tsx +128 -0
  130. package/src/react/components/icon.tsx +5 -18
  131. package/src/react/components/input.tsx +20 -9
  132. package/src/react/components/item-group.tsx +4 -1
  133. package/src/react/components/item.tsx +13 -10
  134. package/src/react/components/progress.tsx +12 -7
  135. package/src/react/components/separator.tsx +8 -1
  136. package/src/react/components/slider.tsx +28 -15
  137. package/src/react/components/switch.tsx +12 -16
  138. package/src/react/components/text.tsx +14 -23
  139. package/src/react/components/toggle-group.tsx +26 -9
  140. package/src/react/hooks/use-snap-colors.ts +128 -0
  141. package/src/react/index.tsx +49 -265
  142. package/src/react/snap-view-core.tsx +343 -0
  143. package/src/react/v1/snap-view.tsx +176 -0
  144. package/src/react/v2/snap-view.tsx +199 -0
  145. package/src/react-native/catalog-renderer.tsx +41 -0
  146. package/src/react-native/components/snap-action-button.tsx +96 -0
  147. package/src/react-native/components/snap-badge.tsx +60 -0
  148. package/src/react-native/components/snap-bar-chart.tsx +73 -0
  149. package/src/react-native/components/snap-cell-grid.tsx +150 -0
  150. package/src/react-native/components/snap-icon.tsx +102 -0
  151. package/src/react-native/components/snap-image.tsx +37 -0
  152. package/src/react-native/components/snap-input.tsx +58 -0
  153. package/src/react-native/components/snap-item-group.tsx +43 -0
  154. package/src/react-native/components/snap-item.tsx +66 -0
  155. package/src/react-native/components/snap-progress.tsx +40 -0
  156. package/src/react-native/components/snap-separator.tsx +32 -0
  157. package/src/react-native/components/snap-slider.tsx +85 -0
  158. package/src/react-native/components/snap-stack.tsx +66 -0
  159. package/src/react-native/components/snap-switch.tsx +46 -0
  160. package/src/react-native/components/snap-text.tsx +51 -0
  161. package/src/react-native/components/snap-toggle-group.tsx +127 -0
  162. package/src/react-native/confetti-overlay.tsx +134 -0
  163. package/src/react-native/index.tsx +83 -0
  164. package/src/react-native/snap-view-core.tsx +212 -0
  165. package/src/react-native/theme.tsx +85 -0
  166. package/src/react-native/types.ts +38 -0
  167. package/src/react-native/use-snap-palette.ts +64 -0
  168. package/src/react-native/v1/snap-view.tsx +229 -0
  169. package/src/react-native/v2/snap-view.tsx +283 -0
  170. package/src/schemas.ts +68 -17
  171. package/src/server/parseRequest.ts +68 -9
  172. package/src/server/verify.ts +2 -0
  173. package/src/ui/README.md +8 -8
  174. package/src/ui/badge.ts +2 -0
  175. package/src/ui/bar-chart.ts +38 -0
  176. package/src/ui/button.ts +1 -1
  177. package/src/ui/catalog.ts +19 -3
  178. package/src/ui/cell-grid.ts +49 -0
  179. package/src/ui/image.ts +1 -1
  180. package/src/ui/index.ts +6 -0
  181. package/src/ui/item.ts +1 -1
  182. package/src/ui/schema.ts +2 -2
  183. package/src/ui/slider.ts +2 -0
  184. package/src/ui/text.ts +2 -2
  185. package/src/validator.ts +251 -2
  186. package/dist/dataStore.d.ts +0 -12
  187. package/dist/dataStore.js +0 -35
  188. package/dist/middleware.d.ts +0 -3
  189. package/dist/middleware.js +0 -3
  190. package/dist/react/hooks/use-snap-accent.d.ts +0 -13
  191. package/dist/react/hooks/use-snap-accent.js +0 -32
  192. package/src/dataStore.ts +0 -62
  193. package/src/middleware.ts +0 -7
  194. package/src/react/hooks/use-snap-accent.ts +0 -45
@@ -0,0 +1,114 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useState } from "react";
3
+ import { Platform, StyleSheet, Text, View } from "react-native";
4
+ import { SnapThemeProvider, useSnapTheme } from "../theme.js";
5
+ import { SnapViewCoreInner } from "../snap-view-core.js";
6
+ import { validateSnapResponse, } from "@farcaster/snap";
7
+ // ─── Constants ───────────────────────────────────────
8
+ const SNAP_MAX_HEIGHT = 500;
9
+ const SNAP_WARNING_HEIGHT = 700;
10
+ // ─── Validation fallback ─────────────────────────────
11
+ function SnapValidationFallback({ message }) {
12
+ const { colors } = useSnapTheme();
13
+ return (_jsx(View, { style: fallbackStyles.container, children: _jsx(Text, { style: [fallbackStyles.text, { color: colors.textSecondary }], children: message ? `Unable to render snap: ${message}` : "Unable to render snap" }) }));
14
+ }
15
+ const fallbackStyles = StyleSheet.create({
16
+ container: {
17
+ width: "100%",
18
+ padding: 16,
19
+ alignItems: "center",
20
+ justifyContent: "center",
21
+ },
22
+ text: {
23
+ fontSize: 14,
24
+ },
25
+ });
26
+ // ─── SnapViewV2 (with validation) ────────────────────
27
+ export function SnapViewV2Inner({ snap, handlers, loading = false, onValidationError, validationErrorFallback, }) {
28
+ const validation = useMemo(() => validateSnapResponse(snap), [snap]);
29
+ const valid = validation.valid;
30
+ const validationMessage = validation.issues[0]?.message;
31
+ useEffect(() => {
32
+ if (!valid) {
33
+ if (onValidationError) {
34
+ onValidationError(validation);
35
+ }
36
+ else {
37
+ // eslint-disable-next-line no-console
38
+ console.warn("[Snap] validation issues:", validation.issues);
39
+ }
40
+ }
41
+ }, [valid, validation, onValidationError]);
42
+ if (!valid) {
43
+ if (validationErrorFallback === null)
44
+ return null;
45
+ return (_jsx(_Fragment, { children: validationErrorFallback ?? _jsx(SnapValidationFallback, { message: validationMessage }) }));
46
+ }
47
+ return (_jsx(SnapViewCoreInner, { snap: snap, handlers: handlers, loading: loading }));
48
+ }
49
+ export function SnapViewV2({ snap, handlers, loading = false, appearance = "dark", colors, onValidationError, validationErrorFallback, }) {
50
+ return (_jsx(SnapThemeProvider, { appearance: appearance, colors: colors, children: _jsx(SnapViewV2Inner, { snap: snap, handlers: handlers, loading: loading, onValidationError: onValidationError, validationErrorFallback: validationErrorFallback }) }));
51
+ }
52
+ // ─── SnapCardV2 (card frame + height limits) ─────────
53
+ function SnapCardV2Inner({ snap, handlers, loading, borderRadius, showOverflowWarning, onValidationError, validationErrorFallback, actionError, appearance, plain, }) {
54
+ const { colors } = useSnapTheme();
55
+ const [contentHeight, setContentHeight] = useState(0);
56
+ const content = (_jsx(SnapViewV2Inner, { snap: snap, handlers: handlers, loading: loading, onValidationError: onValidationError, validationErrorFallback: validationErrorFallback }));
57
+ if (plain) {
58
+ return content;
59
+ }
60
+ const overflowAmount = showOverflowWarning ? contentHeight - SNAP_MAX_HEIGHT : 0;
61
+ return (_jsxs(_Fragment, { children: [_jsxs(View, { style: {
62
+ borderRadius,
63
+ borderWidth: 1,
64
+ borderColor: colors.border,
65
+ backgroundColor: colors.surface,
66
+ maxHeight: showOverflowWarning ? undefined : SNAP_MAX_HEIGHT,
67
+ overflow: "hidden",
68
+ minHeight: 120,
69
+ }, children: [_jsx(View, { collapsable: false, onLayout: (e) => setContentHeight(Math.round(e.nativeEvent.layout.height)), style: { paddingHorizontal: 16, paddingVertical: 16 }, children: content }), showOverflowWarning && contentHeight > SNAP_MAX_HEIGHT && (_jsxs(View, { style: { position: "absolute", top: SNAP_MAX_HEIGHT, left: 0, right: 0, height: overflowAmount, zIndex: 10, pointerEvents: "none" }, children: [_jsx(View, { style: { height: 1, borderTopWidth: 1, borderStyle: "dashed", borderColor: "rgba(255,100,100,0.6)" } }), _jsx(View, { style: { position: "absolute", top: -10, right: 4, backgroundColor: "rgba(0,0,0,0.7)", paddingHorizontal: 4, paddingVertical: 1, borderRadius: 3 }, children: _jsxs(Text, { style: { fontSize: 10, color: "rgba(255,100,100,0.7)", fontFamily: Platform.select({ ios: "Menlo", default: "monospace" }) }, children: [SNAP_MAX_HEIGHT, "px"] }) }), _jsx(View, { style: { flex: 1, backgroundColor: "rgba(255,50,50,0.15)" } })] }))] }), actionError && (_jsx(Text, { style: {
70
+ paddingHorizontal: 12,
71
+ paddingVertical: 8,
72
+ fontSize: 13,
73
+ color: appearance === "dark"
74
+ ? "rgba(255,100,100,0.9)"
75
+ : "rgba(200,0,0,0.8)",
76
+ }, children: actionError }))] }));
77
+ }
78
+ export function SnapCardV2({ snap, handlers, loading = false, appearance = "dark", colors, borderRadius = 16, showOverflowWarning = false, onValidationError, validationErrorFallback, actionError, plain = false, }) {
79
+ return (_jsx(SnapThemeProvider, { appearance: appearance, colors: colors, children: _jsx(SnapCardV2Inner, { snap: snap, handlers: handlers, loading: loading, borderRadius: borderRadius, showOverflowWarning: showOverflowWarning, onValidationError: onValidationError, validationErrorFallback: validationErrorFallback, actionError: actionError, appearance: appearance, plain: plain }) }));
80
+ }
81
+ const cardStyles = StyleSheet.create({
82
+ frameRing: { alignSelf: "stretch" },
83
+ card: { borderWidth: 1, minHeight: 120, overflow: "hidden" },
84
+ body: { paddingHorizontal: 16, paddingVertical: 16 },
85
+ actionError: { paddingHorizontal: 12, paddingVertical: 8, fontSize: 13 },
86
+ warningOverlay: {
87
+ position: "absolute",
88
+ top: SNAP_MAX_HEIGHT,
89
+ left: 0,
90
+ right: 0,
91
+ bottom: 0,
92
+ zIndex: 10,
93
+ },
94
+ warningLine: {
95
+ height: 1,
96
+ borderTopWidth: 1,
97
+ borderStyle: "dashed",
98
+ borderColor: "rgba(255,100,100,0.6)",
99
+ },
100
+ warningLabel: {
101
+ position: "absolute",
102
+ top: -10,
103
+ right: 4,
104
+ backgroundColor: "rgba(0,0,0,0.7)",
105
+ paddingHorizontal: 4,
106
+ paddingVertical: 1,
107
+ borderRadius: 3,
108
+ },
109
+ warningLabelText: {
110
+ fontSize: 10,
111
+ color: "rgba(255,100,100,0.7)",
112
+ fontFamily: Platform.select({ ios: "Menlo", default: "monospace" }),
113
+ },
114
+ });
package/dist/schemas.d.ts CHANGED
@@ -1,8 +1,21 @@
1
1
  import { z } from "zod";
2
2
  import type { Spec } from "@json-render/core";
3
- import { type SnapDataStore } from "./dataStore.js";
3
+ import { type SpecVersion } from "./constants.js";
4
+ declare const themeAccentSchema: z.ZodEnum<{
5
+ gray: "gray";
6
+ blue: "blue";
7
+ red: "red";
8
+ amber: "amber";
9
+ green: "green";
10
+ teal: "teal";
11
+ purple: "purple";
12
+ pink: "pink";
13
+ }>;
4
14
  export declare const snapResponseSchema: z.ZodObject<{
5
- version: z.ZodLiteral<"1.0">;
15
+ version: z.ZodEnum<{
16
+ "1.0": "1.0";
17
+ "2.0": "2.0";
18
+ }>;
6
19
  theme: z.ZodDefault<z.ZodOptional<z.ZodObject<{
7
20
  accent: z.ZodDefault<z.ZodEnum<{
8
21
  gray: "gray";
@@ -21,13 +34,60 @@ export declare const snapResponseSchema: z.ZodObject<{
21
34
  ui: z.ZodCustom<Spec, Spec>;
22
35
  }, z.core.$strict>;
23
36
  export type SnapResponse = z.infer<typeof snapResponseSchema>;
24
- export type SnapHandlerResult = z.input<typeof snapResponseSchema>;
37
+ /**
38
+ * Permissive element input type for snap handler authors.
39
+ * Allows dynamic element construction without requiring exact UIElement types.
40
+ */
41
+ export type SnapElementInput = {
42
+ type: string;
43
+ props?: Record<string, unknown>;
44
+ children?: string[];
45
+ on?: Record<string, unknown>;
46
+ [key: string]: unknown;
47
+ };
48
+ /**
49
+ * Permissive input type for the `ui` field in snap handler return values.
50
+ * Accepts dynamically-built element maps (e.g. `Record<string, SnapElementInput>`)
51
+ * without requiring exact UIElement types.
52
+ */
53
+ export type SnapSpecInput = {
54
+ root: string;
55
+ elements: Record<string, SnapElementInput>;
56
+ state?: Record<string, unknown>;
57
+ };
58
+ /**
59
+ * Return type for snap handler functions.
60
+ * Uses permissive input types so handlers can build elements dynamically
61
+ * without type casts. Runtime validation via the Zod schema still catches invalid shapes.
62
+ */
63
+ export type SnapHandlerResult = {
64
+ version: SpecVersion;
65
+ theme?: {
66
+ accent?: z.input<typeof themeAccentSchema>;
67
+ };
68
+ effects?: z.input<typeof snapResponseSchema>["effects"];
69
+ ui: SnapSpecInput;
70
+ };
25
71
  export declare const payloadSchema: z.ZodObject<{
26
- fid: z.ZodNumber;
72
+ fid: z.ZodOptional<z.ZodNumber>;
27
73
  inputs: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodArray<z.ZodString>]>>>;
28
- button_index: z.ZodNumber;
29
74
  timestamp: z.ZodNumber;
30
- }, z.core.$strict>;
75
+ audience: z.ZodString;
76
+ user: z.ZodObject<{
77
+ fid: z.ZodNumber;
78
+ }, z.core.$strip>;
79
+ surface: z.ZodDiscriminatedUnion<[z.ZodObject<{
80
+ type: z.ZodLiteral<"cast">;
81
+ cast: z.ZodObject<{
82
+ hash: z.ZodString;
83
+ author: z.ZodObject<{
84
+ fid: z.ZodNumber;
85
+ }, z.core.$strip>;
86
+ }, z.core.$strip>;
87
+ }, z.core.$strip>, z.ZodObject<{
88
+ type: z.ZodLiteral<"standalone">;
89
+ }, z.core.$strip>], "type">;
90
+ }, z.core.$strip>;
31
91
  export type SnapPayload = z.infer<typeof payloadSchema>;
32
92
  export declare const ACTION_TYPE_GET: "get";
33
93
  export declare const ACTION_TYPE_POST: "post";
@@ -36,27 +96,54 @@ declare const snapGetActionSchema: z.ZodObject<{
36
96
  }, z.core.$strip>;
37
97
  export type SnapGetAction = z.infer<typeof snapGetActionSchema>;
38
98
  declare const snapPostActionSchema: z.ZodObject<{
39
- fid: z.ZodNumber;
99
+ fid: z.ZodOptional<z.ZodNumber>;
40
100
  inputs: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodArray<z.ZodString>]>>>;
41
- button_index: z.ZodNumber;
42
101
  timestamp: z.ZodNumber;
102
+ audience: z.ZodString;
103
+ user: z.ZodObject<{
104
+ fid: z.ZodNumber;
105
+ }, z.core.$strip>;
106
+ surface: z.ZodDiscriminatedUnion<[z.ZodObject<{
107
+ type: z.ZodLiteral<"cast">;
108
+ cast: z.ZodObject<{
109
+ hash: z.ZodString;
110
+ author: z.ZodObject<{
111
+ fid: z.ZodNumber;
112
+ }, z.core.$strip>;
113
+ }, z.core.$strip>;
114
+ }, z.core.$strip>, z.ZodObject<{
115
+ type: z.ZodLiteral<"standalone">;
116
+ }, z.core.$strip>], "type">;
43
117
  type: z.ZodLiteral<"post">;
44
- }, z.core.$strict>;
118
+ }, z.core.$strip>;
45
119
  export type SnapPostAction = z.infer<typeof snapPostActionSchema>;
46
120
  export declare const snapActionSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
47
121
  type: z.ZodLiteral<"get">;
48
122
  }, z.core.$strip>, z.ZodObject<{
49
- fid: z.ZodNumber;
123
+ fid: z.ZodOptional<z.ZodNumber>;
50
124
  inputs: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodArray<z.ZodString>]>>>;
51
- button_index: z.ZodNumber;
52
125
  timestamp: z.ZodNumber;
126
+ audience: z.ZodString;
127
+ user: z.ZodObject<{
128
+ fid: z.ZodNumber;
129
+ }, z.core.$strip>;
130
+ surface: z.ZodDiscriminatedUnion<[z.ZodObject<{
131
+ type: z.ZodLiteral<"cast">;
132
+ cast: z.ZodObject<{
133
+ hash: z.ZodString;
134
+ author: z.ZodObject<{
135
+ fid: z.ZodNumber;
136
+ }, z.core.$strip>;
137
+ }, z.core.$strip>;
138
+ }, z.core.$strip>, z.ZodObject<{
139
+ type: z.ZodLiteral<"standalone">;
140
+ }, z.core.$strip>], "type">;
53
141
  type: z.ZodLiteral<"post">;
54
- }, z.core.$strict>], "type">;
142
+ }, z.core.$strip>], "type">;
55
143
  export type SnapAction = z.infer<typeof snapActionSchema>;
56
144
  export type SnapContext = {
57
145
  action: SnapAction;
58
146
  request: Request;
59
- data: SnapDataStore;
60
147
  };
61
148
  export type SnapFunction = (ctx: SnapContext) => Promise<SnapHandlerResult>;
62
149
  export {};
package/dist/schemas.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
- import { EFFECT_VALUES, SPEC_VERSION, } from "./constants.js";
3
- import { DEFAULT_THEME_ACCENT, PALETTE_COLOR_VALUES, } from "./colors.js";
2
+ import { EFFECT_VALUES, SUPPORTED_SPEC_VERSIONS, } from "./constants.js";
3
+ import { DEFAULT_THEME_ACCENT, PALETTE_COLOR_VALUES } from "./colors.js";
4
4
  // ─── Theme ─────────────────────────────────────────────
5
5
  const themeAccentSchema = z.enum(PALETTE_COLOR_VALUES, {
6
6
  message: `accent must be a palette color: ${PALETTE_COLOR_VALUES.join(", ")}`,
@@ -15,7 +15,7 @@ const themeSchema = z
15
15
  // typed here via the json-render Spec type.
16
16
  export const snapResponseSchema = z
17
17
  .object({
18
- version: z.literal(SPEC_VERSION),
18
+ version: z.enum(SUPPORTED_SPEC_VERSIONS),
19
19
  theme: themeSchema.optional().default({ accent: DEFAULT_THEME_ACCENT }),
20
20
  effects: z.array(z.enum(EFFECT_VALUES)).optional(),
21
21
  ui: z.custom((val) => val != null &&
@@ -31,24 +31,42 @@ const postInputValueSchema = z.union([
31
31
  z.boolean(),
32
32
  z.array(z.string()),
33
33
  ]);
34
+ const standaloneSurfaceSchema = z.object({
35
+ type: z.literal("standalone"),
36
+ });
37
+ const castSurfaceSchema = z.object({
38
+ type: z.literal("cast"),
39
+ cast: z.object({
40
+ hash: z.string(),
41
+ author: z.object({
42
+ fid: z.number().int().nonnegative(),
43
+ }),
44
+ }),
45
+ });
46
+ const surfaceSchema = z.discriminatedUnion("type", [
47
+ castSurfaceSchema,
48
+ standaloneSurfaceSchema,
49
+ ]);
34
50
  export const payloadSchema = z
35
51
  .object({
36
- fid: z.number().int().nonnegative(),
52
+ fid: z.number().int().nonnegative().optional(), // deprecated in favor of user.fid
37
53
  inputs: z.record(z.string(), postInputValueSchema).default({}),
38
- button_index: z.number().int().nonnegative(),
39
54
  timestamp: z.number().int(),
55
+ audience: z.string(),
56
+ user: z.object({
57
+ fid: z.number().int().nonnegative(),
58
+ }),
59
+ surface: surfaceSchema,
40
60
  })
41
- .strict();
61
+ .strip();
42
62
  export const ACTION_TYPE_GET = "get";
43
63
  export const ACTION_TYPE_POST = "post";
44
64
  const snapGetActionSchema = z.object({
45
65
  type: z.literal(ACTION_TYPE_GET),
46
66
  });
47
- const snapPostActionSchema = payloadSchema
48
- .extend({
67
+ const snapPostActionSchema = payloadSchema.extend({
49
68
  type: z.literal(ACTION_TYPE_POST),
50
- })
51
- .strict();
69
+ });
52
70
  export const snapActionSchema = z.discriminatedUnion("type", [
53
71
  snapGetActionSchema,
54
72
  snapPostActionSchema,
@@ -15,6 +15,12 @@ export type ParseRequestError = {
15
15
  } | {
16
16
  type: "signature";
17
17
  message: string;
18
+ } | {
19
+ type: "origin_mismatch";
20
+ message: string;
21
+ } | {
22
+ type: "fid_mismatch";
23
+ message: string;
18
24
  };
19
25
  export type ParseRequestOptions = {
20
26
  /**
@@ -27,6 +33,10 @@ export type ParseRequestOptions = {
27
33
  * potential replays. Defaults to 300 (5 minutes) when not provided.
28
34
  */
29
35
  maxSkewSeconds?: number;
36
+ /**
37
+ * The origin of the request. Derived from the request when not provided.
38
+ */
39
+ requestOrigin?: string;
30
40
  };
31
41
  export type ParseRequestResult = {
32
42
  success: true;
@@ -51,6 +51,14 @@ export async function parseRequest(request, options = {}) {
51
51
  error: { type: "invalid_json", message: parsed.error.message },
52
52
  };
53
53
  }
54
+ const payloadParsed = payloadSchema.safeParse(decodePayload(parsed.data.payload));
55
+ if (!payloadParsed.success) {
56
+ return {
57
+ success: false,
58
+ error: { type: "validation", issues: payloadParsed.error.issues },
59
+ };
60
+ }
61
+ const body = payloadParsed.data;
54
62
  if (!options.skipJFSVerification) {
55
63
  const jfs = await verifyJFSRequestBody(parsed.data);
56
64
  if (!jfs.valid) {
@@ -59,21 +67,54 @@ export async function parseRequest(request, options = {}) {
59
67
  error: { type: "signature", message: jfs.error.message },
60
68
  };
61
69
  }
70
+ if (jfs.signingUserFid !== body.user.fid) {
71
+ return {
72
+ success: false,
73
+ error: {
74
+ type: "fid_mismatch",
75
+ message: `JFS header fid "${jfs.signingUserFid}" does not match user.fid "${body.user.fid}"`,
76
+ },
77
+ };
78
+ }
62
79
  }
63
- const payloadParsed = payloadSchema.safeParse(decodePayload(parsed.data.payload));
64
- if (!payloadParsed.success) {
80
+ if (Math.abs(nowSec - body.timestamp) > maxSkew) {
65
81
  return {
66
82
  success: false,
67
- error: { type: "validation", issues: payloadParsed.error.issues },
83
+ error: {
84
+ type: "replay",
85
+ message: `timestamp outside allowed skew of ${maxSkew}s`,
86
+ },
68
87
  };
69
88
  }
70
- const body = payloadParsed.data;
71
- if (Math.abs(nowSec - body.timestamp) > maxSkew) {
89
+ // Audience validation: ensure the payload audience matches the server origin.
90
+ let expectedOrigin = options.requestOrigin;
91
+ if (expectedOrigin === undefined) {
92
+ try {
93
+ const url = new URL(request.url);
94
+ const proto = request.headers.get("x-forwarded-proto") ??
95
+ url.protocol.replace(":", "");
96
+ const host = request.headers.get("x-forwarded-host") ?? url.host;
97
+ expectedOrigin = `${proto}://${host}`;
98
+ }
99
+ catch {
100
+ // do nothing
101
+ }
102
+ }
103
+ if (expectedOrigin !== undefined && body.audience !== expectedOrigin) {
72
104
  return {
73
105
  success: false,
74
106
  error: {
75
- type: "replay",
76
- message: `timestamp outside allowed skew of ${maxSkew}s`,
107
+ type: "origin_mismatch",
108
+ message: `payload audience "${body.audience}" does not match expected origin "${expectedOrigin}"`,
109
+ },
110
+ };
111
+ }
112
+ if (body.fid !== undefined && body.fid !== body.user.fid) {
113
+ return {
114
+ success: false,
115
+ error: {
116
+ type: "fid_mismatch",
117
+ message: `fid "${body.fid}" does not match user.fid "${body.user.fid}"`,
77
118
  },
78
119
  };
79
120
  }
@@ -9,6 +9,7 @@ export declare function verifyJFSRequestBody<TPayload>(requestBody: {
9
9
  error: Error;
10
10
  } | {
11
11
  valid: true;
12
+ signingUserFid: number;
12
13
  data: TPayload;
13
14
  }>;
14
15
  export declare function decodePayload<TPayload>(payload: string): TPayload;
@@ -69,6 +69,7 @@ export async function verifyJFSRequestBody(requestBody, options = {}) {
69
69
  return {
70
70
  valid: true,
71
71
  data: payload,
72
+ signingUserFid: header.fid,
72
73
  };
73
74
  }
74
75
  export function decodePayload(payload) {
@@ -1,7 +1,12 @@
1
1
  import { z } from "zod";
2
+ export declare const BADGE_VARIANTS: readonly ["default", "outline"];
2
3
  export declare const BADGE_MAX_LABEL_CHARS = 30;
3
4
  export declare const badgeProps: z.ZodObject<{
4
5
  label: z.ZodString;
6
+ variant: z.ZodOptional<z.ZodEnum<{
7
+ default: "default";
8
+ outline: "outline";
9
+ }>>;
5
10
  color: z.ZodOptional<z.ZodEnum<{
6
11
  gray: "gray";
7
12
  blue: "blue";
@@ -14,18 +19,18 @@ export declare const badgeProps: z.ZodObject<{
14
19
  accent: "accent";
15
20
  }>>;
16
21
  icon: z.ZodOptional<z.ZodEnum<{
17
- check: "check";
18
- repeat: "repeat";
19
22
  "arrow-right": "arrow-right";
20
23
  "arrow-left": "arrow-left";
21
24
  "external-link": "external-link";
22
25
  "chevron-right": "chevron-right";
26
+ check: "check";
23
27
  x: "x";
24
28
  "alert-triangle": "alert-triangle";
25
29
  info: "info";
26
30
  clock: "clock";
27
31
  heart: "heart";
28
32
  "message-circle": "message-circle";
33
+ repeat: "repeat";
29
34
  share: "share";
30
35
  user: "user";
31
36
  users: "users";
package/dist/ui/badge.js CHANGED
@@ -1,9 +1,11 @@
1
1
  import { z } from "zod";
2
2
  import { PROGRESS_COLOR_VALUES } from "../colors.js";
3
3
  import { ICON_NAMES } from "./icon.js";
4
+ export const BADGE_VARIANTS = ["default", "outline"];
4
5
  export const BADGE_MAX_LABEL_CHARS = 30;
5
6
  export const badgeProps = z.object({
6
7
  label: z.string().min(1).max(BADGE_MAX_LABEL_CHARS),
8
+ variant: z.enum(BADGE_VARIANTS).optional(),
7
9
  color: z.enum(PROGRESS_COLOR_VALUES).optional(),
8
10
  icon: z.enum(ICON_NAMES).optional(),
9
11
  });
@@ -0,0 +1,30 @@
1
+ import { z } from "zod";
2
+ export declare const barChartProps: z.ZodObject<{
3
+ bars: z.ZodArray<z.ZodObject<{
4
+ label: z.ZodString;
5
+ value: z.ZodNumber;
6
+ color: z.ZodOptional<z.ZodEnum<{
7
+ gray: "gray";
8
+ blue: "blue";
9
+ red: "red";
10
+ amber: "amber";
11
+ green: "green";
12
+ teal: "teal";
13
+ purple: "purple";
14
+ pink: "pink";
15
+ }>>;
16
+ }, z.core.$strip>>;
17
+ max: z.ZodOptional<z.ZodNumber>;
18
+ color: z.ZodOptional<z.ZodEnum<{
19
+ gray: "gray";
20
+ blue: "blue";
21
+ red: "red";
22
+ amber: "amber";
23
+ green: "green";
24
+ teal: "teal";
25
+ purple: "purple";
26
+ pink: "pink";
27
+ accent: "accent";
28
+ }>>;
29
+ }, z.core.$strip>;
30
+ export type BarChartProps = z.infer<typeof barChartProps>;
@@ -0,0 +1,30 @@
1
+ import { z } from "zod";
2
+ import { BAR_CHART_COLOR_VALUES, PALETTE_COLOR_VALUES } from "../colors.js";
3
+ import { BAR_CHART_MAX_BARS, BAR_CHART_LABEL_MAX_CHARS, } from "../constants.js";
4
+ export const barChartProps = z
5
+ .object({
6
+ bars: z
7
+ .array(z.object({
8
+ label: z.string().min(1).max(BAR_CHART_LABEL_MAX_CHARS),
9
+ value: z.number().nonnegative(),
10
+ color: z.enum(PALETTE_COLOR_VALUES).optional(),
11
+ }))
12
+ .min(1)
13
+ .max(BAR_CHART_MAX_BARS),
14
+ max: z.number().nonnegative().optional(),
15
+ color: z.enum(BAR_CHART_COLOR_VALUES).optional(),
16
+ })
17
+ .superRefine((val, ctx) => {
18
+ if (val.max !== undefined) {
19
+ for (let i = 0; i < val.bars.length; i++) {
20
+ const bar = val.bars[i];
21
+ if (bar.value > val.max) {
22
+ ctx.addIssue({
23
+ code: "custom",
24
+ message: `bar value (${bar.value}) exceeds chart max (${val.max})`,
25
+ path: ["bars", i, "value"],
26
+ });
27
+ }
28
+ }
29
+ }
30
+ });
@@ -1,27 +1,25 @@
1
1
  import { z } from "zod";
2
- export declare const BUTTON_VARIANTS: readonly ["default", "secondary", "outline", "ghost"];
2
+ export declare const BUTTON_VARIANTS: readonly ["secondary", "primary"];
3
3
  export declare const BUTTON_MAX_LABEL_CHARS = 30;
4
4
  export declare const buttonProps: z.ZodObject<{
5
5
  label: z.ZodString;
6
6
  variant: z.ZodOptional<z.ZodEnum<{
7
- default: "default";
8
7
  secondary: "secondary";
9
- outline: "outline";
10
- ghost: "ghost";
8
+ primary: "primary";
11
9
  }>>;
12
10
  icon: z.ZodOptional<z.ZodEnum<{
13
- check: "check";
14
- repeat: "repeat";
15
11
  "arrow-right": "arrow-right";
16
12
  "arrow-left": "arrow-left";
17
13
  "external-link": "external-link";
18
14
  "chevron-right": "chevron-right";
15
+ check: "check";
19
16
  x: "x";
20
17
  "alert-triangle": "alert-triangle";
21
18
  info: "info";
22
19
  clock: "clock";
23
20
  heart: "heart";
24
21
  "message-circle": "message-circle";
22
+ repeat: "repeat";
25
23
  share: "share";
26
24
  user: "user";
27
25
  users: "users";
package/dist/ui/button.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { ICON_NAMES } from "./icon.js";
3
- export const BUTTON_VARIANTS = ["default", "secondary", "outline", "ghost"];
3
+ export const BUTTON_VARIANTS = ["secondary", "primary"];
4
4
  export const BUTTON_MAX_LABEL_CHARS = 30;
5
5
  export const buttonProps = z.object({
6
6
  label: z.string().min(1).max(BUTTON_MAX_LABEL_CHARS),