Skip to content

Commit 72ff9a7

Browse files
ralyodioclaude
andauthored
Release/store swarm cicd (#8)
* feat(store): CI/CD publishing — publisher API tokens + publish script Lets extensions be published from git/CI instead of the web form. The only backend gap was headless auth, so this adds long-lived publisher API tokens: - migration 0005_publisher_tokens (stores only the sha256 of the token) - mint/list/revoke endpoints; minting requires a real session (a leaked CI token can't mint more); store currentUser() resolves `tbpub_…` bearers - scripts/publish-extension.sh: zip → scp to files.profullstack.com → register the new version via the API with the token (generic; any CI) - docs/ci-publishing.md: token + SSH-key setup and a paste-in GitHub workflow scp upload + the version endpoint + slug lookup already existed, so CI reuses them. Typecheck clean; token mint/resolve/list/revoke round-trip verified. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(api): /api/swarm — deepagents agent swarm via @logicsrc/agentswarm Signed-in, bring-your-own-key endpoint that runs a deepagents-backed swarm. Caller supplies provider + apiKey per request (transient, not stored — same pattern as /api/models); anthropic uses ChatAnthropic, other providers use the OpenAI-compatible adapter (incl. optional c0mpute.com GPUs). An optional `rubric` enables the self-check loop. Mounted at /api/swarm, auth via currentUser. Adds @logicsrc/agentswarm + deepagents + @langchain/{langgraph,anthropic,openai}. Typecheck clean (Docker tsconfig), boots, route auth-gated (401 without session). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * ai-sidebar: add email/password sign-in + sign-up (alongside CoinPay) - shared storeSession() persists a TronBrowser session token from any auth method; emailSignIn/emailSignUp hit the same /api/auth/login + /signup the website uses and store the returned token like CoinPay. - options UI gains the email/password fields. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 9d79646 commit 72ff9a7

3 files changed

Lines changed: 100 additions & 16 deletions

File tree

apps/desktop/extensions/ai-sidebar/coinpay-auth.js

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,30 +10,65 @@ async function apiBase() {
1010
return (syncConfig?.url || DEFAULT_API).replace(/\/$/, '');
1111
}
1212

13-
export async function coinpaySignIn() {
13+
// Persist a TronBrowser session token (from any auth method) the same way, so
14+
// pull/push sync works identically whether you signed in with CoinPay or email.
15+
// `method` is just for display ('coinpay' | 'email').
16+
async function storeSession(sessionToken, method) {
1417
const base = await apiBase();
15-
const redirectUri = chrome.identity.getRedirectURL();
16-
const url = `${base}/api/auth/coinpay/login?redirect=${encodeURIComponent(redirectUri)}`;
17-
const redirect = await chrome.identity.launchWebAuthFlow({ url, interactive: true });
18-
const frag = new URL(redirect).hash.slice(1) || new URL(redirect).search.slice(1);
19-
const sessionToken = new URLSearchParams(frag).get('token');
20-
if (!sessionToken) throw new Error('no session token returned');
21-
// Pull the account profile.
2218
let label = '';
2319
try {
2420
const me = await fetch(`${base}/api/auth/me`, { headers: { authorization: `Bearer ${sessionToken}` } });
2521
if (me.ok) { const d = await me.json(); label = d.email || d.id || ''; }
2622
} catch { /* ignore */ }
2723
await chrome.storage.local.set({
28-
coinpay: { sessionToken, label, expiresAt: Date.now() + 30 * 24 * 3600 * 1000 },
24+
coinpay: { sessionToken, label, method, expiresAt: Date.now() + 30 * 24 * 3600 * 1000 },
2925
});
26+
return label;
27+
}
28+
29+
export async function coinpaySignIn() {
30+
const base = await apiBase();
31+
const redirectUri = chrome.identity.getRedirectURL();
32+
const url = `${base}/api/auth/coinpay/login?redirect=${encodeURIComponent(redirectUri)}`;
33+
const redirect = await chrome.identity.launchWebAuthFlow({ url, interactive: true });
34+
const frag = new URL(redirect).hash.slice(1) || new URL(redirect).search.slice(1);
35+
const sessionToken = new URLSearchParams(frag).get('token');
36+
if (!sessionToken) throw new Error('no session token returned');
37+
await storeSession(sessionToken, 'coinpay');
3038
return true;
3139
}
3240

41+
// Email + password sign-in — same /api/auth/login the website uses; the server
42+
// returns a session token we store exactly like the CoinPay one.
43+
export async function emailSignIn(email, password) {
44+
const base = await apiBase();
45+
const r = await fetch(`${base}/api/auth/login`, {
46+
method: 'POST', headers: { 'content-type': 'application/json' },
47+
body: JSON.stringify({ email, password }),
48+
});
49+
const d = await r.json().catch(() => ({}));
50+
if (!r.ok || !d.token) throw new Error(d.error || `sign-in failed (${r.status})`);
51+
await storeSession(d.token, 'email');
52+
return { emailVerified: !!d.emailVerified };
53+
}
54+
55+
// Email + password sign-up — same /api/auth/signup the website uses. This sends
56+
// a verification email and does NOT sign you in; verify, then sign in.
57+
export async function emailSignUp(email, password) {
58+
const base = await apiBase();
59+
const r = await fetch(`${base}/api/auth/signup`, {
60+
method: 'POST', headers: { 'content-type': 'application/json' },
61+
body: JSON.stringify({ email, password }),
62+
});
63+
const d = await r.json().catch(() => ({}));
64+
if (!r.ok) throw new Error(d.error || `sign-up failed (${r.status})`);
65+
return { message: d.message || 'verification email sent — verify, then sign in' };
66+
}
67+
3368
export async function coinpayState() {
3469
const { coinpay } = await chrome.storage.local.get('coinpay');
3570
if (coinpay?.sessionToken && (!coinpay.expiresAt || coinpay.expiresAt > Date.now())) {
36-
return { signedIn: true, label: coinpay.label, token: coinpay.sessionToken };
71+
return { signedIn: true, label: coinpay.label, method: coinpay.method || 'coinpay', token: coinpay.sessionToken };
3772
}
3873
return { signedIn: false };
3974
}

apps/desktop/extensions/ai-sidebar/options.html

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,25 @@
5353
<h1>Settings</h1>
5454

5555
<h2>Account</h2>
56-
<p class="hint">Sign in with <strong>CoinPay</strong> (fully anonymous — email optional). Your
57-
settings &amp; feeds then sync to the cloud SQLite database, or your own self-hosted backend.</p>
56+
<p class="hint">Sign in to sync your settings &amp; feeds to the cloud SQLite database (or your own
57+
self-hosted backend). <strong>CoinPay</strong> is fully anonymous; email is optional. Use the
58+
<em>same</em> account here and on tronbrowser.dev so both stay in sync.</p>
5859
<div id="account" class="hint">Not signed in.</div>
5960
<button id="coinpay" class="ghost">Sign in with CoinPay</button>
6061

62+
<div id="emailAuth">
63+
<div class="hint" style="margin:12px 0 2px">or email</div>
64+
<label for="authEmail">Email</label>
65+
<input id="authEmail" type="email" autocomplete="email" placeholder="you@example.com" />
66+
<label for="authPassword">Password</label>
67+
<input id="authPassword" type="password" autocomplete="current-password" placeholder="8+ characters" />
68+
<div class="row">
69+
<button id="emailLogin" class="ghost">Sign in</button>
70+
<button id="emailSignup" class="ghost">Create account</button>
71+
</div>
72+
<div id="emailMsg" class="hint"></div>
73+
</div>
74+
6175
<label for="cpClient">CoinPay client id (advanced)</label>
6276
<input id="cpClient" placeholder="tronbrowser" />
6377
<label for="syncUrl">Self-hosted sync backend URL (optional)</label>

apps/desktop/extensions/ai-sidebar/options.js

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { PROVIDERS, KNOWN_MODELS, listModels } from './providers.js';
22
import { DEFAULT_FEEDS, parseOpml, toOpml, loadFeeds, saveFeeds } from './feeds.js';
3-
import { coinpaySignIn, coinpayState, coinpaySignOut } from './coinpay-auth.js';
3+
import { coinpaySignIn, coinpayState, coinpaySignOut, emailSignIn, emailSignUp } from './coinpay-auth.js';
44
import { pushSettings, pullSettings } from './settings-store.js';
55
import { encryptVault, decryptVault } from './vault.js';
66
import { connect as btrConnect, disconnect as btrDisconnect, verify as btrVerify } from './bittorrented.js';
@@ -160,24 +160,59 @@ el('saveMarkets').addEventListener('click', async () => {
160160
flash('savedMarkets', 'saved ✓');
161161
});
162162

163-
/* ---------- CoinPay account + sync ---------- */
163+
/* ---------- Account + sync (CoinPay or email — same as the website) ---------- */
164164
async function renderAccount() {
165165
const st = await coinpayState();
166+
const how = st.method === 'email' ? 'email' : 'CoinPay';
166167
el('account').textContent = st.signedIn
167-
? `Signed in with CoinPay${st.label ? ' (' + st.label + ')' : ''} — settings sync to the cloud.`
168+
? `Signed in with ${how}${st.label ? ' (' + st.label + ')' : ''} — settings sync to the cloud.`
168169
: 'Not signed in. (Settings stay on this device until you sign in.)';
169170
el('coinpay').textContent = st.signedIn ? 'Sign out' : 'Sign in with CoinPay';
171+
// The email form is only for signing in; hide it once signed in.
172+
el('emailAuth').style.display = st.signedIn ? 'none' : '';
170173
}
174+
175+
// After any successful sign-in, pull cloud settings down and re-render.
176+
async function afterSignIn() {
177+
await pullSettings();
178+
await loadAll();
179+
}
180+
171181
el('coinpay').addEventListener('click', async () => {
172182
const st = await coinpayState();
173183
if (st.signedIn) { await coinpaySignOut(); }
174184
else {
175-
try { await coinpaySignIn(); await pullSettings(); await loadAll(); }
185+
try { await coinpaySignIn(); await afterSignIn(); }
176186
catch (e) { el('account').textContent = 'Sign-in failed: ' + e.message; return; }
177187
}
178188
renderAccount();
179189
});
180190

191+
el('emailLogin').addEventListener('click', async () => {
192+
const email = el('authEmail').value.trim();
193+
const password = el('authPassword').value;
194+
if (!email || !password) { flash('emailMsg', 'email and password required'); return; }
195+
el('emailMsg').textContent = 'signing in…';
196+
try {
197+
const { emailVerified } = await emailSignIn(email, password);
198+
el('authPassword').value = '';
199+
await afterSignIn();
200+
await renderAccount();
201+
if (!emailVerified) flash('emailMsg', 'signed in — check your inbox to verify your email');
202+
} catch (e) { flash('emailMsg', e.message); }
203+
});
204+
205+
el('emailSignup').addEventListener('click', async () => {
206+
const email = el('authEmail').value.trim();
207+
const password = el('authPassword').value;
208+
if (!email || password.length < 8) { flash('emailMsg', 'email and 8+ char password required'); return; }
209+
el('emailMsg').textContent = 'creating account…';
210+
try {
211+
const { message } = await emailSignUp(email, password);
212+
el('emailMsg').textContent = message; // persistent — user needs to go verify
213+
} catch (e) { flash('emailMsg', e.message); }
214+
});
215+
181216
/* ---------- Feeds ---------- */
182217
async function renderFeeds() {
183218
const feeds = await loadFeeds();

0 commit comments

Comments
 (0)