@farcaster/snap-hono 0.0.0-canary-20260330145610
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 +31 -0
- package/dist/index.js +75 -0
- package/package.json +48 -0
- package/src/index.ts +119 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Hono } from "hono";
|
|
2
|
+
import { type SnapFunction } from "@farcaster/snap";
|
|
3
|
+
export type SnapHandlerOptions = {
|
|
4
|
+
/**
|
|
5
|
+
* Route path to register GET and POST handlers on.
|
|
6
|
+
* @default "/"
|
|
7
|
+
*/
|
|
8
|
+
path?: string;
|
|
9
|
+
/**
|
|
10
|
+
* When true, skip JFS signature verification only. POST bodies must still be JFS-shaped JSON.
|
|
11
|
+
* When omitted, default to {@link envSkipJFSVerification}.
|
|
12
|
+
*/
|
|
13
|
+
skipJFSVerification?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Text shown on GET requests from browsers / non-snap clients.
|
|
16
|
+
* @default "This is a Farcaster Snap server."
|
|
17
|
+
*/
|
|
18
|
+
fallbackText?: string;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Register GET and POST snap handlers on `app` at `options.path` (default `/`).
|
|
22
|
+
*
|
|
23
|
+
* - GET → calls `snapFn({ action: { type: "get" }, request })` and returns the response.
|
|
24
|
+
* - POST → parses the JFS-shaped JSON body; verifies it via {@link verifyJFSRequestBody} unless
|
|
25
|
+
* `skipJFSVerification` is true, then calls `snapFn({ action, request })` and returns the response.
|
|
26
|
+
*
|
|
27
|
+
* All parsing, schema validation, signature verification, and error responses
|
|
28
|
+
* are handled automatically. `SnapContext.request` is the raw `Request` so handlers
|
|
29
|
+
* can read query params, headers, or the URL when needed.
|
|
30
|
+
*/
|
|
31
|
+
export declare function registerSnapHandler(app: Hono, snapFn: SnapFunction, options?: SnapHandlerOptions): void;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { cors } from "hono/cors";
|
|
2
|
+
import { parseRequest, sendResponse, MEDIA_TYPE, } from "@farcaster/snap";
|
|
3
|
+
/**
|
|
4
|
+
* Register GET and POST snap handlers on `app` at `options.path` (default `/`).
|
|
5
|
+
*
|
|
6
|
+
* - GET → calls `snapFn({ action: { type: "get" }, request })` and returns the response.
|
|
7
|
+
* - POST → parses the JFS-shaped JSON body; verifies it via {@link verifyJFSRequestBody} unless
|
|
8
|
+
* `skipJFSVerification` is true, then calls `snapFn({ action, request })` and returns the response.
|
|
9
|
+
*
|
|
10
|
+
* All parsing, schema validation, signature verification, and error responses
|
|
11
|
+
* are handled automatically. `SnapContext.request` is the raw `Request` so handlers
|
|
12
|
+
* can read query params, headers, or the URL when needed.
|
|
13
|
+
*/
|
|
14
|
+
export function registerSnapHandler(app, snapFn, options = {}) {
|
|
15
|
+
const path = options.path ?? "/";
|
|
16
|
+
const fallbackText = options.fallbackText ??
|
|
17
|
+
"This is a Farcaster Snap server. See https://snap.farcaster.xyz for more info.";
|
|
18
|
+
app.use(path, cors({ origin: "*" }));
|
|
19
|
+
app.get(path, async (c) => {
|
|
20
|
+
const accept = c.req.header("Accept");
|
|
21
|
+
if (!clientWantsSnapResponse(accept)) {
|
|
22
|
+
return c.text(fallbackText, 200);
|
|
23
|
+
}
|
|
24
|
+
const response = await snapFn({
|
|
25
|
+
action: { type: "get" },
|
|
26
|
+
request: c.req.raw,
|
|
27
|
+
});
|
|
28
|
+
return sendResponse(response);
|
|
29
|
+
});
|
|
30
|
+
app.post(path, async (c) => {
|
|
31
|
+
const raw = c.req.raw;
|
|
32
|
+
const skipJFSVerification = options.skipJFSVerification !== undefined
|
|
33
|
+
? options.skipJFSVerification
|
|
34
|
+
: envSkipJFSVerification();
|
|
35
|
+
const parsed = await parseRequest(raw, { skipJFSVerification });
|
|
36
|
+
if (!parsed.success) {
|
|
37
|
+
const err = parsed.error;
|
|
38
|
+
switch (err.type) {
|
|
39
|
+
case "method_not_allowed":
|
|
40
|
+
return c.json({ error: err.message }, 405);
|
|
41
|
+
case "invalid_json":
|
|
42
|
+
return c.json({ error: err.message }, 400);
|
|
43
|
+
case "validation":
|
|
44
|
+
return c.json({ error: "invalid POST body", issues: err.issues }, 400);
|
|
45
|
+
case "replay":
|
|
46
|
+
return c.json({ error: err.message }, 400);
|
|
47
|
+
case "signature":
|
|
48
|
+
return c.json({ error: err.message }, 401);
|
|
49
|
+
default: {
|
|
50
|
+
const _exhaustive = err;
|
|
51
|
+
throw new Error(`unexpected parse error: ${String(_exhaustive)}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const response = await snapFn({ action: parsed.action, request: raw });
|
|
56
|
+
return sendResponse(response);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
function clientWantsSnapResponse(accept) {
|
|
60
|
+
if (!accept || accept.trim() === "")
|
|
61
|
+
return false;
|
|
62
|
+
const want = MEDIA_TYPE.toLowerCase();
|
|
63
|
+
for (const part of accept.split(",")) {
|
|
64
|
+
const media = part.trim().split(";")[0]?.trim().toLowerCase();
|
|
65
|
+
if (media === want)
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
function envSkipJFSVerification() {
|
|
71
|
+
const v = process.env.SKIP_JFS_VERIFICATION?.trim().toLowerCase();
|
|
72
|
+
if (v === "1" || v === "true" || v === "yes")
|
|
73
|
+
return true;
|
|
74
|
+
return false;
|
|
75
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@farcaster/snap-hono",
|
|
3
|
+
"version": "0.0.0-canary-20260330145610",
|
|
4
|
+
"description": "Hono integration for Farcaster Snap servers",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/farcasterxyz/snap.git",
|
|
8
|
+
"directory": "pkgs/hono"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"main": "dist/index.js",
|
|
12
|
+
"types": "dist/index.d.ts",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"import": "./dist/index.js",
|
|
17
|
+
"default": "./dist/index.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"src"
|
|
23
|
+
],
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@farcaster/snap": "0.0.0-canary-20260330145610"
|
|
30
|
+
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"@farcaster/snap": "0.0.0-canary-20260330145610",
|
|
33
|
+
"hono": ">=4.0.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@farcaster/jfs": "^0.2.2",
|
|
37
|
+
"@types/node": "^25.5.0",
|
|
38
|
+
"hono": "^4.0.0",
|
|
39
|
+
"typescript": "^5.4.0",
|
|
40
|
+
"vitest": "^1.6.0"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "tsc",
|
|
44
|
+
"clean": "rm -rf dist",
|
|
45
|
+
"test": "vitest run",
|
|
46
|
+
"typecheck": "tsc --noEmit"
|
|
47
|
+
}
|
|
48
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { Hono } from "hono";
|
|
2
|
+
import { cors } from "hono/cors";
|
|
3
|
+
import {
|
|
4
|
+
parseRequest,
|
|
5
|
+
sendResponse,
|
|
6
|
+
MEDIA_TYPE,
|
|
7
|
+
type ParseRequestOptions,
|
|
8
|
+
type SnapFunction,
|
|
9
|
+
} from "@farcaster/snap";
|
|
10
|
+
|
|
11
|
+
export type SnapHandlerOptions = {
|
|
12
|
+
/**
|
|
13
|
+
* Route path to register GET and POST handlers on.
|
|
14
|
+
* @default "/"
|
|
15
|
+
*/
|
|
16
|
+
path?: string;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* When true, skip JFS signature verification only. POST bodies must still be JFS-shaped JSON.
|
|
20
|
+
* When omitted, default to {@link envSkipJFSVerification}.
|
|
21
|
+
*/
|
|
22
|
+
skipJFSVerification?: boolean;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Text shown on GET requests from browsers / non-snap clients.
|
|
26
|
+
* @default "This is a Farcaster Snap server."
|
|
27
|
+
*/
|
|
28
|
+
fallbackText?: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Register GET and POST snap handlers on `app` at `options.path` (default `/`).
|
|
33
|
+
*
|
|
34
|
+
* - GET → calls `snapFn({ action: { type: "get" }, request })` and returns the response.
|
|
35
|
+
* - POST → parses the JFS-shaped JSON body; verifies it via {@link verifyJFSRequestBody} unless
|
|
36
|
+
* `skipJFSVerification` is true, then calls `snapFn({ action, request })` and returns the response.
|
|
37
|
+
*
|
|
38
|
+
* All parsing, schema validation, signature verification, and error responses
|
|
39
|
+
* are handled automatically. `SnapContext.request` is the raw `Request` so handlers
|
|
40
|
+
* can read query params, headers, or the URL when needed.
|
|
41
|
+
*/
|
|
42
|
+
export function registerSnapHandler(
|
|
43
|
+
app: Hono,
|
|
44
|
+
snapFn: SnapFunction,
|
|
45
|
+
options: SnapHandlerOptions = {},
|
|
46
|
+
): void {
|
|
47
|
+
const path = options.path ?? "/";
|
|
48
|
+
const fallbackText =
|
|
49
|
+
options.fallbackText ??
|
|
50
|
+
"This is a Farcaster Snap server. See https://snap.farcaster.xyz for more info.";
|
|
51
|
+
|
|
52
|
+
app.use(path, cors({ origin: "*" }));
|
|
53
|
+
|
|
54
|
+
app.get(path, async (c) => {
|
|
55
|
+
const accept = c.req.header("Accept");
|
|
56
|
+
if (!clientWantsSnapResponse(accept)) {
|
|
57
|
+
return c.text(fallbackText, 200);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const response = await snapFn({
|
|
61
|
+
action: { type: "get" },
|
|
62
|
+
request: c.req.raw,
|
|
63
|
+
});
|
|
64
|
+
return sendResponse(response);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
app.post(path, async (c) => {
|
|
68
|
+
const raw = c.req.raw;
|
|
69
|
+
const skipJFSVerification =
|
|
70
|
+
options.skipJFSVerification !== undefined
|
|
71
|
+
? options.skipJFSVerification
|
|
72
|
+
: envSkipJFSVerification();
|
|
73
|
+
|
|
74
|
+
const parsed = await parseRequest(raw, { skipJFSVerification });
|
|
75
|
+
|
|
76
|
+
if (!parsed.success) {
|
|
77
|
+
const err = parsed.error;
|
|
78
|
+
switch (err.type) {
|
|
79
|
+
case "method_not_allowed":
|
|
80
|
+
return c.json({ error: err.message }, 405);
|
|
81
|
+
case "invalid_json":
|
|
82
|
+
return c.json({ error: err.message }, 400);
|
|
83
|
+
case "validation":
|
|
84
|
+
return c.json(
|
|
85
|
+
{ error: "invalid POST body", issues: err.issues },
|
|
86
|
+
400,
|
|
87
|
+
);
|
|
88
|
+
case "replay":
|
|
89
|
+
return c.json({ error: err.message }, 400);
|
|
90
|
+
case "signature":
|
|
91
|
+
return c.json({ error: err.message }, 401);
|
|
92
|
+
default: {
|
|
93
|
+
const _exhaustive: never = err;
|
|
94
|
+
throw new Error(`unexpected parse error: ${String(_exhaustive)}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const response = await snapFn({ action: parsed.action, request: raw });
|
|
100
|
+
|
|
101
|
+
return sendResponse(response);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function clientWantsSnapResponse(accept: string | undefined): boolean {
|
|
106
|
+
if (!accept || accept.trim() === "") return false;
|
|
107
|
+
const want = MEDIA_TYPE.toLowerCase();
|
|
108
|
+
for (const part of accept.split(",")) {
|
|
109
|
+
const media = part.trim().split(";")[0]?.trim().toLowerCase();
|
|
110
|
+
if (media === want) return true;
|
|
111
|
+
}
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function envSkipJFSVerification(): boolean {
|
|
116
|
+
const v = process.env.SKIP_JFS_VERIFICATION?.trim().toLowerCase();
|
|
117
|
+
if (v === "1" || v === "true" || v === "yes") return true;
|
|
118
|
+
return false;
|
|
119
|
+
}
|