Skip to content

Commit 7d37f37

Browse files
Sentinel-AutonomybuilderSentinel-Autonomybuilder
authored andcommitted
feat(demo): read-only DEMO mode + clone-friction polish
- DEMO=true + DEMO_ADDR=sent1... boots a watch-only session on any operator address. Bech32 validated at boot. POST/PUT/DELETE return 403 with a friendly "clone the repo" message so nothing can be signed by accident. - /api/wallet/status now exposes demo:true; UI shows a topbar banner pointing visitors at the GitHub repo when running in demo. - README: drop the npm-global install path (we are not publishing), replace docker-compose .env clobber with cp .env.example, add npx degit one-liner, and put a "just want to look around" demo-mode quickstart at the top. - engines.node >=20 in package.json so older Node fails fast. - gitignore: privy-wallets.json, .servererr.txt, .serverlog*.txt, social/ — runtime artifacts that should never be tracked.
1 parent b441a93 commit 7d37f37

6 files changed

Lines changed: 118 additions & 10 deletions

File tree

.env.example

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
MNEMONIC=your twelve or twenty four word cosmos sdk mnemonic here
22
PORT=3003
33

4+
# Demo mode (optional). Set DEMO=true to boot the UI in read-only watch-only
5+
# mode mounted on DEMO_ADDR. Every TX-broadcasting endpoint returns 403 — no
6+
# mnemonic is read, no wallet is loaded. Ideal for "I just want to look at it"
7+
# previews you can publish to a public URL without exposing keys.
8+
DEMO=
9+
DEMO_ADDR=
10+
411
# Privy embedded-wallet login (optional). When both are set, the email/Privy
512
# auth card on /login is enabled and /api/wallet/privy-login accepts access
613
# tokens. Get these from https://dashboard.privy.io after creating an app.

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,16 @@ wallet.json
55
.session-key
66
my-plans.json
77
nodes-cache.json
8+
privy-wallets.json
89
sellers.json
910
an.json
1011
server.log
1112
boot.log
1213
*.log
14+
.servererr.txt
15+
.serverlog.txt
16+
.serverlog.err.txt
17+
social/
1318
*.jpg
1419
*.png
1520
CLAUDE.md

README.md

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -47,20 +47,31 @@ See [PLANS.md](./PLANS.md) for the full breakdown: plan lifecycle, plan-based vs
4747

4848
> **This tool operates on Sentinel mainnet. Every transaction costs real P2P tokens. There is no testnet mode.**
4949
50-
### Option A — npm (recommended, one-shot)
50+
### Just want to look around?
51+
52+
Boot a read-only demo mounted on any operator address — no wallet, no tokens, no commitment. Every TX-broadcasting endpoint returns 403 so nothing can be signed by accident.
5153

5254
```bash
53-
npm install -g sentinel-plan-manager
54-
plans --help # verify install
55-
echo "MNEMONIC=word1 word2 ... word24" > .env
56-
npx sentinel-plan-manager # or: node $(npm root -g)/sentinel-plan-manager/server.js
55+
git clone https://github.com/Sentinel-Bluebuilder/sentinel-plan-manager.git
56+
cd sentinel-plan-manager
57+
npm install
58+
DEMO=true DEMO_ADDR=sent1...operator-address... npm start
59+
# → http://localhost:3003 with a demo banner across the top
5760
```
5861

59-
### Option B — git clone (for contributors)
62+
Pick any plan-creating operator address from a chain explorer, paste it into `DEMO_ADDR`, and the dashboard renders that operator's plans, nodes, and subscribers.
63+
64+
### Run your own — Option A: clone
6065

6166
```bash
67+
# Without git history (fastest):
68+
npx degit Sentinel-Bluebuilder/sentinel-plan-manager plan-manager
69+
cd plan-manager
70+
71+
# Or with full git history:
6272
git clone https://github.com/Sentinel-Bluebuilder/sentinel-plan-manager.git
6373
cd sentinel-plan-manager
74+
6475
npm install
6576
cp .env.example .env # then edit .env and paste your mnemonic
6677
npm start
@@ -70,7 +81,7 @@ npm start
7081
7182
Open http://localhost:3003.
7283

73-
### Option C — Docker
84+
### Run your own — Option B: Docker
7485

7586
```bash
7687
# one-liner
@@ -83,13 +94,13 @@ docker run -d --name plan-manager \
8394
sentinel-plan-manager
8495

