Skip to content

Commit 88c7927

Browse files
CodySearsOSryanio
andcommitted
Release v0.16.1
Origin-SHA: 3c0bffc9cbe8efa5f105e702ac2ed8198fc7c337 Co-authored-by: Ryan Ghods <ryan@ryanio.com>
1 parent 94f2f2f commit 88c7927

5 files changed

Lines changed: 98 additions & 8 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ pnpm run type-check # TypeScript type checking
3333
| `src/lib/middleware/` | Gating middleware (predicate gate, x402, x402 facilitators, well-known endpoint) |
3434
| `src/lib/wallet/` | Re-exports from `@opensea/wallet-adapters` (adapters, types, and viem bridge) |
3535
| `src/lib/client/` | Authenticated HTTP clients: EIP-3009 auth, x402 payment, external signer, paid authenticated fetch |
36+
| `src/lib/usage/` | Usage reporters — report tool invocations / x402 settlements to the OpenSea usage endpoint, plus EIP-3009 zero-value auth helpers |
3637
| `src/lib/adapters/` | Framework adapters (Vercel, Cloudflare, Express) |
3738
| `src/lib/utils.ts` | Shared utilities used across `lib/` |
3839
| `src/templates/` | Scaffolding templates for `init` command |

CHANGELOG.md

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

