@le-space/orbitdb-storage-bridge 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,801 @@
1
+ <script>
2
+ import { createEventDispatcher, onMount } from "svelte";
3
+ // Import Carbon components and icons
4
+ import {
5
+ Tile,
6
+ Tabs,
7
+ Tab,
8
+ TabContent,
9
+ Button,
10
+ TextInput,
11
+ TextArea,
12
+ PasswordInput,
13
+ InlineNotification,
14
+ Loading,
15
+ Tag,
16
+ } from "carbon-components-svelte";
17
+ import {
18
+ Cloud,
19
+ Key,
20
+ Email,
21
+ Shuffle,
22
+ CheckmarkFilled,
23
+ Logout,
24
+ Reset,
25
+ } from "carbon-icons-svelte";
26
+ import {
27
+ initializeStorachaClient,
28
+ initializeStorachaClientWithUCAN,
29
+ initializeStorachaClientWithSeed,
30
+ createStorachaAccount,
31
+ listSpaces,
32
+ createSpace,
33
+ generateSeedPhrase,
34
+ validateSeedPhrase,
35
+ } from "./storacha-backup.js";
36
+ import { logger } from "../lib/logger.js";
37
+
38
+ const dispatch = createEventDispatcher();
39
+
40
+ // Component props
41
+ export let autoLogin = true;
42
+ export let showTitle = true;
43
+ export let compact = false;
44
+ export let enableSeedAuth = true;
45
+ export let enableEmailAuth = true;
46
+
47
+ // Authentication state
48
+ let isAuthenticated = false;
49
+ let client = null;
50
+ let currentSpace = null;
51
+ let spaces = [];
52
+ let authMethod = null; // 'credentials', 'ucan', 'seed'
53
+
54
+ // UI state
55
+ let isLoading = false;
56
+ let status = "";
57
+ let error = null;
58
+ let success = null;
59
+ let activeTab = 0; // 0: credentials, 1: ucan, 2: seed, 3: email
60
+
61
+ // Form data
62
+ let email = "";
63
+ let storachaKey = "";
64
+ let storachaProof = "";
65
+ let ucanToken = "";
66
+ let recipientKey = "";
67
+ let seedPhrase = "";
68
+ let seedPassword = "password";
69
+ let delegationToken = "";
70
+ let newSpaceName = "";
71
+
72
+ // LocalStorage keys
73
+ const STORAGE_KEYS = {
74
+ STORACHA_KEY: "storacha_key",
75
+ STORACHA_PROOF: "storacha_proof",
76
+ UCAN_TOKEN: "storacha_ucan_token",
77
+ RECIPIENT_KEY: "storacha_recipient_key",
78
+ SEED_PHRASE: "storacha_seed_phrase",
79
+ SEED_PASSWORD: "storacha_seed_password",
80
+ DELEGATION_TOKEN: "storacha_delegation_token",
81
+ AUTH_METHOD: "storacha_auth_method",
82
+ AUTO_LOGIN: "storacha_auto_login",
83
+ };
84
+
85
+ // Auto-hide messages
86
+ function showMessage(message, type = "info", duration = 5000) {
87
+ if (type === "error") {
88
+ error = message;
89
+ success = null;
90
+ } else {
91
+ success = message;
92
+ error = null;
93
+ }
94
+
95
+ setTimeout(() => {
96
+ error = null;
97
+ success = null;
98
+ }, duration);
99
+ }
100
+
101
+ // Save credentials to localStorage
102
+ function saveCredentials(method, data) {
103
+ try {
104
+ localStorage.setItem(STORAGE_KEYS.AUTH_METHOD, method);
105
+ localStorage.setItem(STORAGE_KEYS.AUTO_LOGIN, "true");
106
+
107
+ switch (method) {
108
+ case "credentials":
109
+ localStorage.setItem(STORAGE_KEYS.STORACHA_KEY, data.key);
110
+ localStorage.setItem(STORAGE_KEYS.STORACHA_PROOF, data.proof);
111
+ break;
112
+ case "ucan":
113
+ localStorage.setItem(STORAGE_KEYS.UCAN_TOKEN, data.ucanToken);
114
+ localStorage.setItem(STORAGE_KEYS.RECIPIENT_KEY, data.recipientKey);
115
+ break;
116
+ case "seed":
117
+ localStorage.setItem(STORAGE_KEYS.SEED_PHRASE, data.seedPhrase);
118
+ localStorage.setItem(STORAGE_KEYS.SEED_PASSWORD, data.seedPassword);
119
+ if (data.delegationToken) {
120
+ localStorage.setItem(
121
+ STORAGE_KEYS.DELEGATION_TOKEN,
122
+ data.delegationToken,
123
+ );
124
+ }
125
+ break;
126
+ }
127
+ } catch (err) {
128
+ logger.warn({ error: err.message }, "Failed to save credentials");
129
+ }
130
+ }
131
+
132
+ // Load credentials from localStorage
133
+ function loadStoredCredentials() {
134
+ try {
135
+ const method = localStorage.getItem(STORAGE_KEYS.AUTH_METHOD);
136
+ const autoLoginEnabled =
137
+ localStorage.getItem(STORAGE_KEYS.AUTO_LOGIN) === "true";
138
+
139
+ if (!method || !autoLoginEnabled) return null;
140
+
141
+ switch (method) {
142
+ case "credentials": {
143
+ const key = localStorage.getItem(STORAGE_KEYS.STORACHA_KEY);
144
+ const proof = localStorage.getItem(STORAGE_KEYS.STORACHA_PROOF);
145
+ if (key && proof) return { method, key, proof };
146
+ break;
147
+ }
148
+ case "ucan": {
149
+ const ucanToken = localStorage.getItem(STORAGE_KEYS.UCAN_TOKEN);
150
+ const recipientKey = localStorage.getItem(STORAGE_KEYS.RECIPIENT_KEY);
151
+ if (ucanToken && recipientKey)
152
+ return { method, ucanToken, recipientKey };
153
+ break;
154
+ }
155
+ case "seed": {
156
+ const seedPhrase = localStorage.getItem(STORAGE_KEYS.SEED_PHRASE);
157
+ const seedPassword = localStorage.getItem(STORAGE_KEYS.SEED_PASSWORD);
158
+ const delegationToken = localStorage.getItem(
159
+ STORAGE_KEYS.DELEGATION_TOKEN,
160
+ );
161
+ if (seedPhrase && seedPassword)
162
+ return { method, seedPhrase, seedPassword, delegationToken };
163
+ break;
164
+ }
165
+ }
166
+ } catch (err) {
167
+ logger.warn({ error: err.message }, "Failed to load credentials");
168
+ }
169
+ return null;
170
+ }
171
+
172
+ // Clear stored credentials
173
+ function clearStoredCredentials() {
174
+ try {
175
+ Object.values(STORAGE_KEYS).forEach((key) => {
176
+ localStorage.removeItem(key);
177
+ });
178
+ } catch (err) {
179
+ logger.warn({ error: err.message }, "Failed to clear credentials");
180
+ }
181
+ }
182
+
183
+ // Authentication methods
184
+ async function authenticateWithCredentials() {
185
+ if (!storachaKey.trim() || !storachaProof.trim()) {
186
+ showMessage("Please provide both Storacha key and proof", "error");
187
+ return;
188
+ }
189
+
190
+ isLoading = true;
191
+ status = "Authenticating with credentials...";
192
+
193
+ try {
194
+ client = await initializeStorachaClient(
195
+ storachaKey.trim(),
196
+ storachaProof.trim(),
197
+ );
198
+ authMethod = "credentials";
199
+
200
+ await loadSpaces();
201
+
202
+ saveCredentials("credentials", {
203
+ key: storachaKey.trim(),
204
+ proof: storachaProof.trim(),
205
+ });
206
+ isAuthenticated = true;
207
+
208
+ showMessage("Successfully authenticated with Storacha credentials!");
209
+ dispatch("authenticated", { client, method: "credentials", spaces });
210
+ } catch (err) {
211
+ showMessage(`Authentication failed: ${err.message}`, "error");
212
+ } finally {
213
+ isLoading = false;
214
+ status = "";
215
+ }
216
+ }
217
+
218
+ async function authenticateWithUCAN() {
219
+ if (!ucanToken.trim() || !recipientKey.trim()) {
220
+ showMessage("Please provide both UCAN token and recipient key", "error");
221
+ return;
222
+ }
223
+
224
+ isLoading = true;
225
+ status = "Authenticating with UCAN...";
226
+
227
+ try {
228
+ client = await initializeStorachaClientWithUCAN(
229
+ ucanToken.trim(),
230
+ recipientKey.trim(),
231
+ );
232
+ authMethod = "ucan";
233
+
234
+ await loadSpaces();
235
+
236
+ saveCredentials("ucan", {
237
+ ucanToken: ucanToken.trim(),
238
+ recipientKey: recipientKey.trim(),
239
+ });
240
+ isAuthenticated = true;
241
+
242
+ showMessage("Successfully authenticated with UCAN delegation!");
243
+ dispatch("authenticated", { client, method: "ucan", spaces });
244
+ } catch (err) {
245
+ showMessage(`UCAN authentication failed: ${err.message}`, "error");
246
+ } finally {
247
+ isLoading = false;
248
+ status = "";
249
+ }
250
+ }
251
+
252
+ async function authenticateWithSeed() {
253
+ if (!seedPhrase.trim()) {
254
+ showMessage("Please provide a seed phrase", "error");
255
+ return;
256
+ }
257
+
258
+ if (!validateSeedPhrase(seedPhrase.trim())) {
259
+ showMessage("Invalid seed phrase format", "error");
260
+ return;
261
+ }
262
+
263
+ isLoading = true;
264
+ status = "Authenticating with seed phrase...";
265
+
266
+ try {
267
+ const result = await initializeStorachaClientWithSeed(
268
+ seedPhrase.trim(),
269
+ seedPassword.trim() || "password",
270
+ delegationToken.trim() || null,
271
+ );
272
+
273
+ client = result.client;
274
+ authMethod = "seed";
275
+
276
+ await loadSpaces();
277
+
278
+ saveCredentials("seed", {
279
+ seedPhrase: seedPhrase.trim(),
280
+ seedPassword: seedPassword.trim() || "password",
281
+ delegationToken: delegationToken.trim(),
282
+ });
283
+ isAuthenticated = true;
284
+
285
+ const message = delegationToken.trim()
286
+ ? "Successfully authenticated with seed phrase and delegation!"
287
+ : "Authenticated with seed phrase (limited functionality without delegation)";
288
+ showMessage(message);
289
+ dispatch("authenticated", {
290
+ client,
291
+ method: "seed",
292
+ identity: result.identity,
293
+ spaces,
294
+ });
295
+ } catch (err) {
296
+ showMessage(`Seed authentication failed: ${err.message}`, "error");
297
+ } finally {
298
+ isLoading = false;
299
+ status = "";
300
+ }
301
+ }
302
+
303
+ async function createAccount() {
304
+ if (!email.trim()) {
305
+ showMessage("Please enter your email address", "error");
306
+ return;
307
+ }
308
+
309
+ isLoading = true;
310
+ status = "Creating account...";
311
+
312
+ try {
313
+ const result = await createStorachaAccount(email.trim());
314
+ if (result.success) {
315
+ showMessage(result.message);
316
+ } else {
317
+ showMessage(result.error, "error");
318
+ }
319
+ } catch (err) {
320
+ showMessage(`Account creation failed: ${err.message}`, "error");
321
+ } finally {
322
+ isLoading = false;
323
+ status = "";
324
+ }
325
+ }
326
+
327
+ // Space management
328
+ async function loadSpaces() {
329
+ if (!client) return;
330
+
331
+ try {
332
+ spaces = await listSpaces(client);
333
+ currentSpace = client.currentSpace();
334
+ } catch (err) {
335
+ logger.warn({ error: err.message }, "Failed to load spaces");
336
+ spaces = [];
337
+ }
338
+ }
339
+
340
+ async function createNewSpace() {
341
+ if (!newSpaceName.trim()) {
342
+ showMessage("Please enter a space name", "error");
343
+ return;
344
+ }
345
+
346
+ isLoading = true;
347
+ status = "Creating space...";
348
+
349
+ try {
350
+ const result = await createSpace(client, newSpaceName.trim());
351
+ if (result.success) {
352
+ showMessage(`Space "${newSpaceName}" created successfully!`);
353
+ newSpaceName = "";
354
+ await loadSpaces();
355
+ } else {
356
+ showMessage(result.error, "error");
357
+ }
358
+ } catch (err) {
359
+ showMessage(`Failed to create space: ${err.message}`, "error");
360
+ } finally {
361
+ isLoading = false;
362
+ status = "";
363
+ }
364
+ }
365
+
366
+ async function selectSpace(space) {
367
+ try {
368
+ await client.setCurrentSpace(space.did);
369
+ currentSpace = space;
370
+ showMessage(`Switched to space: ${space.name}`);
371
+ dispatch("spaceChanged", { space });
372
+ } catch (err) {
373
+ showMessage(`Failed to switch space: ${err.message}`, "error");
374
+ }
375
+ }
376
+
377
+ // Logout
378
+ function logout() {
379
+ isAuthenticated = false;
380
+ client = null;
381
+ currentSpace = null;
382
+ spaces = [];
383
+ authMethod = null;
384
+ clearStoredCredentials();
385
+ clearForms();
386
+ showMessage("Logged out successfully");
387
+ dispatch("logout");
388
+ }
389
+
390
+ // Clear forms
391
+ function clearForms() {
392
+ email = "";
393
+ storachaKey = "";
394
+ storachaProof = "";
395
+ ucanToken = "";
396
+ recipientKey = "";
397
+ seedPhrase = "";
398
+ seedPassword = "password";
399
+ delegationToken = "";
400
+ newSpaceName = "";
401
+ }
402
+
403
+ // Helper function to get correct tab index based on enabled auth methods
404
+ function getTabIndex(method) {
405
+ const tabs = ['credentials', 'ucan'];
406
+ if (enableSeedAuth) tabs.push('seed');
407
+ if (enableEmailAuth) tabs.push('email');
408
+ return tabs.indexOf(method);
409
+ }
410
+
411
+ // Utility functions
412
+ function generateNewSeedPhrase() {
413
+ seedPhrase = generateSeedPhrase();
414
+ showMessage("New seed phrase generated!");
415
+ }
416
+
417
+ // Auto-login on mount
418
+ onMount(async () => {
419
+ if (!autoLogin) return;
420
+
421
+ const stored = loadStoredCredentials();
422
+ if (!stored) return;
423
+
424
+ logger.info({ method: stored.method }, `🔄 Auto-login with ${stored.method}...`);
425
+
426
+ // Check if the stored method is enabled
427
+ const tabIndex = getTabIndex(stored.method);
428
+ if (tabIndex === -1) {
429
+ logger.warn({ method: stored.method }, `Stored auth method '${stored.method}' is disabled`);
430
+ clearStoredCredentials();
431
+ return;
432
+ }
433
+
434
+ // Set form values
435
+ switch (stored.method) {
436
+ case "credentials":
437
+ storachaKey = stored.key;
438
+ storachaProof = stored.proof;
439
+ activeTab = tabIndex;
440
+ await authenticateWithCredentials();
441
+ break;
442
+ case "ucan":
443
+ ucanToken = stored.ucanToken;
444
+ recipientKey = stored.recipientKey;
445
+ activeTab = tabIndex;
446
+ await authenticateWithUCAN();
447
+ break;
448
+ case "seed":
449
+ if (!enableSeedAuth) {
450
+ logger.warn("Seed auth is disabled");
451
+ return;
452
+ }
453
+ seedPhrase = stored.seedPhrase;
454
+ seedPassword = stored.seedPassword;
455
+ delegationToken = stored.delegationToken || "";
456
+ activeTab = tabIndex;
457
+ await authenticateWithSeed();
458
+ break;
459
+ }
460
+ });
461
+
462
+ // Export authentication status for parent components
463
+ export { isAuthenticated, client, currentSpace, spaces, authMethod };
464
+ </script>
465
+
466
+ <Tile>
467
+ <!-- Header -->
468
+ {#if showTitle}
469
+ <div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:1rem;">
470
+ <Cloud size={20} />
471
+ <h3 style="font-size:1.125rem;font-weight:600;margin:0;">
472
+ Storacha Authentication
473
+ </h3>
474
+ </div>
475
+ {/if}
476
+
477
+ <!-- Status Messages -->
478
+ {#if error}
479
+ <InlineNotification
480
+ kind="error"
481
+ title="Error"
482
+ subtitle={error}
483
+ style="margin-bottom:1rem;"
484
+ />
485
+ {/if}
486
+
487
+ {#if success}
488
+ <InlineNotification
489
+ kind="success"
490
+ title="Success"
491
+ subtitle={success}
492
+ style="margin-bottom:1rem;"
493
+ />
494
+ {/if}
495
+
496
+ {#if isLoading && status}
497
+ <InlineNotification
498
+ kind="info"
499
+ title="Processing"
500
+ subtitle={status}
501
+ style="margin-bottom:1rem;"
502
+ >
503
+ <Loading withOverlay={false} small />
504
+ </InlineNotification>
505
+ {/if}
506
+
507
+ {#if !isAuthenticated}
508
+ <!-- Authentication Tabs -->
509
+ <Tabs bind:selected={activeTab}>
510
+ <Tab>
511
+ <div style="display:flex;align-items:center;gap:0.25rem;">
512
+ <Key size={16} />
513
+ Credentials
514
+ </div>
515
+ </Tab>
516
+ <Tab>
517
+ <div style="display:flex;align-items:center;gap:0.25rem;">🎫 UCAN</div>
518
+ </Tab>
519
+ {#if enableSeedAuth}
520
+ <Tab>
521
+ <div style="display:flex;align-items:center;gap:0.25rem;">
522
+ <Shuffle size={16} />
523
+ Seed
524
+ </div>
525
+ </Tab>
526
+ {/if}
527
+ {#if enableEmailAuth}
528
+ <Tab>
529
+ <div style="display:flex;align-items:center;gap:0.25rem;">
530
+ <Email size={16} />
531
+ Email
532
+ </div>
533
+ </Tab>
534
+ {/if}
535
+
536
+ <svelte:fragment slot="content">
537
+ <TabContent>
538
+ <!-- Credentials Tab -->
539
+ <div style="margin-top:1rem;">
540
+ <h4 style="font-weight:500;margin-bottom:1rem;">
541
+ Storacha Key & Proof
542
+ </h4>
543
+ <div style="display:flex;flex-direction:column;gap:1rem;">
544
+ <PasswordInput
545
+ labelText="Private Key"
546
+ placeholder="MgCZ9...your-storacha-private-key"
547
+ bind:value={storachaKey}
548
+ disabled={isLoading}
549
+ tooltipAlignment="center"
550
+ tooltipPosition="bottom"
551
+ />
552
+ <TextArea
553
+ labelText="Proof (Delegation)"
554
+ placeholder="uCAIS...your-storacha-proof"
555
+ rows={3}
556
+ bind:value={storachaProof}
557
+ disabled={isLoading}
558
+ />
559
+ <Button
560
+ on:click={authenticateWithCredentials}
561
+ disabled={isLoading ||
562
+ !storachaKey.trim() ||
563
+ !storachaProof.trim()}
564
+ style="width:100%;"
565
+ >
566
+ {isLoading ? "Authenticating..." : "Login with Credentials"}
567
+ </Button>
568
+ </div>
569
+ </div>
570
+ </TabContent>
571
+
572
+ <TabContent>
573
+ <!-- UCAN Tab -->
574
+ <div style="margin-top:1rem;">
575
+ <h4 style="font-weight:500;margin-bottom:1rem;">UCAN Delegation</h4>
576
+ <div style="display:flex;flex-direction:column;gap:1rem;">
577
+ <TextArea
578
+ labelText="UCAN Token (Base64)"
579
+ placeholder="eyJh...base64-encoded-ucan-token"
580
+ rows={3}
581
+ bind:value={ucanToken}
582
+ disabled={isLoading}
583
+ />
584
+ <TextArea
585
+ labelText="Recipient Key (JSON)"
586
+ placeholder="did key as json"
587
+ rows={3}
588
+ bind:value={recipientKey}
589
+ disabled={isLoading}
590
+ />
591
+ <Button
592
+ kind="secondary"
593
+ on:click={authenticateWithUCAN}
594
+ disabled={isLoading ||
595
+ !ucanToken.trim() ||
596
+ !recipientKey.trim()}
597
+ style="width:100%;"
598
+ >
599
+ {isLoading ? "Authenticating..." : "Login with UCAN"}
600
+ </Button>
601
+ </div>
602
+ </div>
603
+ </TabContent>
604
+
605
+ {#if enableSeedAuth}
606
+ <TabContent>
607
+ <!-- Seed Tab -->
608
+ <div style="margin-top:1rem;">
609
+ <h4 style="font-weight:500;margin-bottom:1rem;">Seed Phrase</h4>
610
+ <div style="display:flex;flex-direction:column;gap:1rem;">
611
+ <div style="display:flex;flex-direction:column;gap:0.5rem;">
612
+ <div
613
+ style="display:flex;justify-content:space-between;align-items:center;"
614
+ >
615
+ <span style="font-size:0.875rem;font-weight:500;"
616
+ >Seed Phrase (12-24 words)</span
617
+ >
618
+ <Button
619
+ kind="ghost"
620
+ size="sm"
621
+ icon={Reset}
622
+ on:click={generateNewSeedPhrase}
623
+ disabled={isLoading}
624
+ >
625
+ Generate
626
+ </Button>
627
+ </div>
628
+ <TextArea
629
+ placeholder="abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art"
630
+ rows={2}
631
+ bind:value={seedPhrase}
632
+ disabled={isLoading}
633
+ />
634
+ </div>
635
+ <PasswordInput
636
+ labelText="Password (optional)"
637
+ placeholder="password"
638
+ bind:value={seedPassword}
639
+ disabled={isLoading}
640
+ />
641
+ <div style="display:flex;flex-direction:column;gap:0.5rem;">
642
+ <TextArea
643
+ labelText="Delegation Token (optional)"
644
+ placeholder="eyJh...base64-encoded-delegation-token"
645
+ rows={2}
646
+ bind:value={delegationToken}
647
+ disabled={isLoading}
648
+ />
649
+ <p
650
+ style="font-size:0.75rem;color:var(--cds-text-secondary);margin:0;"
651
+ >
652
+ Without delegation, you can only create identities but not
653
+ upload to Storacha
654
+ </p>
655
+ </div>
656
+ <Button
657
+ kind="tertiary"
658
+ on:click={authenticateWithSeed}
659
+ disabled={isLoading || !seedPhrase.trim()}
660
+ style="width:100%;"
661
+ >
662
+ {isLoading ? "Authenticating..." : "Login with Seed"}
663
+ </Button>
664
+ </div>
665
+ </div>
666
+ </TabContent>
667
+ {/if}
668
+
669
+ {#if enableEmailAuth}
670
+ <TabContent>
671
+ <!-- Email Tab -->
672
+ <div style="margin-top:1rem;">
673
+ <h4 style="font-weight:500;margin-bottom:1rem;">Create Account</h4>
674
+ <div style="display:flex;flex-direction:column;gap:1rem;">
675
+ <TextInput
676
+ labelText="Email Address"
677
+ placeholder="Enter your email address"
678
+ bind:value={email}
679
+ disabled={isLoading}
680
+ />
681
+ <Button
682
+ kind="primary"
683
+ on:click={createAccount}
684
+ disabled={isLoading || !email.trim()}
685
+ style="width:100%;"
686
+ >
687
+ {isLoading ? "Creating..." : "Create Account"}
688
+ </Button>
689
+ <p
690
+ style="font-size:0.75rem;color:var(--cds-text-secondary);margin:0;"
691
+ >
692
+ This will guide you through manual account creation at
693
+ web3.storage
694
+ </p>
695
+ </div>
696
+ </div>
697
+ </TabContent>
698
+ {/if}
699
+ </svelte:fragment>
700
+ </Tabs>
701
+ {:else}
702
+ <!-- Authenticated State -->
703
+ <div
704
+ style="display:flex;align-items:center;justify-content:space-between;margin-bottom:1rem;"
705
+ >
706
+ <div style="display:flex;align-items:center;gap:0.5rem;">
707
+ <Tag type="green">
708
+ <CheckmarkFilled size={16} slot="icon" />
709
+ Connected via {authMethod}
710
+ </Tag>
711
+ {#if currentSpace}
712
+ <Tag type="outline">
713
+ Space: {currentSpace.name ||
714
+ currentSpace.did?.slice(-8) ||
715
+ "Unknown"}
716
+ </Tag>
717
+ {/if}
718
+ </div>
719
+
720
+ <Button
721
+ kind="ghost"
722
+ iconDescription="Logout"
723
+ icon={Logout}
724
+ on:click={logout}
725
+ size="sm"
726
+ >
727
+ Logout
728
+ </Button>
729
+ </div>
730
+
731
+ <!-- Space Management -->
732
+ {#if !compact}
733
+ <div
734
+ style="border-top:1px solid var(--cds-border-subtle);padding-top:1rem;"
735
+ >
736
+ <h4 style="margin:0 0 0.75rem 0;font-size:0.875rem;font-weight:600;">
737
+ Spaces ({spaces.length})
738
+ </h4>
739
+
740
+ <!-- Create New Space -->
741
+ <div
742
+ style="display:flex;gap:0.5rem;align-items:center;margin-bottom:0.75rem;"
743
+ >
744
+ <TextInput
745
+ labelText="New space name"
746
+ placeholder="New space name"
747
+ bind:value={newSpaceName}
748
+ style="flex:1;"
749
+ />
750
+ <Button
751
+ kind="primary"
752
+ on:click={createNewSpace}
753
+ disabled={isLoading || !newSpaceName.trim()}
754
+ size="sm"
755
+ >
756
+ Add
757
+ </Button>
758
+ </div>
759
+
760
+ <!-- Spaces List -->
761
+ {#if spaces.length === 0}
762
+ <InlineNotification
763
+ kind="info"
764
+ title="No spaces"
765
+ subtitle="No spaces found"
766
+ />
767
+ {:else}
768
+ <div
769
+ style="max-height:10rem;overflow:auto;display:flex;flex-direction:column;gap:0.5rem;"
770
+ >
771
+ {#each spaces as space (space.did)}
772
+ <div
773
+ style="display:flex;align-items:center;justify-content:space-between;border:1px solid var(--cds-border-subtle);padding:0.5rem;border-radius:0.25rem;background:var(--cds-layer);"
774
+ >
775
+ <div>
776
+ <div style="font-size:0.9rem;font-weight:500;">
777
+ {space.name || "Unnamed Space"}
778
+ </div>
779
+ <div
780
+ style="font-family:monospace;font-size:0.75rem;color:var(--cds-text-secondary);"
781
+ >
782
+ {space.did.slice(0, 20)}...
783
+ </div>
784
+ </div>
785
+ {#if currentSpace?.did !== space.did}
786
+ <Button
787
+ kind="ghost"
788
+ size="sm"
789
+ on:click={() => selectSpace(space)}>Select</Button
790
+ >
791
+ {:else}
792
+ <Tag type="green">Current</Tag>
793
+ {/if}
794
+ </div>
795
+ {/each}
796
+ </div>
797
+ {/if}
798
+ </div>
799
+ {/if}
800
+ {/if}
801
+ </Tile>