@farcaster/snap-hono 2.0.5 → 2.1.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/index.d.ts +3 -2
- package/dist/index.js +17 -3
- package/dist/og-image.js +2 -4
- package/dist/payloadToResponse.js +2 -2
- package/dist/renderSnapPage.js +5 -7
- package/package.json +2 -2
- package/src/index.ts +24 -3
- package/src/og-image.ts +6 -3
- package/src/payloadToResponse.ts +6 -8
- package/src/renderSnapPage.ts +5 -6
package/dist/index.d.ts
CHANGED
|
@@ -12,7 +12,8 @@ export type SnapHandlerOptions = {
|
|
|
12
12
|
*/
|
|
13
13
|
path?: string;
|
|
14
14
|
/**
|
|
15
|
-
* When true, skip JFS signature verification only. POST bodies must still be JFS
|
|
15
|
+
* When true, skip JFS signature verification only. POST bodies must still be a JFS envelope:
|
|
16
|
+
* JSON `{ header, payload, signature }` or the same compact dot-separated string as GET’s `X-Snap-Payload`.
|
|
16
17
|
* When omitted, default to {@link envSkipJFSVerification}.
|
|
17
18
|
*/
|
|
18
19
|
skipJFSVerification?: boolean;
|
|
@@ -38,7 +39,7 @@ export type SnapHandlerOptions = {
|
|
|
38
39
|
* Register GET and POST snap handlers on `app` at `options.path` (default `/`).
|
|
39
40
|
*
|
|
40
41
|
* - GET → calls `snapFn(ctx)` with `ctx.action.type === "get"` and returns the response.
|
|
41
|
-
* - POST → parses the JFS
|
|
42
|
+
* - POST → parses the JFS envelope (JSON object or compact string); verifies via {@link verifyJFSRequestBody} unless
|
|
42
43
|
* `skipJFSVerification` is true, then calls `snapFn(ctx)` with the parsed post action and returns the response.
|
|
43
44
|
*
|
|
44
45
|
* All parsing, schema validation, signature verification, and error responses
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { cors } from "hono/cors";
|
|
2
|
-
import { MEDIA_TYPE, ACTION_TYPE_GET, } from "@farcaster/snap";
|
|
2
|
+
import { MEDIA_TYPE, ACTION_TYPE_GET, SNAP_PAYLOAD_HEADER, } from "@farcaster/snap";
|
|
3
3
|
import { parseRequest } from "@farcaster/snap/server";
|
|
4
4
|
import { brandedFallbackHtml } from "./fallback.js";
|
|
5
5
|
import { payloadToResponse, snapHeaders } from "./payloadToResponse.js";
|
|
@@ -9,7 +9,7 @@ import { renderSnapPageToPng, renderWithDedup, etagForPage, } from "./og-image.j
|
|
|
9
9
|
* Register GET and POST snap handlers on `app` at `options.path` (default `/`).
|
|
10
10
|
*
|
|
11
11
|
* - GET → calls `snapFn(ctx)` with `ctx.action.type === "get"` and returns the response.
|
|
12
|
-
* - POST → parses the JFS
|
|
12
|
+
* - POST → parses the JFS envelope (JSON object or compact string); verifies via {@link verifyJFSRequestBody} unless
|
|
13
13
|
* `skipJFSVerification` is true, then calls `snapFn(ctx)` with the parsed post action and returns the response.
|
|
14
14
|
*
|
|
15
15
|
* All parsing, schema validation, signature verification, and error responses
|
|
@@ -102,8 +102,21 @@ export function registerSnapHandler(app, snapFn, options = {}) {
|
|
|
102
102
|
]),
|
|
103
103
|
});
|
|
104
104
|
}
|
|
105
|
+
const skipJFSVerification = options.skipJFSVerification !== undefined
|
|
106
|
+
? options.skipJFSVerification
|
|
107
|
+
: envSkipJFSVerification();
|
|
108
|
+
const parsed = await parseRequest(c.req.raw, {
|
|
109
|
+
skipJFSVerification,
|
|
110
|
+
requestOrigin: snapOriginFromRequest(c.req.raw),
|
|
111
|
+
});
|
|
112
|
+
if (!parsed.success) {
|
|
113
|
+
const msg = "message" in parsed.error
|
|
114
|
+
? parsed.error.message
|
|
115
|
+
: "failed to parse request";
|
|
116
|
+
return c.json({ error: msg }, 400);
|
|
117
|
+
}
|
|
105
118
|
const response = await snapFn({
|
|
106
|
-
action:
|
|
119
|
+
action: parsed.action,
|
|
107
120
|
request: c.req.raw,
|
|
108
121
|
});
|
|
109
122
|
return payloadToResponse(response, {
|
|
@@ -171,6 +184,7 @@ function stripAuthHeaders(request) {
|
|
|
171
184
|
const headers = new Headers(request.headers);
|
|
172
185
|
headers.delete("cookie");
|
|
173
186
|
headers.delete("authorization");
|
|
187
|
+
headers.delete(SNAP_PAYLOAD_HEADER);
|
|
174
188
|
return new Request(request.url, { method: request.method, headers });
|
|
175
189
|
}
|
|
176
190
|
function resourcePathFromRequest(url) {
|
package/dist/og-image.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DEFAULT_THEME_ACCENT, PALETTE_LIGHT_HEX } from "@farcaster/snap";
|
|
1
|
+
import { DEFAULT_THEME_ACCENT, PALETTE_LIGHT_HEX, resolveSnapColorHex, } from "@farcaster/snap";
|
|
2
2
|
import satori from "satori";
|
|
3
3
|
import { Resvg, initWasm } from "@resvg/resvg-wasm";
|
|
4
4
|
/** Content width inside the OG card (`OG_CARD_OUTER_WIDTH_PX` − horizontal padding 24px×2). */
|
|
@@ -164,9 +164,7 @@ function accentHex(accent) {
|
|
|
164
164
|
PALETTE_LIGHT_HEX[DEFAULT_THEME_ACCENT]);
|
|
165
165
|
}
|
|
166
166
|
function colorHex(color, accent) {
|
|
167
|
-
|
|
168
|
-
return accent;
|
|
169
|
-
return PALETTE_LIGHT_HEX[color] ?? accent;
|
|
167
|
+
return resolveSnapColorHex(color, { accentHex: accent, appearance: "light" });
|
|
170
168
|
}
|
|
171
169
|
function mapText(el) {
|
|
172
170
|
const size = String(el.size ?? "md");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { MEDIA_TYPE, validateSnapResponse, snapResponseSchema, } from "@farcaster/snap";
|
|
1
|
+
import { MEDIA_TYPE, validateSnapResponse, snapResponseSchema, SNAP_PAYLOAD_HEADER, } from "@farcaster/snap";
|
|
2
2
|
import { snapJsonRenderCatalog } from "@farcaster/snap/ui";
|
|
3
3
|
const DEFAULT_LINK_MEDIA_TYPES = [MEDIA_TYPE, "text/html"];
|
|
4
4
|
export function payloadToResponse(payload, options = {}) {
|
|
@@ -32,7 +32,7 @@ function errorResponse(error, issues) {
|
|
|
32
32
|
export function snapHeaders(resourcePath, currentMediaType, availableMediaTypes) {
|
|
33
33
|
return {
|
|
34
34
|
"Content-Type": `${currentMediaType}; charset=utf-8`,
|
|
35
|
-
Vary:
|
|
35
|
+
Vary: `Accept, ${SNAP_PAYLOAD_HEADER}`,
|
|
36
36
|
Link: buildSnapAlternateLinkHeader(resourcePath, availableMediaTypes),
|
|
37
37
|
};
|
|
38
38
|
}
|
package/dist/renderSnapPage.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DEFAULT_THEME_ACCENT, PALETTE_LIGHT_HEX,
|
|
1
|
+
import { DEFAULT_THEME_ACCENT, PALETTE_LIGHT_HEX, resolveSnapColorHex, } from "@farcaster/snap";
|
|
2
2
|
export function extractPageMeta(spec) {
|
|
3
3
|
let title = "Farcaster Snap";
|
|
4
4
|
let description = "";
|
|
@@ -82,9 +82,7 @@ function accentHex(accent) {
|
|
|
82
82
|
: PALETTE_LIGHT_HEX[DEFAULT_THEME_ACCENT];
|
|
83
83
|
}
|
|
84
84
|
function colorHex(color, accent) {
|
|
85
|
-
|
|
86
|
-
return accent;
|
|
87
|
-
return PALETTE_LIGHT_HEX[color] ?? accent;
|
|
85
|
+
return resolveSnapColorHex(color, { accentHex: accent, appearance: "light" });
|
|
88
86
|
}
|
|
89
87
|
/** Readable foreground for a hex background (YIQ contrast check). */
|
|
90
88
|
function fgForBg(hex) {
|
|
@@ -304,8 +302,8 @@ function renderElement(key, spec, accent) {
|
|
|
304
302
|
const gapMap = isHorizontal ? hGap : vGap;
|
|
305
303
|
const gapVal = gapMap[String(p.gap ?? "md")] ?? (isHorizontal ? "8px" : "16px");
|
|
306
304
|
const dir = isHorizontal ? "row" : "column";
|
|
307
|
-
const
|
|
308
|
-
const
|
|
305
|
+
const wrapStyle = isHorizontal ? "flex-wrap:nowrap;" : "";
|
|
306
|
+
const alignItems = isHorizontal ? "align-items:stretch;" : "";
|
|
309
307
|
const justifyMap = {
|
|
310
308
|
start: "flex-start",
|
|
311
309
|
center: "center",
|
|
@@ -315,7 +313,7 @@ function renderElement(key, spec, accent) {
|
|
|
315
313
|
};
|
|
316
314
|
const jc = p.justify ? justifyMap[String(p.justify)] : undefined;
|
|
317
315
|
const childIds = el.children ?? [];
|
|
318
|
-
let html = `<div style="display:flex;width:100%;flex-direction:${dir};gap:${gapVal};${
|
|
316
|
+
let html = `<div style="display:flex;width:100%;min-width:0;box-sizing:border-box;flex-direction:${dir};gap:${gapVal};${wrapStyle}${alignItems}${jc ? `justify-content:${jc};` : ""}">`;
|
|
319
317
|
for (const childKey of childIds) {
|
|
320
318
|
const flex = isHorizontal ? "flex:1;min-width:0;" : "";
|
|
321
319
|
html += `<div style="${flex}">${renderElement(childKey, spec, accent)}</div>`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@farcaster/snap-hono",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Hono integration for Farcaster Snap servers",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@resvg/resvg-wasm": "^2.6.2",
|
|
30
30
|
"satori": "^0.10.0",
|
|
31
|
-
"@farcaster/snap": "2.
|
|
31
|
+
"@farcaster/snap": "2.2.0"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
34
|
"hono": ">=4.0.0"
|
package/src/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
MEDIA_TYPE,
|
|
5
5
|
type SnapFunction,
|
|
6
6
|
ACTION_TYPE_GET,
|
|
7
|
+
SNAP_PAYLOAD_HEADER,
|
|
7
8
|
} from "@farcaster/snap";
|
|
8
9
|
import { parseRequest } from "@farcaster/snap/server";
|
|
9
10
|
import { brandedFallbackHtml } from "./fallback";
|
|
@@ -29,7 +30,8 @@ export type SnapHandlerOptions = {
|
|
|
29
30
|
path?: string;
|
|
30
31
|
|
|
31
32
|
/**
|
|
32
|
-
* When true, skip JFS signature verification only. POST bodies must still be JFS
|
|
33
|
+
* When true, skip JFS signature verification only. POST bodies must still be a JFS envelope:
|
|
34
|
+
* JSON `{ header, payload, signature }` or the same compact dot-separated string as GET’s `X-Snap-Payload`.
|
|
33
35
|
* When omitted, default to {@link envSkipJFSVerification}.
|
|
34
36
|
*/
|
|
35
37
|
skipJFSVerification?: boolean;
|
|
@@ -59,7 +61,7 @@ export type SnapHandlerOptions = {
|
|
|
59
61
|
* Register GET and POST snap handlers on `app` at `options.path` (default `/`).
|
|
60
62
|
*
|
|
61
63
|
* - GET → calls `snapFn(ctx)` with `ctx.action.type === "get"` and returns the response.
|
|
62
|
-
* - POST → parses the JFS
|
|
64
|
+
* - POST → parses the JFS envelope (JSON object or compact string); verifies via {@link verifyJFSRequestBody} unless
|
|
63
65
|
* `skipJFSVerification` is true, then calls `snapFn(ctx)` with the parsed post action and returns the response.
|
|
64
66
|
*
|
|
65
67
|
* All parsing, schema validation, signature verification, and error responses
|
|
@@ -175,8 +177,26 @@ export function registerSnapHandler(
|
|
|
175
177
|
});
|
|
176
178
|
}
|
|
177
179
|
|
|
180
|
+
const skipJFSVerification =
|
|
181
|
+
options.skipJFSVerification !== undefined
|
|
182
|
+
? options.skipJFSVerification
|
|
183
|
+
: envSkipJFSVerification();
|
|
184
|
+
|
|
185
|
+
const parsed = await parseRequest(c.req.raw, {
|
|
186
|
+
skipJFSVerification,
|
|
187
|
+
requestOrigin: snapOriginFromRequest(c.req.raw),
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
if (!parsed.success) {
|
|
191
|
+
const msg =
|
|
192
|
+
"message" in parsed.error
|
|
193
|
+
? parsed.error.message
|
|
194
|
+
: "failed to parse request";
|
|
195
|
+
return c.json({ error: msg }, 400);
|
|
196
|
+
}
|
|
197
|
+
|
|
178
198
|
const response = await snapFn({
|
|
179
|
-
action:
|
|
199
|
+
action: parsed.action,
|
|
180
200
|
request: c.req.raw,
|
|
181
201
|
});
|
|
182
202
|
|
|
@@ -261,6 +281,7 @@ function stripAuthHeaders(request: Request): Request {
|
|
|
261
281
|
const headers = new Headers(request.headers);
|
|
262
282
|
headers.delete("cookie");
|
|
263
283
|
headers.delete("authorization");
|
|
284
|
+
headers.delete(SNAP_PAYLOAD_HEADER);
|
|
264
285
|
return new Request(request.url, { method: request.method, headers });
|
|
265
286
|
}
|
|
266
287
|
|
package/src/og-image.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { SnapHandlerResult, SnapSpec } from "@farcaster/snap";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_THEME_ACCENT,
|
|
4
|
+
PALETTE_LIGHT_HEX,
|
|
5
|
+
resolveSnapColorHex,
|
|
6
|
+
} from "@farcaster/snap";
|
|
3
7
|
import satori from "satori";
|
|
4
8
|
import { Resvg, initWasm } from "@resvg/resvg-wasm";
|
|
5
9
|
|
|
@@ -250,8 +254,7 @@ function accentHex(accent: string | undefined): string {
|
|
|
250
254
|
}
|
|
251
255
|
|
|
252
256
|
function colorHex(color: string | undefined, accent: string): string {
|
|
253
|
-
|
|
254
|
-
return PALETTE_LIGHT_HEX[color as keyof typeof PALETTE_LIGHT_HEX] ?? accent;
|
|
257
|
+
return resolveSnapColorHex(color, { accentHex: accent, appearance: "light" });
|
|
255
258
|
}
|
|
256
259
|
|
|
257
260
|
function mapText(el: El): VNode {
|
package/src/payloadToResponse.ts
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
type SnapHandlerResult,
|
|
4
4
|
validateSnapResponse,
|
|
5
5
|
snapResponseSchema,
|
|
6
|
+
SNAP_PAYLOAD_HEADER,
|
|
6
7
|
} from "@farcaster/snap";
|
|
7
8
|
import { snapJsonRenderCatalog } from "@farcaster/snap/ui";
|
|
8
9
|
|
|
@@ -43,13 +44,10 @@ export function payloadToResponse(
|
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
function errorResponse(error: string, issues: unknown[]): Response {
|
|
46
|
-
return new Response(
|
|
47
|
-
|
|
48
|
-
{
|
|
49
|
-
|
|
50
|
-
headers: { "Content-Type": "application/json; charset=utf-8" },
|
|
51
|
-
},
|
|
52
|
-
);
|
|
47
|
+
return new Response(JSON.stringify({ error, issues }), {
|
|
48
|
+
status: 400,
|
|
49
|
+
headers: { "Content-Type": "application/json; charset=utf-8" },
|
|
50
|
+
});
|
|
53
51
|
}
|
|
54
52
|
|
|
55
53
|
export function snapHeaders(
|
|
@@ -59,7 +57,7 @@ export function snapHeaders(
|
|
|
59
57
|
) {
|
|
60
58
|
return {
|
|
61
59
|
"Content-Type": `${currentMediaType}; charset=utf-8`,
|
|
62
|
-
Vary:
|
|
60
|
+
Vary: `Accept, ${SNAP_PAYLOAD_HEADER}`,
|
|
63
61
|
Link: buildSnapAlternateLinkHeader(resourcePath, availableMediaTypes),
|
|
64
62
|
};
|
|
65
63
|
}
|
package/src/renderSnapPage.ts
CHANGED
|
@@ -7,7 +7,7 @@ import type {
|
|
|
7
7
|
import {
|
|
8
8
|
DEFAULT_THEME_ACCENT,
|
|
9
9
|
PALETTE_LIGHT_HEX,
|
|
10
|
-
|
|
10
|
+
resolveSnapColorHex,
|
|
11
11
|
} from "@farcaster/snap";
|
|
12
12
|
|
|
13
13
|
// ─── OG meta ────────────────────────────────────────────
|
|
@@ -130,8 +130,7 @@ function accentHex(accent: PaletteColor | undefined): string {
|
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
function colorHex(color: string | undefined, accent: string): string {
|
|
133
|
-
|
|
134
|
-
return (PALETTE_LIGHT_HEX as Record<string, string>)[color] ?? accent;
|
|
133
|
+
return resolveSnapColorHex(color, { accentHex: accent, appearance: "light" });
|
|
135
134
|
}
|
|
136
135
|
|
|
137
136
|
/** Readable foreground for a hex background (YIQ contrast check). */
|
|
@@ -395,8 +394,8 @@ function renderElement(key: string, spec: SnapSpec, accent: string): string {
|
|
|
395
394
|
const gapVal =
|
|
396
395
|
gapMap[String(p.gap ?? "md")] ?? (isHorizontal ? "8px" : "16px");
|
|
397
396
|
const dir = isHorizontal ? "row" : "column";
|
|
398
|
-
const
|
|
399
|
-
const
|
|
397
|
+
const wrapStyle = isHorizontal ? "flex-wrap:nowrap;" : "";
|
|
398
|
+
const alignItems = isHorizontal ? "align-items:stretch;" : "";
|
|
400
399
|
const justifyMap: Record<string, string> = {
|
|
401
400
|
start: "flex-start",
|
|
402
401
|
center: "center",
|
|
@@ -406,7 +405,7 @@ function renderElement(key: string, spec: SnapSpec, accent: string): string {
|
|
|
406
405
|
};
|
|
407
406
|
const jc = p.justify ? justifyMap[String(p.justify)] : undefined;
|
|
408
407
|
const childIds = el.children ?? [];
|
|
409
|
-
let html = `<div style="display:flex;width:100%;flex-direction:${dir};gap:${gapVal};${
|
|
408
|
+
let html = `<div style="display:flex;width:100%;min-width:0;box-sizing:border-box;flex-direction:${dir};gap:${gapVal};${wrapStyle}${alignItems}${
|
|
410
409
|
jc ? `justify-content:${jc};` : ""
|
|
411
410
|
}">`;
|
|
412
411
|
for (const childKey of childIds) {
|