@farcaster/snap 1.4.1 → 1.5.1

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/constants.ts CHANGED
@@ -62,79 +62,14 @@ export const SPACER_SIZE_VALUES = [
62
62
  SPACER_SIZE.large,
63
63
  ] as const;
64
64
 
65
- /**
66
- * Named color palette for snaps. Snap authors specify a name; the client maps
67
- * it to a hex value appropriate for its current light/dark mode.
68
- *
69
- * Light-mode hex values (used by emulator):
70
- * gray=#8F8F8F blue=#006BFF red=#FC0036 amber=#FFAE00
71
- * green=#28A948 teal=#00AC96 purple=#8B5CF6 pink=#F32782
72
- *
73
- * Dark-mode hex values (for reference; client-owned):
74
- * gray=#8F8F8F blue=#006FFE red=#F13342 amber=#FFAE00
75
- * green=#00AC3A teal=#00AA96 purple=#A78BFA pink=#F12B82
76
- */
77
- export const PALETTE_COLOR = {
78
- gray: "gray",
79
- blue: "blue",
80
- red: "red",
81
- amber: "amber",
82
- green: "green",
83
- teal: "teal",
84
- purple: "purple",
85
- pink: "pink",
86
- } as const;
87
-
88
- export const PALETTE_COLOR_ACCENT = "accent" as const;
89
-
90
- export const PALETTE_COLOR_VALUES = [
91
- PALETTE_COLOR.gray,
92
- PALETTE_COLOR.blue,
93
- PALETTE_COLOR.red,
94
- PALETTE_COLOR.amber,
95
- PALETTE_COLOR.green,
96
- PALETTE_COLOR.teal,
97
- PALETTE_COLOR.purple,
98
- PALETTE_COLOR.pink,
99
- ] as const;
100
-
101
- export type PaletteColor = (typeof PALETTE_COLOR_VALUES)[number];
102
-
103
- /** Light-mode hex for each palette color (emulator / reference client). */
104
- export const PALETTE_LIGHT_HEX: Record<PaletteColor, string> = {
105
- gray: "#8F8F8F",
106
- blue: "#006BFF",
107
- red: "#FC0036",
108
- amber: "#FFAE00",
109
- green: "#28A948",
110
- teal: "#00AC96",
111
- purple: "#8B5CF6",
112
- pink: "#F32782",
113
- };
114
-
115
- /** Dark-mode hex for each palette color (reference). */
116
- export const PALETTE_DARK_HEX: Record<PaletteColor, string> = {
117
- gray: "#8F8F8F",
118
- blue: "#006FFE",
119
- red: "#F13342",
120
- amber: "#FFAE00",
121
- green: "#00AC3A",
122
- teal: "#00AA96",
123
- purple: "#A78BFA",
124
- pink: "#F12B82",
125
- };
126
-
127
- export const PROGRESS_COLOR_VALUES = [
128
- PALETTE_COLOR_ACCENT,
129
- ...PALETTE_COLOR_VALUES,
130
- ] as const;
131
-
132
65
  export const LIST_STYLE_VALUES = ["ordered", "unordered", "plain"] as const;
133
66
 
134
67
  export const DEFAULT_LIST_STYLE = "ordered" as const;
135
68
 
136
69
  export const GRID_CELL_SIZE_VALUES = ["auto", "square"] as const;
137
70
  export const GRID_GAP_VALUES = ["none", "small", "medium"] as const;
71
+ export const DEFAULT_GRID_GAP =
72
+ "small" as const satisfies (typeof GRID_GAP_VALUES)[number];
138
73
 
