Skip to content

Commit 71a82e8

Browse files
committed
Sync from opensea-devtools
1 parent 92490af commit 71a82e8

18 files changed

Lines changed: 2277 additions & 41 deletions

src/__tests__/auth.test.ts

Lines changed: 106 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@ import { afterEach, describe, expect, it, vi } from "vitest"
33
// Hardhat/Anvil account #0 — deterministic test key, never holds real funds
44
const PRIVATE_KEY =
55
"0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
6+
const BANKR_ADDRESS = "0x8b8e1C20E0630De8C60f0e0D5C3e9C7c20F0c20e"
67

78
afterEach(() => {
89
vi.unstubAllGlobals()
910
vi.restoreAllMocks()
11+
delete process.env.TOOL_SDK_PRIVATE_KEY
12+
delete process.env.BANKR_API_KEY
1013
})
1114

1215
describe("auth command", () => {
@@ -61,7 +64,6 @@ describe("auth command", () => {
6164
expect(calls[0].headers["content-type"]).toBe("application/json")
6265

6366
logSpy.mockRestore()
64-
delete process.env.TOOL_SDK_PRIVATE_KEY
6567
})
6668

6769
it("prints hint on 401 auth failure", async () => {
@@ -93,7 +95,6 @@ describe("auth command", () => {
9395
expect(output).toContain("SIWE authentication failed")
9496

9597
logSpy.mockRestore()
96-
delete process.env.TOOL_SDK_PRIVATE_KEY
9798
})
9899

99100
it("prints hint on 403 with predicate address and toolId", async () => {
@@ -131,6 +132,108 @@ describe("auth command", () => {
131132
expect(output).toContain("tool-sdk inspect --tool-id 42")
132133

133134
logSpy.mockRestore()
134-
delete process.env.TOOL_SDK_PRIVATE_KEY
135+
})
136+
})
137+
138+
function stubBankrFetch() {
139+
return vi.fn(async (url: string, init?: RequestInit) => {
140+
if (typeof url === "string" && url.includes("/wallet/info")) {
141+
return new Response(JSON.stringify({ address: BANKR_ADDRESS }), {
142+
status: 200,
143+
})
144+
}
145+
if (typeof url === "string" && url.includes("/wallet/sign")) {
146+
return new Response(JSON.stringify({ signature: "0xdeadbeef" }), {
147+
status: 200,
148+
})
149+
}
150+
// Tool endpoint
151+
const headers = Object.fromEntries(
152+
Object.entries(init?.headers ?? {}),
153+
) as Record<string, string>
154+
return new Response(JSON.stringify({ result: "ok", headers }), {
155+
status: 200,
156+
})
157+
})
158+
}
159+
160+
describe("auth command with --bankr-key", () => {
161+
it("uses Bankr signer when BANKR_API_KEY is set", async () => {
162+
const fetchMock = stubBankrFetch()
163+
vi.stubGlobal("fetch", fetchMock)
164+
165+
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {})
166+
process.env.BANKR_API_KEY = "test-bankr-key"
167+
168+
const { authCommand } = await import("../cli/commands/auth.js")
169+
170+
await authCommand.parseAsync([
171+
"node",
172+
"auth",
173+
"https://tool.example.com/api",
174+
"--body",
175+
"{}",
176+
])
177+
178+
// Should have called /wallet/info first
179+
expect(fetchMock.mock.calls[0][0]).toContain("/wallet/info")
180+
181+
// Should print the Bankr address
182+
const output = logSpy.mock.calls.map(c => c.join(" ")).join("\n")
183+
expect(output.toLowerCase()).toContain(BANKR_ADDRESS.toLowerCase())
184+
185+
logSpy.mockRestore()
186+
})
187+
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+
})
194+
195+
process.env.TOOL_SDK_PRIVATE_KEY = PRIVATE_KEY
196+
process.env.BANKR_API_KEY = "test-bankr-key"
197+
198+
const { authCommand } = await import("../cli/commands/auth.js")
199+
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")
209+
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)
213+
})
214+
215+
it("shows updated error when neither key is provided", async () => {
216+
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {})
217+
vi.spyOn(console, "log").mockImplementation(() => {})
218+
const exitSpy = vi.spyOn(process, "exit").mockImplementation(() => {
219+
throw new Error("process.exit")
220+
})
221+
222+
const { authCommand } = await import("../cli/commands/auth.js")
223+
224+
await expect(
225+
authCommand.parseAsync([
226+
"node",
227+
"auth",
228+
"https://tool.example.com/api",
229+
"--body",
230+
"{}",
231+
]),
232+
).rejects.toThrow("process.exit")
233+
234+
const output = errorSpy.mock.calls.map(c => c.join(" ")).join("\n")
235+
expect(output).toContain("TOOL_SDK_PRIVATE_KEY")
236+
expect(output).toContain("BANKR_API_KEY")
237+
expect(exitSpy).toHaveBeenCalledWith(1)
135238
})
136239
})

src/__tests__/deploy.test.ts

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ import {
1212
import {
1313
execCmd,
1414
extractDeploymentUrl,
15+
isSensitiveEnvVar,
1516
parseEnvExample,
17+
tryRecoverDeployUrl,
1618
} from "../cli/commands/deploy.js"
1719

1820
const fixturesDir = join(import.meta.dirname, "__fixtures_deploy__")
@@ -124,6 +126,45 @@ describe("extractDeploymentUrl", () => {
124126
})
125127
})
126128

