Skip to content

Commit 34a07dc

Browse files
committed
Release v1.5.0
1 parent 7ffeb6a commit 34a07dc

10 files changed

Lines changed: 487 additions & 2 deletions

File tree

CHANGELOG.md

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

3+
## 1.5.0
4+
5+
### Minor Changes
6+
7+
- 9ecf704: Provider-aware wallet hardening across Privy, Turnkey, Fireblocks, and Bankr.
8+
9+
**`@opensea/wallet-adapters`**
10+
11+
- New `WalletInfo` discriminated union exported.
12+
- New optional `getWalletInfo()` method on `WalletAdapter` (implemented by all four managed providers).
13+
- Privy adapter: optional `PRIVY_AUTH_SIGNING_KEY` env var enables `privy-authorization-signature` header on `/rpc` requests via `@privy-io/node` (added as optional peer dependency), supporting the `owner_id` + `additional_signer` hardening pattern.
14+
- Privy adapter: `personal_sign` now sends `params.encoding` ("utf-8" / "hex") to satisfy Privy's RPC schema (was previously omitting this and getting 400s on owner-gated wallets).
15+
- Privy adapter: 401 errors with `Invalid app ID or app secret` body now include a `printf %s` hint for the `echo` vs `echo -n` debugging dead-end.
16+
- Top-of-file security-model docstrings on all four adapters declaring signing-only intent and forbidding mutation surfaces.
17+
18+
**`@opensea/cli`**
19+
20+
- New `opensea wallet` command group with three subcommands:
21+
- `wallet info` — provider-aware posture readout, hardening warnings to stderr, structured info to stdout.
22+
- `wallet create` — Privy-only, `POST /v1/wallets`. Optional `--owner-public-key` registers an `owner_id` at create time. Narrow mutation surface: creates new resources only.
23+
- `wallet generate-auth-key` — pure-local P-256 keypair generation, no API calls.
24+
25+
### Patch Changes
26+
27+
- Updated dependencies [9ecf704]
28+
- @opensea/wallet-adapters@0.3.0
29+
330
## 1.4.2
431

532
### Patch Changes

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@opensea/cli",
3-
"version": "1.4.2",
3+
"version": "1.5.0",
44
"type": "module",
55
"description": "OpenSea CLI - Query the OpenSea API from the command line or programmatically",
66
"main": "dist/index.js",
@@ -24,7 +24,7 @@
2424
},
2525
"dependencies": {
2626
"@opensea/api-types": "^0.2.3",
27-
"@opensea/wallet-adapters": "^0.2.1",
27+
"@opensea/wallet-adapters": "^0.3.0",
2828
"commander": "^14.0.3",
2929
"zod": "^4.3.6"
3030
},

