@farcaster/snap 2.1.2 → 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 +1 -0
- package/dist/constants.js +5 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/react/hooks/use-snap-colors.js +2 -9
- 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/ui/catalog.d.ts +2 -2
- package/dist/ui/catalog.js +1 -1
- package/dist/ui/cell-grid.d.ts +2 -2
- package/dist/ui/cell-grid.js +11 -2
- package/dist/verify.test.js +3 -3
- package/llms.txt +11 -1
- package/package.json +1 -1
- package/src/colors.ts +27 -0
- package/src/constants.ts +6 -1
- package/src/index.ts +6 -0
- package/src/react/hooks/use-snap-colors.ts +3 -8
- 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/ui/catalog.ts +1 -1
- package/src/ui/cell-grid.ts +15 -2
- package/src/verify.test.ts +3 -3
package/src/server/verify.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
|
-
compact,
|
|
3
2
|
decode,
|
|
4
3
|
decodePayload as jfsDecodePayload,
|
|
5
4
|
encodePayload as jfsEncodePayload,
|
|
5
|
+
toJsonFarcasterSignature,
|
|
6
6
|
verify,
|
|
7
|
+
type JsonFarcasterSignature,
|
|
8
|
+
type DecodedJsonFarcasterSignature,
|
|
7
9
|
} from "@farcaster/jfs";
|
|
8
10
|
import { hexToBytes, type Hex } from "viem";
|
|
9
11
|
import {
|
|
@@ -11,12 +13,39 @@ import {
|
|
|
11
13
|
getActiveEd25519SignerKeysFromHubHttp,
|
|
12
14
|
} from "./hubs";
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Parse a JFS object or string into normalized form (see {@link toJsonFarcasterSignature} / `uncompact`).
|
|
18
|
+
*/
|
|
19
|
+
export function parseJfs(
|
|
20
|
+
text: string,
|
|
21
|
+
): { ok: true; jfs: JsonFarcasterSignature } | { ok: false; error: string } {
|
|
22
|
+
const trimmed = text.trim();
|
|
23
|
+
|
|
24
|
+
let jfsFromJson: unknown;
|
|
25
|
+
try {
|
|
26
|
+
jfsFromJson = JSON.parse(trimmed);
|
|
27
|
+
} catch {
|
|
28
|
+
jfsFromJson = undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (jfsFromJson !== undefined && isJfsObject(jfsFromJson)) {
|
|
32
|
+
return { ok: true, jfs: jfsFromJson };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const jfsFromString = tryUncompactJfsString(trimmed);
|
|
36
|
+
if (jfsFromString) {
|
|
37
|
+
return { ok: true, jfs: jfsFromString };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
ok: false,
|
|
42
|
+
error:
|
|
43
|
+
"invalid JFS envelope: must be JSON with header, payload, and signature fields, or a JFS compact string (three dot-separated segments)",
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function verifyJFS<TPayload>(
|
|
48
|
+
jfs: JsonFarcasterSignature,
|
|
20
49
|
options: {
|
|
21
50
|
hubHttpBaseUrl?: string;
|
|
22
51
|
} = {},
|
|
@@ -31,23 +60,16 @@ export async function verifyJFSRequestBody<TPayload>(
|
|
|
31
60
|
data: TPayload;
|
|
32
61
|
}
|
|
33
62
|
> {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
compactJfs = compact({
|
|
37
|
-
header: requestBody.header,
|
|
38
|
-
payload: requestBody.payload,
|
|
39
|
-
signature: requestBody.signature,
|
|
40
|
-
});
|
|
41
|
-
} catch (error) {
|
|
63
|
+
const decoded = tryDecodeJfs<TPayload>(jfs);
|
|
64
|
+
if (!decoded) {
|
|
42
65
|
return {
|
|
43
66
|
valid: false,
|
|
44
|
-
error:
|
|
67
|
+
error: new Error("invalid JFS envelope"),
|
|
45
68
|
};
|
|
46
69
|
}
|
|
47
70
|
|
|
48
|
-
let decoded: ReturnType<typeof decode<TPayload>>;
|
|
49
71
|
try {
|
|
50
|
-
|
|
72
|
+
await verify({ data: jfs, strict: true, keyTypes: ["app_key"] });
|
|
51
73
|
} catch (error) {
|
|
52
74
|
return {
|
|
53
75
|
valid: false,
|
|
@@ -55,15 +77,26 @@ export async function verifyJFSRequestBody<TPayload>(
|
|
|
55
77
|
};
|
|
56
78
|
}
|
|
57
79
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
} catch (error) {
|
|
61
|
-
return {
|
|
62
|
-
valid: false,
|
|
63
|
-
error: error instanceof Error ? error : new Error(String(error)),
|
|
64
|
-
};
|
|
65
|
-
}
|
|
80
|
+
return hubVerifyDecodedPayload(decoded, options);
|
|
81
|
+
}
|
|
66
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Verify that the signing key for a JFS payload is an active signer for the FID.
|
|
85
|
+
*/
|
|
86
|
+
async function hubVerifyDecodedPayload<TPayload>(
|
|
87
|
+
decoded: DecodedJsonFarcasterSignature<TPayload>,
|
|
88
|
+
options: { hubHttpBaseUrl?: string },
|
|
89
|
+
): Promise<
|
|
90
|
+
| {
|
|
91
|
+
valid: false;
|
|
92
|
+
error: Error;
|
|
93
|
+
}
|
|
94
|
+
| {
|
|
95
|
+
valid: true;
|
|
96
|
+
signingUserFid: number;
|
|
97
|
+
data: TPayload;
|
|
98
|
+
}
|
|
99
|
+
> {
|
|
67
100
|
const { header, payload } = decoded;
|
|
68
101
|
|
|
69
102
|
const keys = await getActiveEd25519SignerKeysFromHubHttp(
|
|
@@ -121,6 +154,46 @@ export function encodePayload<TPayload>(payload: TPayload): string {
|
|
|
121
154
|
return jfsEncodePayload(payload);
|
|
122
155
|
}
|
|
123
156
|
|
|
157
|
+
/**
|
|
158
|
+
* Normalize a compact JFS string to `{ header, payload, signature }` using
|
|
159
|
+
* `@farcaster/jfs` {@link toJsonFarcasterSignature} (which delegates to `uncompact` for strings).
|
|
160
|
+
* Returns null if the string is malformed.
|
|
161
|
+
*/
|
|
162
|
+
function tryUncompactJfsString(value: string): JsonFarcasterSignature | null {
|
|
163
|
+
try {
|
|
164
|
+
return toJsonFarcasterSignature(value.trim());
|
|
165
|
+
} catch {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Fully decode a JFS envelope or compact string via `@farcaster/jfs` {@link decode}:
|
|
172
|
+
* parsed header object, parsed payload, signature bytes.
|
|
173
|
+
*/
|
|
174
|
+
function tryDecodeJfs<TPayload>(
|
|
175
|
+
input: JsonFarcasterSignature | string,
|
|
176
|
+
): DecodedJsonFarcasterSignature<TPayload> | null {
|
|
177
|
+
try {
|
|
178
|
+
return decode<TPayload>(input);
|
|
179
|
+
} catch {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function isJfsObject(v: unknown): v is JsonFarcasterSignature {
|
|
185
|
+
return (
|
|
186
|
+
v !== null &&
|
|
187
|
+
typeof v === "object" &&
|
|
188
|
+
"header" in v &&
|
|
189
|
+
typeof v.header === "string" &&
|
|
190
|
+
"payload" in v &&
|
|
191
|
+
typeof v.payload === "string" &&
|
|
192
|
+
"signature" in v &&
|
|
193
|
+
typeof v.signature === "string"
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
124
197
|
function bytesEqual(a: Uint8Array, b: Uint8Array): boolean {
|
|
125
198
|
if (a.length !== b.length) return false;
|
|
126
199
|
let diff = 0;
|
package/src/ui/catalog.ts
CHANGED
|
@@ -107,7 +107,7 @@ export const snapJsonRenderCatalog = defineCatalog(snapJsonRenderSchema, {
|
|
|
107
107
|
cell_grid: {
|
|
108
108
|
props: cellGridProps,
|
|
109
109
|
description:
|
|
110
|
-
"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.",
|
|
110
|
+
"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.",
|
|
111
111
|
},
|
|
112
112
|
},
|
|
113
113
|
actions: {
|
package/src/ui/cell-grid.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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 {
|
|
4
4
|
GRID_MIN_COLS,
|
|
5
5
|
GRID_MAX_COLS,
|
|
@@ -8,10 +8,23 @@ import {
|
|
|
8
8
|
GRID_GAP_VALUES,
|
|
9
9
|
} from "../constants.js";
|
|
10
10
|
|
|
11
|
+
/** Palette name or `#rrggbb`; input is trimmed so palette and hex rules match runtime resolvers. */
|
|
12
|
+
const cellGridCellColorSchema = z.preprocess(
|
|
13
|
+
(v) => (typeof v === "string" ? v.trim() : v),
|
|
14
|
+
z.union([
|
|
15
|
+
z.enum(PALETTE_COLOR_VALUES),
|
|
16
|
+
z
|
|
17
|
+
.string()
|
|
18
|
+
.refine(isSnapHexColorString, {
|
|
19
|
+
message: "cell_grid cell hex color must be #rrggbb",
|
|
20
|
+
}),
|
|
21
|
+
]),
|
|
22
|
+
);
|
|
23
|
+
|
|
11
24
|
const cellGridCellSchema = z.object({
|
|
12
25
|
row: z.number().int().nonnegative(),
|
|
13
26
|
col: z.number().int().nonnegative(),
|
|
14
|
-
color:
|
|
27
|
+
color: cellGridCellColorSchema.optional(),
|
|
15
28
|
content: z.string().optional(),
|
|
16
29
|
});
|
|
17
30
|
|
package/src/verify.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
import {
|
|
2
|
+
import { verifyJFS } from "./server/verify";
|
|
3
3
|
|
|
4
4
|
const validRequestBody = `{
|
|
5
5
|
"header":"eyJmaWQiOjI2MTMxOSwidHlwZSI6ImFwcF9rZXkiLCJrZXkiOiIweGY0ZGQyNjczYTUzMjEwYzQ3ZGYzZjFmNTk0NjZlZTdhMTM3ZmQxOGQ5NTVjMmU2OGExMmQwOTE2MGE2NmMyMTUifQ",
|
|
@@ -11,7 +11,7 @@ const validRequestBody = `{
|
|
|
11
11
|
const HUB_SIGNER_KEY_HEX =
|
|
12
12
|
"f4dd2673a53210c47df3f1f59466ee7a137fd18d955c2e68a12d09160a66c215";
|
|
13
13
|
|
|
14
|
-
describe("
|
|
14
|
+
describe("verifyJFS", () => {
|
|
15
15
|
beforeEach(() => {
|
|
16
16
|
vi.stubGlobal(
|
|
17
17
|
"fetch",
|
|
@@ -53,7 +53,7 @@ describe("verifyJFSRequestBody", () => {
|
|
|
53
53
|
});
|
|
54
54
|
|
|
55
55
|
it("accepts JSON JFS body and verifies crypto + hub signer list", async () => {
|
|
56
|
-
const result = await
|
|
56
|
+
const result = await verifyJFS(JSON.parse(validRequestBody));
|
|
57
57
|
expect(result.valid).toBe(true);
|
|
58
58
|
if (result.valid) {
|
|
59
59
|
expect(result.data).toEqual({
|