feat: push token generation UI in settings

Author: Aaron Steven White
Commit 6b408bbc3c7e0d8ff39912d270f285a595e5b94c
Parent: d57a1de96c
Structural diff unavailable

These commits were pushed via plain git push, so no pre-parsed schemas are available. Install git-remote-cospan and re-push via panproto:// to see scope-level changes, breaking change detection, and semantic diffs.

brew install panproto/tap/git-remote-cospan
1 file changed +103 -1
@@ -3,6 +3,7 @@
33 	import { getAuth } from '$lib/stores/auth.svelte';
44 	import KeyForm from '$lib/components/settings/KeyForm.svelte';
55 	import { listKeys, addKey, deleteKey, type Key, type KeyType } from '$lib/api/keys.js';
6+	import { xrpcProcedure } from '$lib/api/client.js';
67 	import { formatDate } from '$lib/utils/time.js';
78 
89 	let auth = $derived(getAuth());
@@ -10,10 +11,40 @@
1011 	let loading = $state(true);
1112 	let deletingKey = $state<string | null>(null);
1213 	let showForm = $state(false);
13-	let activeTab: KeyType = $state('ssh');
14+	let activeTab: KeyType | 'push' = $state('ssh');
1415 
1516 	let filteredKeys = $derived(keys.filter((k) => k.type === activeTab));
1617 
18+	// Push token state
19+	let pushToken = $state<string | null>(null);
20+	let pushTokenLoading = $state(false);
21+	let pushTokenError = $state<string | null>(null);
22+	let pushTokenCopied = $state(false);
23+
24+	async function generatePushToken() {
25+		pushTokenLoading = true;
26+		pushTokenError = null;
27+		pushToken = null;
28+		try {
29+			const result = await xrpcProcedure<{ token: string; did: string; expiresIn: number }>(
30+				'dev.cospan.repo.createPushToken',
31+				{}
32+			);
33+			pushToken = result.token;
34+		} catch (e: any) {
35+			pushTokenError = e.message ?? 'Failed to generate token';
36+		} finally {
37+			pushTokenLoading = false;
38+		}
39+	}
40+
41+	async function copyToken() {
42+		if (!pushToken) return;
43+		await navigator.clipboard.writeText(pushToken);
44+		pushTokenCopied = true;
45+		setTimeout(() => { pushTokenCopied = false; }, 2000);
46+	}
47+
1748 	onMount(async () => {
1849 		if (!auth.authenticated || !auth.did) {
1950 			loading = false;
@@ -102,8 +133,78 @@
102133 			>
103134 				GPG Keys
104135 			</button>
136+			<button
137+				onclick={() => { activeTab = 'push'; }}
138+				class="border-b-2 px-4 py-2 text-sm font-medium transition-colors
139+					{activeTab === 'push'
140+						? 'border-accent text-text-primary'
141+						: 'border-transparent text-text-secondary hover:text-text-primary'}"
142+			>
143+				Push Tokens
144+			</button>
105145 		</div>
106146 
147+		{#if activeTab === 'push'}
148+			<!-- Push Tokens tab -->
149+			<div class="rounded-lg border border-border bg-surface-1 p-6">
150+				<h2 class="mb-2 text-sm font-medium text-text-primary">Git Push Token</h2>
151+				<p class="mb-4 text-xs text-text-secondary">
152+					Generate a short-lived token to authenticate <code class="rounded bg-surface-2 px-1">git push</code> to cospan-node.
153+					Tokens expire after 1 hour.
154+				</p>
155+
156+				<div class="mb-4 rounded-md border border-border bg-surface-0 p-3 text-xs text-text-secondary">
157+					<p class="mb-2 font-medium text-text-primary">Usage:</p>
158+					<ol class="list-inside list-decimal space-y-1">
159+						<li>Click "Generate Token" below</li>
160+						<li>Copy the token</li>
161+						<li>When git prompts for credentials:</li>
162+					</ol>
163+					<div class="mt-2 rounded bg-surface-2 px-3 py-2 font-mono text-[11px]">
164+						<div>Username: <span class="text-accent">{auth.did ?? 'your-did'}</span></div>
165+						<div>Password: <span class="text-accent">(paste the token)</span></div>
166+					</div>
167+					<div class="mt-2 font-mono text-[11px] text-text-muted">
168+						git remote add cospan https://node.cospan.dev/{auth.did ?? 'your-did'}/REPO<br/>
169+						git push cospan main
170+					</div>
171+				</div>
172+
173+				{#if pushToken}
174+					<div class="mb-4 rounded-md border border-emerald-500/20 bg-emerald-500/5 p-3">
175+						<div class="mb-2 flex items-center justify-between">
176+							<span class="text-xs font-medium text-emerald-400">Token generated (expires in 1 hour)</span>
177+							<button
178+								onclick={copyToken}
179+								class="rounded bg-emerald-500/15 px-2 py-1 text-[11px] font-medium text-emerald-400 transition-colors hover:bg-emerald-500/25"
180+							>
181+								{pushTokenCopied ? 'Copied!' : 'Copy'}
182+							</button>
183+						</div>
184+						<code class="block break-all rounded bg-surface-2 px-3 py-2 font-mono text-[11px] text-text-primary">
185+							{pushToken}
186+						</code>
187+						<p class="mt-2 text-[10px] text-text-muted">
188+							This token will not be shown again. Generate a new one if it expires.
189+						</p>
190+					</div>
191+				{/if}
192+
193+				{#if pushTokenError}
194+					<div class="mb-4 rounded-md bg-red-500/10 px-3 py-2 text-xs text-red-400">
195+						{pushTokenError}
196+					</div>
197+				{/if}
198+
199+				<button
200+					onclick={generatePushToken}
201+					disabled={pushTokenLoading}
202+					class="rounded-md bg-accent px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-accent-hover disabled:opacity-50"
203+				>
204+					{pushTokenLoading ? 'Generating...' : 'Generate Token'}
205+				</button>
206+			</div>
207+		{:else}
107208 		<!-- Add key button / form -->
108209 		{#if showForm}
109210 			<div class="mb-6 rounded-lg border border-border bg-surface-1 p-6">
@@ -172,5 +273,6 @@
172273 				{/each}
173274 			</div>
174275 		{/if}
276+		{/if}<!-- close push tab {:else} -->
175277 	{/if}
176278 </section>
cospan · schematic version control on atproto built on AT Protocol