8596
# or with compose (includes named volume + auto-restart)
86-
echo "MNEMONIC=word1 word2 ... word24" > .env
97+
cp .env.example .env # then edit .env and paste your mnemonic
8798
docker compose up -d
8899
```
89100

90101
State (`.wallet.json`, `my-plans.json`, `nodes-cache.json`) lives in the `/data` volume so it survives container recreations. Leave `MNEMONIC` unset to create a wallet from the UI on first boot instead.
91102

92-
### Option D — Multi-user deploy (public domain)
103+
### Option C — Multi-user deploy (public domain)
93104

94105
Want to deploy this to a domain and let anyone sign in with their own mnemonic? Set `MULTI_USER=true`:
95106

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@
4040
"rpc"
4141
],
4242
"license": "MIT",
43+
"engines": {
44+
"node": ">=20"
45+
},
4346
"dependencies": {
4447
"@cosmjs/encoding": "^0.38.1",
4548
"@cosmjs/proto-signing": "^0.32.4",

public/index.html

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1222,6 +1222,31 @@
12221222
}
12231223
html[data-theme="light"] .topbar { background: rgba(246, 247, 251, 0.82); }
12241224

1225+
.demo-banner {
1226+
display: flex; align-items: center; gap: 12px;
1227+
padding: 10px 28px;
1228+
background: linear-gradient(90deg, rgba(245, 158, 11, 0.14), rgba(245, 158, 11, 0.06));
1229+
border-bottom: 1px solid rgba(245, 158, 11, 0.32);
1230+
font-size: 13px; color: var(--text);
1231+
}
1232+
.demo-banner-dot {
1233+
width: 8px; height: 8px; border-radius: 50%;
1234+
background: #f59e0b; box-shadow: 0 0 0 3px rgba(245, 158, 11, 0.18);
1235+
flex: 0 0 auto;
1236+
}
1237+
.demo-banner-text { flex: 1; min-width: 0; line-height: 1.5; }
1238+
.demo-banner-text code {
1239+
background: rgba(0,0,0,0.3); padding: 1px 5px; border-radius: 4px;
1240+
font-size: 12px; font-family: ui-monospace, Menlo, Consolas, monospace;
1241+
}
1242+
html[data-theme="light"] .demo-banner-text code { background: rgba(0,0,0,0.06); }
1243+
.demo-banner-cta {
1244+
flex: 0 0 auto;
1245+
font-size: 13px; font-weight: 600; color: #f59e0b;
1246+
text-decoration: none; white-space: nowrap;
1247+
}
1248+
.demo-banner-cta:hover { text-decoration: underline; }
1249+
12251250
.topbar-title {
12261251
font-size: 16px;
12271252
font-weight: 600;
@@ -4801,6 +4826,13 @@ <h3 id="securityFaqTitle">
48014826
</button>
48024827
</div>
48034828
</header>
4829+
<div id="demoBanner" class="demo-banner" style="display:none">
4830+
<div class="demo-banner-dot"></div>
4831+
<div class="demo-banner-text">
4832+
<strong>Demo mode</strong> — read-only preview on a public operator. Clone the repo and set <code>MNEMONIC</code> in <code>.env</code> to run your own.
4833+
</div>
4834+
<a class="demo-banner-cta" href="https://github.com/Sentinel-Bluebuilder/sentinel-plan-manager" target="_blank" rel="noopener">Get the source →</a>
4835+
</div>
48044836
<div id="app">
48054837
<div class="loading-state"><div class="loading-spinner"></div><div class="loading-text">Connecting to Sentinel network...</div></div>
48064838
</div>
@@ -5668,6 +5700,8 @@ <h3 id="securityFaqTitle">
56685700
S.wallet = d;
56695701
S.dvpnPrice = d.dvpnPriceUsd;
56705702
if (typeof d.multiUser === 'boolean') S.multiUser = d.multiUser;
5703+
const demoBanner = $('demoBanner');
5704+
if (demoBanner) demoBanner.style.display = d.kind === 'demo' ? 'flex' : 'none';
56715705
walletAddress = d.address || '';
56725706
ADDR_SHORT = walletAddress ? walletAddress.slice(0, 10) + '...' + walletAddress.slice(-6) : '?';
56735707
if (walletAddress) {

server.js

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,30 @@ const DATA_DIR = process.env.DATA_DIR || __dirname;
6666
try { mkdirSync(DATA_DIR, { recursive: true }); } catch {}
6767
initSession(DATA_DIR);
6868

69+
// ─── Demo Mode ────────────────────────────────────────────────────────────────
70+
// Read-only browse: any visitor sees the UI mounted on a watch-only address
71+
// without supplying a mnemonic. Every TX-broadcasting endpoint returns 403.
72+
// Set DEMO_ADDR to any sent1... operator address you want visitors to view.
73+
const DEMO_MODE = String(process.env.DEMO || '').toLowerCase() === 'true';
74+
const DEMO_ADDR = (process.env.DEMO_ADDR || '').trim();
75+
if (DEMO_MODE) {
76+
if (!DEMO_ADDR || !DEMO_ADDR.startsWith('sent1')) {
77+
console.error('[demo] DEMO=true requires DEMO_ADDR=sent1... (operator address to display).');
78+
process.exit(1);
79+
}
80+
// Validate bech32 checksum so a typo fails fast at boot instead of crashing
81+
// on first request (keplrSessionFromAddress throws on invalid checksum).
82+
try {
83+
const { fromBech32 } = await import('@cosmjs/encoding');
84+
const { prefix } = fromBech32(DEMO_ADDR);
85+
if (prefix !== 'sent') throw new Error(`expected sent prefix, got ${prefix}`);
86+
} catch (err) {
87+
console.error(`[demo] DEMO_ADDR is not a valid sentinel address: ${err.message}`);
88+
process.exit(1);
89+
}
90+
console.log(`[demo] Read-only mode enabled — mounted on ${DEMO_ADDR}. Writes return 403.`);
91+
}
92+
6993
const app = express();
7094
app.use(express.json({ limit: '32kb' }));
7195
// Suppress fingerprinting header.
@@ -135,11 +159,20 @@ app.use(express.static(join(__dirname, 'public'), {
135159
// the rest of the request chain inside that session's AsyncLocalStorage
136160
// context. Handlers call `getAddr()` / `getSigningClient()` as before;
137161
// those helpers automatically resolve to the per-request wallet.
162+
//
163+
// In DEMO_MODE, every request without a real auth cookie is mounted on the
164+
// configured DEMO_ADDR as a watch-only session (kind: 'demo'). The write
165+
// gate below rejects POST/PUT/DELETE before any TX can be broadcast.
138166
app.use(async (req, res, next) => {
139167
const cookies = parseCookies(req.headers.cookie);
140168
const mnemonicToken = cookies[COOKIE_NAME];
141169
const keplrToken = cookies[KEPLR_COOKIE_NAME];
142170

171+
if (DEMO_MODE && !mnemonicToken && !keplrToken) {
172+
const session = keplrSessionFromAddress(DEMO_ADDR, null, 'demo');
173+
return runWithSession(session, () => next());
174+
}
175+
143176
if (mnemonicToken) {
144177
try {
145178
const mnemonic = decryptMnemonic(mnemonicToken);
@@ -174,6 +207,15 @@ app.use(async (req, res, next) => {
174207
next();
175208
});
176209

210+
// ─── Demo Write Gate ──────────────────────────────────────────────────────────
211+
// Demo sessions can read but not write. Reject any state-changing method
212+
// (POST/PUT/DELETE/PATCH) with a clear 403 so the UI can surface a banner.
213+
app.use((req, res, next) => {
214+
if (req.method === 'GET' || req.method === 'HEAD' || req.method === 'OPTIONS') return next();
215+
if (currentSession()?.kind !== 'demo') return next();
216+
return res.status(403).json({ error: 'Demo mode is read-only — clone the repo and set MNEMONIC to make transactions.', demo: true });
217+
});
218+
177219
// ─── Plan ID Persistence ──────────────────────────────────────────────────────
178220
// Keyed by wallet address so multi-user deploys keep each operator's plan
179221
// list separate. Legacy flat-array files (single-user installs) are migrated
@@ -1089,7 +1131,13 @@ setInterval(() => {
10891131
}, 60_000).unref();
10901132

10911133
app.get('/api/wallet/status', (req, res) => {
1092-
res.json({ loaded: !!getAddr(), address: getAddr() || null, multiUser: isMultiUser() });
1134+
const isDemo = currentSession()?.kind === 'demo';
1135+
res.json({
1136+
loaded: !!getAddr(),
1137+
address: getAddr() || null,
1138+
multiUser: isMultiUser(),
1139+
demo: isDemo,
1140+
});
10931141
});
10941142

10951143
app.post('/api/wallet/generate', rateLimit('wgen', 10, 60_000), async (req, res) => {

0 commit comments

Comments
 (0)