@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.
@@ -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
+ }