Skip to content

Commit a988502

Browse files
ryanioCodySearsOSdevin-ai-integration[bot]
committed
Release v1.12.0
Origin-SHA: f6f7bb6935b34b86858e8a800e61e4a225ee7bcc Co-authored-by: Cody <132297275+CodySearsOS@users.noreply.github.com> Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent 25466ea commit a988502

37 files changed

Lines changed: 2317 additions & 663 deletions

.agents/rules.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ CLI (src/cli.ts) SDK (src/sdk.ts)
5353
| `src/parse.ts` | Shared `--body <path>` JSON file reading utility. |
5454
| `src/health.ts` | Health-check logic used by the `health` command. |
5555
| `src/wallet/` | Wallet adapter re-exports from `@opensea/wallet-adapters` and chain ID resolution. |
56+
| `src/auth/store.ts` | Persists auth tokens to `~/.opensea/auth.json` (used by both `auth` and `login` commands). |
57+
| `src/auth/oauth-login.ts` | OAuth 2.1 authorization-code + PKCE loopback helper (used by the `login` command). |
5658
| `src/types/api.ts` | TypeScript interfaces matching OpenSea API v2 response shapes. |
5759
| `src/types/index.ts` | Re-exports API types plus internal config types. |
5860

@@ -72,7 +74,7 @@ Each domain has both a CLI command file (`src/commands/<domain>.ts`) and an SDK
7274

7375
- **accounts** - Account profile lookup
7476
- **assets** - Asset movement transactions (transfers)
75-
- **auth** - API key request and management
77+
- **auth** - SIWE login and scoped token management (`login`, `status`, `refresh`, `revoke`, `tokens`, `scopes`, `clear`) plus API key requests (`request-key`)
7678
- **chains** - Chain information and supported networks
7779
- **collections** - Collection metadata, stats, traits
7880
- **drops** - NFT drop details, listing, and minting
@@ -85,7 +87,9 @@ Each domain has both a CLI command file (`src/commands/<domain>.ts`) and an SDK
8587
- **swaps** - Token swap quotes
8688
- **token-groups** - Token group details and listings
8789
- **tokens** - Fungible token trending/top/details
90+
- **tools** - Search, list, and inspect registered AI agent tools (ERC-8257)
8891
- **transactions** - Transaction status and receipts
92+
- **login** - Keyless OAuth 2.1 (authorization-code + PKCE) login against the OpenSea authorization server; falls back to device flow for headless environments
8993
- **wallet** - Inspect the active wallet adapter's security posture
9094

9195
## Conventions
@@ -121,7 +125,7 @@ import type { SomeType } from "../types/index.js"
121125

