@farcaster/snap-hono 1.1.2 → 1.1.4
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 +1 -1
- package/dist/index.js +44 -5
- package/dist/payloadToResponse.d.ts +12 -7
- package/dist/payloadToResponse.js +18 -9
- package/package.json +2 -2
- package/src/index.ts +48 -6
- package/src/payloadToResponse.ts +37 -9
package/dist/index.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export type SnapHandlerOptions = {
|
|
|
12
12
|
*/
|
|
13
13
|
skipJFSVerification?: boolean;
|
|
14
14
|
/**
|
|
15
|
-
*
|
|
15
|
+
* Visible message in the HTML page served on GET when the client does not request snap JSON.
|
|
16
16
|
* @default "This is a Farcaster Snap server."
|
|
17
17
|
*/
|
|
18
18
|
fallbackText?: string;
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { cors } from "hono/cors";
|
|
2
2
|
import { MEDIA_TYPE } from "@farcaster/snap";
|
|
3
3
|
import { parseRequest } from "@farcaster/snap/server";
|
|
4
|
-
import { payloadToResponse } from "./payloadToResponse.js";
|
|
4
|
+
import { payloadToResponse, snapHeaders } from "./payloadToResponse.js";
|
|
5
5
|
/**
|
|
6
6
|
* Register GET and POST snap handlers on `app` at `options.path` (default `/`).
|
|
7
7
|
*
|
|
@@ -16,18 +16,28 @@ import { payloadToResponse } from "./payloadToResponse.js";
|
|
|
16
16
|
export function registerSnapHandler(app, snapFn, options = {}) {
|
|
17
17
|
const path = options.path ?? "/";
|
|
18
18
|
const fallbackText = options.fallbackText ??
|
|
19
|
-
"This is a Farcaster Snap server. See https://snap.farcaster.xyz for more info.";
|
|
19
|
+
"This is a Farcaster Snap server. See <a href='https://snap.farcaster.xyz'>snap.farcaster.xyz</a> for more info.";
|
|
20
20
|
app.use(path, cors({ origin: "*" }));
|
|
21
21
|
app.get(path, async (c) => {
|
|
22
|
+
const resourcePath = resourcePathFromRequest(c.req.url);
|
|
22
23
|
const accept = c.req.header("Accept");
|
|
23
24
|
if (!clientWantsSnapResponse(accept)) {
|
|
24
|
-
return
|
|
25
|
+
return new Response(fallbackHtmlDocument(fallbackText), {
|
|
26
|
+
status: 200,
|
|
27
|
+
headers: snapHeaders(resourcePath, "text/html", [
|
|
28
|
+
MEDIA_TYPE,
|
|
29
|
+
"text/html",
|
|
30
|
+
]),
|
|
31
|
+
});
|
|
25
32
|
}
|
|
26
33
|
const response = await snapFn({
|
|
27
34
|
action: { type: "get" },
|
|
28
35
|
request: c.req.raw,
|
|
29
36
|
});
|
|
30
|
-
return payloadToResponse(response
|
|
37
|
+
return payloadToResponse(response, {
|
|
38
|
+
resourcePath,
|
|
39
|
+
mediaTypes: [MEDIA_TYPE, "text/html"],
|
|
40
|
+
});
|
|
31
41
|
});
|
|
32
42
|
app.post(path, async (c) => {
|
|
33
43
|
const raw = c.req.raw;
|
|
@@ -55,9 +65,38 @@ export function registerSnapHandler(app, snapFn, options = {}) {
|
|
|
55
65
|
}
|
|
56
66
|
}
|
|
57
67
|
const response = await snapFn({ action: parsed.action, request: raw });
|
|
58
|
-
return payloadToResponse(response
|
|
68
|
+
return payloadToResponse(response, {
|
|
69
|
+
resourcePath: resourcePathFromRequest(raw.url),
|
|
70
|
+
mediaTypes: [MEDIA_TYPE, "text/html"],
|
|
71
|
+
});
|
|
59
72
|
});
|
|
60
73
|
}
|
|
74
|
+
function resourcePathFromRequest(url) {
|
|
75
|
+
const u = new URL(url);
|
|
76
|
+
return u.pathname + u.search;
|
|
77
|
+
}
|
|
78
|
+
function escapeHtml(text) {
|
|
79
|
+
return text
|
|
80
|
+
.replace(/&/g, "&")
|
|
81
|
+
.replace(/</g, "<")
|
|
82
|
+
.replace(/>/g, ">")
|
|
83
|
+
.replace(/"/g, """)
|
|
84
|
+
.replace(/'/g, "'");
|
|
85
|
+
}
|
|
86
|
+
function fallbackHtmlDocument(message) {
|
|
87
|
+
const body = escapeHtml(message);
|
|
88
|
+
return `<!DOCTYPE html>
|
|
89
|
+
<html lang="en">
|
|
90
|
+
<head>
|
|
91
|
+
<meta charset="utf-8">
|
|
92
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
93
|
+
<title>Farcaster Snap</title>
|
|
94
|
+
</head>
|
|
95
|
+
<body>
|
|
96
|
+
<p>${body}</p>
|
|
97
|
+
</body>
|
|
98
|
+
</html>`;
|
|
99
|
+
}
|
|
61
100
|
function clientWantsSnapResponse(accept) {
|
|
62
101
|
if (!accept || accept.trim() === "")
|
|
63
102
|
return false;
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { type SnapResponse } from "@farcaster/snap";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
type PayloadToResponseOptions = {
|
|
3
|
+
resourcePath: string;
|
|
4
|
+
mediaTypes: string[];
|
|
5
|
+
};
|
|
6
|
+
export declare function payloadToResponse(payload: SnapResponse, options?: Partial<PayloadToResponseOptions>): Response;
|
|
7
|
+
export declare function snapHeaders(resourcePath: string, currentMediaType: string, availableMediaTypes: string[]): {
|
|
8
|
+
"Content-Type": string;
|
|
9
|
+
Vary: string;
|
|
10
|
+
Link: string;
|
|
11
|
+
};
|
|
12
|
+
export declare function buildSnapAlternateLinkHeader(resourcePath: string, mediaTypes: string[]): string;
|
|
13
|
+
export {};
|
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import { MEDIA_TYPE, rootSchema, validatePage, } from "@farcaster/snap";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
* On validation failure returns JSON `{ "error": "...", "issues": [...] }` with status 400.
|
|
7
|
-
*/
|
|
8
|
-
export function payloadToResponse(payload) {
|
|
2
|
+
const DEFAULT_LINK_MEDIA_TYPES = [MEDIA_TYPE, "text/html"];
|
|
3
|
+
export function payloadToResponse(payload, options = {}) {
|
|
4
|
+
const resourcePath = options.resourcePath ?? "/";
|
|
5
|
+
const mediaTypes = options.mediaTypes ?? [...DEFAULT_LINK_MEDIA_TYPES];
|
|
9
6
|
const validation = validatePage(payload);
|
|
10
7
|
if (!validation.valid) {
|
|
11
8
|
return new Response(JSON.stringify({
|
|
@@ -22,8 +19,20 @@ export function payloadToResponse(payload) {
|
|
|
22
19
|
return new Response(JSON.stringify(finalized), {
|
|
23
20
|
status: 200,
|
|
24
21
|
headers: {
|
|
25
|
-
|
|
26
|
-
Vary: "Accept",
|
|
22
|
+
...snapHeaders(resourcePath, MEDIA_TYPE, mediaTypes),
|
|
27
23
|
},
|
|
28
24
|
});
|
|
29
25
|
}
|
|
26
|
+
export function snapHeaders(resourcePath, currentMediaType, availableMediaTypes) {
|
|
27
|
+
return {
|
|
28
|
+
"Content-Type": `${currentMediaType}; charset=utf-8`,
|
|
29
|
+
Vary: "Accept",
|
|
30
|
+
Link: buildSnapAlternateLinkHeader(resourcePath, availableMediaTypes),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export function buildSnapAlternateLinkHeader(resourcePath, mediaTypes) {
|
|
34
|
+
const p = resourcePath.startsWith("/") ? resourcePath : `/${resourcePath}`;
|
|
35
|
+
return mediaTypes
|
|
36
|
+
.map((mediaType) => `<${p}>; rel="alternate"; type="${mediaType}"`)
|
|
37
|
+
.join(", ");
|
|
38
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@farcaster/snap-hono",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.4",
|
|
4
4
|
"description": "Hono integration for Farcaster Snap servers",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
},
|
|
27
27
|
"license": "MIT",
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@farcaster/snap": "1.2.
|
|
29
|
+
"@farcaster/snap": "1.2.2"
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
32
32
|
"hono": ">=4.0.0"
|
package/src/index.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { Hono } from "hono";
|
|
|
2
2
|
import { cors } from "hono/cors";
|
|
3
3
|
import { MEDIA_TYPE, type SnapFunction } from "@farcaster/snap";
|
|
4
4
|
import { parseRequest } from "@farcaster/snap/server";
|
|
5
|
-
import { payloadToResponse } from "./payloadToResponse";
|
|
5
|
+
import { payloadToResponse, snapHeaders } from "./payloadToResponse";
|
|
6
6
|
|
|
7
7
|
export type SnapHandlerOptions = {
|
|
8
8
|
/**
|
|
@@ -18,7 +18,7 @@ export type SnapHandlerOptions = {
|
|
|
18
18
|
skipJFSVerification?: boolean;
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
|
-
*
|
|
21
|
+
* Visible message in the HTML page served on GET when the client does not request snap JSON.
|
|
22
22
|
* @default "This is a Farcaster Snap server."
|
|
23
23
|
*/
|
|
24
24
|
fallbackText?: string;
|
|
@@ -43,21 +43,31 @@ export function registerSnapHandler(
|
|
|
43
43
|
const path = options.path ?? "/";
|
|
44
44
|
const fallbackText =
|
|
45
45
|
options.fallbackText ??
|
|
46
|
-
"This is a Farcaster Snap server. See https://snap.farcaster.xyz for more info.";
|
|
46
|
+
"This is a Farcaster Snap server. See <a href='https://snap.farcaster.xyz'>snap.farcaster.xyz</a> for more info.";
|
|
47
47
|
|
|
48
48
|
app.use(path, cors({ origin: "*" }));
|
|
49
49
|
|
|
50
50
|
app.get(path, async (c) => {
|
|
51
|
+
const resourcePath = resourcePathFromRequest(c.req.url);
|
|
51
52
|
const accept = c.req.header("Accept");
|
|
52
53
|
if (!clientWantsSnapResponse(accept)) {
|
|
53
|
-
return
|
|
54
|
+
return new Response(fallbackHtmlDocument(fallbackText), {
|
|
55
|
+
status: 200,
|
|
56
|
+
headers: snapHeaders(resourcePath, "text/html", [
|
|
57
|
+
MEDIA_TYPE,
|
|
58
|
+
"text/html",
|
|
59
|
+
]),
|
|
60
|
+
});
|
|
54
61
|
}
|
|
55
62
|
|
|
56
63
|
const response = await snapFn({
|
|
57
64
|
action: { type: "get" },
|
|
58
65
|
request: c.req.raw,
|
|
59
66
|
});
|
|
60
|
-
return payloadToResponse(response
|
|
67
|
+
return payloadToResponse(response, {
|
|
68
|
+
resourcePath,
|
|
69
|
+
mediaTypes: [MEDIA_TYPE, "text/html"],
|
|
70
|
+
});
|
|
61
71
|
});
|
|
62
72
|
|
|
63
73
|
app.post(path, async (c) => {
|
|
@@ -94,10 +104,42 @@ export function registerSnapHandler(
|
|
|
94
104
|
|
|
95
105
|
const response = await snapFn({ action: parsed.action, request: raw });
|
|
96
106
|
|
|
97
|
-
return payloadToResponse(response
|
|
107
|
+
return payloadToResponse(response, {
|
|
108
|
+
resourcePath: resourcePathFromRequest(raw.url),
|
|
109
|
+
mediaTypes: [MEDIA_TYPE, "text/html"],
|
|
110
|
+
});
|
|
98
111
|
});
|
|
99
112
|
}
|
|
100
113
|
|
|
114
|
+
function resourcePathFromRequest(url: string): string {
|
|
115
|
+
const u = new URL(url);
|
|
116
|
+
return u.pathname + u.search;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function escapeHtml(text: string): string {
|
|
120
|
+
return text
|
|
121
|
+
.replace(/&/g, "&")
|
|
122
|
+
.replace(/</g, "<")
|
|
123
|
+
.replace(/>/g, ">")
|
|
124
|
+
.replace(/"/g, """)
|
|
125
|
+
.replace(/'/g, "'");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function fallbackHtmlDocument(message: string): string {
|
|
129
|
+
const body = escapeHtml(message);
|
|
130
|
+
return `<!DOCTYPE html>
|
|
131
|
+
<html lang="en">
|
|
132
|
+
<head>
|
|
133
|
+
<meta charset="utf-8">
|
|
134
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
135
|
+
<title>Farcaster Snap</title>
|
|
136
|
+
</head>
|
|
137
|
+
<body>
|
|
138
|
+
<p>${body}</p>
|
|
139
|
+
</body>
|
|
140
|
+
</html>`;
|
|
141
|
+
}
|
|
142
|
+
|
|
101
143
|
function clientWantsSnapResponse(accept: string | undefined): boolean {
|
|
102
144
|
if (!accept || accept.trim() === "") return false;
|
|
103
145
|
const want = MEDIA_TYPE.toLowerCase();
|
package/src/payloadToResponse.ts
CHANGED
|
@@ -5,13 +5,20 @@ import {
|
|
|
5
5
|
validatePage,
|
|
6
6
|
} from "@farcaster/snap";
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
type PayloadToResponseOptions = {
|
|
9
|
+
resourcePath: string;
|
|
10
|
+
mediaTypes: string[];
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const DEFAULT_LINK_MEDIA_TYPES = [MEDIA_TYPE, "text/html"] as const;
|
|
14
|
+
|
|
15
|
+
export function payloadToResponse(
|
|
16
|
+
payload: SnapResponse,
|
|
17
|
+
options: Partial<PayloadToResponseOptions> = {},
|
|
18
|
+
): Response {
|
|
19
|
+
const resourcePath = options.resourcePath ?? "/";
|
|
20
|
+
const mediaTypes = options.mediaTypes ?? [...DEFAULT_LINK_MEDIA_TYPES];
|
|
21
|
+
|
|
15
22
|
const validation = validatePage(payload);
|
|
16
23
|
if (!validation.valid) {
|
|
17
24
|
return new Response(
|
|
@@ -32,8 +39,29 @@ export function payloadToResponse(payload: SnapResponse): Response {
|
|
|
32
39
|
return new Response(JSON.stringify(finalized), {
|
|
33
40
|
status: 200,
|
|
34
41
|
headers: {
|
|
35
|
-
|
|
36
|
-
Vary: "Accept",
|
|
42
|
+
...snapHeaders(resourcePath, MEDIA_TYPE, mediaTypes),
|
|
37
43
|
},
|
|
38
44
|
});
|
|
39
45
|
}
|
|
46
|
+
|
|
47
|
+
export function snapHeaders(
|
|
48
|
+
resourcePath: string,
|
|
49
|
+
currentMediaType: string,
|
|
50
|
+
availableMediaTypes: string[],
|
|
51
|
+
) {
|
|
52
|
+
return {
|
|
53
|
+
"Content-Type": `${currentMediaType}; charset=utf-8`,
|
|
54
|
+
Vary: "Accept",
|
|
55
|
+
Link: buildSnapAlternateLinkHeader(resourcePath, availableMediaTypes),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function buildSnapAlternateLinkHeader(
|
|
60
|
+
resourcePath: string,
|
|
61
|
+
mediaTypes: string[],
|
|
62
|
+
): string {
|
|
63
|
+
const p = resourcePath.startsWith("/") ? resourcePath : `/${resourcePath}`;
|
|
64
|
+
return mediaTypes
|
|
65
|
+
.map((mediaType) => `<${p}>; rel="alternate"; type="${mediaType}"`)
|
|
66
|
+
.join(", ");
|
|
67
|
+
}
|