@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.
@@ -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<{
@@ -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: z.enum(PALETTE_COLOR_VALUES).optional(),
16
+ color: cellGridCellColorSchema.optional(),
8
17
  content: z.string().optional(),
9
18
  });
10
19
  export const cellGridProps = z
@@ -1,5 +1,5 @@
1
1
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
- import { verifyJFSRequestBody } from "./server/verify.js";
2
+ import { verifyJFS } from "./server/verify.js";
3
3
  const validRequestBody = `{
4
4
  "header":"eyJmaWQiOjI2MTMxOSwidHlwZSI6ImFwcF9rZXkiLCJrZXkiOiIweGY0ZGQyNjczYTUzMjEwYzQ3ZGYzZjFmNTk0NjZlZTdhMTM3ZmQxOGQ5NTVjMmU2OGExMmQwOTE2MGE2NmMyMTUifQ",
5
5
  "payload":"eyJmaWQiOjI2MTMxOSwiaW5wdXRzIjp7ImRpc3BsYXkiOiJJU08gKFVUQykifSwiYnV0dG9uX2luZGV4IjowLCJ0aW1lc3RhbXAiOjE3NzQ2OTMyMTN9",
@@ -7,7 +7,7 @@ const validRequestBody = `{
7
7
  }`;
8
8
  /** Matches JFS header `key` (Ed25519 public key, 32 bytes hex without `0x` in hub JSON field). */
9
9
  const HUB_SIGNER_KEY_HEX = "f4dd2673a53210c47df3f1f59466ee7a137fd18d955c2e68a12d09160a66c215";
10
- describe("verifyJFSRequestBody", () => {
10
+ describe("verifyJFS", () => {
11
11
  beforeEach(() => {
12
12
  vi.stubGlobal("fetch", vi.fn(async (input) => {
13
13
  const u = String(input);
@@ -41,7 +41,7 @@ describe("verifyJFSRequestBody", () => {
41
41
  vi.unstubAllGlobals();
42
42
  });
43
43
  it("accepts JSON JFS body and verifies crypto + hub signer list", async () => {
44
- const result = await verifyJFSRequestBody(JSON.parse(validRequestBody));
44
+ const result = await verifyJFS(JSON.parse(validRequestBody));
45
45
  expect(result.valid).toBe(true);
46
46
  if (result.valid) {
47
47
  expect(result.data).toEqual({
package/llms.txt CHANGED
@@ -95,7 +95,7 @@ Top-level fields: `version` (required, `"1.0"` or `"2.0"`), `theme` (optional, `
95
95
  - `name` (string, optional): POST inputs key. Default: `"grid_tap"`
96
96
  - `cols` (number, required, 2–32)
97
97
  - `rows` (number, required, 2–16)
98
- - `cells` (array, required): sparse list of `{ row, col, color?: PaletteColor, content?: string }`
98
+ - `cells` (array, required): sparse list of `{ row, col, color?: PaletteColor | #rrggbb, content?: string }`
99
99
  - `gap` (optional): `"none"` (0px) | `"sm"` (1px) | `"md"` (2px) | `"lg"` (4px). Default: `"sm"`
100
100
  - `rowHeight` (number, optional, 8–64): pixel height per row. Default: 28. Grid height = rows × rowHeight
101
101
  - `select` (optional): `"off"` | `"single"` | `"multiple"`. Default: `"off"`. With `select: "off"`, bind `on.press` for press-to-act (each press writes `"row,col"` to `inputs[name]` and fires the action). With `"single"` / `"multiple"`, presses accumulate selection state and pair with a separate submit `button`; `on.press` is ignored.
@@ -172,6 +172,16 @@ Bound to buttons via `on.press`:
172
172
  | `send_token` | `token`, `amount?`, `recipientFid?`, `recipientAddress?` | Send token flow |
173
173
  | `swap_token` | `sellToken?`, `buyToken?` | Swap token flow |
174
174
 