122126
export function domainCommand(
123127
getClient: () => OpenSeaClient,
124-
getFormat: () => "json" | "table",
128+
getFormat: () => "json" | "table" | "toon",
125129
): Command {
126130
const cmd = new Command("domain").description("Description")
127131

@@ -246,10 +250,7 @@ All new code must include tests with coverage:
246250

247251
### CI (GitHub Actions)
248252

249-
The CI workflow (`.github/workflows/ci.yml`) runs on all PRs and pushes to `main` with two parallel jobs:
250-
251-
- **lint** job: `format:check`, `lint`, `type-check`
252-
- **test** job: `vitest run --coverage`
253+
The CI workflow (`.github/workflows/ci.yml`) runs on all PRs and pushes to `main` in a single `build-and-test` job that runs, in order: `format:check`, `lint`, `type-check`, `test`, `build`.
253254

254255
All CI checks must pass before merging.
255256

CHANGELOG.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,55 @@
11
# @opensea/cli
22

3+
## 1.12.0
4+
5+
### Minor Changes
6+
7+
- df2b152: Add `opensea auth link-wallet` for public SIWX wallet-link flows.
8+
- 4bef9a5: Add `opensea api request` for authenticated API v2 GET, POST, PUT, PATCH, and DELETE requests.
9+
10+
### Patch Changes
11+
12+
- 6096c0d: Deduplicate Commander option definitions with shared builders while preserving command behavior.
13+
- a917e48: Request the generated public API scope set during OAuth login so stale or deferred project roles cannot break the default flow.
14+
- 2459068: Align wallet-auth scope metadata with the production OpenAPI specification.
15+
- Updated dependencies [df2b152]
16+
- Updated dependencies [2459068]
17+
- Updated dependencies [0df96eb]
18+
- Updated dependencies [4bef9a5]
19+
- @opensea/api-types@0.8.0
20+
- @opensea/sdk@11.4.0
21+
22+
## 1.11.0
23+
24+
### Minor Changes
25+
26+
- ef89be8: Add auth commands (login, status, refresh, revoke, tokens, scopes, clear) with token persistence in `~/.opensea/auth.json`. Support `--auth-token` and `--auth-base-url` global options.
27+
- e61a57c: Add keyless `opensea login` command using OAuth 2.1 (authorization-code + PKCE via a loopback redirect, with a device authorization fallback for headless environments). No private key or SIWE signing required. The resulting scoped token is written to the shared `~/.opensea/auth.json` store so every other command picks it up transparently. Configure the public client via `--client-id` or `OPENSEA_OAUTH_CLIENT_ID`.
28+
- c460fc1: Add wallet trading P&L commands: `accounts pnl`, `accounts closed-positions`,
29+
and `accounts token-transfers` (the last requires `--contract-address` and
30+
`--chain`).
31+
32+
### Patch Changes
33+
34+
- Updated dependencies [ef89be8]
35+
- Updated dependencies [b816727]
36+
- Updated dependencies [e61a57c]
37+
- Updated dependencies [c9d8cb1]
38+
- Updated dependencies [ef89be8]
39+
- Updated dependencies [e59df7f]
40+
- Updated dependencies [c460fc1]
41+
- Updated dependencies [c460fc1]
42+
- Updated dependencies [ef89be8]
43+
- @opensea/sdk@11.2.0
44+
- @opensea/api-types@0.6.0
45+
46+
## 1.10.1
47+
48+
### Patch Changes
49+
50+
- Updated dependencies
51+
- @opensea/api-types@0.5.0
52+
353
## 1.10.0
454

555
### Minor Changes

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,19 @@ opensea --api-key your-api-key collections get mfers
5151

5252
Get an API key instantly via the command above, or get a full key at [opensea.io/settings/developer](https://opensea.io/settings/developer) for higher rate limits. See [API key docs](https://docs.opensea.io/reference/api-keys) for details.
5353

54+
Wallet-authenticated endpoints also require a scoped token:
55+
56+
```bash
57+
opensea login
58+
opensea auth status
59+
opensea api request GET /api/v2/account/0xYOUR_WALLET/favorites --params '{"limit":1}'
60+
```
61+
62+
Use `--scopes` with `opensea login` to request a narrower scope set. The `api
63+
request` command supports GET, POST, PUT, PATCH, and DELETE plus JSON body files,
64+
so newly published scoped endpoints are available without waiting for a dedicated
65+
command.
66+
5467
## Quick Start
5568

5669
```bash
@@ -89,6 +102,7 @@ opensea --format table collections stats mfers
89102
| `tokens` | Get trending tokens, top tokens, and token details |
90103
| `swaps` | Get swap quotes for token trading |
91104
| `accounts` | Get account details |
105+
| `api request` | Call any API v2 endpoint with the active API key and scoped token |
92106

93107
Global options: `--api-key`, `--chain` (default: ethereum), `--format` (json/table/toon), `--base-url`
94108

biome.json

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,23 @@
11
{
22
"$schema": "node_modules/@biomejs/biome/configuration_schema.json",
3+
"vcs": {
4+
"enabled": true,
5+
"clientKind": "git",
6+
"useIgnoreFile": true
7+
},
38
"files": {
49
"includes": [
510
"**/*.ts",
611
"**/*.js",
712
"**/*.json",
8-
"!**/node_modules",
13+
"!**/.claude",
14+
"!**/typechain",
915
"!**/dist",
10-
"!**/lib",
1116
"!**/coverage",
12-
"!**/.nyc_output",
13-
"!packages/sdk/src/typechain",
14-
"!**/typechain",
15-
"!packages/api-types/src/generated.ts",
16-
"!**/pnpm-lock.yaml",
17-
"!**/package-lock.json"
17+
"!**/.vercel",
18+
"!**/package-lock.json",
19+
"!**/src/generated.ts",
20+
"!**/src/*-generated.ts"
1821
]
1922
},
2023
"formatter": {
@@ -35,7 +38,7 @@
3538
"linter": {
3639
"enabled": true,
3740
"rules": {
38-
"recommended": true,
41+
"preset": "recommended",
3942
"suspicious": {
4043
"noConsole": "off",
4144
"noExplicitAny": "off"

package.json

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@opensea/cli",
3-
"version": "1.10.0",
3+
"version": "1.12.0",
44
"type": "module",
55
"description": "OpenSea CLI - Query the OpenSea API from the command line or programmatically",
66
"main": "dist/index.js",
@@ -14,17 +14,18 @@
1414
"scripts": {
1515
"build": "tsup",
1616
"dev": "tsup --watch",
17-
"format": "biome format --config-path . --write .",
18-
"format:check": "biome format --config-path . .",
19-
"lint": "biome check --config-path . .",
20-
"lint:fix": "biome check --config-path . --write .",
17+
"format": "biome format --write .",
18+
"format:check": "biome format .",
19+
"lint": "biome check .",
20+
"lint:fix": "biome check --write .",
2121
"type-check": "tsc --noEmit",
2222
"test": "vitest run",
2323
"prepublishOnly": "npm run build"
2424
},
2525
"dependencies": {
26-
"@opensea/api-types": "^0.4.4",
27-
"@opensea/wallet-adapters": "^0.3.0",
26+
"@opensea/api-types": "^0.8.0",
27+
"@opensea/sdk": "^11.4.0",
28+
"@opensea/wallet-adapters": "^0.3.2",
2829
"commander": "^14.0.3",
2930
"zod": "^4.3.6"
3031
},
@@ -34,7 +35,7 @@
3435
"tsup": "^8.3.0",
3536
"typescript": "^6.0.2",
3637
"vitest": "^3.2.4",
37-
"@biomejs/biome": "^2.4.10"
38+
"@biomejs/biome": "^2.5.2"
3839
},
3940
"engines": {
4041
"node": ">=18.0.0"

src/auth/oauth-login.ts

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import { createServer } from "node:http"
2+
import type { AddressInfo } from "node:net"
3+
import type { OpenSeaOAuth } from "@opensea/sdk"
4+
5+
/**
6+
* Loopback IP the CLI binds for the OAuth redirect (RFC 8252 §7.3). Using the
7+
* literal `127.0.0.1` (not `localhost`) avoids DNS rebinding and matches the
8+
* loopback redirect URIs pre-registered on the public client.
9+
*/
10+
const LOOPBACK_HOST = "127.0.0.1"
11+
12+
const CALLBACK_PATH = "/callback"
13+
14+
/**
15+
* Run the authorization-code + PKCE flow over a temporary loopback HTTP server.
16+
*
17+
* Spins up `http://127.0.0.1:<random-port>/callback`, opens the browser to the
18+
* authorization endpoint, waits for the redirect, validates `state`, and
19+
* exchanges the code for tokens. No client secret, no private key.
20+
*/
21+
export async function loginWithLoopback(
22+
oauth: OpenSeaOAuth,
23+
options: {
24+
scopes: string[]
25+
openBrowser: (url: string) => void | Promise<void>
26+
timeoutMs?: number
27+
},
28+
): Promise<import("@opensea/sdk").OAuthToken> {
29+
const { server, port } = await startServer()
30+
const redirectUri = `http://${LOOPBACK_HOST}:${port}${CALLBACK_PATH}`
31+
32+
try {
33+
const request = await oauth.createAuthorizationRequest({
34+
redirectUri,
35+
scopes: options.scopes,
36+
})
37+
38+
const codePromise = waitForCode(server, request.state, options.timeoutMs)
39+
await options.openBrowser(request.url)
40+
const code = await codePromise
41+
42+
return await oauth.exchangeCode({
43+
code,
44+
codeVerifier: request.codeVerifier,
45+
redirectUri,
46+
})
47+
} finally {
48+
server.close()
49+
}
50+
}
51+
52+
function startServer(): Promise<{
53+
server: import("node:http").Server
54+
port: number
55+
}> {
56+
return new Promise((resolve, reject) => {
57+
const server = createServer()
58+
server.on("error", reject)
59+
server.listen(0, LOOPBACK_HOST, () => {
60+
const address = server.address() as AddressInfo
61+
resolve({ server, port: address.port })
62+
})
63+
})
64+
}
65+
66+
/** Default time to wait for the browser redirect before giving up. */
67+
const DEFAULT_TIMEOUT_MS = 5 * 60_000
68+
69+
function waitForCode(
70+
server: import("node:http").Server,
71+
expectedState: string,
72+
timeoutMs = DEFAULT_TIMEOUT_MS,
73+
): Promise<string> {
74+
return new Promise((resolve, reject) => {
75+
// Guard against a late or duplicate callback settling the promise twice
76+
// and against handling requests after we've already resolved/rejected.
77+
let settled = false
78+
const timer = setTimeout(() => {
79+
if (settled) return
80+
settled = true
81+
server.removeAllListeners("request")
82+
reject(new Error("Timed out waiting for authorization redirect"))
83+
}, timeoutMs)
84+
85+
server.on("request", (req, res) => {
86+
const url = new URL(req.url ?? "/", `http://${LOOPBACK_HOST}`)
87+
if (url.pathname !== CALLBACK_PATH) {
88+
res.writeHead(404).end()
89+
return
90+
}
91+
if (settled) {
92+
res.writeHead(404).end()
93+
return
94+
}
95+
96+
const error = url.searchParams.get("error")
97+
const code = url.searchParams.get("code")
98+
const state = url.searchParams.get("state")
99+
100+
const finish = (message: string, ok: boolean) => {
101+
res.writeHead(ok ? 200 : 400, { "Content-Type": "text/html" })
102+
res.end(renderResultPage(message, ok))
103+
}
104+
const settle = () => {
105+
settled = true
106+
clearTimeout(timer)
107+
}
108+
109+
if (error) {
110+
finish(`Authorization failed: ${error}`, false)
111+
settle()
112+
reject(new Error(`Authorization failed: ${error}`))
113+
return
114+
}
115+
if (state !== expectedState) {
116+
finish("Authorization failed: state mismatch", false)
117+
settle()
118+
reject(new Error("State mismatch in authorization response"))
119+
return
120+
}
121+
if (!code) {
122+
finish("Authorization failed: missing code", false)
123+
settle()
124+
reject(new Error("No authorization code in redirect"))
125+
return
126+
}
127+
128+
finish(
129+
"Login successful. You can close this tab and return to the CLI.",
130+
true,
131+
)
132+
settle()
133+
resolve(code)
134+
})
135+
})
136+
}
137+
138+
function renderResultPage(message: string, ok: boolean): string {
139+
const color = ok ? "#2081e2" : "#e23b2b"
140+
return `<!doctype html><html><head><meta charset="utf-8"><title>OpenSea CLI</title></head><body style="font-family:system-ui,sans-serif;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;background:#f5f7fb"><div style="text-align:center"><h2 style="color:${color}">OpenSea</h2><p>${escapeHtml(message)}</p></div></body></html>`
141+
}
142+
143+
/**
144+
* Escape a string for safe interpolation into HTML text content. The OAuth
145+
* `error` param is attacker-influenced (it comes from the redirect query
146+
* string), so it must be encoded before it reaches the result page.
147+
*/
148+
function escapeHtml(value: string): string {
149+
return value
150+
.replace(/&/g, "&amp;")
151+
.replace(/</g, "&lt;")
152+
.replace(/>/g, "&gt;")
153+
.replace(/"/g, "&quot;")
154+
.replace(/'/g, "&#39;")
155+
}

0 commit comments

Comments
 (0)