Skip to content

Commit d4eaf5d

Browse files
committed
Sync from opensea-devtools
1 parent 617c504 commit d4eaf5d

18 files changed

Lines changed: 1710 additions & 123 deletions

AGENTS.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,16 @@ pnpm run type-check # TypeScript type checking
2121
| `src/index.ts` | Library entry point — public `tool-sdk` exports |
2222
| `src/cli.ts` | CLI entry point (Commander program wiring) |
2323
| `src/types.ts` | Shared public types |
24-
| `src/cli/commands/` | CLI commands: `register`, `validate`, `verify`, `hash`, `init` |
24+
| `src/cli/commands/` | CLI commands: `auth`, `deploy`, `dry-run-gate`, `dry-run-predicate-gate`, `export`, `hash`, `init`, `inspect`, `pay`, `register`, `smoke`, `update-metadata`, `validate`, `verify` |
2525
| `src/lib/onchain/abis.ts` | TypeScript ABI definitions mirroring Solidity interfaces |
2626
| `src/lib/onchain/chains.ts` | Deployed contract addresses per chain |
2727
| `src/lib/onchain/registry.ts` | `ToolRegistryClient` — onchain interaction wrapper |
2828
| `src/lib/onchain/hash.ts` | JCS keccak256 manifest hashing |
29+
| `src/lib/onchain/access.ts` | Access-check helpers for tool gating |
30+
| `src/lib/onchain/predicate-clients.ts` | Typed clients for predicate contracts |
2931
| `src/lib/manifest/` | Manifest schema, validation, types |
3032
| `src/lib/handler/` | `createToolHandler` — Web Request/Response handler factory |
31-
| `src/lib/middleware/` | Gating middleware (NFT gate, x402, well-known endpoint) |
33+
| `src/lib/middleware/` | Gating middleware (NFT gate, predicate gate, x402, x402 facilitators, well-known endpoint) |
3234
| `src/lib/wallet/` | Re-exports from `@opensea/wallet-adapters` (adapters, types, and viem bridge) |
3335
| `src/lib/adapters/` | Framework adapters (Vercel, Cloudflare, Express) |
3436
| `src/lib/utils.ts` | Shared utilities used across `lib/` |

src/__tests__/auth.test.ts

Lines changed: 66 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ const BANKR_ADDRESS = "0x8b8e1C20E0630De8C60f0e0D5C3e9C7c20F0c20e"
88
afterEach(() => {
99
vi.unstubAllGlobals()
1010
vi.restoreAllMocks()
11-
delete process.env.TOOL_SDK_PRIVATE_KEY
11+
delete process.env.PRIVATE_KEY
12+
delete process.env.RPC_URL
1213
delete process.env.BANKR_API_KEY
1314
})
1415

