Skip to content

Commit 4ab9585

Browse files
authored
chore: release v3.1.0 [minor] — debug interaction logs, vitest security patch, e2e gates + fork/npm leakage hardening (#726)
2 parents 55b2db2 + 59a40d3 commit 4ab9585

30 files changed

Lines changed: 5979 additions & 172 deletions
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
---
2+
name: stark-cdp-with-wallet
3+
description: Use when a task needs a real Braavos wallet driven end-to-end (unlock, connect, approve a transaction) against a live network like Starknet Sepolia, without a human clicking the extension popup. Triggers on requests to automate Braavos, drive a real wallet transaction, or record a browser demo that needs a connected wallet.
4+
---
5+
6+
# stark-cdp-with-wallet
7+
8+
## Overview
9+
10+
Proving the frontend half of a real Sepolia deploy needs an actual Braavos
11+
wallet extension connected and signing — `stark-smoke-test`'s devnet gate
12+
only exercises the burner wallet, never a real extension. This skill closes
13+
that gap with raw Chrome DevTools Protocol over WebSocket, **zero npm
14+
dependencies**, matching the in-repo precedent
15+
`.claude/skills/stark-smoke-test/stark-smoke-test-browser.mjs` (same CDP
16+
client shape, same `waitFor`/`openTab`/`connectSession` idiom).
17+
18+
This is an extraction, not a rewrite: every function here used to live
19+
inline in `stark-demo-web.mjs` (formerly `.claude/workflows/`). Splitting it
20+
out mirrors scaffold-stylus's `cdp-with-wallet` skill
21+
(`.claude/skills/cdp-with-wallet/` at `origin/release/v0.2.0` in
22+
scaffold-stylus) — same shape (`launch.mjs` for Chrome/profile management,
23+
a wallet-primitives file for unlock/connect/approve), adapted for Braavos
24+
instead of MetaMask. The blueprint technique for both is an internal
25+
Playwright e2e helper's `braavos.ts`: enumerate CDP targets from
26+
`GET http://localhost:<port>/json`, find the extension's own page by URL
27+
prefix, attach a WebSocket directly to it. Only the *technique* carries
28+
over — Braavos and MetaMask have different selectors, different popup
29+
shapes (a side-panel singleton vs. a fresh `notification.html` popup per
30+
request), and different discovery protocols (Wallet Standard vs. EIP-6963).
31+
32+
## Files
33+
34+
```
35+
.claude/skills/stark-cdp-with-wallet/
36+
SKILL.md this file
37+
launch.mjs Chrome launcher against the PERSISTENT debug profile
38+
(Braavos already installed there), CDP port/target helpers,
39+
profile-lock detection
40+
braavos.mjs Keychain password read, wallet setup-state check, unlock(),
41+
and approveBraavosRequest() (connect + tx-confirm — Braavos
42+
renders both through the same side-panel component)
43+
```
44+
45+
`stark-demo-web.mjs` (`.claude/skills/stark-demo-video/`) is the only current
46+
caller — it imports these primitives and supplies the app-specific parts
47+
(clicking this app's own "Connect Wallet" button, reading `deployedContracts.ts`,
48+
screencast/MP4 assembly). That split follows the same boundary as
49+
scaffold-stylus's `cdp-with-wallet` vs. `demo-video`: this skill only knows
50+
how to drive Braavos's own extension pages; it has no opinion about the
51+
dapp under test.
52+
53+
## Can-do / cannot-do
54+
55+
| Can do | Cannot do |
56+
|---|---|
57+
| Unlock an already-initialised Braavos vault given its password, via CDP `Input.insertText` (never the page's own `HTMLInputElement` value setter, which some wallet UIs scuttle) | Create a wallet, import a seed phrase, or otherwise initialise a vault — by design, not a limitation |
58+
| Distinguish "not set up" from "locked"/"unlocked" from real `chrome.storage.local` evidence (`checkWalletSetupState`), not from the popup's password-field signal alone (both states show no password field) | Guarantee any *other* extension's popups are automatable the same way — only this Braavos build's (4.19.6) side-panel shape has been reasoned about here |
59+
| Approve a connect or transaction-confirm request by finding and clicking its real button via CDP DOM search — structural queries first (locale-proof), text-word fallback only once structural search is measured to find nothing | Read or validate what a transaction actually does before approving it — `approveBraavosRequest` clicks the primary action, it does not decide whether confirming is safe. Isolation (testnet-only funds) is the actual safety control |
60+
| Recover from the side panel losing its pending-request state mid-unlock (`lostContext`) so the caller can retry the click that re-triggers a fresh request | Assume Braavos's side panel is a fresh popup per request — it's a **singleton** target reused across requests; matching "any new target" misses this and was a real bug caught before shipping |
61+
62+
## Secret handling
63+
64+
```bash
65+
security find-generic-password -s scaffold-stark-braavos -w
66+
```
67+
68+
`readBraavosPasswordFromKeychain()` reads this and only this — never an env
69+
var, never logged, never returned in a caller's report detail. Finding an
70+
entry is **not** the same as the password being correct: the entry can hold
71+
a stale value from before the wallet's password last changed. Callers must
72+
always follow this with a real `attemptUnlock()`/`approveBraavosRequest()`
73+
call; never trust `found` alone as "password confirmed".
74+
75+
The wallet being automated must hold **testnet funds only**.
76+
`approveBraavosRequest()` clicks the primary action; it does not read or
77+
understand what it's confirming.
78+
79+
## Procedure
80+
81+
```js
82+
import { CDP_PORT, PROFILE_DIR, launchPersistentChrome, killPersistentChrome } from "./launch.mjs";
83+
import { readBraavosPasswordFromKeychain, checkWalletSetupState, attemptUnlock, waitForBraavosRequestTarget, approveBraavosRequest } from "./braavos.mjs";
84+
85+
const { password } = readBraavosPasswordFromKeychain();
86+
const launch = await launchPersistentChrome();
87+
try {
88+
const setup = await checkWalletSetupState();
89+
if (!setup.vaultPresent) throw new Error("Braavos not set up on this profile");
90+
91+
const unlock = await attemptUnlock(password);
92+
// unlock.state is "UNLOCKED" | "NO_PASSWORD_FIELD" | "REJECTED" | "INSERT_FAILED" | "UNKNOWN"
93+
94+
// Caller drives the dapp's own "Connect" button here (app-specific — see
95+
// stark-demo-web.mjs's connectBraavosWallet), then:
96+
const request = await waitForBraavosRequestTarget(8_000);
97+
if (request) {
98+
const approved = await approveBraavosRequest(request, password, logDir);
99+
// A 4th, optional argument (attachExtraNetworkCapture) lets the caller
100+
// attach its own app-specific Network-domain instrumentation to the
101+
// Braavos session — e.g. stark-demo-web.mjs's attachTxHashCapture — so
102+
// this module never depends back on its own caller.
103+
// approved.lostContext === true means the side panel reset mid-unlock —
104+
// retry the dapp-side click that re-triggers the request.
105+
}
106+
} finally {
107+
await killPersistentChrome();
108+
}
109+
```
110+
111+
## Known traps (measured, not assumed)
112+
113+
- **Braavos's side panel is a singleton**, reused across requests — its URL
114+
goes from `side-panel.html?nav={"path":"/dapp-request",...}` to bare
115+
`side-panel.html` after an in-panel unlock. `waitForBraavosRequestTarget`
116+
matches on URL path content, never on "any new target ID".
117+
- **The service-worker target can exist before `chrome.*` finishes
118+
bootstrapping.** `checkWalletSetupState` forces a fresh wake via the popup
119+
on every attempt (not just when no target is found) rather than trusting a
120+
possibly-still-starting worker reference.
121+
- **A blind last-candidate click is banned.** An earlier version clicked
122+
"whatever's last" by screen position and left a real tx request pending
123+
forever (neither confirmed nor rejected). `approveBraavosRequest` only
124+
clicks a candidate identified by text/role, and re-polls with a fresh DOM
125+
query if the click doesn't visibly change the panel.
126+
- **The tx-confirm Sign button stays disabled until fee estimation
127+
completes** — a loading shimmer can make the fee line read empty rather
128+
than "-", which a naive check reads as "settled". Only a fee line
129+
containing a digit counts; an explicit "Transaction execution error" fails
130+
fast instead of waiting out the poll.

0 commit comments

Comments
 (0)