Skip to content

Commit f0e3115

Browse files
committed
Release v1.14.1
Origin-SHA: 52ef447c3567b0003ad94b3598e72066fe9cfa09
1 parent bc3a5ec commit f0e3115

14 files changed

Lines changed: 164 additions & 23 deletions

File tree

.agents/rules.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ CLI (src/cli.ts) SDK (src/sdk.ts)
5555
| `src/wallet/` | Wallet adapter re-exports from `@opensea/wallet-adapters` and chain ID resolution. |
5656
| `src/auth/store.ts` | Persists auth tokens to `~/.opensea/auth.json` (used by both `auth` and `login` commands). |
5757
| `src/auth/oauth-login.ts` | OAuth 2.1 authorization-code + PKCE loopback helper (used by the `login` command). |
58+
| `src/auth/siwe-login.ts` | SIWE sign-in helper (used by the `login` command for private-key server-side agents). |
59+
| `src/auth/private-key.ts` | Private-key resolution + wallet construction for SIWE login. |
5860
| `src/types/api.ts` | TypeScript interfaces matching OpenSea API v2 response shapes. |
5961
| `src/types/index.ts` | Re-exports API types plus internal config types. |
6062

@@ -82,7 +84,7 @@ Each domain has both a CLI command file (`src/commands/<domain>.ts`) and an SDK
8284
- **accounts** - Account profile lookup
8385
- **api** - Low-level escape hatch to call any OpenSea API v2 endpoint (CLI only — no SDK class)
8486
- **assets** - Asset movement transactions (transfers)
85-
- **auth** - SIWE login and scoped token management (`login`, `status`, `refresh`, `revoke`, `tokens`, `scopes`, `clear`) plus API key requests (`request-key`)
87+
- **auth** - SIWE login and scoped token management (`login`, `link-wallet`, `status`, `refresh`, `revoke`, `tokens`, `scopes`, `clear`) plus API key requests (`request-key`)
8688
- **chains** - Chain information and supported networks
8789
- **collections** - Collection metadata, stats, traits
8890
- **drops** - NFT drop details, listing, and minting
@@ -97,8 +99,9 @@ Each domain has both a CLI command file (`src/commands/<domain>.ts`) and an SDK
9799
- **tokens** - Fungible token trending/top/details
98100
- **tools** - Search, list, and inspect registered AI agent tools (ERC-8257)
99101
- **transactions** - Transaction status and receipts
100-
- **login** - Keyless OAuth 2.1 (authorization-code + PKCE) login against the OpenSea authorization server; falls back to device flow for headless environments
102+
- **login** - Keyless OAuth 2.1 (authorization-code + PKCE) login against the OpenSea authorization server, or SIWE sign-in with a private key (`--private-key`) for server-side agents; falls back to device flow (`--device`) for headless environments
101103
- **wallet** - Inspect the active wallet adapter's security posture
104+
- **whoami** - Show the current authenticated wallet, scopes, and project roles decoded from the persisted access token (CLI only — no SDK class)
102105

103106
## Conventions
104107

CHANGELOG.md

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

3+
## 1.14.1
4+
5+
### Patch Changes
6+
7+
- 06e96e1: Use the current SIWE session, scoped-token creation, token-exchange, session refresh, and session-only revocation endpoints in the SDK and CLI.
8+
- Updated dependencies [06e96e1]
9+
- Updated dependencies [feb1446]
10+
- @opensea/sdk@11.4.9
11+
- @opensea/api-types@0.8.2
12+
313
## 1.14.0
414