129+
describe("isSensitiveEnvVar", () => {
130+
it.each([
131+
"OPENSEA_API_KEY",
132+
"ANTHROPIC_API_KEY",
133+
"MY_SECRET",
134+
"AUTH_TOKEN",
135+
"DB_PASSWORD",
136+
"WALLET_PRIVATE",
137+
])("should return true for sensitive var %s", name => {
138+
expect(isSensitiveEnvVar(name)).toBe(true)
139+
})
140+
141+
it.each([
142+
"CREATOR_ADDRESS",
143+
"TOOL_ENDPOINT",
144+
"DATABASE_URL",
145+
"NODE_ENV",
146+
"PORT",
147+
"KEYBOARD_LAYOUT",
148+
])("should return false for non-sensitive var %s", name => {
149+
expect(isSensitiveEnvVar(name)).toBe(false)
150+
})
151+
152+
it("should be case-insensitive", () => {
153+
expect(isSensitiveEnvVar("my_api_key")).toBe(true)
154+
expect(isSensitiveEnvVar("My_Secret")).toBe(true)
155+
expect(isSensitiveEnvVar("auth_token")).toBe(true)
156+
})
157+
158+
it("should only match at the end of the name", () => {
159+
expect(isSensitiveEnvVar("SECRET_NAME")).toBe(false)
160+
expect(isSensitiveEnvVar("TOKEN_EXPIRY")).toBe(false)
161+
expect(isSensitiveEnvVar("PASSWORD_RESET_URL")).toBe(false)
162+
expect(isSensitiveEnvVar("RESET_TOKEN_COUNT")).toBe(false)
163+
expect(isSensitiveEnvVar("MY_SECRET_NAME")).toBe(false)
164+
expect(isSensitiveEnvVar("MY_KEY_STORE")).toBe(false)
165+
})
166+
})
167+
127168
describe("execCmd", () => {
128169
it("should execute a simple command and return output", () => {
129170
const result = execCmd("echo hello", { silent: true })
@@ -139,3 +180,85 @@ describe("execCmd", () => {
139180
expect(result).toBe("test-input")
140181
})
141182
})
183+
184+
describe("tryRecoverDeployUrl", () => {
185+
afterEach(() => {
186+
vi.restoreAllMocks()
187+
})
188+
189+
it("should return undefined when no URL in error message", async () => {
190+
const result = await tryRecoverDeployUrl(
191+
"Something went wrong",
192+
fixturesDir,
193+
)
194+
expect(result).toBeUndefined()
195+
})
196+
197+
it("should return URL when manifest responds 200", async () => {
198+
const manifestDir = join(fixturesDir, "recover-200")
199+
mkdirSync(join(manifestDir, "src"), { recursive: true })
200+
writeFileSync(join(manifestDir, "src", "manifest.ts"), "")
201+
writeFileSync(
202+
join(manifestDir, "package.json"),
203+
JSON.stringify({ name: "my-tool" }),
204+
)
205+
206+
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ status: 200 }))
207+
208+
const result = await tryRecoverDeployUrl(
209+
"Error: command exited with code 1\nhttps://my-tool-abc123.vercel.app\nsome other output",
210+
manifestDir,
211+
)
212+
expect(result).toBe("https://my-tool-abc123.vercel.app")
213+
})
214+
215+
it("should return undefined when manifest responds non-200", async () => {
216+
const manifestDir = join(fixturesDir, "recover-404")
217+
mkdirSync(join(manifestDir, "src"), { recursive: true })
218+
writeFileSync(join(manifestDir, "src", "manifest.ts"), "")
219+
writeFileSync(
220+
join(manifestDir, "package.json"),
221+
JSON.stringify({ name: "my-tool" }),
222+
)
223+
224+
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ status: 404 }))
225+
226+
const result = await tryRecoverDeployUrl(
227+
"Error: failed\nhttps://my-tool-abc123.vercel.app",
228+
manifestDir,
229+
)
230+
expect(result).toBeUndefined()
231+
})
232+
233+
it("should return URL when manifest URL cannot be derived", async () => {
234+
const emptyDir = join(fixturesDir, "recover-no-manifest")
235+
mkdirSync(emptyDir, { recursive: true })
236+
237+
const result = await tryRecoverDeployUrl(
238+
"Error: exit 1\nhttps://my-tool-abc123.vercel.app",
239+
emptyDir,
240+
)
241+
expect(result).toBe("https://my-tool-abc123.vercel.app")
242+
})
243+
244+
it("should return undefined when fetch throws", async () => {
245+
const manifestDir = join(fixturesDir, "recover-fetch-err")
246+
mkdirSync(join(manifestDir, "src"), { recursive: true })
247+
writeFileSync(join(manifestDir, "src", "manifest.ts"), "")
248+
writeFileSync(
249+
join(manifestDir, "package.json"),
250+
JSON.stringify({ name: "my-tool" }),
251+
)
252+
253+
vi.stubGlobal(
254+
"fetch",
255+
vi.fn().mockRejectedValue(new Error("network error")),
256+
)
257+
258+
const result = await tryRecoverDeployUrl(
259+
"Error: failed\nhttps://my-tool-abc123.vercel.app",
260+
manifestDir,
261+
)
262+
expect(result).toBeUndefined()
263+
})
264+
})

0 commit comments

Comments
 (0)