@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/dist/colors.d.ts +32 -0
- package/dist/colors.js +64 -0
- package/dist/constants.d.ts +12 -35
- package/dist/constants.js +19 -67
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/schemas.d.ts +143 -10
- package/dist/schemas.js +140 -32
- package/dist/server/parseRequest.d.ts +6 -0
- package/dist/server/parseRequest.js +1 -2
- package/dist/ui/bar-chart.js +2 -1
- package/dist/ui/button-group.d.ts +10 -2
- package/dist/ui/button-group.js +12 -4
- package/dist/ui/button.d.ts +6 -4
- package/dist/ui/button.js +2 -1
- package/dist/ui/catalog.d.ts +17 -9
- package/dist/ui/catalog.js +7 -4
- package/dist/ui/group.d.ts +0 -1
- package/dist/ui/group.js +2 -1
- package/dist/ui/progress.js +1 -1
- package/dist/ui/schema.js +1 -1
- package/dist/ui/spacer.d.ts +1 -1
- package/dist/ui/spacer.js +2 -2
- package/dist/ui/toggle.d.ts +1 -1
- package/dist/ui/toggle.js +1 -1
- package/package.json +1 -1
- package/src/colors.ts +73 -0
- package/src/constants.ts +22 -77
- package/src/index.ts +14 -2
- package/src/schemas.ts +181 -43
- package/src/server/parseRequest.ts +8 -2
- package/src/ui/bar-chart.ts +2 -5
- package/src/ui/button-group.ts +22 -9
- package/src/ui/button.ts +2 -1
- package/src/ui/catalog.ts +8 -4
- package/src/ui/group.ts +2 -1
- package/src/ui/progress.ts +1 -1
- package/src/ui/schema.ts +1 -1
- package/src/ui/spacer.ts +2 -2
- package/src/ui/toggle.ts +1 -1
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
|
-
|
|
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.
|
|
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 "./
|
|
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
|
|
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).
|
|
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
|
|
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
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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();
|
package/src/ui/bar-chart.ts
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
3
|
-
|
|
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
|
package/src/ui/button-group.ts
CHANGED
|
@@ -1,13 +1,26 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
BUTTON_GROUP_STYLE,
|
|
4
|
+
BUTTON_GROUP_STYLE_VALUES,
|
|
5
|
+
LIMITS,
|
|
6
|
+
} from "../constants.js";
|
|
3
7
|
|
|
4
|
-
export const buttonGroupProps = z
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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,
|
|
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
|
-
|
|
126
|
+
snap_client: {
|
|
123
127
|
description:
|
|
124
|
-
"
|
|
125
|
-
params:
|
|
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
package/src/ui/progress.ts
CHANGED
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 /
|
|
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).
|
|
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