src/cli.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
swapsCommand,
1616
tokenGroupsCommand,
1717
tokensCommand,
18+
walletCommand,
1819
} from "./commands/index.js"
1920
import { type OutputFormat, setOutputOptions } from "./output.js"
2021
import { parseIntOption } from "./parse.js"
@@ -132,6 +133,7 @@ program.addCommand(
132133
program.addCommand(searchCommand(getClient, getFormat))
133134
program.addCommand(swapsCommand(getClient, getFormat))
134135
program.addCommand(healthCommand(getClient, getFormat))
136+
program.addCommand(walletCommand(getFormat))
135137

136138
async function main() {
137139
try {

src/commands/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ export { searchCommand } from "./search.js"
1212
export { swapsCommand } from "./swaps.js"
1313
export { tokenGroupsCommand } from "./token-groups.js"
1414
export { tokensCommand } from "./tokens.js"
15+
export { walletCommand } from "./wallet.js"

src/commands/wallet.ts

Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
import { generateKeyPairSync } from "node:crypto"
2+
import { createWalletFromEnv, type WalletInfo } from "@opensea/wallet-adapters"
3+
import { Command } from "commander"
4+
import type { OutputFormat } from "../output.js"
5+
import { formatOutput } from "../output.js"
6+
7+
const PRIVY_API_BASE = "https://api.privy.io"
8+
9+
/**
10+
* `opensea wallet info` reports the security posture of the active
11+
* wallet adapter — what credential is in env, what hardening is in
12+
* place, what is missing. Read-only. Provider-aware.
13+
*
14+
* Warnings go to stderr; data goes to stdout. Exit code is 0 on
15+
* success regardless of warnings; nonzero only on auth/network
16+
* failure.
17+
*/
18+
export function walletCommand(getFormat: () => OutputFormat): Command {
19+
const cmd = new Command("wallet").description(
20+
"Inspect the active wallet adapter's security posture",
21+
)
22+
23+
cmd
24+
.command("generate-auth-key")
25+
.description(
26+
"Generate a P-256 keypair for use as a Privy authorization key. " +
27+
"Pure-local — no API calls. The private key is for the agent " +
28+
"(PRIVY_AUTH_SIGNING_KEY) or for off-machine storage if registering " +
29+
"as owner_id; the public key is what you register with Privy.",
30+
)
31+
.action(() => {
32+
const { publicKey, privateKey } = generateKeyPairSync("ec", {
33+
namedCurve: "P-256",
34+
})
35+
const privatePkcs8 = privateKey
36+
.export({ type: "pkcs8", format: "der" })
37+
.toString("base64")
38+
const publicSpki = publicKey
39+
.export({ type: "spki", format: "der" })
40+
.toString("base64")
41+
console.log(
42+
formatOutput(
43+
{
44+
privateKey: privatePkcs8,
45+
publicKey: publicSpki,
46+
format: "PKCS8 (private) / SPKI (public), base64-encoded P-256",
47+
usage:
48+
"Register publicKey with Privy as additional_signer or owner. " +
49+
"Set privateKey as PRIVY_AUTH_SIGNING_KEY (additional_signer) " +
50+
"OR keep it OFF the agent host (owner). Never put both keys " +
51+
"on the agent.",
52+
},
53+
getFormat(),
54+
),
55+
)
56+
})
57+
58+
cmd
59+
.command("create")
60+
.description(
61+
"Create a new Privy server wallet. Privy-only. Creates a NEW " +
62+
"resource — cannot touch existing wallets. New wallet has no " +
63+
"funds and no policy until separately configured. Reads " +
64+
"PRIVY_APP_ID and PRIVY_APP_SECRET from env.",
65+
)
66+
.option(
67+
"--chain-type <type>",
68+
"Chain type for the wallet (default: ethereum)",
69+
"ethereum",
70+
)
71+
.option(
72+
"--owner-public-key <base64>",
73+
"Optional SPKI base64-encoded P-256 public key to register as " +
74+
"owner. Recommended: pass the public key from `opensea wallet " +
75+
"generate-auth-key` (whose private half you keep OFF this " +
76+
"machine). Without an owner_id, env credentials can rewrite the " +
77+
"wallet's policy unilaterally.",
78+
)
79+
.action(async (options: { chainType: string; ownerPublicKey?: string }) => {
80+
const appId = process.env.PRIVY_APP_ID
81+
const appSecret = process.env.PRIVY_APP_SECRET
82+
if (!appId || !appSecret) {
83+
console.error(
84+
"PRIVY_APP_ID and PRIVY_APP_SECRET must be set in env to create " +
85+
"a Privy server wallet.",
86+
)
87+
process.exit(1)
88+
return
89+
}
90+
try {
91+
const baseUrl = process.env.PRIVY_API_BASE_URL ?? PRIVY_API_BASE
92+
const credentials = Buffer.from(`${appId}:${appSecret}`).toString(
93+
"base64",
94+
)
95+
const body: Record<string, unknown> = { chain_type: options.chainType }
96+
if (options.ownerPublicKey) {
97+
body.owner = { public_key: options.ownerPublicKey }
98+
}
99+
const response = await fetch(`${baseUrl}/v1/wallets`, {
100+
method: "POST",
101+
headers: {
102+
Authorization: `Basic ${credentials}`,
103+
"privy-app-id": appId,
104+
"Content-Type": "application/json",
105+
},
106+
body: JSON.stringify(body),
107+
})
108+
if (!response.ok) {
109+
const errorBody = await response.text()
110+
const hint =
111+
response.status === 401 &&
112+
errorBody.includes("Invalid app ID or app secret")
113+
? " (hint: if you tested via curl, use 'printf %s \"$id:$secret\" | base64' — 'echo' adds a trailing newline that breaks basic auth)"
114+
: ""
115+
console.error(
116+
JSON.stringify(
117+
{
118+
error: "Wallet create failed",
119+
status: response.status,
120+
body: errorBody + hint,
121+
},
122+
null,
123+
2,
124+
),
125+
)
126+
process.exit(1)
127+
}
128+
const data = (await response.json()) as {
129+
id: string
130+
address: string
131+
chain_type: string
132+
owner_id?: string | null
133+
}
134+
if (!options.ownerPublicKey) {
135+
console.error(
136+
"WARNING: created without --owner-public-key. The wallet has " +
137+
"no owner_id and the credentials in env can rewrite its " +
138+
"policy unilaterally. Register an owner before applying a " +
139+
"policy or funding the wallet (see " +
140+
"https://github.com/ProjectOpenSea/opensea-skill/blob/main/docs/policy-administration.md).",
141+
)
142+
}
143+
console.log(
144+
formatOutput(
145+
{
146+
id: data.id,
147+
address: data.address,
148+
chainType: data.chain_type,
149+
ownerId: data.owner_id ?? null,
150+
nextSteps: options.ownerPublicKey
151+
? "Set PRIVY_WALLET_ID to this id, apply a policy via " +
152+
"https://github.com/ProjectOpenSea/opensea-skill/blob/main/docs/policy-administration.md, fund the wallet, then " +
153+
"run `opensea wallet info`."
154+
: "Register an owner_id BEFORE funding. See " +
155+
"https://github.com/ProjectOpenSea/opensea-skill/blob/main/docs/policy-administration.md and opensea-wallet/references/wallet-setup.md.",
156+
},
157+
getFormat(),
158+
),
159+
)
160+
} catch (error) {
161+
console.error(
162+
JSON.stringify(
163+
{ error: "Wallet error", message: (error as Error).message },
164+
null,
165+
2,
166+
),
167+
)
168+
process.exit(1)
169+
}
170+
})
171+
172+
cmd
173+
.command("info")
174+
.description(
175+
"Show wallet address, policy/role posture, and hardening warnings",
176+
)
177+
.action(async () => {
178+
try {
179+
const adapter = createWalletFromEnv()
180+
if (!adapter.getWalletInfo) {
181+
console.error(
182+
`Provider ${adapter.name} does not expose wallet info. ` +
183+
"This is expected for the private-key adapter.",
184+
)
185+
const address = await adapter.getAddress()
186+
console.log(
187+
formatOutput({ provider: adapter.name, address }, getFormat()),
188+
)
189+
return
190+
}
191+
const info = await adapter.getWalletInfo()
192+
for (const warning of warningsForInfo(info)) {
193+
console.error(`WARNING: ${warning}`)
194+
}
195+
console.log(formatOutput(info, getFormat()))
196+
} catch (error) {
197+
console.error(
198+
JSON.stringify(
199+
{
200+
error: "Wallet error",
201+
message: (error as Error).message,
202+
},
203+
null,
204+
2,
205+
),
206+
)
207+
process.exit(1)
208+
}
209+
})
210+
211+
return cmd
212+
}
213+
214+
/**
215+
* Translate provider-specific posture flags into human-readable
216+
* warnings. Each warning maps to a documented hardening step in
217+
* `packages/skill/opensea-wallet/references/wallet-setup.md` and
218+
* `https://github.com/ProjectOpenSea/opensea-skill/blob/main/docs/policy-administration.md`.
219+
*/
220+
function warningsForInfo(info: WalletInfo): string[] {
221+
switch (info.provider) {
222+
case "privy": {
223+
const out: string[] = []
224+
if (!info.ownerEnforcesAuthKey) {
225+
out.push(
226+
"Wallet has no owner_id — the credentials in env can rewrite " +
227+
"policy unilaterally. Register an authorization key on the " +
228+
"wallet (see https://github.com/ProjectOpenSea/opensea-skill/blob/main/docs/policy-administration.md and " +
229+
"opensea-wallet/references/wallet-setup.md, Privy section).",
230+
)
231+
}
232+
if (info.policyIds.length === 0) {
233+
out.push(
234+
"Wallet has no policy_ids — there is no on-chain spend " +
235+
"enforcement. Apply a per-tx cap policy (see " +
236+
"opensea-wallet/references/wallet-policies.md).",
237+
)
238+
}
239+
return out
240+
}
241+
case "turnkey": {
242+
const out: string[] = []
243+
if (info.isRootUser) {
244+
out.push(
245+
`API user "${info.username || info.userId}" is in the root ` +
246+
"quorum. Root users bypass Turnkey's policy engine entirely. " +
247+
"Create a non-root signer-only API user instead (see " +
248+
"opensea-wallet/references/wallet-setup.md, Turnkey section).",
249+
)
250+
}
251+
return out
252+
}
253+
case "fireblocks":
254+
return [
255+
"Fireblocks does not expose API-user role via API. Confirm at " +
256+
"console.fireblocks.io that this key has the `Signer` role " +
257+
"and not `Admin` or any broader role. Re-confirm whenever " +
258+
"the key is rotated.",
259+
]
260+
case "bankr":
261+
return [
262+
"Bankr does not expose API-key scope flags via API. Confirm at " +
263+
"bankr.bot/api that this key has appropriate readOnly / " +
264+
"allowedRecipients / allowedIps / daily-limit settings. " +
265+
"Re-confirm whenever the key is rotated.",
266+
]
267+
default: {
268+
const _exhaustive: never = info
269+
return _exhaustive
270+
}
271+
}
272+
}

src/wallet/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export type {
66
TransactionResult,
77
WalletAdapter,
88
WalletCapabilities,
9+
WalletInfo,
910
WalletProvider,
1011
} from "@opensea/wallet-adapters"
1112
export {

test/cli-api-error.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ vi.mock("../src/commands/index.js", () => ({
1717
tokenGroupsCommand: () => new Command("token-groups"),
1818
tokensCommand: () => new Command("tokens"),
1919
healthCommand: () => new Command("health"),
20+
walletCommand: () => new Command("wallet"),
2021
}))
2122

2223
const exitSpy = vi

test/cli-network-error.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ vi.mock("../src/commands/index.js", () => ({
1616
tokenGroupsCommand: () => new Command("token-groups"),
1717
tokensCommand: () => new Command("tokens"),
1818
healthCommand: () => new Command("health"),
19+
walletCommand: () => new Command("wallet"),
1920
}))
2021

2122
const exitSpy = vi

test/cli-rate-limit.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ vi.mock("../src/commands/index.js", () => ({
1717
tokenGroupsCommand: () => new Command("token-groups"),
1818
tokensCommand: () => new Command("tokens"),
1919
healthCommand: () => new Command("health"),
20+
walletCommand: () => new Command("wallet"),
2021
}))
2122

2223
const exitSpy = vi

0 commit comments

Comments
 (0)