175
+ ## Authenticated requests
176
+
177
+ POST requests carry a JFS envelope (JSON object **or** compact dot-separated string). `parseRequest` validates the payload and (unless `skipJFSVerification`) cryptographically verifies the JFS against an active hub signer for `user.fid`. On the server, `ctx.action.user.fid` is **always present and verified** for `type === "post"`.
178
+
179
+ GET requests MAY include optional viewer identity in the `X-Snap-Payload` request header (a JFS compact string with the same shape as POST minus `inputs` and `fid`). When present and valid, `ctx.action` on GET MAY include `user`, `timestamp`, `audience`, and `surface`.
180
+
181
+ `ctx.action.user` on GET is **best-effort and never guaranteed** — older or custom clients, cache layers, crawlers, and `curl` may yield an anonymous GET even for users who have POSTed to this snap before. Always render a working anonymous first load; treat viewer fields on GET as a strict enhancement. `parseRequest` (and `@farcaster/snap-hono`'s GET handler) silently fall back to anonymous `{ type: "get" }` when `X-Snap-Payload` is missing or invalid.
182
+
183
+ When responses depend on viewer identity, send `Vary: Accept, X-Snap-Payload` and consider `Cache-Control: private` so caches don't serve viewer-specific bodies to other viewers (`@farcaster/snap-hono` sets `Vary` automatically).
184
+
175
185
  ## Icon Names (34)
176
186
 
177
187
  `arrow-right`, `arrow-left`, `external-link`, `chevron-right`, `check`, `x`, `alert-triangle`, `info`, `clock`, `heart`, `message-circle`, `repeat`, `share`, `user`, `users`, `star`, `trophy`, `zap`, `flame`, `gift`, `image`, `play`, `pause`, `wallet`, `coins`, `plus`, `minus`, `refresh-cw`, `bookmark`, `thumbs-up`, `thumbs-down`, `trending-up`, `trending-down`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@farcaster/snap",
3
- "version": "2.1.2",
3
+ "version": "2.2.0",
4
4
  "description": "Farcaster Snaps 🫰",
5
5
  "repository": {
6
6
  "type": "git",
package/src/colors.ts CHANGED
@@ -38,6 +38,33 @@ export const PALETTE_COLOR_VALUES = [
38
38
 
39
39
  export type PaletteColor = (typeof PALETTE_COLOR_VALUES)[number];
40
40
 
41
+ /** Strict `#rrggbb` literal used by cell_grid (and clients that accept hex). */
42
+ const SNAP_HEX_6 = /^#[0-9a-fA-F]{6}$/;
43
+
44
+ export function isSnapHexColorString(s: string): boolean {
45
+ return SNAP_HEX_6.test(s.trim());
46
+ }
47
+
48
+ /**
49
+ * Resolve a snap color token for inline styles: `accent`, palette names, or
50
+ * literal `#rrggbb`. Unknown values fall back to `accentHex` (same as legacy
51
+ * `colorHex` behavior for non-hex strings).
52
+ */
53
+ export function resolveSnapColorHex(
54
+ color: string | undefined,
55
+ opts: { accentHex: string; appearance: "light" | "dark" },
56
+ ): string {
57
+ if (!color || color === PALETTE_COLOR_ACCENT) return opts.accentHex;
58
+ const trimmed = color.trim();
59
+ if (isSnapHexColorString(trimmed)) return trimmed;
60
+ const map =
61
+ opts.appearance === "dark" ? PALETTE_DARK_HEX : PALETTE_LIGHT_HEX;
62
+ if (Object.hasOwn(map, trimmed)) {
63
+ return map[trimmed as PaletteColor];
64
+ }
65
+ return opts.accentHex;
66
+ }
67
+
41
68
  /** Light-mode hex for each palette color (emulator / reference client). */
42
69
  export const PALETTE_LIGHT_HEX: Record<PaletteColor, string> = {
43
70
  gray: "#6E6A86",
package/src/constants.ts CHANGED
@@ -1,9 +1,14 @@
1
1
  export const SPEC_VERSION_1 = "1.0" as const;
2
2
  export const SPEC_VERSION_2 = "2.0" as const;
3
3
  export const SPEC_VERSION = SPEC_VERSION_2;
4
- export const SUPPORTED_SPEC_VERSIONS = [SPEC_VERSION_1, SPEC_VERSION_2] as const;
4
+ export const SUPPORTED_SPEC_VERSIONS = [
5
+ SPEC_VERSION_1,
6
+ SPEC_VERSION_2,
7
+ ] as const;
5
8
  export type SpecVersion = (typeof SUPPORTED_SPEC_VERSIONS)[number];
6
9
 
10
+ export const SNAP_PAYLOAD_HEADER = "X-Snap-Payload" as const;
11
+
7
12
  export const MEDIA_TYPE = "application/vnd.farcaster.snap+json" as const;
8
13
 
9
14
  export const EFFECT_VALUES = ["confetti"] as const;
package/src/index.ts CHANGED
@@ -8,6 +8,7 @@ export {
8
8
  SPEC_VERSION_2,
9
9
  SUPPORTED_SPEC_VERSIONS,
10
10
  type SpecVersion,
11
+ SNAP_PAYLOAD_HEADER,
11
12
  MEDIA_TYPE,
12
13
  EFFECT_VALUES,
13
14
  POST_GRID_TAP_KEY,
@@ -23,6 +24,8 @@ export {
23
24
  PALETTE_COLOR_VALUES,
24
25
  PALETTE_LIGHT_HEX,
25
26
  PALETTE_DARK_HEX,
27
+ isSnapHexColorString,
28
+ resolveSnapColorHex,
26
29
  type PaletteColor,
27
30
  } from "./colors";
28
31
  export {
@@ -30,7 +33,9 @@ export {
30
33
  ACTION_TYPE_POST,
31
34
  snapResponseSchema,
32
35
  payloadSchema,
36
+ getPayloadSchema,
33
37
  type SnapAction,
38
+ type SnapGetAction,
34
39
  type SnapContext,
35
40
  type SnapResponse,
36
41
  type SnapHandlerResult,
@@ -38,5 +43,6 @@ export {
38
43
  type SnapSpecInput,
39
44
  type SnapFunction,
40
45
  type SnapPayload,
46
+ type SnapGetPayload,
41
47
  } from "./schemas";
42
48
  export { validateSnapResponse, type ValidationResult } from "./validator";
@@ -4,8 +4,7 @@ import { useMemo } from "react";
4
4
  import { useStateStore } from "@json-render/react";
5
5
  import { resolveSnapPaletteHex } from "../lib/resolve-palette-hex";
6
6
  import { useSnapPreviewPageAccent, useSnapAppearance } from "../accent-context";
7
- import type { PaletteColor } from "@farcaster/snap";
8
- import { PALETTE_DARK_HEX, PALETTE_LIGHT_HEX } from "@farcaster/snap";
7
+ import { resolveSnapColorHex } from "@farcaster/snap";
9
8
 
10
9
  /** Readable foreground color (black or white) for a given hex background. */
11
10
  export function pickForegroundForBg(hex: string): string {
@@ -76,7 +75,6 @@ function buildSnapColors(
76
75
  const accent = resolveSnapPaletteHex(accentName, mode);
77
76
  const accentFg = pickForegroundForBg(accent);
78
77
  const neutrals = mode === "dark" ? NEUTRAL_DARK : NEUTRAL_LIGHT;
79
- const paletteMap = mode === "dark" ? PALETTE_DARK_HEX : PALETTE_LIGHT_HEX;
80
78
 
81
79
  const accentHover =
82
80
  mode === "light"
@@ -87,11 +85,8 @@ function buildSnapColors(
87
85
 
88
86
  const paletteHex = (name: string) => resolveSnapPaletteHex(name, mode);
89
87
 
90
- const colorHex = (name: string | undefined) => {
91
- if (!name || name === "accent") return accent;
92
- if (Object.hasOwn(paletteMap, name)) return paletteMap[name as PaletteColor];
93
- return accent;
94
- };
88
+ const colorHex = (name: string | undefined) =>
89
+ resolveSnapColorHex(name, { accentHex: accent, appearance: mode });
95
90
 
96
91
  return {
97
92
  accent,
@@ -3,6 +3,7 @@ import {
3
3
  PALETTE_COLOR_VALUES,
4
4
  PALETTE_LIGHT_HEX,
5
5
  PALETTE_DARK_HEX,
6
+ resolveSnapColorHex,
6
7
  type PaletteColor,
7
8
  } from "@farcaster/snap";
8
9
  import { useStateStore } from "@json-render/react-native";
@@ -35,7 +36,7 @@ export function useSnapPalette() {
35
36
  const accentHex = resolveHex(accentName, mode);
36
37
 
37
38
  const hex = (semantic: string) =>
38
- semantic === "accent" ? accentHex : resolveHex(semantic, mode);
39
+ resolveSnapColorHex(semantic, { accentHex, appearance: mode });
39
40
 
40
41
  return { appearance: mode, accentName, accentHex, hex };
41
42
  }
package/src/schemas.ts CHANGED
@@ -104,26 +104,36 @@ const surfaceSchema = z.discriminatedUnion("type", [
104
104
  standaloneSurfaceSchema,
105
105
  ]);
106
106
 
107
+ const fidSchema = z.number().int().nonnegative();
108
+ const userSchema = z.object({ fid: fidSchema });
109
+
107
110
  export const payloadSchema = z
108
111
  .object({
109
- fid: z.number().int().nonnegative().optional(), // deprecated in favor of user.fid
112
+ fid: fidSchema.optional(), // deprecated in favor of user.fid
110
113
  inputs: z.record(z.string(), postInputValueSchema).default({}),
111
114
  timestamp: z.number().int(),
112
115
  audience: z.string(),
113
- user: z.object({
114
- fid: z.number().int().nonnegative(),
115
- }),
116
+ user: userSchema,
116
117
  surface: surfaceSchema,
117
118
  })
118
119
  .strip();
119
120
 
120
121
  export type SnapPayload = z.infer<typeof payloadSchema>;
121
122
 
123
+ /** JFS payload shape for POST minus deprecated `fid`; used for GET auth via payload header. */
124
+ export const getPayloadSchema = payloadSchema.omit({ inputs: true, fid: true });
125
+
126
+ export type SnapGetPayload = z.infer<typeof getPayloadSchema>;
127
+
122
128
  export const ACTION_TYPE_GET = "get" as const;
123
129
  export const ACTION_TYPE_POST = "post" as const;
124
130
 
125
131
  const snapGetActionSchema = z.object({
126
132
  type: z.literal(ACTION_TYPE_GET),
133
+ user: userSchema.optional(),
134
+ timestamp: z.number().int().optional(),
135
+ audience: z.string().optional(),
136
+ surface: surfaceSchema.optional(),
127
137
  });
128
138
 
129
139
  export type SnapGetAction = z.infer<typeof snapGetActionSchema>;
@@ -1,4 +1,10 @@
1
- export { verifyJFSRequestBody, decodePayload, encodePayload } from "./verify";
1
+ export {
2
+ verifyJFS as verifyJFSRequestBody, // deprecated alias. drop in v3
3
+ parseJfs,
4
+ verifyJFS,
5
+ decodePayload,
6
+ encodePayload,
7
+ } from "./verify";
2
8
  export {
3
9
  DEFAULT_SNAP_HUB_HTTP_BASE_URL,
4
10
  getActiveEd25519SignerKeysFromHubHttp,
@@ -1,11 +1,15 @@
1
+ import { z } from "zod";
1
2
  import {
2
3
  ACTION_TYPE_GET,
3
4
  ACTION_TYPE_POST,
5
+ getPayloadSchema,
4
6
  payloadSchema,
5
7
  type SnapAction,
8
+ type SnapPayload,
9
+ type SnapGetPayload,
6
10
  } from "../schemas";
7
- import { decodePayload, verifyJFSRequestBody } from "./verify";
8
- import { z } from "zod";
11
+ import { decodePayload, parseJfs, verifyJFS } from "./verify";
12
+ import { SNAP_PAYLOAD_HEADER } from "../constants";
9
13
 
10
14
  const DEFAULT_SNAP_POST_MAX_SKEW_SECONDS = 300 as const;
11
15
 
@@ -62,98 +66,156 @@ export type ParseRequestResult =
62
66
  | { success: true; action: SnapAction }
63
67
  | { success: false; error: ParseRequestError };
64
68
 
65
- const requestBodySchema = z.object({
66
- header: z.string(),
67
- payload: z.string(),
68
- signature: z.string(),
69
- });
70
-
71
69
  /**
72
70
  * Parse and validate Farcaster snap requests:
73
- * - `GET` is allowed for first-page loads and returns `{ type: "get" }`.
74
- * - `POST`: the body must be JSON in JFS form (`header` / `payload` / `signature`) even if JFS verification is skipped.
71
+ * - `GET`: returns `{ type: "get" }`, or optional viewer fields when `X-Snap-Payload`
72
+ * carries a JFS compact string whose decoded payload validates against {@link getPayloadSchema}.
73
+ * - `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.
75
74
  */
76
75
  export async function parseRequest(
77
76
  request: Request,
78
77
  options: ParseRequestOptions = {},
79
78
  ): Promise<ParseRequestResult> {
80
- if (!["GET", "POST"].includes(request.method)) {
81
- return {
82
- success: false,
83
- error: {
84
- type: "method_not_allowed",
85
- message: `expected POST, received ${request.method}`,
86
- },
87
- };
79
+ if (request.method === "GET") {
80
+ return await parseGetRequest(request, options);
81
+ }
82
+ if (request.method === "POST") {
83
+ return await parsePostRequest(request, options);
88
84
  }
85
+ return {
86
+ success: false,
87
+ error: {
88
+ type: "method_not_allowed",
89
+ message: `expected GET or POST, received ${request.method}`,
90
+ },
91
+ };
92
+ }
89
93
 
90
- if (request.method === "GET") {
91
- return {
92
- success: true,
93
- action: { type: ACTION_TYPE_GET },
94
- };
94
+ async function parseGetRequest(
95
+ request: Request,
96
+ options: ParseRequestOptions,
97
+ ): Promise<ParseRequestResult> {
98
+ const compactHeader = request.headers.get(SNAP_PAYLOAD_HEADER)?.trim();
99
+ if (!compactHeader) {
100
+ return { success: true, action: { type: ACTION_TYPE_GET } };
95
101
  }
96
102
 
97
- const maxSkew = options.maxSkewSeconds ?? DEFAULT_SNAP_POST_MAX_SKEW_SECONDS;
98
- const nowSec = Math.floor(Date.now() / 1000);
103
+ const result = await validateJfsPayload({
104
+ jfsText: compactHeader,
105
+ schema: getPayloadSchema,
106
+ request,
107
+ options,
108
+ invalidJsonMessage: `${SNAP_PAYLOAD_HEADER} must be a valid JFS compact string`,
109
+ });
110
+ if (!result.ok) {
111
+ return { success: false, error: result.error };
112
+ }
99
113
 
100
- const text = await request.text();
114
+ return {
115
+ success: true,
116
+ action: { type: ACTION_TYPE_GET, ...result.payload },
117
+ };
118
+ }
101
119
 
102
- let jsonBody: unknown;
103
- try {
104
- jsonBody = JSON.parse(text);
105
- } catch {
120
+ async function parsePostRequest(
121
+ request: Request,
122
+ options: ParseRequestOptions,
123
+ ): Promise<ParseRequestResult> {
124
+ const result = await validateJfsPayload({
125
+ jfsText: await request.text(),
126
+ schema: payloadSchema,
127
+ request,
128
+ options,
129
+ });
130
+ if (!result.ok) {
131
+ return { success: false, error: result.error };
132
+ }
133
+
134
+ const payload = result.payload;
135
+ if (payload.fid !== undefined && payload.fid !== payload.user.fid) {
106
136
  return {
107
137
  success: false,
108
138
  error: {
109
- type: "invalid_json",
110
- message: "request body is not valid JSON",
139
+ type: "fid_mismatch",
140
+ message: `fid "${payload.fid}" does not match user.fid "${payload.user.fid}"`,
111
141
  },
112
142
  };
113
143
  }
114
144
 
115
- const parsed = requestBodySchema.safeParse(jsonBody);
116
- if (!parsed.success) {
145
+ return {
146
+ success: true,
147
+ action: { type: ACTION_TYPE_POST, ...payload },
148
+ };
149
+ }
150
+
151
+ /**
152
+ * Shared pipeline for authenticated snap requests: parse the JFS envelope,
153
+ * decode and schema-validate the payload, optionally verify the JFS signature
154
+ * against an active hub signer (matching `user.fid`), then check timestamp
155
+ * skew and that `audience` matches the request origin.
156
+ *
157
+ * Both GET (payload header) and POST (request body) feed into this.
158
+ */
159
+ async function validateJfsPayload<T extends SnapPayload | SnapGetPayload>({
160
+ jfsText,
161
+ schema,
162
+ request,
163
+ options,
164
+ invalidJsonMessage,
165
+ }: {
166
+ jfsText: string;
167
+ schema: z.ZodType<T>;
168
+ request: Request;
169
+ options: ParseRequestOptions;
170
+ invalidJsonMessage?: string;
171
+ }): Promise<
172
+ { ok: true; payload: T } | { ok: false; error: ParseRequestError }
173
+ > {
174
+ const parsed = parseJfs(jfsText);
175
+ if (!parsed.ok) {
117
176
  return {
118
- success: false,
119
- error: { type: "invalid_json", message: parsed.error.message },
177
+ ok: false,
178
+ error: {
179
+ type: "invalid_json",
180
+ message: invalidJsonMessage ?? parsed.error,
181
+ },
120
182
  };
121
183
  }
184
+ const jfs = parsed.jfs;
122
185
 
123
- const payloadParsed = payloadSchema.safeParse(
124
- decodePayload(parsed.data.payload),
125
- );
186
+ const payloadParsed = schema.safeParse(decodePayload(jfs.payload));
126
187
  if (!payloadParsed.success) {
127
188
  return {
128
- success: false,
189
+ ok: false,
129
190
  error: { type: "validation", issues: payloadParsed.error.issues },
130
191
  };
131
192
  }
132
-
133
- const body = payloadParsed.data;
193
+ const payload = payloadParsed.data;
134
194
 
135
195
  if (!options.skipJFSVerification) {
136
- const jfs = await verifyJFSRequestBody(parsed.data);
137
- if (!jfs.valid) {
196
+ const verified = await verifyJFS(jfs);
197
+ if (!verified.valid) {
138
198
  return {
139
- success: false,
140
- error: { type: "signature", message: jfs.error.message },
199
+ ok: false,
200
+ error: { type: "signature", message: verified.error.message },
141
201
  };
142
202
  }
143
- if (jfs.signingUserFid !== body.user.fid) {
203
+ if (verified.signingUserFid !== payload.user.fid) {
144
204
  return {
145
- success: false,
205
+ ok: false,
146
206
  error: {
147
207
  type: "fid_mismatch",
148
- message: `JFS header fid "${jfs.signingUserFid}" does not match user.fid "${body.user.fid}"`,
208
+ message: `JFS header fid "${verified.signingUserFid}" does not match user.fid "${payload.user.fid}"`,
149
209
  },
150
210
  };
151
211
  }
152
212
  }
153
213
 
154
- if (Math.abs(nowSec - body.timestamp) > maxSkew) {
214
+ const maxSkew = options.maxSkewSeconds ?? DEFAULT_SNAP_POST_MAX_SKEW_SECONDS;
215
+ const nowSec = Math.floor(Date.now() / 1000);
216
+ if (Math.abs(nowSec - payload.timestamp) > maxSkew) {
155
217
  return {
156
- success: false,
218
+ ok: false,
157
219
  error: {
158
220
  type: "replay",
159
221
  message: `timestamp outside allowed skew of ${maxSkew}s`,
@@ -176,31 +238,15 @@ export async function parseRequest(
176
238
  }
177
239
  }
178
240
 
179
- if (expectedOrigin !== undefined && body.audience !== expectedOrigin) {
241
+ if (expectedOrigin !== undefined && payload.audience !== expectedOrigin) {
180
242
  return {
181
- success: false,
243
+ ok: false,
182
244
  error: {
183
245
  type: "origin_mismatch",
184
- message: `payload audience "${body.audience}" does not match expected origin "${expectedOrigin}"`,
246
+ message: `payload audience "${payload.audience}" does not match expected origin "${expectedOrigin}"`,
185
247
  },
186
248
  };
187
249
  }
188
250
 
189
- if (body.fid !== undefined && body.fid !== body.user.fid) {
190
- return {
191
- success: false,
192
- error: {
193
- type: "fid_mismatch",
194
- message: `fid "${body.fid}" does not match user.fid "${body.user.fid}"`,
195
- },
196
- };
197
- }
198
-
199
- return {
200
- success: true,
201
- action: {
202
- type: ACTION_TYPE_POST,
203
- ...body,
204
- },
205
- };
251
+ return { ok: true, payload };
206
252
  }