@farcaster/snap 2.1.1 → 2.2.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/dist/colors.d.ts +10 -0
- package/dist/colors.js +22 -0
- package/dist/constants.d.ts +3 -1
- package/dist/constants.js +7 -2
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/react/components/cell-grid.js +13 -14
- package/dist/react/components/image.js +5 -1
- package/dist/react/components/stack.js +53 -3
- package/dist/react/components/text.js +7 -1
- package/dist/react/hooks/use-snap-colors.js +2 -9
- package/dist/react/stack-direction-context.d.ts +7 -0
- package/dist/react/stack-direction-context.js +10 -0
- package/dist/react-native/components/snap-cell-grid.js +5 -7
- package/dist/react-native/components/snap-image.js +15 -2
- package/dist/react-native/components/snap-item.js +12 -2
- package/dist/react-native/components/snap-progress.js +8 -2
- package/dist/react-native/components/snap-stack.d.ts +1 -1
- package/dist/react-native/components/snap-stack.js +85 -10
- package/dist/react-native/components/snap-text.js +7 -2
- package/dist/react-native/stack-direction-context.d.ts +7 -0
- package/dist/react-native/stack-direction-context.js +9 -0
- package/dist/react-native/use-snap-palette.js +2 -2
- package/dist/schemas.d.ts +52 -0
- package/dist/schemas.js +10 -4
- package/dist/server/index.d.ts +2 -1
- package/dist/server/index.js +2 -1
- package/dist/server/parseRequest.d.ts +4 -3
- package/dist/server/parseRequest.js +91 -67
- package/dist/server/verify.d.ts +12 -5
- package/dist/server/verify.js +67 -19
- package/dist/stack-horizontal-utils.d.ts +4 -0
- package/dist/stack-horizontal-utils.js +29 -0
- package/dist/ui/catalog.d.ts +3 -2
- package/dist/ui/catalog.js +2 -2
- package/dist/ui/cell-grid.d.ts +2 -2
- package/dist/ui/cell-grid.js +11 -2
- package/dist/ui/stack.d.ts +1 -0
- package/dist/ui/stack.js +8 -0
- package/dist/verify.test.js +3 -3
- package/llms.txt +13 -2
- package/package.json +1 -1
- package/src/colors.ts +27 -0
- package/src/constants.ts +8 -2
- package/src/index.ts +6 -0
- package/src/react/components/cell-grid.tsx +17 -24
- package/src/react/components/image.tsx +8 -1
- package/src/react/components/stack.tsx +84 -11
- package/src/react/components/text.tsx +8 -1
- package/src/react/hooks/use-snap-colors.ts +3 -8
- package/src/react/stack-direction-context.tsx +27 -0
- package/src/react-native/components/snap-cell-grid.tsx +5 -11
- package/src/react-native/components/snap-image.tsx +17 -2
- package/src/react-native/components/snap-item.tsx +14 -2
- package/src/react-native/components/snap-progress.tsx +8 -2
- package/src/react-native/components/snap-stack.tsx +116 -14
- package/src/react-native/components/snap-text.tsx +7 -2
- package/src/react-native/stack-direction-context.tsx +25 -0
- package/src/react-native/use-snap-palette.ts +2 -1
- package/src/schemas.ts +14 -4
- package/src/server/index.ts +7 -1
- package/src/server/parseRequest.ts +117 -71
- package/src/server/verify.ts +99 -26
- package/src/stack-horizontal-utils.ts +27 -0
- package/src/ui/catalog.ts +2 -2
- package/src/ui/cell-grid.ts +15 -2
- package/src/ui/stack.ts +8 -0
- package/src/verify.test.ts +3 -3
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext } from "react";
|
|
3
|
+
const SnapStackDirectionContext = createContext(undefined);
|
|
4
|
+
export function SnapStackDirectionProvider({ direction, children, }) {
|
|
5
|
+
return (_jsx(SnapStackDirectionContext.Provider, { value: direction, children: children }));
|
|
6
|
+
}
|
|
7
|
+
export function useSnapStackDirection() {
|
|
8
|
+
return useContext(SnapStackDirectionContext);
|
|
9
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DEFAULT_THEME_ACCENT, PALETTE_COLOR_VALUES, PALETTE_LIGHT_HEX, PALETTE_DARK_HEX, } from "@farcaster/snap";
|
|
1
|
+
import { DEFAULT_THEME_ACCENT, PALETTE_COLOR_VALUES, PALETTE_LIGHT_HEX, PALETTE_DARK_HEX, resolveSnapColorHex, } from "@farcaster/snap";
|
|
2
2
|
import { useStateStore } from "@json-render/react-native";
|
|
3
3
|
import { useSnapTheme } from "./theme.js";
|
|
4
4
|
function resolveHex(name, appearance) {
|
|
@@ -23,7 +23,7 @@ export function useSnapPalette() {
|
|
|
23
23
|
const { get } = useStateStore();
|
|
24
24
|
const accentName = themeAccentFromStore(get);
|
|
25
25
|
const accentHex = resolveHex(accentName, mode);
|
|
26
|
-
const hex = (semantic) => semantic
|
|
26
|
+
const hex = (semantic) => resolveSnapColorHex(semantic, { accentHex, appearance: mode });
|
|
27
27
|
return { appearance: mode, accentName, accentHex, hex };
|
|
28
28
|
}
|
|
29
29
|
/** `#RRGGBB` + alpha → `rgba(...)` for React Native styles. */
|
package/dist/schemas.d.ts
CHANGED
|
@@ -89,10 +89,46 @@ export declare const payloadSchema: z.ZodObject<{
|
|
|
89
89
|
}, z.core.$strip>], "type">;
|
|
90
90
|
}, z.core.$strip>;
|
|
91
91
|
export type SnapPayload = z.infer<typeof payloadSchema>;
|
|
92
|
+
/** JFS payload shape for POST minus deprecated `fid`; used for GET auth via payload header. */
|
|
93
|
+
export declare const getPayloadSchema: z.ZodObject<{
|
|
94
|
+
user: z.ZodObject<{
|
|
95
|
+
fid: z.ZodNumber;
|
|
96
|
+
}, z.core.$strip>;
|
|
97
|
+
timestamp: z.ZodNumber;
|
|
98
|
+
audience: z.ZodString;
|
|
99
|
+
surface: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
100
|
+
type: z.ZodLiteral<"cast">;
|
|
101
|
+
cast: z.ZodObject<{
|
|
102
|
+
hash: z.ZodString;
|
|
103
|
+
author: z.ZodObject<{
|
|
104
|
+
fid: z.ZodNumber;
|
|
105
|
+
}, z.core.$strip>;
|
|
106
|
+
}, z.core.$strip>;
|
|
107
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
108
|
+
type: z.ZodLiteral<"standalone">;
|
|
109
|
+
}, z.core.$strip>], "type">;
|
|
110
|
+
}, z.core.$strip>;
|
|
111
|
+
export type SnapGetPayload = z.infer<typeof getPayloadSchema>;
|
|
92
112
|
export declare const ACTION_TYPE_GET: "get";
|
|
93
113
|
export declare const ACTION_TYPE_POST: "post";
|
|
94
114
|
declare const snapGetActionSchema: z.ZodObject<{
|
|
95
115
|
type: z.ZodLiteral<"get">;
|
|
116
|
+
user: z.ZodOptional<z.ZodObject<{
|
|
117
|
+
fid: z.ZodNumber;
|
|
118
|
+
}, z.core.$strip>>;
|
|
119
|
+
timestamp: z.ZodOptional<z.ZodNumber>;
|
|
120
|
+
audience: z.ZodOptional<z.ZodString>;
|
|
121
|
+
surface: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
122
|
+
type: z.ZodLiteral<"cast">;
|
|
123
|
+
cast: z.ZodObject<{
|
|
124
|
+
hash: z.ZodString;
|
|
125
|
+
author: z.ZodObject<{
|
|
126
|
+
fid: z.ZodNumber;
|
|
127
|
+
}, z.core.$strip>;
|
|
128
|
+
}, z.core.$strip>;
|
|
129
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
130
|
+
type: z.ZodLiteral<"standalone">;
|
|
131
|
+
}, z.core.$strip>], "type">>;
|
|
96
132
|
}, z.core.$strip>;
|
|
97
133
|
export type SnapGetAction = z.infer<typeof snapGetActionSchema>;
|
|
98
134
|
declare const snapPostActionSchema: z.ZodObject<{
|
|
@@ -119,6 +155,22 @@ declare const snapPostActionSchema: z.ZodObject<{
|
|
|
119
155
|
export type SnapPostAction = z.infer<typeof snapPostActionSchema>;
|
|
120
156
|
export declare const snapActionSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
121
157
|
type: z.ZodLiteral<"get">;
|
|
158
|
+
user: z.ZodOptional<z.ZodObject<{
|
|
159
|
+
fid: z.ZodNumber;
|
|
160
|
+
}, z.core.$strip>>;
|
|
161
|
+
timestamp: z.ZodOptional<z.ZodNumber>;
|
|
162
|
+
audience: z.ZodOptional<z.ZodString>;
|
|
163
|
+
surface: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
164
|
+
type: z.ZodLiteral<"cast">;
|
|
165
|
+
cast: z.ZodObject<{
|
|
166
|
+
hash: z.ZodString;
|
|
167
|
+
author: z.ZodObject<{
|
|
168
|
+
fid: z.ZodNumber;
|
|
169
|
+
}, z.core.$strip>;
|
|
170
|
+
}, z.core.$strip>;
|
|
171
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
172
|
+
type: z.ZodLiteral<"standalone">;
|
|
173
|
+
}, z.core.$strip>], "type">>;
|
|
122
174
|
}, z.core.$strip>, z.ZodObject<{
|
|
123
175
|
fid: z.ZodOptional<z.ZodNumber>;
|
|
124
176
|
inputs: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodArray<z.ZodString>]>>>;
|
package/dist/schemas.js
CHANGED
|
@@ -47,22 +47,28 @@ const surfaceSchema = z.discriminatedUnion("type", [
|
|
|
47
47
|
castSurfaceSchema,
|
|
48
48
|
standaloneSurfaceSchema,
|
|
49
49
|
]);
|
|
50
|
+
const fidSchema = z.number().int().nonnegative();
|
|
51
|
+
const userSchema = z.object({ fid: fidSchema });
|
|
50
52
|
export const payloadSchema = z
|
|
51
53
|
.object({
|
|
52
|
-
fid:
|
|
54
|
+
fid: fidSchema.optional(), // deprecated in favor of user.fid
|
|
53
55
|
inputs: z.record(z.string(), postInputValueSchema).default({}),
|
|
54
56
|
timestamp: z.number().int(),
|
|
55
57
|
audience: z.string(),
|
|
56
|
-
user:
|
|
57
|
-
fid: z.number().int().nonnegative(),
|
|
58
|
-
}),
|
|
58
|
+
user: userSchema,
|
|
59
59
|
surface: surfaceSchema,
|
|
60
60
|
})
|
|
61
61
|
.strip();
|
|
62
|
+
/** JFS payload shape for POST minus deprecated `fid`; used for GET auth via payload header. */
|
|
63
|
+
export const getPayloadSchema = payloadSchema.omit({ inputs: true, fid: true });
|
|
62
64
|
export const ACTION_TYPE_GET = "get";
|
|
63
65
|
export const ACTION_TYPE_POST = "post";
|
|
64
66
|
const snapGetActionSchema = z.object({
|
|
65
67
|
type: z.literal(ACTION_TYPE_GET),
|
|
68
|
+
user: userSchema.optional(),
|
|
69
|
+
timestamp: z.number().int().optional(),
|
|
70
|
+
audience: z.string().optional(),
|
|
71
|
+
surface: surfaceSchema.optional(),
|
|
66
72
|
});
|
|
67
73
|
const snapPostActionSchema = payloadSchema.extend({
|
|
68
74
|
type: z.literal(ACTION_TYPE_POST),
|
package/dist/server/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
export { verifyJFSRequestBody,
|
|
1
|
+
export { verifyJFS as verifyJFSRequestBody, // deprecated alias. drop in v3
|
|
2
|
+
parseJfs, verifyJFS, decodePayload, encodePayload, } from "./verify.js";
|
|
2
3
|
export { DEFAULT_SNAP_HUB_HTTP_BASE_URL, getActiveEd25519SignerKeysFromHubHttp, } from "./hubs.js";
|
|
3
4
|
export { parseRequest, type ParseRequestError, type ParseRequestOptions, type ParseRequestResult, } from "./parseRequest.js";
|
package/dist/server/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
export { verifyJFSRequestBody,
|
|
1
|
+
export { verifyJFS as verifyJFSRequestBody, // deprecated alias. drop in v3
|
|
2
|
+
parseJfs, verifyJFS, decodePayload, encodePayload, } from "./verify.js";
|
|
2
3
|
export { DEFAULT_SNAP_HUB_HTTP_BASE_URL, getActiveEd25519SignerKeysFromHubHttp, } from "./hubs.js";
|
|
3
4
|
export { parseRequest, } from "./parseRequest.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type SnapAction } from "../schemas.js";
|
|
2
1
|
import { z } from "zod";
|
|
2
|
+
import { type SnapAction } from "../schemas.js";
|
|
3
3
|
export type ParseRequestError = {
|
|
4
4
|
type: "method_not_allowed";
|
|
5
5
|
message: string;
|
|
@@ -47,7 +47,8 @@ export type ParseRequestResult = {
|
|
|
47
47
|
};
|
|
48
48
|
/**
|
|
49
49
|
* Parse and validate Farcaster snap requests:
|
|
50
|
-
* - `GET
|
|
51
|
-
*
|
|
50
|
+
* - `GET`: returns `{ type: "get" }`, or optional viewer fields when `X-Snap-Payload`
|
|
51
|
+
* carries a JFS compact string whose decoded payload validates against {@link getPayloadSchema}.
|
|
52
|
+
* - `POST`: the body must be a JFS envelope — either JSON `{ header, payload, signature }` or the same **compact** string form as GET (`BASE64URL(header).BASE64URL(payload).BASE64URL(signature)`), even if JFS verification is skipped.
|
|
52
53
|
*/
|
|
53
54
|
export declare function parseRequest(request: Request, options?: ParseRequestOptions): Promise<ParseRequestResult>;
|
|
@@ -1,85 +1,124 @@
|
|
|
1
|
-
import { ACTION_TYPE_GET, ACTION_TYPE_POST, payloadSchema, } from "../schemas.js";
|
|
2
|
-
import { decodePayload,
|
|
3
|
-
import {
|
|
1
|
+
import { ACTION_TYPE_GET, ACTION_TYPE_POST, getPayloadSchema, payloadSchema, } from "../schemas.js";
|
|
2
|
+
import { decodePayload, parseJfs, verifyJFS } from "./verify.js";
|
|
3
|
+
import { SNAP_PAYLOAD_HEADER } from "../constants.js";
|
|
4
4
|
const DEFAULT_SNAP_POST_MAX_SKEW_SECONDS = 300;
|
|
5
|
-
const requestBodySchema = z.object({
|
|
6
|
-
header: z.string(),
|
|
7
|
-
payload: z.string(),
|
|
8
|
-
signature: z.string(),
|
|
9
|
-
});
|
|
10
5
|
/**
|
|
11
6
|
* Parse and validate Farcaster snap requests:
|
|
12
|
-
* - `GET
|
|
13
|
-
*
|
|
7
|
+
* - `GET`: returns `{ type: "get" }`, or optional viewer fields when `X-Snap-Payload`
|
|
8
|
+
* carries a JFS compact string whose decoded payload validates against {@link getPayloadSchema}.
|
|
9
|
+
* - `POST`: the body must be a JFS envelope — either JSON `{ header, payload, signature }` or the same **compact** string form as GET (`BASE64URL(header).BASE64URL(payload).BASE64URL(signature)`), even if JFS verification is skipped.
|
|
14
10
|
*/
|
|
15
11
|
export async function parseRequest(request, options = {}) {
|
|
16
|
-
if (!["GET", "POST"].includes(request.method)) {
|
|
17
|
-
return {
|
|
18
|
-
success: false,
|
|
19
|
-
error: {
|
|
20
|
-
type: "method_not_allowed",
|
|
21
|
-
message: `expected POST, received ${request.method}`,
|
|
22
|
-
},
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
12
|
if (request.method === "GET") {
|
|
26
|
-
return
|
|
27
|
-
success: true,
|
|
28
|
-
action: { type: ACTION_TYPE_GET },
|
|
29
|
-
};
|
|
13
|
+
return await parseGetRequest(request, options);
|
|
30
14
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const text = await request.text();
|
|
34
|
-
let jsonBody;
|
|
35
|
-
try {
|
|
36
|
-
jsonBody = JSON.parse(text);
|
|
15
|
+
if (request.method === "POST") {
|
|
16
|
+
return await parsePostRequest(request, options);
|
|
37
17
|
}
|
|
38
|
-
|
|
18
|
+
return {
|
|
19
|
+
success: false,
|
|
20
|
+
error: {
|
|
21
|
+
type: "method_not_allowed",
|
|
22
|
+
message: `expected GET or POST, received ${request.method}`,
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
async function parseGetRequest(request, options) {
|
|
27
|
+
const compactHeader = request.headers.get(SNAP_PAYLOAD_HEADER)?.trim();
|
|
28
|
+
if (!compactHeader) {
|
|
29
|
+
return { success: true, action: { type: ACTION_TYPE_GET } };
|
|
30
|
+
}
|
|
31
|
+
const result = await validateJfsPayload({
|
|
32
|
+
jfsText: compactHeader,
|
|
33
|
+
schema: getPayloadSchema,
|
|
34
|
+
request,
|
|
35
|
+
options,
|
|
36
|
+
invalidJsonMessage: `${SNAP_PAYLOAD_HEADER} must be a valid JFS compact string`,
|
|
37
|
+
});
|
|
38
|
+
if (!result.ok) {
|
|
39
|
+
return { success: false, error: result.error };
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
success: true,
|
|
43
|
+
action: { type: ACTION_TYPE_GET, ...result.payload },
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
async function parsePostRequest(request, options) {
|
|
47
|
+
const result = await validateJfsPayload({
|
|
48
|
+
jfsText: await request.text(),
|
|
49
|
+
schema: payloadSchema,
|
|
50
|
+
request,
|
|
51
|
+
options,
|
|
52
|
+
});
|
|
53
|
+
if (!result.ok) {
|
|
54
|
+
return { success: false, error: result.error };
|
|
55
|
+
}
|
|
56
|
+
const payload = result.payload;
|
|
57
|
+
if (payload.fid !== undefined && payload.fid !== payload.user.fid) {
|
|
39
58
|
return {
|
|
40
59
|
success: false,
|
|
41
60
|
error: {
|
|
42
|
-
type: "
|
|
43
|
-
message: "
|
|
61
|
+
type: "fid_mismatch",
|
|
62
|
+
message: `fid "${payload.fid}" does not match user.fid "${payload.user.fid}"`,
|
|
44
63
|
},
|
|
45
64
|
};
|
|
46
65
|
}
|
|
47
|
-
|
|
48
|
-
|
|
66
|
+
return {
|
|
67
|
+
success: true,
|
|
68
|
+
action: { type: ACTION_TYPE_POST, ...payload },
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Shared pipeline for authenticated snap requests: parse the JFS envelope,
|
|
73
|
+
* decode and schema-validate the payload, optionally verify the JFS signature
|
|
74
|
+
* against an active hub signer (matching `user.fid`), then check timestamp
|
|
75
|
+
* skew and that `audience` matches the request origin.
|
|
76
|
+
*
|
|
77
|
+
* Both GET (payload header) and POST (request body) feed into this.
|
|
78
|
+
*/
|
|
79
|
+
async function validateJfsPayload({ jfsText, schema, request, options, invalidJsonMessage, }) {
|
|
80
|
+
const parsed = parseJfs(jfsText);
|
|
81
|
+
if (!parsed.ok) {
|
|
49
82
|
return {
|
|
50
|
-
|
|
51
|
-
error: {
|
|
83
|
+
ok: false,
|
|
84
|
+
error: {
|
|
85
|
+
type: "invalid_json",
|
|
86
|
+
message: invalidJsonMessage ?? parsed.error,
|
|
87
|
+
},
|
|
52
88
|
};
|
|
53
89
|
}
|
|
54
|
-
const
|
|
90
|
+
const jfs = parsed.jfs;
|
|
91
|
+
const payloadParsed = schema.safeParse(decodePayload(jfs.payload));
|
|
55
92
|
if (!payloadParsed.success) {
|
|
56
93
|
return {
|
|
57
|
-
|
|
94
|
+
ok: false,
|
|
58
95
|
error: { type: "validation", issues: payloadParsed.error.issues },
|
|
59
96
|
};
|
|
60
97
|
}
|
|
61
|
-
const
|
|
98
|
+
const payload = payloadParsed.data;
|
|
62
99
|
if (!options.skipJFSVerification) {
|
|
63
|
-
const
|
|
64
|
-
if (!
|
|
100
|
+
const verified = await verifyJFS(jfs);
|
|
101
|
+
if (!verified.valid) {
|
|
65
102
|
return {
|
|
66
|
-
|
|
67
|
-
error: { type: "signature", message:
|
|
103
|
+
ok: false,
|
|
104
|
+
error: { type: "signature", message: verified.error.message },
|
|
68
105
|
};
|
|
69
106
|
}
|
|
70
|
-
if (
|
|
107
|
+
if (verified.signingUserFid !== payload.user.fid) {
|
|
71
108
|
return {
|
|
72
|
-
|
|
109
|
+
ok: false,
|
|
73
110
|
error: {
|
|
74
111
|
type: "fid_mismatch",
|
|
75
|
-
message: `JFS header fid "${
|
|
112
|
+
message: `JFS header fid "${verified.signingUserFid}" does not match user.fid "${payload.user.fid}"`,
|
|
76
113
|
},
|
|
77
114
|
};
|
|
78
115
|
}
|
|
79
116
|
}
|
|
80
|
-
|
|
117
|
+
const maxSkew = options.maxSkewSeconds ?? DEFAULT_SNAP_POST_MAX_SKEW_SECONDS;
|
|
118
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
119
|
+
if (Math.abs(nowSec - payload.timestamp) > maxSkew) {
|
|
81
120
|
return {
|
|
82
|
-
|
|
121
|
+
ok: false,
|
|
83
122
|
error: {
|
|
84
123
|
type: "replay",
|
|
85
124
|
message: `timestamp outside allowed skew of ${maxSkew}s`,
|
|
@@ -100,29 +139,14 @@ export async function parseRequest(request, options = {}) {
|
|
|
100
139
|
// do nothing
|
|
101
140
|
}
|
|
102
141
|
}
|
|
103
|
-
if (expectedOrigin !== undefined &&
|
|
142
|
+
if (expectedOrigin !== undefined && payload.audience !== expectedOrigin) {
|
|
104
143
|
return {
|
|
105
|
-
|
|
144
|
+
ok: false,
|
|
106
145
|
error: {
|
|
107
146
|
type: "origin_mismatch",
|
|
108
|
-
message: `payload audience "${
|
|
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}"`,
|
|
147
|
+
message: `payload audience "${payload.audience}" does not match expected origin "${expectedOrigin}"`,
|
|
118
148
|
},
|
|
119
149
|
};
|
|
120
150
|
}
|
|
121
|
-
return {
|
|
122
|
-
success: true,
|
|
123
|
-
action: {
|
|
124
|
-
type: ACTION_TYPE_POST,
|
|
125
|
-
...body,
|
|
126
|
-
},
|
|
127
|
-
};
|
|
151
|
+
return { ok: true, payload };
|
|
128
152
|
}
|
package/dist/server/verify.d.ts
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import { type JsonFarcasterSignature } from "@farcaster/jfs";
|
|
2
|
+
/**
|
|
3
|
+
* Parse a JFS object or string into normalized form (see {@link toJsonFarcasterSignature} / `uncompact`).
|
|
4
|
+
*/
|
|
5
|
+
export declare function parseJfs(text: string): {
|
|
6
|
+
ok: true;
|
|
7
|
+
jfs: JsonFarcasterSignature;
|
|
8
|
+
} | {
|
|
9
|
+
ok: false;
|
|
10
|
+
error: string;
|
|
11
|
+
};
|
|
12
|
+
export declare function verifyJFS<TPayload>(jfs: JsonFarcasterSignature, options?: {
|
|
6
13
|
hubHttpBaseUrl?: string;
|
|
7
14
|
}): Promise<{
|
|
8
15
|
valid: false;
|
package/dist/server/verify.js
CHANGED
|
@@ -1,33 +1,40 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { decode, decodePayload as jfsDecodePayload, encodePayload as jfsEncodePayload, toJsonFarcasterSignature, verify, } from "@farcaster/jfs";
|
|
2
2
|
import { hexToBytes } from "viem";
|
|
3
3
|
import { DEFAULT_SNAP_HUB_HTTP_BASE_URL, getActiveEd25519SignerKeysFromHubHttp, } from "./hubs.js";
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Parse a JFS object or string into normalized form (see {@link toJsonFarcasterSignature} / `uncompact`).
|
|
6
|
+
*/
|
|
7
|
+
export function parseJfs(text) {
|
|
8
|
+
const trimmed = text.trim();
|
|
9
|
+
let jfsFromJson;
|
|
6
10
|
try {
|
|
7
|
-
|
|
8
|
-
header: requestBody.header,
|
|
9
|
-
payload: requestBody.payload,
|
|
10
|
-
signature: requestBody.signature,
|
|
11
|
-
});
|
|
11
|
+
jfsFromJson = JSON.parse(trimmed);
|
|
12
12
|
}
|
|
13
|
-
catch
|
|
14
|
-
|
|
15
|
-
valid: false,
|
|
16
|
-
error: error instanceof Error ? error : new Error(String(error)),
|
|
17
|
-
};
|
|
13
|
+
catch {
|
|
14
|
+
jfsFromJson = undefined;
|
|
18
15
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
decoded = decode(compactJfs);
|
|
16
|
+
if (jfsFromJson !== undefined && isJfsObject(jfsFromJson)) {
|
|
17
|
+
return { ok: true, jfs: jfsFromJson };
|
|
22
18
|
}
|
|
23
|
-
|
|
19
|
+
const jfsFromString = tryUncompactJfsString(trimmed);
|
|
20
|
+
if (jfsFromString) {
|
|
21
|
+
return { ok: true, jfs: jfsFromString };
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
ok: false,
|
|
25
|
+
error: "invalid JFS envelope: must be JSON with header, payload, and signature fields, or a JFS compact string (three dot-separated segments)",
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export async function verifyJFS(jfs, options = {}) {
|
|
29
|
+
const decoded = tryDecodeJfs(jfs);
|
|
30
|
+
if (!decoded) {
|
|
24
31
|
return {
|
|
25
32
|
valid: false,
|
|
26
|
-
error:
|
|
33
|
+
error: new Error("invalid JFS envelope"),
|
|
27
34
|
};
|
|
28
35
|
}
|
|
29
36
|
try {
|
|
30
|
-
await verify({ data:
|
|
37
|
+
await verify({ data: jfs, strict: true, keyTypes: ["app_key"] });
|
|
31
38
|
}
|
|
32
39
|
catch (error) {
|
|
33
40
|
return {
|
|
@@ -35,6 +42,12 @@ export async function verifyJFSRequestBody(requestBody, options = {}) {
|
|
|
35
42
|
error: error instanceof Error ? error : new Error(String(error)),
|
|
36
43
|
};
|
|
37
44
|
}
|
|
45
|
+
return hubVerifyDecodedPayload(decoded, options);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Verify that the signing key for a JFS payload is an active signer for the FID.
|
|
49
|
+
*/
|
|
50
|
+
async function hubVerifyDecodedPayload(decoded, options) {
|
|
38
51
|
const { header, payload } = decoded;
|
|
39
52
|
const keys = await getActiveEd25519SignerKeysFromHubHttp(options.hubHttpBaseUrl ?? DEFAULT_SNAP_HUB_HTTP_BASE_URL, header.fid);
|
|
40
53
|
if (!keys.ok) {
|
|
@@ -78,6 +91,41 @@ export function decodePayload(payload) {
|
|
|
78
91
|
export function encodePayload(payload) {
|
|
79
92
|
return jfsEncodePayload(payload);
|
|
80
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* Normalize a compact JFS string to `{ header, payload, signature }` using
|
|
96
|
+
* `@farcaster/jfs` {@link toJsonFarcasterSignature} (which delegates to `uncompact` for strings).
|
|
97
|
+
* Returns null if the string is malformed.
|
|
98
|
+
*/
|
|
99
|
+
function tryUncompactJfsString(value) {
|
|
100
|
+
try {
|
|
101
|
+
return toJsonFarcasterSignature(value.trim());
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Fully decode a JFS envelope or compact string via `@farcaster/jfs` {@link decode}:
|
|
109
|
+
* parsed header object, parsed payload, signature bytes.
|
|
110
|
+
*/
|
|
111
|
+
function tryDecodeJfs(input) {
|
|
112
|
+
try {
|
|
113
|
+
return decode(input);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function isJfsObject(v) {
|
|
120
|
+
return (v !== null &&
|
|
121
|
+
typeof v === "object" &&
|
|
122
|
+
"header" in v &&
|
|
123
|
+
typeof v.header === "string" &&
|
|
124
|
+
"payload" in v &&
|
|
125
|
+
typeof v.payload === "string" &&
|
|
126
|
+
"signature" in v &&
|
|
127
|
+
typeof v.signature === "string");
|
|
128
|
+
}
|
|
81
129
|
function bytesEqual(a, b) {
|
|
82
130
|
if (a.length !== b.length)
|
|
83
131
|
return false;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
export declare function horizontalChildrenAreAllButtons(children: ReactNode): boolean;
|
|
3
|
+
/** Direct snap catalog children under a stack (used for all-button grid column count). */
|
|
4
|
+
export declare function countRenderableChildren(children: ReactNode): number;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Children, isValidElement } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* True when every rendered child comes from a catalog `button` element.
|
|
4
|
+
* json-render passes `{ element: { type, props, ... } }` into each catalog component.
|
|
5
|
+
*/
|
|
6
|
+
function isRenderableChild(c) {
|
|
7
|
+
if (c == null)
|
|
8
|
+
return false;
|
|
9
|
+
if (typeof c === "boolean")
|
|
10
|
+
return false;
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
export function horizontalChildrenAreAllButtons(children) {
|
|
14
|
+
const items = Children.toArray(children).filter(isRenderableChild);
|
|
15
|
+
if (items.length === 0)
|
|
16
|
+
return false;
|
|
17
|
+
for (const child of items) {
|
|
18
|
+
if (!isValidElement(child))
|
|
19
|
+
return false;
|
|
20
|
+
const typ = child.props.element?.type;
|
|
21
|
+
if (typ !== "button")
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
/** Direct snap catalog children under a stack (used for all-button grid column count). */
|
|
27
|
+
export function countRenderableChildren(children) {
|
|
28
|
+
return Children.toArray(children).filter(isRenderableChild).length;
|
|
29
|
+
}
|
package/dist/ui/catalog.d.ts
CHANGED
|
@@ -318,6 +318,7 @@ export declare const snapJsonRenderCatalog: import("@json-render/core").Catalog<
|
|
|
318
318
|
between: "between";
|
|
319
319
|
around: "around";
|
|
320
320
|
}>>;
|
|
321
|
+
columns: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<2>, z.ZodLiteral<3>, z.ZodLiteral<4>, z.ZodLiteral<5>, z.ZodLiteral<6>]>>;
|
|
321
322
|
}, z.core.$strip>;
|
|
322
323
|
description: string;
|
|
323
324
|
};
|
|
@@ -379,7 +380,7 @@ export declare const snapJsonRenderCatalog: import("@json-render/core").Catalog<
|
|
|
379
380
|
cells: z.ZodArray<z.ZodObject<{
|
|
380
381
|
row: z.ZodNumber;
|
|
381
382
|
col: z.ZodNumber;
|
|
382
|
-
color: z.ZodOptional<z.ZodEnum<{
|
|
383
|
+
color: z.ZodOptional<z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodUnion<readonly [z.ZodEnum<{
|
|
383
384
|
gray: "gray";
|
|
384
385
|
blue: "blue";
|
|
385
386
|
red: "red";
|
|
@@ -388,7 +389,7 @@ export declare const snapJsonRenderCatalog: import("@json-render/core").Catalog<
|
|
|
388
389
|
teal: "teal";
|
|
389
390
|
purple: "purple";
|
|
390
391
|
pink: "pink";
|
|
391
|
-
}
|
|
392
|
+
}>, z.ZodString]>>>;
|
|
392
393
|
content: z.ZodOptional<z.ZodString>;
|
|
393
394
|
}, z.core.$strip>>;
|
|
394
395
|
gap: z.ZodOptional<z.ZodEnum<{
|
package/dist/ui/catalog.js
CHANGED
|
@@ -78,7 +78,7 @@ export const snapJsonRenderCatalog = defineCatalog(snapJsonRenderSchema, {
|
|
|
78
78
|
},
|
|
79
79
|
stack: {
|
|
80
80
|
props: stackProps,
|
|
81
|
-
description: "Layout container — direction: vertical (default) | horizontal. Children are element ids in order.",
|
|
81
|
+
description: "Layout container — direction: vertical (default) | horizontal. Children are element ids in order. Horizontal stacks use a single flex row so peers stay side-by-side and shrink with min-width 0. Nested stacks participate as flexible row peers. All-button horizontal stacks use an equal N-column grid where N is the number of buttons (1–6). Optional `columns` (`2`–`6`) forces an explicit equal grid for mixed children.",
|
|
82
82
|
},
|
|
83
83
|
text: {
|
|
84
84
|
props: textProps,
|
|
@@ -90,7 +90,7 @@ export const snapJsonRenderCatalog = defineCatalog(snapJsonRenderSchema, {
|
|
|
90
90
|
},
|
|
91
91
|
cell_grid: {
|
|
92
92
|
props: cellGridProps,
|
|
93
|
-
description: "Cell grid — sparse colored cells on a rows×cols grid. Two interaction modes: leave select 'off' and bind on.press to fire an action per cell press (inputs[name] is the pressed 'row,col' before the action runs); or set select 'single'/'multiple' for press-to-select with a visual ring (no auto-fire — pair with a separate submit button). on.press is ignored when select is on.",
|
|
93
|
+
description: "Cell grid — sparse colored cells on a rows×cols grid. Cell color is a palette name or literal #rrggbb hex (hex ignores page accent). Two interaction modes: leave select 'off' and bind on.press to fire an action per cell press (inputs[name] is the pressed 'row,col' before the action runs); or set select 'single'/'multiple' for press-to-select with a visual ring (no auto-fire — pair with a separate submit button). on.press is ignored when select is on.",
|
|
94
94
|
},
|
|
95
95
|
},
|
|
96
96
|
actions: {
|
package/dist/ui/cell-grid.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export declare const cellGridProps: z.ZodObject<{
|
|
|
6
6
|
cells: z.ZodArray<z.ZodObject<{
|
|
7
7
|
row: z.ZodNumber;
|
|
8
8
|
col: z.ZodNumber;
|
|
9
|
-
color: z.ZodOptional<z.ZodEnum<{
|
|
9
|
+
color: z.ZodOptional<z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodUnion<readonly [z.ZodEnum<{
|
|
10
10
|
gray: "gray";
|
|
11
11
|
blue: "blue";
|
|
12
12
|
red: "red";
|
|
@@ -15,7 +15,7 @@ export declare const cellGridProps: z.ZodObject<{
|
|
|
15
15
|
teal: "teal";
|
|
16
16
|
purple: "purple";
|
|
17
17
|
pink: "pink";
|
|
18
|
-
}
|
|
18
|
+
}>, z.ZodString]>>>;
|
|
19
19
|
content: z.ZodOptional<z.ZodString>;
|
|
20
20
|
}, z.core.$strip>>;
|
|
21
21
|
gap: z.ZodOptional<z.ZodEnum<{
|
package/dist/ui/cell-grid.js
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { PALETTE_COLOR_VALUES } from "../colors.js";
|
|
2
|
+
import { isSnapHexColorString, PALETTE_COLOR_VALUES } from "../colors.js";
|
|
3
3
|
import { GRID_MIN_COLS, GRID_MAX_COLS, GRID_MIN_ROWS, GRID_MAX_ROWS, GRID_GAP_VALUES, } from "../constants.js";
|
|
4
|
+
/** Palette name or `#rrggbb`; input is trimmed so palette and hex rules match runtime resolvers. */
|
|
5
|
+
const cellGridCellColorSchema = z.preprocess((v) => (typeof v === "string" ? v.trim() : v), z.union([
|
|
6
|
+
z.enum(PALETTE_COLOR_VALUES),
|
|
7
|
+
z
|
|
8
|
+
.string()
|
|
9
|
+
.refine(isSnapHexColorString, {
|
|
10
|
+
message: "cell_grid cell hex color must be #rrggbb",
|
|
11
|
+
}),
|
|
12
|
+
]));
|
|
4
13
|
const cellGridCellSchema = z.object({
|
|
5
14
|
row: z.number().int().nonnegative(),
|
|
6
15
|
col: z.number().int().nonnegative(),
|
|
7
|
-
color:
|
|
16
|
+
color: cellGridCellColorSchema.optional(),
|
|
8
17
|
content: z.string().optional(),
|
|
9
18
|
});
|
|
10
19
|
export const cellGridProps = z
|
package/dist/ui/stack.d.ts
CHANGED
|
@@ -20,5 +20,6 @@ export declare const stackProps: z.ZodObject<{
|
|
|
20
20
|
between: "between";
|
|
21
21
|
around: "around";
|
|
22
22
|
}>>;
|
|
23
|
+
columns: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<2>, z.ZodLiteral<3>, z.ZodLiteral<4>, z.ZodLiteral<5>, z.ZodLiteral<6>]>>;
|
|
23
24
|
}, z.core.$strip>;
|
|
24
25
|
export type StackProps = z.infer<typeof stackProps>;
|
package/dist/ui/stack.js
CHANGED
|
@@ -6,4 +6,12 @@ export const stackProps = z.object({
|
|
|
6
6
|
direction: z.enum(STACK_DIRECTIONS).optional(),
|
|
7
7
|
gap: z.enum(STACK_GAPS).optional(),
|
|
8
8
|
justify: z.enum(STACK_JUSTIFY).optional(),
|
|
9
|
+
/** Horizontal stacks only: fixed column grid (`2`–`6`). Prefer omitting this when children are stacks — they flex as row peers automatically. */
|
|
10
|
+
columns: z.union([
|
|
11
|
+
z.literal(2),
|
|
12
|
+
z.literal(3),
|
|
13
|
+
z.literal(4),
|
|
14
|
+
z.literal(5),
|
|
15
|
+
z.literal(6),
|
|
16
|
+
]).optional(),
|
|
9
17
|
});
|