@@ -28,7 +29,8 @@ describe("auth command", () => {
2829
vi.stubGlobal("fetch", fetchMock)
2930

3031
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {})
31-
process.env.TOOL_SDK_PRIVATE_KEY = PRIVATE_KEY
32+
process.env.PRIVATE_KEY = PRIVATE_KEY
33+
process.env.RPC_URL = "http://localhost:8545"
3234

3335
const { authCommand } = await import("../cli/commands/auth.js")
3436

@@ -66,6 +68,45 @@ describe("auth command", () => {
6668
logSpy.mockRestore()
6769
})
6870

71+
it("sends SIWE header when using --wallet-provider flag", async () => {
72+
const calls: { url: string; headers: Record<string, string> }[] = []
73+
74+
const fetchMock = vi.fn(async (url: string, init?: RequestInit) => {
75+
const headers = Object.fromEntries(
76+
Object.entries(init?.headers ?? {}),
77+
) as Record<string, string>
78+
calls.push({ url: url as string, headers })
79+
80+
return new Response(JSON.stringify({ result: "ok" }), { status: 200 })
81+
})
82+
83+
vi.stubGlobal("fetch", fetchMock)
84+
85+
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {})
86+
process.env.PRIVATE_KEY = PRIVATE_KEY
87+
process.env.RPC_URL = "http://localhost:8545"
88+
89+
const { authCommand } = await import("../cli/commands/auth.js")
90+
91+
await authCommand.parseAsync([
92+
"node",
93+
"auth",
94+
"https://tool.example.com/api",
95+
"--wallet-provider",
96+
"private-key",
97+
"--body",
98+
"{}",
99+
])
100+
101+
expect(fetchMock).toHaveBeenCalledTimes(1)
102+
103+
const authHeader = calls[0].headers.Authorization
104+
expect(authHeader).toBeDefined()
105+
expect(authHeader).toMatch(/^SIWE /)
106+
107+
logSpy.mockRestore()
108+
})
109+
69110
it("prints hint on 401 auth failure", async () => {
70111
vi.stubGlobal(
71112
"fetch",
@@ -79,7 +120,8 @@ describe("auth command", () => {
79120
)
80121

81122
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {})
82-
process.env.TOOL_SDK_PRIVATE_KEY = PRIVATE_KEY
123+
process.env.PRIVATE_KEY = PRIVATE_KEY
124+
process.env.RPC_URL = "http://localhost:8545"
83125

84126
const { authCommand } = await import("../cli/commands/auth.js")
85127

@@ -114,7 +156,8 @@ describe("auth command", () => {
114156
)
115157

116158
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {})
117-
process.env.TOOL_SDK_PRIVATE_KEY = PRIVATE_KEY
159+
process.env.PRIVATE_KEY = PRIVATE_KEY
160+
process.env.RPC_URL = "http://localhost:8545"
118161

119162
const { authCommand } = await import("../cli/commands/auth.js")
120163

@@ -185,34 +228,32 @@ describe("auth command with --bankr-key", () => {
185228
logSpy.mockRestore()
186229
})
187230

188-
it("errors when both --key and --bankr-key are provided", async () => {
189-
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {})
190-
vi.spyOn(console, "log").mockImplementation(() => {})
191-
const exitSpy = vi.spyOn(process, "exit").mockImplementation(() => {
192-
throw new Error("process.exit")
193-
})
231+
it("prefers Bankr when both PRIVATE_KEY and BANKR_API_KEY are set", async () => {
232+
const fetchMock = stubBankrFetch()
233+
vi.stubGlobal("fetch", fetchMock)
194234

195-
process.env.TOOL_SDK_PRIVATE_KEY = PRIVATE_KEY
235+
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {})
236+
process.env.PRIVATE_KEY = PRIVATE_KEY
237+
process.env.RPC_URL = "http://localhost:8545"
196238
process.env.BANKR_API_KEY = "test-bankr-key"
197239

198240
const { authCommand } = await import("../cli/commands/auth.js")
199241

200-
await expect(
201-
authCommand.parseAsync([
202-
"node",
203-
"auth",
204-
"https://tool.example.com/api",
205-
"--body",
206-
"{}",
207-
]),
208-
).rejects.toThrow("process.exit")
242+
await authCommand.parseAsync([
243+
"node",
244+
"auth",
245+
"https://tool.example.com/api",
246+
"--body",
247+
"{}",
248+
])
209249

210-
const output = errorSpy.mock.calls.map(c => c.join(" ")).join("\n")
211-
expect(output).toContain("Both --key and --bankr-key")
212-
expect(exitSpy).toHaveBeenCalledWith(1)
250+
// Bankr path: first call should be /wallet/info
251+
expect(fetchMock.mock.calls[0][0]).toContain("/wallet/info")
252+
253+
logSpy.mockRestore()
213254
})
214255

215-
it("shows updated error when neither key is provided", async () => {
256+
it("shows error when no wallet env vars are set", async () => {
216257
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {})
217258
vi.spyOn(console, "log").mockImplementation(() => {})
218259
const exitSpy = vi.spyOn(process, "exit").mockImplementation(() => {
@@ -232,7 +273,7 @@ describe("auth command with --bankr-key", () => {
232273
).rejects.toThrow("process.exit")
233274

234275
const output = errorSpy.mock.calls.map(c => c.join(" ")).join("\n")
235-
expect(output).toContain("TOOL_SDK_PRIVATE_KEY")
276+
expect(output).toContain("PRIVATE_KEY")
236277
expect(output).toContain("BANKR_API_KEY")
237278
expect(exitSpy).toHaveBeenCalledWith(1)
238279
})

src/__tests__/deploy-integration.test.ts

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,70 @@ afterAll(() => {
3434
rmSync(fixturesDir, { recursive: true, force: true })
3535
})
3636

37+
describe("fetchExistingEnvVars", () => {
38+
let execSyncMock: ReturnType<typeof vi.fn>
39+
40+
beforeEach(async () => {
41+
const cp = await import("node:child_process")
42+
execSyncMock = cp.execSync as ReturnType<typeof vi.fn>
43+
})
44+
45+
afterEach(() => {
46+
vi.restoreAllMocks()
47+
})
48+
49+
it("should return a set of existing env var names", async () => {
50+
execSyncMock.mockReturnValue(
51+
JSON.stringify([
52+
{ key: "API_KEY" },
53+
{ key: "SECRET_TOKEN" },
54+
{ key: "TOOL_ENDPOINT" },
55+
]),
56+
)
57+
58+
const { fetchExistingEnvVars } = await import("../cli/commands/deploy.js")
59+
const result = fetchExistingEnvVars()
60+
61+
expect(result).toBeInstanceOf(Set)
62+
expect(result.has("API_KEY")).toBe(true)
63+
expect(result.has("SECRET_TOKEN")).toBe(true)
64+
expect(result.has("TOOL_ENDPOINT")).toBe(true)
65+
expect(result.has("NONEXISTENT")).toBe(false)
66+
})
67+
68+
it("should return empty set when vercel env ls fails", async () => {
69+
execSyncMock.mockImplementation(() => {
70+
throw new Error("Not authenticated")
71+
})
72+
73+
const { fetchExistingEnvVars } = await import("../cli/commands/deploy.js")
74+
const result = fetchExistingEnvVars()
75+
76+
expect(result).toBeInstanceOf(Set)
77+
expect(result.size).toBe(0)
78+
})
79+
80+
it("should return empty set when vercel env ls returns invalid JSON", async () => {
81+
execSyncMock.mockReturnValue("not valid json")
82+
83+
const { fetchExistingEnvVars } = await import("../cli/commands/deploy.js")
84+
const result = fetchExistingEnvVars()
85+
86+
expect(result).toBeInstanceOf(Set)
87+
expect(result.size).toBe(0)
88+
})
89+
90+
it("should return empty set for empty array response", async () => {
91+
execSyncMock.mockReturnValue("[]")
92+
93+
const { fetchExistingEnvVars } = await import("../cli/commands/deploy.js")
94+
const result = fetchExistingEnvVars()
95+
96+
expect(result).toBeInstanceOf(Set)
97+
expect(result.size).toBe(0)
98+
})
99+
})
100+
37101
describe("deploy command (mocked shell)", () => {
38102
let execSyncMock: ReturnType<typeof vi.fn>
39103
let origCwd: string
@@ -342,4 +406,145 @@ describe("deploy command (mocked shell)", () => {
342406
}
343407
}
344408
})
409+
410+
it("should skip env var prompts for vars already set in Vercel", async () => {
411+
vi.spyOn(process, "exit").mockImplementation(code => {
412+
throw new ExitError(code as number)
413+
})
414+
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {})
415+
vi.spyOn(console, "error").mockImplementation(() => {})
416+
417+
const testDir = setupTestDir("skip-existing", {
418+
envExample: "API_KEY=your-key\nSECRET=your-secret\nTOOL_ENDPOINT=auto\n",
419+
manifest: `export const manifest = { name: "my-tool" }`,
420+
})
421+
422+
setupExecMock({
423+
"vercel --version": "Vercel CLI 33.0.0",
424+
"vercel whoami": "test-user",
425+
"vercel env ls": JSON.stringify([{ key: "API_KEY" }]),
426+
"vercel env add": "",
427+
"vercel deploy --prod --force": "https://my-tool-final.vercel.app",
428+
"vercel deploy --prod": "https://my-tool-abc123.vercel.app",
429+
})
430+
431+
const validManifest = {
432+
type: "https://eips.ethereum.org/EIPS/eip-XXXX#tool-manifest-v1",
433+
name: "my-tool",
434+
description: "A test tool",
435+
endpoint: "https://my-tool-final.vercel.app",
436+
inputs: { type: "object", properties: {} },
437+
outputs: { type: "object", properties: {} },
438+
creatorAddress: "0xabcdefabcdef1234567890abcdefabcdef123456",
439+
}
440+
441+
const originalFetch = globalThis.fetch
442+
globalThis.fetch = vi
443+
.fn()
444+
.mockResolvedValue(
445+
new Response(JSON.stringify(validManifest), { status: 200 }),
446+
)
447+
448+
process.chdir(testDir)
449+
450+
const origSecret = process.env.SECRET
451+
process.env.SECRET = "secret-value"
452+
453+
try {
454+
const { deployCommand } = await import("../cli/commands/deploy.js")
455+
await deployCommand.parseAsync(
456+
["--host", "vercel", "--non-interactive"],
457+
{ from: "user" },
458+
)
459+
460+
expect(logSpy).toHaveBeenCalledWith(
461+
expect.stringContaining("API_KEY already set, skipping"),
462+
)
463+
464+
expect(execSyncMock).not.toHaveBeenCalledWith(
465+
expect.stringContaining("env add API_KEY"),
466+
expect.anything(),
467+
)
468+
469+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining("Set SECRET"))
470+
} finally {
471+
globalThis.fetch = originalFetch
472+
if (origSecret === undefined) {
473+
delete process.env.SECRET
474+
} else {
475+
process.env.SECRET = origSecret
476+
}
477+
}
478+
})
479+
480+
it("should fall through to prompt flow when vercel env ls fails", async () => {
481+
vi.spyOn(process, "exit").mockImplementation(code => {
482+
throw new ExitError(code as number)
483+
})
484+
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {})
485+
vi.spyOn(console, "error").mockImplementation(() => {})
486+
487+
const testDir = setupTestDir("env-ls-fail", {
488+
envExample: "API_KEY=your-key\nTOOL_ENDPOINT=auto\n",
489+
manifest: `export const manifest = { name: "my-tool" }`,
490+
})
491+
492+
execSyncMock.mockImplementation((cmd: string) => {
493+
const cmdStr = String(cmd)
494+
if (cmdStr.includes("vercel env ls")) throw new Error("Network error")
495+
if (cmdStr.includes("vercel --version")) return "Vercel CLI 33.0.0"
496+
if (cmdStr.includes("vercel whoami")) return "test-user"
497+
if (cmdStr.includes("vercel env add")) return ""
498+
if (cmdStr.includes("vercel deploy --prod --force"))
499+
return "https://my-tool-final.vercel.app"
500+
if (cmdStr.includes("vercel deploy --prod"))
501+
return "https://my-tool-abc123.vercel.app"
502+
return ""
503+
})
504+
505+
const validManifest = {
506+
type: "https://eips.ethereum.org/EIPS/eip-XXXX#tool-manifest-v1",
507+
name: "my-tool",
508+
description: "A test tool",
509+
endpoint: "https://my-tool-final.vercel.app",
510+
inputs: { type: "object", properties: {} },
511+
outputs: { type: "object", properties: {} },
512+
creatorAddress: "0xabcdefabcdef1234567890abcdefabcdef123456",
513+
}
514+
515+
const originalFetch = globalThis.fetch
516+
globalThis.fetch = vi
517+
.fn()
518+
.mockResolvedValue(
519+
new Response(JSON.stringify(validManifest), { status: 200 }),
520+
)
521+
522+
process.chdir(testDir)
523+
524+
const origApiKey = process.env.API_KEY
525+
process.env.API_KEY = "test-key-value"
526+
527+
try {
528+
const { deployCommand } = await import("../cli/commands/deploy.js")
529+
await deployCommand.parseAsync(
530+
["--host", "vercel", "--non-interactive"],
531+
{ from: "user" },
532+
)
533+
534+
expect(logSpy).toHaveBeenCalledWith(
535+
expect.stringContaining("Could not fetch existing env vars"),
536+
)
537+
538+
expect(logSpy).toHaveBeenCalledWith(
539+
expect.stringContaining("Set API_KEY"),
540+
)
541+
} finally {
542+
globalThis.fetch = originalFetch
543+
if (origApiKey === undefined) {
544+
delete process.env.API_KEY
545+
} else {
546+
process.env.API_KEY = origApiKey
547+
}
548+
}
549+
})
345550
})

0 commit comments

Comments
 (0)