3+
## 0.16.1
4+
5+
### Patch Changes
6+
7+
- 8812960: Require `validBefore` in predicate-gate authorizations. `verifyXPaymentAuth` previously left `validBefore` optional: a caller could sign a `TransferWithAuthorization` with `validBefore=0` and omit the field, so the expiry check was skipped and the gate accepted an unbounded, non-expiring proof. The field is now required in both the X-Payment and EIP-3009 auth paths, with the now-dead `validBefore !== undefined` guards and the `?? "0"` recovery fallback removed. Thanks @Nexory (ProjectOpenSea/tool-sdk#10).
8+
39
## 0.16.0
410

511
### Minor Changes

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@opensea/tool-sdk",
3-
"version": "0.16.0",
3+
"version": "0.16.1",
44
"type": "module",
55
"description": "SDK and CLI for building ERC-8257 compliant AI agent tools",
66
"repository": {

src/__tests__/predicate-gate.test.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,39 @@ describe("predicateGate", () => {
496496
expect(body.error).toMatch(/missing required fields/i)
497497
})
498498

499+
it("EIP-3009: returns 401 when validBefore is missing", async () => {
500+
const { predicateGate } = await import(
501+
"../lib/middleware/predicate-gate.js"
502+
)
503+
const gate = predicateGate({ toolId: TEST_TOOL_ID })
504+
const ctx: Partial<ToolContext> = { gates: {} }
505+
506+
// Omitting validBefore must be rejected at the required-field guard with a
507+
// clear error, matching the X-Payment path. Without the guard the request
508+
// is only incidentally rejected (BigInt(undefined) throws during recovery,
509+
// surfacing as "invalid signature"); a future `?? "0"` fallback would let
510+
// an unbounded proof through, so the field must be explicitly required.
511+
const token = makeEip3009Token()
512+
const payload = JSON.parse(
513+
atob(token.replace(/-/g, "+").replace(/_/g, "/")),
514+
)
515+
delete payload.validBefore
516+
const badToken = btoa(JSON.stringify(payload))
517+
.replace(/\+/g, "-")
518+
.replace(/\//g, "_")
519+
.replace(/=/g, "")
520+
const request = new Request("https://example.com/api", {
521+
method: "POST",
522+
headers: { Authorization: `EIP-3009 ${badToken}` },
523+
})
524+
525+
const response = await gate.check(request, ctx)
526+
527+
expect(response?.status).toBe(401)
528+
const body = await response?.json()
529+
expect(body.error).toMatch(/missing required fields/i)
530+
})
531+
499532
it("EIP-3009: returns 401 when value !== '0'", async () => {
500533
const { predicateGate } = await import(
501534
"../lib/middleware/predicate-gate.js"
@@ -683,6 +716,54 @@ describe("predicateGate", () => {
683716
})
684717
})
685718

719+
it("X-Payment: rejects an authorization that omits validBefore", async () => {
720+
const { predicateGate } = await import(
721+
"../lib/middleware/predicate-gate.js"
722+
)
723+
const operator =
724+
"0x5ECA0441311643608a8c9Ab8B250f695Dd32E2a8" as `0x${string}`
725+
const gate = predicateGate({
726+
toolId: TEST_TOOL_ID,
727+
operatorAddress: operator,
728+
})
729+
const ctx: Partial<ToolContext> = { gates: {} }
730+
731+
// Authorization with NO validBefore. The signer (recovery mock -> TEST_CALLER)
732+
// matches `from`, so the expiry check is the only bound on this token. When
733+
// validBefore is absent that check is skipped, so an unbounded (non-expiring)
734+
// proof is accepted. The sibling EIP-3009 path (verifyEip3009Auth) requires
735+
// validBefore, so the two auth paths must agree: reject when it is missing.
736+
const header = Buffer.from(
737+
JSON.stringify({
738+
x402Version: 1,
739+
scheme: "exact",
740+
network: "base",
741+
payload: {
742+
signature: "0xabcd",
743+
authorization: {
744+
from: TEST_CALLER,
745+
to: operator,
746+
value: "0",
747+
validAfter: "0",
748+
// validBefore intentionally omitted
749+
nonce:
750+
"0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
751+
},
752+
},
753+
}),
754+
).toString("base64")
755+
const request = new Request("https://example.com/api", {
756+
method: "POST",
757+
headers: { "X-Payment": header },
758+
})
759+
760+
const response = await gate.check(request, ctx)
761+
762+
expect(response?.status).toBe(401)
763+
const body = await response?.json()
764+
expect(body.error).toMatch(/missing required authorization fields/i)
765+
})
766+
686767
it("X-Payment: takes precedence over Authorization header", async () => {
687768
const { predicateGate } = await import(
688769
"../lib/middleware/predicate-gate.js"

src/lib/middleware/predicate-gate.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -378,11 +378,12 @@ async function verifyEip3009Auth(
378378
!authorization.from ||
379379
!authorization.to ||
380380
!authorization.signature ||
381-
!authorization.nonce
381+
!authorization.nonce ||
382+
!authorization.validBefore
382383
) {
383384
return {
384385
error:
385-
"Predicate gate: EIP-3009 token missing required fields (from, to, signature, nonce)",
386+
"Predicate gate: EIP-3009 token missing required fields (from, to, signature, nonce, validBefore)",
386387
status: 401,
387388
}
388389
}
@@ -406,8 +407,8 @@ async function verifyEip3009Auth(
406407
}
407408
}
408409

409-
// Check expiry (validBefore)
410-
if (authorization.validBefore !== undefined) {
410+
// Check expiry (validBefore is required above, so it is always present here)
411+
{
411412
const validBefore = BigInt(authorization.validBefore)
412413
const nowSec = BigInt(Math.floor(Date.now() / 1000))
413414
if (nowSec >= validBefore) {
@@ -522,7 +523,7 @@ async function verifyXPaymentAuth(
522523
const auth = paymentPayload.payload?.authorization
523524
const signature = paymentPayload.payload?.signature
524525

525-
if (!auth?.from || !auth?.to || !signature || !auth?.nonce) {
526+
if (!auth?.from || !auth?.to || !signature || !auth?.nonce || !auth?.validBefore) {
526527
return {
527528
error:
528529
"Predicate gate: X-Payment missing required authorization fields",
@@ -557,7 +558,8 @@ async function verifyXPaymentAuth(
557558
}
558559
}
559560

560-
if (auth.validBefore !== undefined) {
561+
// validBefore is required above, so it is always present here
562+
{
561563
const validBefore = BigInt(auth.validBefore)
562564
const nowSec = BigInt(Math.floor(Date.now() / 1000))
563565
if (nowSec >= validBefore) {
@@ -595,7 +597,7 @@ async function verifyXPaymentAuth(
595597
to: auth.to as `0x${string}`,
596598
value: BigInt(auth.value ?? "0"),
597599
validAfter: BigInt(auth.validAfter ?? "0"),
598-
validBefore: BigInt(auth.validBefore ?? "0"),
600+
validBefore: BigInt(auth.validBefore),
599601
nonce: auth.nonce as `0x${string}`,
600602
},
601603
signature: signature as `0x${string}`,

0 commit comments

Comments
 (0)