139
74
  export const BUTTON_GROUP_STYLE = {
140
75
  row: "row",
@@ -152,14 +87,32 @@ export const BUTTON_ACTION = {
152
87
  post: "post",
153
88
  link: "link",
154
89
  mini_app: "mini_app",
155
- sdk: "sdk",
90
+ client: "client",
156
91
  } as const;
157
92
 
158
93
  export const BUTTON_ACTION_VALUES = [
159
94
  BUTTON_ACTION.post,
160
95
  BUTTON_ACTION.link,
161
96
  BUTTON_ACTION.mini_app,
162
- BUTTON_ACTION.sdk,
97
+ BUTTON_ACTION.client,
98
+ ] as const;
99
+
100
+ export const CLIENT_ACTION = {
101
+ view_cast: "view_cast",
102
+ view_profile: "view_profile",
103
+ compose_cast: "compose_cast",
104
+ view_token: "view_token",
105
+ send_token: "send_token",
106
+ swap_token: "swap_token",
107
+ } as const;
108
+
109
+ export const CLIENT_ACTION_VALUES = [
110
+ CLIENT_ACTION.view_cast,
111
+ CLIENT_ACTION.view_profile,
112
+ CLIENT_ACTION.compose_cast,
113
+ CLIENT_ACTION.view_token,
114
+ CLIENT_ACTION.send_token,
115
+ CLIENT_ACTION.swap_token,
163
116
  ] as const;
164
117
 
165
118
  export const BUTTON_STYLE = {
@@ -175,11 +128,6 @@ export const BUTTON_STYLE_VALUES = [
175
128
  export const BUTTON_LAYOUT_VALUES = ["stack", "row", "grid"] as const;
176
129
  export const DEFAULT_BUTTON_LAYOUT = BUTTON_LAYOUT_VALUES[0];
177
130
 
178
- export const BAR_CHART_COLOR_VALUES = [
179
- PALETTE_COLOR_ACCENT,
180
- ...PALETTE_COLOR_VALUES,
181
- ] as const;
182
-
183
131
  export const EFFECT_VALUES = ["confetti"] as const;
184
132
 
185
133
  export const GROUP_LAYOUT_VALUES = ["row"] as const;
@@ -213,9 +161,6 @@ export const HTTP_PREFIX = "http://" as const;
213
161
  /** 6-digit hex only (#RRGGBB); used for grid cell backgrounds (free hex). */
214
162
  export const HEX_COLOR_6_RE = /^#[0-9a-fA-F]{6}$/;
215
163
 
216
- /** Default snap accent when `page.theme` or `page.theme.accent` is omitted (SPEC.md). */
217
- export const DEFAULT_THEME_ACCENT = PALETTE_COLOR.purple;
218
-
219
164
  export const TEXT_CONTENT_MAX = {
220
165
  [TEXT_STYLE.title]: 80,
221
166
  [TEXT_STYLE.body]: 160,
package/src/index.ts CHANGED
@@ -3,24 +3,33 @@ export {
3
3
  PAGE_ROOT_TYPE,
4
4
  ELEMENT_TYPE,
5
5
  MEDIA_TYPE,
6
- DEFAULT_THEME_ACCENT,
7
6
  DEFAULT_LIST_STYLE,
8
7
  DEFAULT_SLIDER_STEP,
8
+ CLIENT_ACTION,
9
+ CLIENT_ACTION_VALUES,
10
+ } from "./constants";
11
+ export {
12
+ DEFAULT_THEME_ACCENT,
9
13
  PALETTE_COLOR,
10
14
  PALETTE_COLOR_ACCENT,
11
15
  PALETTE_COLOR_VALUES,
12
16
  PALETTE_LIGHT_HEX,
13
17
  PALETTE_DARK_HEX,
14
18
  type PaletteColor,
15
- } from "./constants";
19
+ } from "./colors";
16
20
  export {
21
+ ACTION_TYPE_GET,
22
+ ACTION_TYPE_POST,
17
23
  snapResponseSchema,
18
24
  firstPageResponseSchema,
19
25
  payloadSchema,
26
+ clientActionSchema,
27
+ createDefaultDataStore,
20
28
  type Button,
21
29
  type Element,
22
30
  type Elements,
23
31
  type GroupChildElement,
32
+ type ClientAction,
24
33
  type SnapAction,
25
34
  type SnapPageElementInput,
26
35
  type SnapContext,
@@ -28,6 +37,9 @@ export {
28
37
  type SnapHandlerResult,
29
38
  type SnapFunction,
30
39
  type SnapPayload,
40
+ type DataStoreValue,
41
+ type SnapDataStore,
42
+ type SnapDataStoreOperations,
31
43
  } from "./schemas";
32
44
  export {
33
45
  validateSnapResponse,
package/src/schemas.ts CHANGED
@@ -1,16 +1,16 @@
1
1
  import { z } from "zod";
2
2
  import {
3
- BAR_CHART_COLOR_VALUES,
4
3
  BUTTON_ACTION,
5
4
  BUTTON_ACTION_VALUES,
5
+ CLIENT_ACTION,
6
6
  BUTTON_GROUP_STYLE,
7
7
  BUTTON_GROUP_STYLE_VALUES,
8
8
  BUTTON_LAYOUT_VALUES,
9
9
  BUTTON_STYLE_VALUES,
10
10
  DEFAULT_BUTTON_LAYOUT,
11
+ DEFAULT_GRID_GAP,
11
12
  DEFAULT_LIST_STYLE,
12
13
  DEFAULT_SLIDER_STEP,
13
- DEFAULT_THEME_ACCENT,
14
14
  EFFECT_VALUES,
15
15
  ELEMENT_TYPE,
16
16
  GRID_CELL_SIZE_VALUES,
@@ -25,8 +25,6 @@ import {
25
25
  LIST_STYLE_VALUES,
26
26
  MEDIA_ELEMENT_TYPES,
27
27
  PAGE_ROOT_TYPE,
28
- PALETTE_COLOR_VALUES,
29
- PROGRESS_COLOR_VALUES,
30
28
  SLIDER_STEP_ALIGN_EPS,
31
29
  SPACER_SIZE,
32
30
  SPACER_SIZE_VALUES,
@@ -36,6 +34,12 @@ import {
36
34
  TEXT_STYLE,
37
35
  TEXT_STYLE_VALUES,
38
36
  } from "./constants";
37
+ import {
38
+ BAR_CHART_COLOR_VALUES,
39
+ DEFAULT_THEME_ACCENT,
40
+ PALETTE_COLOR_VALUES,
41
+ PROGRESS_COLOR_VALUES,
42
+ } from "./colors";
39
43
 
40
44
  /**
41
45
  * post/link/mini_app targets must be HTTPS in production; allow HTTP only for
@@ -66,33 +70,10 @@ const themeSchema = z
66
70
  })
67
71
  .strict();
68
72
 
69
- const httpsUrl = z.string().refine((s) => s.startsWith(HTTPS_PREFIX), {
70
- message: "URL must use HTTPS",
73
+ const imageUrlSchema = z.string().refine((s) => s.startsWith(HTTPS_PREFIX), {
74
+ message: "image URL must use HTTPS",
71
75
  });
72
76
 
73
- function hasAllowedMediaExtension(
74
- urlString: string,
75
- allowedExtensions: string[],
76
- ): boolean {
77
- try {
78
- const url = new URL(urlString);
79
- if (url.protocol !== "https:") return false;
80
- const lowerPathname = url.pathname.toLowerCase();
81
- return allowedExtensions.some((extension) =>
82
- lowerPathname.endsWith(`.${extension}`),
83
- );
84
- } catch {
85
- return false;
86
- }
87
- }
88
-
89
- const imageUrlSchema = z
90
- .string()
91
- .refine((s) => hasAllowedMediaExtension(s, ["jpg", "png", "gif", "webp"]), {
92
- message:
93
- "image URL must use HTTPS and end with a supported extension (.jpg, .png, .gif, .webp)",
94
- });
95
-
96
77
  const textAlignSchema = z.enum(TEXT_ALIGN_VALUES);
97
78
 
98
79
  const textElementSchema = z
@@ -206,7 +187,7 @@ const gridElementSchema = z
206
187
  rows: z.number().int().min(LIMITS.minGridRows).max(LIMITS.maxGridRows),
207
188
  cells: z.array(gridCellSchema),
208
189
  cellSize: z.enum(GRID_CELL_SIZE_VALUES).optional(),
209
- gap: z.enum(GRID_GAP_VALUES).optional(),
190
+ gap: z.enum(GRID_GAP_VALUES).default(DEFAULT_GRID_GAP),
210
191
  interactive: z.boolean().optional(),
211
192
  })
212
193
  .superRefine((val, ctx) => {
@@ -368,26 +349,146 @@ const buttonActionSchema = z.enum(BUTTON_ACTION_VALUES);
368
349
 
369
350
  const buttonStyleSchema = z.enum(BUTTON_STYLE_VALUES);
370
351
 
352
+ /* ------------------------------------------------------------------ */
353
+ /* Client action schemas */
354
+ /* ------------------------------------------------------------------ */
355
+
356
+ const viewCastClientActionSchema = z
357
+ .object({
358
+ type: z.literal(CLIENT_ACTION.view_cast),
359
+ hash: z.string().min(1),
360
+ })
361
+ .strict();
362
+
363
+ const viewProfileClientActionSchema = z
364
+ .object({
365
+ type: z.literal(CLIENT_ACTION.view_profile),
366
+ fid: z.number().int().nonnegative(),
367
+ })
368
+ .strict();
369
+
370
+ const composeCastClientActionSchema = z
371
+ .object({
372
+ type: z.literal(CLIENT_ACTION.compose_cast),
373
+ text: z.string().optional(),
374
+ embeds: z
375
+ .array(z.string())
376
+ .max(2, { message: "compose_cast embeds: max 2 URLs" })
377
+ .optional(),
378
+ parent: z
379
+ .object({
380
+ type: z.literal("cast"),
381
+ hash: z.string().min(1),
382
+ })
383
+ .strict()
384
+ .optional(),
385
+ channelKey: z.string().optional(),
386
+ })
387
+ .strict();
388
+
389
+ const viewTokenClientActionSchema = z
390
+ .object({
391
+ type: z.literal(CLIENT_ACTION.view_token),
392
+ /** CAIP-19 asset ID (e.g. "eip155:8453/erc20:0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913") */
393
+ token: z.string().min(1),
394
+ })
395
+ .strict();
396
+
397
+ const sendTokenClientActionSchema = z
398
+ .object({
399
+ type: z.literal(CLIENT_ACTION.send_token),
400
+ /** CAIP-19 asset ID */
401
+ token: z.string().optional(),
402
+ /** Amount in raw token units (e.g. "1000000" for 1 USDC) */
403
+ amount: z.string().optional(),
404
+ recipientFid: z.number().int().nonnegative().optional(),
405
+ recipientAddress: z.string().optional(),
406
+ })
407
+ .strict();
408
+
409
+ const swapTokenClientActionSchema = z
410
+ .object({
411
+ type: z.literal(CLIENT_ACTION.swap_token),
412
+ /** CAIP-19 asset ID to sell */
413
+ sellToken: z.string().optional(),
414
+ /** CAIP-19 asset ID to buy */
415
+ buyToken: z.string().optional(),
416
+ /** Amount in raw token units */
417
+ sellAmount: z.string().optional(),
418
+ })
419
+ .strict();
420
+
421
+ export const clientActionSchema = z.discriminatedUnion("type", [
422
+ viewCastClientActionSchema,
423
+ viewProfileClientActionSchema,
424
+ composeCastClientActionSchema,
425
+ viewTokenClientActionSchema,
426
+ sendTokenClientActionSchema,
427
+ swapTokenClientActionSchema,
428
+ ]);
429
+
430
+ export type ClientAction = z.infer<typeof clientActionSchema>;
431
+
432
+ /* ------------------------------------------------------------------ */
433
+ /* Button schema */
434
+ /* ------------------------------------------------------------------ */
435
+
371
436
  const buttonSchema = z
372
437
  .object({
373
438
  label: z.string().min(1).max(LIMITS.maxButtonLabelChars),
374
439
  action: buttonActionSchema,
375
- /** URL (HTTPS for post/link/mini_app) or SDK action id (e.g. cast:view:...) */
376
- target: z.string().min(1),
440
+ /** URL target for post/link/mini_app buttons */
441
+ target: z.string().min(1).optional(),
442
+ /** Structured client action for client buttons */
443
+ client_action: clientActionSchema.optional(),
377
444
  style: buttonStyleSchema.optional(),
378
445
  })
379
446
  .superRefine((val, ctx) => {
380
- if (
381
- (val.action === BUTTON_ACTION.post ||
382
- val.action === BUTTON_ACTION.link ||
383
- val.action === BUTTON_ACTION.mini_app) &&
384
- !isSecureOrLoopbackHttpButtonTarget(val.target)
385
- ) {
386
- ctx.addIssue({
387
- code: "custom",
388
- message: `button target must use HTTPS (or http:// on localhost / 127.0.0.1 for development) for action "${val.action}" (received: ${val.target})`,
389
- path: ["target"],
390
- });
447
+ if (val.action === BUTTON_ACTION.client) {
448
+ // client buttons require client_action, must not have target
449
+ if (val.client_action === undefined) {
450
+ ctx.addIssue({
451
+ code: "custom",
452
+ message: `button with action "client" must include a "client_action" object`,
453
+ path: ["client_action"],
454
+ });
455
+ }
456
+ if (val.target !== undefined) {
457
+ ctx.addIssue({
458
+ code: "custom",
459
+ message: `button with action "client" must not include "target"`,
460
+ path: ["target"],
461
+ });
462
+ }
463
+ } else {
464
+ // post/link/mini_app buttons require target, must not have client_action
465
+ if (val.target === undefined) {
466
+ ctx.addIssue({
467
+ code: "custom",
468
+ message: `button with action "${val.action}" must include a "target" URL`,
469
+ path: ["target"],
470
+ });
471
+ }
472
+ if (val.client_action !== undefined) {
473
+ ctx.addIssue({
474
+ code: "custom",
475
+ message: `button with action "${val.action}" must not include "client_action"`,
476
+ path: ["client_action"],
477
+ });
478
+ }
479
+ if (
480
+ val.target &&
481
+ (val.action === BUTTON_ACTION.post ||
482
+ val.action === BUTTON_ACTION.link ||
483
+ val.action === BUTTON_ACTION.mini_app) &&
484
+ !isSecureOrLoopbackHttpButtonTarget(val.target)
485
+ ) {
486
+ ctx.addIssue({
487
+ code: "custom",
488
+ message: `button target must use HTTPS (or http:// on localhost / 127.0.0.1 for development) for action "${val.action}" (received: ${val.target})`,
489
+ path: ["target"],
490
+ });
491
+ }
391
492
  }
392
493
  });
393
494
 
@@ -578,9 +679,46 @@ export const snapActionSchema = z.discriminatedUnion("type", [
578
679
 
579
680
  export type SnapAction = z.infer<typeof snapActionSchema>;
580
681
 
682
+ export type DataStoreValue =
683
+ | string
684
+ | number
685
+ | boolean
686
+ | null
687
+ | DataStoreValue[]
688
+ | { [key: string]: DataStoreValue };
689
+
690
+ export type SnapDataStoreOperations = {
691
+ get(key: string): Promise<DataStoreValue | null>;
692
+ set(key: string, value: DataStoreValue): Promise<void>;
693
+ };
694
+
695
+ export type SnapDataStore = SnapDataStoreOperations & {
696
+ withLock<T>(fn: (store: SnapDataStoreOperations) => Promise<T>): Promise<T>;
697
+ };
698
+
699
+ export function createDefaultDataStore(): SnapDataStore {
700
+ const err = new Error(
701
+ "Data store is not configured. Use withUpstash() from @farcaster/snap-upstash or provide a data store implementation.",
702
+ );
703
+ return {
704
+ get(_key: string): Promise<never> {
705
+ return Promise.reject(err);
706
+ },
707
+ set(_key: string, _value: DataStoreValue): Promise<never> {
708
+ return Promise.reject(err);
709
+ },
710
+ withLock<T>(
711
+ _fn: (store: SnapDataStoreOperations) => Promise<T>,
712
+ ): Promise<never> {
713
+ return Promise.reject(err);
714
+ },
715
+ };
716
+ }
717
+
581
718
  export type SnapContext = {
582
719
  action: SnapAction;
583
720
  request: Request;
721
+ data: SnapDataStore;
584
722
  };
585
723
 
586
724
  export type SnapFunction = (ctx: SnapContext) => Promise<SnapHandlerResult>;
@@ -7,7 +7,6 @@ import {
7
7
  import { decodePayload, verifyJFSRequestBody } from "./verify";
8
8
  import { z } from "zod";
9
9
 
10
- /** Default replay window per SPEC.md § Replay Protection (5 minutes). */
11
10
  const DEFAULT_SNAP_POST_MAX_SKEW_SECONDS = 300 as const;
12
11
 
13
12
  export type ParseRequestError =
@@ -37,6 +36,13 @@ export type ParseRequestOptions = {
37
36
  * When true, skip {@link verifyJFSRequestBody} (signature checks).
38
37
  */
39
38
  skipJFSVerification?: boolean;
39
+
40
+ /**
41
+ * Maximum allowed absolute difference between the request timestamp and the
42
+ * server clock, in seconds. Requests outside this window are rejected as
43
+ * potential replays. Defaults to 300 (5 minutes) when not provided.
44
+ */
45
+ maxSkewSeconds?: number;
40
46
  };
41
47
 
42
48
  export type ParseRequestResult =
@@ -75,7 +81,7 @@ export async function parseRequest(
75
81
  };
76
82
  }
77
83
 
78
- const maxSkew = DEFAULT_SNAP_POST_MAX_SKEW_SECONDS;
84
+ const maxSkew = options.maxSkewSeconds ?? DEFAULT_SNAP_POST_MAX_SKEW_SECONDS;
79
85
  const nowSec = Math.floor(Date.now() / 1000);
80
86
 
81
87
  const text = await request.text();
@@ -1,9 +1,6 @@
1
1
  import { z } from "zod";
2
- import {
3
- BAR_CHART_COLOR_VALUES,
4
- LIMITS,
5
- PALETTE_COLOR_VALUES,
6
- } from "../constants.js";
2
+ import { LIMITS } from "../constants.js";
3
+ import { BAR_CHART_COLOR_VALUES, PALETTE_COLOR_VALUES } from "../colors.js";
7
4
 
8
5
  export const barChartProps = z.object({
9
6
  bars: z
@@ -1,13 +1,26 @@
1
1
  import { z } from "zod";
2
- import { BUTTON_GROUP_STYLE_VALUES, LIMITS } from "../constants.js";
2
+ import {
3
+ BUTTON_GROUP_STYLE,
4
+ BUTTON_GROUP_STYLE_VALUES,
5
+ LIMITS,
6
+ } from "../constants.js";
3
7
 
4
- export const buttonGroupProps = z.object({
5
- name: z.string().min(1),
6
- options: z
7
- .array(z.string())
8
- .min(LIMITS.minButtonGroupOptions)
9
- .max(LIMITS.maxButtonGroupOptions),
10
- style: z.enum(BUTTON_GROUP_STYLE_VALUES).optional(),
11
- });
8
+ export const buttonGroupProps = z
9
+ .object({
10
+ name: z.string().min(1),
11
+ options: z
12
+ .array(z.string().max(LIMITS.maxButtonGroupOptionChars))
13
+ .min(LIMITS.minButtonGroupOptions)
14
+ .max(LIMITS.maxButtonGroupOptions),
15
+ style: z.enum(BUTTON_GROUP_STYLE_VALUES).optional(),
16
+ })
17
+ .transform((val) => ({
18
+ ...val,
19
+ style:
20
+ val.style ??
21
+ (val.options.length <= 3
22
+ ? BUTTON_GROUP_STYLE.row
23
+ : BUTTON_GROUP_STYLE.stack),
24
+ }));
12
25
 
13
26
  export type ButtonGroupProps = z.infer<typeof buttonGroupProps>;
package/src/ui/button.ts CHANGED
@@ -4,7 +4,8 @@ import { BUTTON_ACTION_VALUES, BUTTON_STYLE_VALUES } from "../constants.js";
4
4
  export const actionButtonProps = z.object({
5
5
  label: z.string(),
6
6
  action: z.enum(BUTTON_ACTION_VALUES),
7
- target: z.string(),
7
+ target: z.string().optional(),
8
+ client_action: z.record(z.string(), z.unknown()).optional(),
8
9
  style: z.enum(BUTTON_STYLE_VALUES).optional(),
9
10
  });
10
11
 
package/src/ui/catalog.ts CHANGED
@@ -29,6 +29,10 @@ const snapTargetParams = z.object({
29
29
  target: z.string(),
30
30
  });
31
31
 
32
+ const snapClientParams = z.object({
33
+ client_action: z.record(z.string(), z.unknown()),
34
+ });
35
+
32
36
  /**
33
37
  * Basic catalog: one json-render component per snap element type, plus ActionButton for snap buttons.
34
38
  * Does not validate cross-field rules (media count, height budget); snap JSON still goes through `@farcaster/snap` validation.
@@ -102,7 +106,7 @@ export const snapJsonRenderCatalog = defineCatalog(snapJsonRenderSchema, {
102
106
  ActionButton: {
103
107
  props: actionButtonProps,
104
108
  description:
105
- "Snap action button: post (next page), link (browser), mini_app, sdk — target is HTTPS URL or SDK id.",
109
+ "Snap action button: post (next page), link (browser), mini_app, client — target is HTTPS URL or client_action object.",
106
110
  },
107
111
  },
108
112
  actions: {
@@ -119,10 +123,10 @@ export const snapJsonRenderCatalog = defineCatalog(snapJsonRenderSchema, {
119
123
  description: "Open `target` as an in-app Farcaster mini app.",
120
124
  params: snapTargetParams,
121
125
  },
122
- snap_sdk: {
126
+ snap_client: {
123
127
  description:
124
- "Run a Farcaster client SDK action (cast:view, user:follow, …).",
125
- params: snapTargetParams,
128
+ "Trigger a Farcaster client action (view_cast, view_profile, compose_cast, …).",
129
+ params: snapClientParams,
126
130
  },
127
131
  },
128
132
  });
package/src/ui/group.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import { z } from "zod";
2
+ import { GROUP_LAYOUT_VALUES } from "../constants.js";
2
3
 
3
4
  export const groupProps = z.object({
4
- layout: z.enum(["row", "grid"]),
5
+ layout: z.enum(GROUP_LAYOUT_VALUES),
5
6
  });
6
7
 
7
8
  export type GroupProps = z.infer<typeof groupProps>;
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { PROGRESS_COLOR_VALUES } from "../constants.js";
2
+ import { PROGRESS_COLOR_VALUES } from "../colors.js";
3
3
 
4
4
  export const progressProps = z.object({
5
5
  value: z.number(),
package/src/ui/schema.ts CHANGED
@@ -31,7 +31,7 @@ export const snapJsonRenderSchema = defineSchema(
31
31
  defaultRules: [
32
32
  "You are generating auxiliary UI for a Farcaster Snap. Prefer components matching snap element types (Text, Image, ButtonGroup, …).",
33
33
  "Snap pages use a Stack root with at most 5 body children and 1 media element (Image or Grid); keep generated trees small.",
34
- "Bottom-of-card snap buttons are ActionButton components; use actions post / link / mini_app / sdk per SPEC.md.",
34
+ "Bottom-of-card snap buttons are ActionButton components; use actions post / link / mini_app / client.",
35
35
  ],
36
36
  },
37
37
  );
package/src/ui/spacer.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { z } from "zod";
2
- import { SPACER_SIZE_VALUES } from "../constants.js";
2
+ import { SPACER_SIZE, SPACER_SIZE_VALUES } from "../constants.js";
3
3
 
4
4
  export const spacerProps = z.object({
5
- size: z.enum(SPACER_SIZE_VALUES).optional(),
5
+ size: z.enum(SPACER_SIZE_VALUES).default(SPACER_SIZE.medium),
6
6
  });
7
7
 
8
8
  export type SpacerProps = z.infer<typeof spacerProps>;
package/src/ui/toggle.ts CHANGED
@@ -3,7 +3,7 @@ import { z } from "zod";
3
3
  export const toggleProps = z.object({
4
4
  name: z.string().min(1),
5
5
  label: z.string(),
6
- value: z.boolean().optional(),
6
+ value: z.boolean().default(false),
7
7
  });
8
8
 
9
9
  export type ToggleProps = z.infer<typeof toggleProps>;