Skip to content

Commit ebff0f9

Browse files
Release v0.28.4
Origin-SHA: 53bac65c7774e3b75a547bdf348186d4f2d11857 Co-authored-by: Benjamin C. LeFevre <35812202+BCLeFevre@users.noreply.github.com>
1 parent 546b37c commit ebff0f9

12 files changed

Lines changed: 771 additions & 63 deletions

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
# @opensea/tool-sdk
22

3+
## 0.28.4
4+
5+
### Patch Changes
6+
7+
- a541527: Reject negative and malformed x402 payment amounts. A negative `maxAmountRequired` previously slipped under the signed-BigInt `maxAmount` cap check and, when signed, wrapped into a huge positive `uint256` EIP-3009 authorization — bypassing the caller's spending limit. Amounts are now validated as canonical non-negative integers in `validatePaymentRequirements` (unconditionally, even without a cap) and again in the EIP-3009 signer as defense-in-depth for direct `signX402Payment` callers.
8+
- Updated dependencies [569fd4f]
9+
- Updated dependencies [a541527]
10+
- @opensea/wallet-adapters@0.3.3
11+
312
## 0.28.3
413

514
### Patch Changes

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@opensea/tool-sdk",
3-
"version": "0.28.3",
3+
"version": "0.28.4",
44
"type": "module",
55
"description": "SDK and CLI for building ERC-8257 compliant AI agent tools",
66
"repository": {
@@ -40,7 +40,7 @@
4040
},
4141
"dependencies": {
4242
"@clack/prompts": "0.10.1",
43-
"@opensea/wallet-adapters": "^0.3.2",
43+
"@opensea/wallet-adapters": "^0.3.3",
4444
"@x402/core": "2.15.0",
4545
"@x402/fetch": "2.15.0",
4646
"canonicalize": "2.1.0",

src/__tests__/describe-tool-access.test.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,70 @@ describe("describeToolAccess bounds enforcement", () => {
171171
])
172172
})
173173

174+
it("reports open access when the tool has no predicate (zero address)", async () => {
175+
mockGetToolConfig.mockResolvedValueOnce({
176+
creator: getAddress("0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"),
177+
metadataURI: "https://example.com/manifest.json",
178+
manifestHash:
179+
"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" as const,
180+
accessPredicate:
181+
"0x0000000000000000000000000000000000000000" as `0x${string}`,
182+
})
183+
const { describeToolAccess } = await import("../lib/onchain/access.js")
184+
185+
const result = await describeToolAccess({ toolId: TEST_TOOL_ID })
186+
187+
expect(result).toEqual({
188+
openAccess: true,
189+
predicateAddress: null,
190+
predicateName: null,
191+
requirements: [],
192+
logic: "AND",
193+
})
194+
// Open-access tools must not staticcall the (nonexistent) predicate.
195+
expect(mockReadContract).not.toHaveBeenCalled()
196+
})
197+
198+
it("degrades predicateName to null when name() reverts", async () => {
199+
mockReadContract.mockImplementation((args: { functionName: string }) => {
200+
if (args.functionName === "name") {
201+
return Promise.reject(new Error("execution reverted"))
202+
}
203+
return Promise.resolve([[goodRequirement], 0])
204+
})
205+
const { describeToolAccess } = await import("../lib/onchain/access.js")
206+
207+
const result = await describeToolAccess({ toolId: TEST_TOOL_ID })
208+
209+
expect(result.predicateName).toBeNull()
210+
expect(result.requirements).toEqual([goodRequirement])
211+
})
212+
213+
it("degrades to empty requirements when getRequirements() reverts", async () => {
214+
mockReadContract.mockImplementation((args: { functionName: string }) => {
215+
if (args.functionName === "name") {
216+
return Promise.resolve("MyPredicate")
217+
}
218+
return Promise.reject(new Error("execution reverted"))
219+
})
220+
const { describeToolAccess } = await import("../lib/onchain/access.js")
221+
222+
const result = await describeToolAccess({ toolId: TEST_TOOL_ID })
223+
224+
expect(result.predicateName).toBe("MyPredicate")
225+
expect(result.requirements).toEqual([])
226+
expect(result.logic).toBe("AND")
227+
})
228+
229+
it("maps a logic value of 1 to OR", async () => {
230+
routeReadContract({ requirements: [goodRequirement], logic: 1 })
231+
const { describeToolAccess } = await import("../lib/onchain/access.js")
232+
233+
const result = await describeToolAccess({ toolId: TEST_TOOL_ID })
234+
235+
expect(result.logic).toBe("OR")
236+
})
237+
174238
it("accepts entries exactly at the data and label caps", async () => {
175239
const dataAtCap = `0x${"00".repeat(4096)}` as `0x${string}`
176240
const labelAtCap = "x".repeat(256)

src/__tests__/handler.test.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,59 @@ describe("createToolHandler", () => {
408408
expect(reportSettled).toHaveBeenCalledOnce()
409409
})
410410

411+
it("auto-detects the Vercel request-context waitUntil when none is configured", async () => {
412+
let releaseReport: () => void = () => {}
413+
const reportSettled = vi.fn()
414+
const pendingReport = new Promise<void>(resolve => {
415+
releaseReport = () => {
416+
reportSettled()
417+
resolve()
418+
}
419+
})
420+
vi.mocked(createEip3009UsageReporter).mockReturnValueOnce(
421+
vi.fn().mockReturnValue(pendingReport),
422+
)
423+
424+
// Simulate the Vercel runtime by populating the request-context global the
425+
// handler reads (the same one `@vercel/functions` uses). No `waitUntil`
426+
// config is passed, so the handler must discover this one on its own.
427+
const registered: Promise<unknown>[] = []
428+
const vercelWaitUntil = vi.fn((p: Promise<unknown>) => {
429+
registered.push(p)
430+
})
431+
const contextSymbol = Symbol.for("@vercel/request-context")
432+
;(globalThis as Record<symbol, unknown>)[contextSymbol] = {
433+
get: () => ({ waitUntil: vercelWaitUntil }),
434+
}
435+
436+
try {
437+
const handler = createToolHandler({
438+
manifest: testManifest,
439+
inputSchema: InputSchema,
440+
outputSchema: OutputSchema,
441+
usageReporting: {} as Eip3009UsageReporterConfig,
442+
handler: async input => ({ result: `Echo: ${input.query}` }),
443+
})
444+
const request = new Request("https://test.example.com/api", {
445+
method: "POST",
446+
headers: { "Content-Type": "application/json" },
447+
body: JSON.stringify({ query: "test" }),
448+
})
449+
450+
const response = await handler(request)
451+
452+
expect(response.status).toBe(200)
453+
expect(vercelWaitUntil).toHaveBeenCalledOnce()
454+
expect(reportSettled).not.toHaveBeenCalled()
455+
456+
releaseReport()
457+
await registered[0]
458+
expect(reportSettled).toHaveBeenCalledOnce()
459+
} finally {
460+
;(globalThis as Record<symbol, unknown>)[contextSymbol] = undefined
461+
}
462+
})
463+
411464
it("does not call settle() when a gate short-circuits with a response", async () => {
412465
const settle = vi.fn()
413466
const gate: GateMiddleware = {

0 commit comments

Comments
 (0)