@farcaster/snap 1.17.1 → 1.18.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/schemas.d.ts +6 -11
- package/dist/schemas.js +2 -9
- package/dist/server/parseRequest.js +20 -27
- package/dist/ui/catalog.d.ts +5 -1
- package/dist/ui/schema.d.ts +5 -1
- package/dist/ui/schema.js +1 -1
- package/dist/validator.js +5 -0
- package/package.json +1 -1
- package/src/schemas.ts +2 -9
- package/src/server/parseRequest.ts +21 -31
- package/src/ui/schema.ts +1 -1
- package/src/validator.ts +6 -0
package/dist/schemas.d.ts
CHANGED
|
@@ -68,17 +68,12 @@ export type SnapHandlerResult = {
|
|
|
68
68
|
effects?: z.input<typeof snapResponseSchema>["effects"];
|
|
69
69
|
ui: SnapSpecInput;
|
|
70
70
|
};
|
|
71
|
-
/**
|
|
72
|
-
* @deprecated `nonce` and `audience` are currently optional for backward
|
|
73
|
-
* compatibility but will become **required** in a future major version.
|
|
74
|
-
* Clients should always include both fields.
|
|
75
|
-
*/
|
|
76
71
|
export declare const payloadSchema: z.ZodObject<{
|
|
77
72
|
fid: z.ZodNumber;
|
|
78
73
|
inputs: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodArray<z.ZodString>]>>>;
|
|
79
74
|
timestamp: z.ZodNumber;
|
|
80
|
-
nonce: z.
|
|
81
|
-
audience: z.
|
|
75
|
+
nonce: z.ZodString;
|
|
76
|
+
audience: z.ZodString;
|
|
82
77
|
}, z.core.$strip>;
|
|
83
78
|
export type SnapPayload = z.infer<typeof payloadSchema>;
|
|
84
79
|
export declare const ACTION_TYPE_GET: "get";
|
|
@@ -91,8 +86,8 @@ declare const snapPostActionSchema: z.ZodObject<{
|
|
|
91
86
|
fid: z.ZodNumber;
|
|
92
87
|
inputs: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodArray<z.ZodString>]>>>;
|
|
93
88
|
timestamp: z.ZodNumber;
|
|
94
|
-
nonce: z.
|
|
95
|
-
audience: z.
|
|
89
|
+
nonce: z.ZodString;
|
|
90
|
+
audience: z.ZodString;
|
|
96
91
|
type: z.ZodLiteral<"post">;
|
|
97
92
|
}, z.core.$strip>;
|
|
98
93
|
export type SnapPostAction = z.infer<typeof snapPostActionSchema>;
|
|
@@ -102,8 +97,8 @@ export declare const snapActionSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
102
97
|
fid: z.ZodNumber;
|
|
103
98
|
inputs: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodArray<z.ZodString>]>>>;
|
|
104
99
|
timestamp: z.ZodNumber;
|
|
105
|
-
nonce: z.
|
|
106
|
-
audience: z.
|
|
100
|
+
nonce: z.ZodString;
|
|
101
|
+
audience: z.ZodString;
|
|
107
102
|
type: z.ZodLiteral<"post">;
|
|
108
103
|
}, z.core.$strip>], "type">;
|
|
109
104
|
export type SnapAction = z.infer<typeof snapActionSchema>;
|
package/dist/schemas.js
CHANGED
|
@@ -31,20 +31,13 @@ const postInputValueSchema = z.union([
|
|
|
31
31
|
z.boolean(),
|
|
32
32
|
z.array(z.string()),
|
|
33
33
|
]);
|
|
34
|
-
/**
|
|
35
|
-
* @deprecated `nonce` and `audience` are currently optional for backward
|
|
36
|
-
* compatibility but will become **required** in a future major version.
|
|
37
|
-
* Clients should always include both fields.
|
|
38
|
-
*/
|
|
39
34
|
export const payloadSchema = z
|
|
40
35
|
.object({
|
|
41
36
|
fid: z.number().int().nonnegative(),
|
|
42
37
|
inputs: z.record(z.string(), postInputValueSchema).default({}),
|
|
43
38
|
timestamp: z.number().int(),
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
/** @deprecated Will become required. Clients should always send the target server origin. */
|
|
47
|
-
audience: z.string().optional(),
|
|
39
|
+
nonce: z.string(),
|
|
40
|
+
audience: z.string(),
|
|
48
41
|
})
|
|
49
42
|
.strip();
|
|
50
43
|
export const ACTION_TYPE_GET = "get";
|
|
@@ -77,36 +77,29 @@ export async function parseRequest(request, options = {}) {
|
|
|
77
77
|
},
|
|
78
78
|
};
|
|
79
79
|
}
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
try {
|
|
90
|
-
const url = new URL(request.url);
|
|
91
|
-
const proto = request.headers.get("x-forwarded-proto") ??
|
|
92
|
-
url.protocol.replace(":", "");
|
|
93
|
-
const host = request.headers.get("x-forwarded-host") ?? url.host;
|
|
94
|
-
expectedOrigin = `${proto}://${host}`;
|
|
95
|
-
}
|
|
96
|
-
catch {
|
|
97
|
-
// do nothing
|
|
98
|
-
}
|
|
80
|
+
// Audience validation: ensure the payload audience matches the server origin.
|
|
81
|
+
let expectedOrigin = options.requestOrigin;
|
|
82
|
+
if (expectedOrigin === undefined) {
|
|
83
|
+
try {
|
|
84
|
+
const url = new URL(request.url);
|
|
85
|
+
const proto = request.headers.get("x-forwarded-proto") ??
|
|
86
|
+
url.protocol.replace(":", "");
|
|
87
|
+
const host = request.headers.get("x-forwarded-host") ?? url.host;
|
|
88
|
+
expectedOrigin = `${proto}://${host}`;
|
|
99
89
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
success: false,
|
|
103
|
-
error: {
|
|
104
|
-
type: "origin_mismatch",
|
|
105
|
-
message: `payload audience "${body.audience}" does not match expected origin "${expectedOrigin}"`,
|
|
106
|
-
},
|
|
107
|
-
};
|
|
90
|
+
catch {
|
|
91
|
+
// do nothing
|
|
108
92
|
}
|
|
109
93
|
}
|
|
94
|
+
if (expectedOrigin !== undefined && body.audience !== expectedOrigin) {
|
|
95
|
+
return {
|
|
96
|
+
success: false,
|
|
97
|
+
error: {
|
|
98
|
+
type: "origin_mismatch",
|
|
99
|
+
message: `payload audience "${body.audience}" does not match expected origin "${expectedOrigin}"`,
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
110
103
|
return {
|
|
111
104
|
success: true,
|
|
112
105
|
action: {
|
package/dist/ui/catalog.d.ts
CHANGED
|
@@ -10,7 +10,11 @@ export declare const snapJsonRenderCatalog: import("@json-render/core").Catalog<
|
|
|
10
10
|
root: import("@json-render/core").SchemaType<"string", unknown>;
|
|
11
11
|
elements: import("@json-render/core").SchemaType<"record", import("@json-render/core").SchemaType<"object", {
|
|
12
12
|
type: import("@json-render/core").SchemaType<"ref", string>;
|
|
13
|
-
props:
|
|
13
|
+
props: {
|
|
14
|
+
optional: true;
|
|
15
|
+
kind: "propsOf";
|
|
16
|
+
inner?: string;
|
|
17
|
+
};
|
|
14
18
|
children: {
|
|
15
19
|
optional: true;
|
|
16
20
|
kind: "array";
|
package/dist/ui/schema.d.ts
CHANGED
|
@@ -7,7 +7,11 @@ export declare const snapJsonRenderSchema: import("@json-render/core").Schema<{
|
|
|
7
7
|
root: import("@json-render/core").SchemaType<"string", unknown>;
|
|
8
8
|
elements: import("@json-render/core").SchemaType<"record", import("@json-render/core").SchemaType<"object", {
|
|
9
9
|
type: import("@json-render/core").SchemaType<"ref", string>;
|
|
10
|
-
props:
|
|
10
|
+
props: {
|
|
11
|
+
optional: true;
|
|
12
|
+
kind: "propsOf";
|
|
13
|
+
inner?: string;
|
|
14
|
+
};
|
|
11
15
|
children: {
|
|
12
16
|
optional: true;
|
|
13
17
|
kind: "array";
|
package/dist/ui/schema.js
CHANGED
|
@@ -8,7 +8,7 @@ export const snapJsonRenderSchema = defineSchema((s) => ({
|
|
|
8
8
|
root: s.string(),
|
|
9
9
|
elements: s.record(s.object({
|
|
10
10
|
type: s.ref("catalog.components"),
|
|
11
|
-
props: s.propsOf("catalog.components"),
|
|
11
|
+
props: { ...s.propsOf("catalog.components"), optional: true },
|
|
12
12
|
children: { ...s.array(s.string()), optional: true },
|
|
13
13
|
})),
|
|
14
14
|
}),
|
package/dist/validator.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { snapResponseSchema } from "./schemas.js";
|
|
2
2
|
import { MAX_CHILDREN, MAX_DEPTH, MAX_ELEMENTS, MAX_ROOT_CHILDREN, SPEC_VERSION_1 } from "./constants.js";
|
|
3
|
+
import { snapJsonRenderCatalog } from "./ui/catalog.js";
|
|
3
4
|
// ─── Helpers ──────────────────────────────────────────
|
|
4
5
|
/** Actions whose `params.target` must be a valid URL. */
|
|
5
6
|
const URL_TARGET_ACTIONS = new Set(["submit", "open_url", "open_mini_app"]);
|
|
@@ -202,6 +203,10 @@ export function validateSnapResponse(json) {
|
|
|
202
203
|
if (urlIssues.length > 0) {
|
|
203
204
|
return { valid: false, issues: urlIssues };
|
|
204
205
|
}
|
|
206
|
+
const catalogResult = snapJsonRenderCatalog.validate(ui);
|
|
207
|
+
if (!catalogResult.success) {
|
|
208
|
+
return { valid: false, issues: catalogResult.error?.issues ?? [] };
|
|
209
|
+
}
|
|
205
210
|
}
|
|
206
211
|
return { valid: true, issues: [] };
|
|
207
212
|
}
|
package/package.json
CHANGED
package/src/schemas.ts
CHANGED
|
@@ -81,20 +81,13 @@ const postInputValueSchema = z.union([
|
|
|
81
81
|
z.array(z.string()),
|
|
82
82
|
]);
|
|
83
83
|
|
|
84
|
-
/**
|
|
85
|
-
* @deprecated `nonce` and `audience` are currently optional for backward
|
|
86
|
-
* compatibility but will become **required** in a future major version.
|
|
87
|
-
* Clients should always include both fields.
|
|
88
|
-
*/
|
|
89
84
|
export const payloadSchema = z
|
|
90
85
|
.object({
|
|
91
86
|
fid: z.number().int().nonnegative(),
|
|
92
87
|
inputs: z.record(z.string(), postInputValueSchema).default({}),
|
|
93
88
|
timestamp: z.number().int(),
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
/** @deprecated Will become required. Clients should always send the target server origin. */
|
|
97
|
-
audience: z.string().optional(),
|
|
89
|
+
nonce: z.string(),
|
|
90
|
+
audience: z.string(),
|
|
98
91
|
})
|
|
99
92
|
.strip();
|
|
100
93
|
|
|
@@ -148,39 +148,29 @@ export async function parseRequest(
|
|
|
148
148
|
};
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
-
//
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
try {
|
|
164
|
-
const url = new URL(request.url);
|
|
165
|
-
const proto =
|
|
166
|
-
request.headers.get("x-forwarded-proto") ??
|
|
167
|
-
url.protocol.replace(":", "");
|
|
168
|
-
const host = request.headers.get("x-forwarded-host") ?? url.host;
|
|
169
|
-
expectedOrigin = `${proto}://${host}`;
|
|
170
|
-
} catch {
|
|
171
|
-
// do nothing
|
|
172
|
-
}
|
|
151
|
+
// Audience validation: ensure the payload audience matches the server origin.
|
|
152
|
+
let expectedOrigin = options.requestOrigin;
|
|
153
|
+
if (expectedOrigin === undefined) {
|
|
154
|
+
try {
|
|
155
|
+
const url = new URL(request.url);
|
|
156
|
+
const proto =
|
|
157
|
+
request.headers.get("x-forwarded-proto") ??
|
|
158
|
+
url.protocol.replace(":", "");
|
|
159
|
+
const host = request.headers.get("x-forwarded-host") ?? url.host;
|
|
160
|
+
expectedOrigin = `${proto}://${host}`;
|
|
161
|
+
} catch {
|
|
162
|
+
// do nothing
|
|
173
163
|
}
|
|
164
|
+
}
|
|
174
165
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
166
|
+
if (expectedOrigin !== undefined && body.audience !== expectedOrigin) {
|
|
167
|
+
return {
|
|
168
|
+
success: false,
|
|
169
|
+
error: {
|
|
170
|
+
type: "origin_mismatch",
|
|
171
|
+
message: `payload audience "${body.audience}" does not match expected origin "${expectedOrigin}"`,
|
|
172
|
+
},
|
|
173
|
+
};
|
|
184
174
|
}
|
|
185
175
|
|
|
186
176
|
return {
|
package/src/ui/schema.ts
CHANGED
|
@@ -11,7 +11,7 @@ export const snapJsonRenderSchema = defineSchema(
|
|
|
11
11
|
elements: s.record(
|
|
12
12
|
s.object({
|
|
13
13
|
type: s.ref("catalog.components"),
|
|
14
|
-
props: s.propsOf("catalog.components"),
|
|
14
|
+
props: { ...s.propsOf("catalog.components"), optional: true },
|
|
15
15
|
children: { ...s.array(s.string()), optional: true },
|
|
16
16
|
}),
|
|
17
17
|
),
|
package/src/validator.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { snapResponseSchema } from "./schemas";
|
|
3
3
|
import { MAX_CHILDREN, MAX_DEPTH, MAX_ELEMENTS, MAX_ROOT_CHILDREN, SPEC_VERSION_1 } from "./constants";
|
|
4
|
+
import { snapJsonRenderCatalog } from "./ui/catalog.js";
|
|
4
5
|
|
|
5
6
|
export type ValidationResult = {
|
|
6
7
|
valid: boolean;
|
|
@@ -255,6 +256,11 @@ export function validateSnapResponse(json: unknown): ValidationResult {
|
|
|
255
256
|
if (urlIssues.length > 0) {
|
|
256
257
|
return { valid: false, issues: urlIssues };
|
|
257
258
|
}
|
|
259
|
+
|
|
260
|
+
const catalogResult = snapJsonRenderCatalog.validate(ui);
|
|
261
|
+
if (!catalogResult.success) {
|
|
262
|
+
return { valid: false, issues: catalogResult.error?.issues ?? [] };
|
|
263
|
+
}
|
|
258
264
|
}
|
|
259
265
|
|
|
260
266
|
return { valid: true, issues: [] };
|