515
### Minor Changes

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,9 @@ server-side agents, set `OPENSEA_PRIVATE_KEY` and pass `--private-key` (or pass
6666
environment variable is recommended to keep the key out of shell history. The
6767
`api request` command supports GET, POST, PUT, PATCH, and DELETE plus JSON body
6868
files, so newly published scoped endpoints are available without waiting for a
69-
dedicated command.
69+
dedicated command. SIWE logins retain the session needed for session-only PAT
70+
revocation; `opensea auth revoke` refreshes that session before deleting the
71+
token.
7072

7173
## Quick Start
7274

package-lock.json

Lines changed: 11 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@opensea/cli",
3-
"version": "1.14.0",
3+
"version": "1.14.1",
44
"type": "module",
55
"description": "OpenSea CLI - Query the OpenSea API from the command line or programmatically",
66
"main": "dist/index.js",
@@ -23,8 +23,8 @@
2323
"prepublishOnly": "npm run build"
2424
},
2525
"dependencies": {
26-
"@opensea/api-types": "^0.8.1",
27-
"@opensea/sdk": "^11.4.8",
26+
"@opensea/api-types": "^0.8.2",
27+
"@opensea/sdk": "^11.4.9",
2828
"@opensea/wallet-adapters": "^0.3.3",
2929
"commander": "^14.0.3",
3030
"zod": "^4.3.6"

src/auth/siwe-login.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export interface SiweLoginResult {
4242
accessToken: string
4343
refreshToken: string
4444
scopedTokenId: string
45+
sessionCookie: string
4546
expiresAt: Date
4647
requestedScopes: string[]
4748
scopes: string[]
@@ -130,6 +131,26 @@ export async function exchangeScopedToken(
130131
return response.json() as Promise<ScopedTokenExchangeResponse>
131132
}
132133

134+
/**
135+
* Rotate a SIWE browser session and return the replacement cookies. Scoped
136+
* token management is session-only, so callers refresh before a management
137+
* request instead of trying to use the wallet JWT as an administrator token.
138+
*/
139+
export async function refreshSiweSession(
140+
baseUrl: string,
141+
cookie: string,
142+
): Promise<string> {
143+
const response = await fetch(`${baseUrl}/api/v2/auth/session/refresh`, {
144+
method: "POST",
145+
headers: { Cookie: cookie },
146+
})
147+
if (!response.ok) {
148+
const body = await response.text().catch(() => "")
149+
throw new Error(`Session refresh failed (${response.status}): ${body}`)
150+
}
151+
return sessionCookie(response.headers)
152+
}
153+
133154
/**
134155
* Authenticate with SIWE using a private key and exchange the resulting
135156
* scoped token for a wallet-bound access token.
@@ -166,6 +187,8 @@ export async function loginWithSiwe({
166187
const { nonce } = (await nonceRes.json()) as { nonce: string }
167188

168189
// 2. Build & sign SIWE message
190+
// Webauth uses SIWE only to establish the wallet session. PAT scopes are
191+
// authorized separately by the session-authenticated token request below.
169192
const message = createSiwxMessage({
170193
address,
171194
chainArch: "EVM",
@@ -239,6 +262,7 @@ export async function loginWithSiwe({
239262
accessToken: tokenData.accessToken,
240263
refreshToken: scopedToken.token,
241264
scopedTokenId: scopedToken.id,
265+
sessionCookie: cookie,
242266
expiresAt,
243267
requestedScopes: scopes,
244268
scopes: grantedScopes,

src/auth/store.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export interface StoredToken {
1717
accessToken: string
1818
refreshToken: string
1919
scopedTokenId?: string
20+
sessionCookie?: string
2021
expiresAt: string
2122
requestedScopes: string[]
2223
scopes: string[]
@@ -42,6 +43,7 @@ const storedTokenSchema = z
4243
accessToken: z.string().min(1),
4344
refreshToken: z.string().min(1),
4445
scopedTokenId: z.string().min(1).optional(),
46+
sessionCookie: z.string().min(1).optional(),
4547
expiresAt: z.iso.datetime(),
4648
requestedScopes: z.array(z.string().min(1)),
4749
scopes: z.array(z.string().min(1)),
@@ -57,6 +59,23 @@ const storedTokenSchema = z
5759
authMethod: z.enum(["oauth", "siwe"]),
5860
})
5961
.strict()
62+
.superRefine((token, context) => {
63+
if (token.authMethod !== "siwe") return
64+
if (!token.scopedTokenId) {
65+
context.addIssue({
66+
code: "custom",
67+
path: ["scopedTokenId"],
68+
message: "SIWE tokens require a scoped token id",
69+
})
70+
}
71+
if (!token.sessionCookie) {
72+
context.addIssue({
73+
code: "custom",
74+
path: ["sessionCookie"],
75+
message: "SIWE tokens require session cookies",
76+
})
77+
}
78+
})
6079

6180
const authStoreSchema = z
6281
.object({

src/commands/auth.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
DEFAULT_TOKEN_TTL_SECONDS,
1818
exchangeScopedToken,
1919
loginWithSiwe,
20+
refreshSiweSession,
2021
} from "../auth/siwe-login.js"
2122
import {
2223
clearTokens,
@@ -100,6 +101,7 @@ export function authCommand(
100101
accessToken: result.accessToken,
101102
refreshToken: result.refreshToken,
102103
scopedTokenId: result.scopedTokenId,
104+
sessionCookie: result.sessionCookie,
103105
expiresAt: result.expiresAt.toISOString(),
104106
requestedScopes: result.requestedScopes,
105107
scopes: result.scopes,
@@ -368,12 +370,25 @@ export function authCommand(
368370
}
369371
const baseUrl = getBaseUrl() ?? DEFAULT_BASE_URL
370372
let res: Response
371-
if (token.authMethod === "siwe" && token.scopedTokenId) {
373+
if (token.authMethod === "siwe") {
374+
if (!token.scopedTokenId || !token.sessionCookie) {
375+
throw new Error(
376+
"Stored SIWE login cannot manage its token. Run `opensea login --private-key` again.",
377+
)
378+
}
379+
const refreshedCookie = await refreshSiweSession(
380+
baseUrl,
381+
token.sessionCookie,
382+
)
383+
// Refresh tokens rotate. Persist the replacement before revocation so
384+
// a transient DELETE failure can be retried without reusing the old
385+
// session token.
386+
saveToken({ ...token, sessionCookie: refreshedCookie })
372387
res = await fetch(
373388
`${baseUrl}/api/v2/auth/tokens/${token.scopedTokenId}`,
374389
{
375390
method: "DELETE",
376-
headers: { Authorization: `Bearer ${token.accessToken}` },
391+
headers: { Cookie: refreshedCookie },
377392
},
378393
)
379394
} else {

src/commands/login.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ export function loginCommand(
123123
accessToken: result.accessToken,
124124
refreshToken: result.refreshToken,
125125
scopedTokenId: result.scopedTokenId,
126+
sessionCookie: result.sessionCookie,
126127
expiresAt: result.expiresAt.toISOString(),
127128
requestedScopes: scopes,
128129
scopes: result.scopes,

test/auth/store.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,13 +114,38 @@ describe("auth store", () => {
114114
...baseToken,
115115
authMethod: "siwe" as const,
116116
scopedTokenId: "381768924447939181",
117+
sessionCookie: "access_token=session; refresh_token=refresh",
117118
}
118119

119120
saveToken(siweToken)
120121

121122
expect(loadCurrentToken()).toEqual(siweToken)
122123
})
123124

125+
test("rejects SIWE stores without session management credentials", () => {
126+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {})
127+
mkdirSync(`${testHome}/.opensea`, { recursive: true })
128+
writeFileSync(
129+
`${testHome}/.opensea/auth.json`,
130+
JSON.stringify({
131+
currentAddress: "0xabc",
132+
tokens: {
133+
"0xabc": {
134+
...baseToken,
135+
address: "0xabc",
136+
authMethod: "siwe",
137+
scopedTokenId: "pat-id",
138+
},
139+
},
140+
}),
141+
)
142+
143+
expect(loadCurrentToken()).toBeUndefined()
144+
expect(warn).toHaveBeenCalledWith(
145+
expect.stringContaining("corrupted or incompatible"),
146+
)
147+
})
148+
124149
test("rejects prerelease stores missing requested scopes", () => {
125150
const warn = vi.spyOn(console, "warn").mockImplementation(() => {})
126151
mkdirSync(`${testHome}/.opensea`, { recursive: true })

0 commit comments

Comments
 (0)