@le-space/p2pass 0.2.0 → 0.3.1

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.
@@ -4,7 +4,14 @@
4
4
  readSigningPreferenceFromStorage,
5
5
  writeSigningPreferenceToStorage,
6
6
  } from '../identity/signing-preference.js';
7
- import { Upload, LogOut, Loader2, AlertCircle, CheckCircle, Download } from 'lucide-svelte';
7
+ // Per-icon entrypoints avoid the root `lucide-svelte` barrel (`export *`), which can trigger
8
+ // "Importing binding name 'default' cannot be resolved by star export entries" in strict ESM.
9
+ import Upload from 'lucide-svelte/icons/upload';
10
+ import LogOut from 'lucide-svelte/icons/log-out';
11
+ import Loader2 from 'lucide-svelte/icons/loader-2';
12
+ import AlertCircle from 'lucide-svelte/icons/alert-circle';
13
+ import CheckCircle from 'lucide-svelte/icons/check-circle';
14
+ import Download from 'lucide-svelte/icons/download';
8
15
  import { getSpaceUsage } from './storacha-backup.js';
9
16
  import { OrbitDBStorachaBridge } from 'orbitdb-storacha-bridge';
10
17
  import { IdentityService, hasLocalPasskeyHint } from '../identity/identity-service.js';
@@ -15,6 +22,7 @@
15
22
  storeDelegation,
16
23
  loadStoredDelegation,
17
24
  clearStoredDelegation,
25
+ formatDelegationsTooltipSummary,
18
26
  } from '../ucan/storacha-auth.js';
19
27
  import {
20
28
  openDeviceRegistry,
@@ -27,6 +35,9 @@
27
35
  storeArchiveEntry,
28
36
  listDelegations,
29
37
  storeDelegationEntry,
38
+ removeDeviceEntry,
39
+ delegationsEntriesForDevice,
40
+ hashCredentialId,
30
41
  } from '../registry/device-registry.js';
31
42
  import {
32
43
  createManifest,
@@ -86,6 +97,92 @@
86
97
  return '\uD83D\uDCF1';
87
98
  }
88
99
 
100
+ /** @param {{ mode?: string, algorithm?: string } | null} sm */
101
+ function passkeyKindFromSigningMode(sm) {
102
+ if (!sm) return null;
103
+ if (sm.mode === 'worker') return 'worker-ed25519';
104
+ if (sm.algorithm === 'P-256') return 'hardware-p256';
105
+ return 'hardware-ed25519';
106
+ }
107
+
108
+ /** Passkey kind label for a registry device row (stored passkey_kind, else local session). */
109
+ function linkedDevicePasskeyLabel(/** @type {Record<string, unknown>} */ device) {
110
+ const k = device.passkey_kind;
111
+ if (typeof k === 'string' && k) return k;
112
+ if (signingMode?.did && device.ed25519_did === signingMode.did) {
113
+ return passkeyKindFromSigningMode(signingMode);
114
+ }
115
+ return null;
116
+ }
117
+
118
+ /** UCAN delegation counts keyed by device DID (see {@link delegationsEntriesForDevice}). */
119
+ let ucanCountsByDid = $state(/** @type {Record<string, number>} */ ({}));
120
+ /** Parsed UCAN summary for each device row’s badge `title`. */
121
+ let ucanTooltipByDid = $state(/** @type {Record<string, string>} */ ({}));
122
+
123
+ async function refreshLinkedDeviceDelegationCounts() {
124
+ if (!registryDb) {
125
+ ucanCountsByDid = {};
126
+ ucanTooltipByDid = {};
127
+ return;
128
+ }
129
+ try {
130
+ const delegations = await listDelegations(registryDb);
131
+ const ownerDid = localStorage.getItem(OWNER_DID_KEY) || signingMode?.did || '';
132
+ /** @type {Record<string, number>} */
133
+ const next = {};
134
+ /** @type {Record<string, string>} */
135
+ const tips = {};
136
+ for (const dev of devices) {
137
+ const did = dev.ed25519_did;
138
+ if (typeof did !== 'string' || !did) continue;
139
+ const entries = delegationsEntriesForDevice(delegations, did, ownerDid);
140
+ next[did] = entries.length;
141
+ if (entries.length > 0) {
142
+ tips[did] = await formatDelegationsTooltipSummary(
143
+ /** @type {Array<{ delegation?: string, space_did?: string, label?: string }>} */ (
144
+ entries
145
+ )
146
+ );
147
+ }
148
+ }
149
+ ucanCountsByDid = next;
150
+ ucanTooltipByDid = tips;
151
+ } catch {
152
+ ucanCountsByDid = {};
153
+ ucanTooltipByDid = {};
154
+ }
155
+ }
156
+
157
+ async function confirmRemoveLinkedDevice(/** @type {Record<string, unknown>} */ device) {
158
+ const label = String(device.device_label || device.ed25519_did || 'this device');
159
+ const credId = device.credential_id;
160
+ if (typeof credId !== 'string' || !credId) {
161
+ showMessage('Cannot remove device: missing credential id.', 'error');
162
+ return;
163
+ }
164
+ if (
165
+ !confirm(
166
+ `Remove linked device "${label}" from the registry? Its OrbitDB write access will be revoked.`
167
+ )
168
+ ) {
169
+ return;
170
+ }
171
+ const db = deviceManager?.getRegistryDb?.() ?? registryDb;
172
+ if (!db) {
173
+ showMessage('Cannot remove device: registry not ready.', 'error');
174
+ return;
175
+ }
176
+ try {
177
+ await removeDeviceEntry(db, credId);
178
+ devices = devices.filter((d) => d.ed25519_did !== device.ed25519_did);
179
+ await refreshLinkedDeviceDelegationCounts();
180
+ showMessage('Device removed from linked devices.');
181
+ } catch (err) {
182
+ showMessage(`Failed to remove device: ${err?.message || err}`, 'error');
183
+ }
184
+ }
185
+
89
186
  // Component state
90
187
  let showStoracha = $state(true);
91
188
  let isLoading = $state(false);
@@ -225,6 +322,15 @@
225
322
  };
226
323
  });
227
324
 
325
+ /** Per-device UCAN delegation counts for linked-device badges. */
326
+ $effect(() => {
327
+ registryDb;
328
+ devices;
329
+ signingMode?.did;
330
+ isLoggedIn;
331
+ void refreshLinkedDeviceDelegationCounts();
332
+ });
333
+
228
334
  /** Start MultiDeviceManager once libp2p + registry exist (handles restored signingMode / late orbitdb). */
229
335
  $effect(() => {
230
336
  if (!signingMode?.did || !orbitdb || !libp2p || !registryDb || deviceManager) return;
@@ -344,7 +450,12 @@
344
450
 
345
451
  if (store) {
346
452
  const spaceDid = storeSpaceDid || client.currentSpace()?.did?.() || '';
347
- await storeDelegation(delegationStr, storeRegistryDb, spaceDid);
453
+ await storeDelegation(
454
+ delegationStr,
455
+ storeRegistryDb,
456
+ spaceDid,
457
+ signingMode?.did || ''
458
+ );
348
459
  }
349
460
 
350
461
  currentSpace = client.currentSpace();
@@ -552,7 +663,14 @@
552
663
  if (!registryDb || !signingMode?.did) return;
553
664
  try {
554
665
  const existing = await getDeviceByDID(registryDb, signingMode.did);
555
- if (existing) return;
666
+ const kind = passkeyKindFromSigningMode(signingMode);
667
+ if (existing) {
668
+ if (kind && !existing.passkey_kind) {
669
+ const k = await hashCredentialId(existing.credential_id);
670
+ await registryDb.put(k, { ...existing, passkey_kind: kind });
671
+ }
672
+ return;
673
+ }
556
674
  const credential = loadWebAuthnCredentialSafe();
557
675
  await registerDevice(registryDb, {
558
676
  credential_id:
@@ -568,6 +686,7 @@
568
686
  created_at: Date.now(),
569
687
  status: 'active',
570
688
  ed25519_did: signingMode.did,
689
+ passkey_kind: kind,
571
690
  });
572
691
  console.log('[ui] Self-registered device in registry');
573
692
  } catch (err) {
@@ -625,7 +744,7 @@
625
744
  credential,
626
745
  orbitdb,
627
746
  libp2p,
628
- identity: { id: signingMode.did },
747
+ identity: { id: signingMode.did, passkeyKind: passkeyKindFromSigningMode(signingMode) },
629
748
  onPairingRequest: async (request) => {
630
749
  pairingFlow(
631
750
  'ALICE',
@@ -659,6 +778,15 @@
659
778
  const dbAddr = registryDb.address?.toString?.() || registryDb.address;
660
779
  if (dbAddr) await deviceManager.openExistingDb(dbAddr);
661
780
 
781
+ // Use the same KV instance as MultiDeviceManager (sync listeners + replication). A second
782
+ // `openDeviceRegistry(..., sameAddress)` in openExistingDb is a distinct replica; reading
783
+ // `registryDb` from initRegistryDb after reload could list fewer devices than the manager.
784
+ const managedDb = deviceManager.getRegistryDb();
785
+ if (managedDb) {
786
+ registryDb = managedDb;
787
+ await identityService.setRegistry(registryDb);
788
+ }
789
+
662
790
  devices = await deviceManager.listDevices();
663
791
  peerInfo = deviceManager.getPeerInfo();
664
792
  console.log('[ui] MultiDeviceManager initialized');
@@ -671,9 +799,13 @@
671
799
 
672
800
  async function handleTabSwitch(tab) {
673
801
  activeTab = tab;
674
- if (tab === 'passkeys' && registryDb) {
802
+ if (tab === 'passkeys') {
675
803
  try {
676
- devices = await listRegistryDevices(registryDb);
804
+ if (deviceManager) {
805
+ devices = await deviceManager.listDevices();
806
+ } else if (registryDb) {
807
+ devices = await listRegistryDevices(registryDb);
808
+ }
677
809
  } catch (err) {
678
810
  console.warn('[ui] Failed to load devices:', err.message);
679
811
  }
@@ -733,7 +865,13 @@
733
865
  await storeArchiveEntry(newDb, did, archive.ciphertext, archive.iv);
734
866
  }
735
867
  for (const d of delegations) {
736
- await storeDelegationEntry(newDb, d.delegation, d.space_did);
868
+ await storeDelegationEntry(
869
+ newDb,
870
+ d.delegation,
871
+ d.space_did,
872
+ d.label,
873
+ d.stored_by_did
874
+ );
737
875
  }
738
876
  console.log('[ui] Registry migration complete after', Date.now() - start, 'ms');
739
877
  return true;
@@ -1102,6 +1240,91 @@
1102
1240
  {/if}
1103
1241
  {/snippet}
1104
1242
 
1243
+ {#snippet yourDidCard()}
1244
+ <div
1245
+ data-testid="storacha-your-did"
1246
+ style="border-radius: 0.375rem; border: 1px solid rgba(233, 19, 21, 0.3); background: linear-gradient(to right, #ffffff, #EFE3F3); padding: 0.625rem 0.75rem; box-shadow: 0 1px 2px 0 rgba(0,0,0,0.05);"
1247
+ >
1248
+ <div
1249
+ style="font-size: 0.625rem; font-weight: 600; color: #6b7280; font-family: 'DM Sans', sans-serif; margin-bottom: 0.25rem; text-transform: uppercase; letter-spacing: 0.05em;"
1250
+ >
1251
+ Your DID
1252
+ </div>
1253
+ <div style="display: flex; align-items: center; gap: 0.5rem;">
1254
+ <code
1255
+ style="flex: 1; font-size: 0.75rem; color: #374151; font-family: 'DM Mono', monospace; word-break: break-all; line-height: 1.4;"
1256
+ >
1257
+ {signingMode?.did
1258
+ ? signingMode.did.length > 40
1259
+ ? signingMode.did.slice(0, 20) + '...' + signingMode.did.slice(-16)
1260
+ : signingMode.did
1261
+ : 'N/A'}
1262
+ </code>
1263
+ <button
1264
+ class="storacha-btn-icon"
1265
+ onclick={() => {
1266
+ if (signingMode?.did) {
1267
+ navigator.clipboard.writeText(signingMode.did);
1268
+ showMessage('DID copied to clipboard!');
1269
+ }
1270
+ }}
1271
+ style="border-radius: 0.25rem; padding: 0.25rem; color: #0176CE; transition: all 150ms; border: none; background: transparent; cursor: pointer; flex-shrink: 0;"
1272
+ title="Copy full DID"
1273
+ aria-label="Copy DID to clipboard"
1274
+ >
1275
+ <svg
1276
+ style="height: 0.875rem; width: 0.875rem;"
1277
+ fill="none"
1278
+ viewBox="0 0 24 24"
1279
+ stroke="currentColor"
1280
+ >
1281
+ <path
1282
+ stroke-linecap="round"
1283
+ stroke-linejoin="round"
1284
+ stroke-width="2"
1285
+ d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
1286
+ />
1287
+ </svg>
1288
+ </button>
1289
+ </div>
1290
+ </div>
1291
+ {/snippet}
1292
+
1293
+ {#snippet storachaConnectedBanner()}
1294
+ <div
1295
+ style="display: flex; align-items: center; justify-content: space-between; border-radius: 0.375rem; border: 1px solid #E91315; background: linear-gradient(to right, #BDE0FF, #FFE4AE); padding: 0.75rem; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -2px rgba(0,0,0,0.1);"
1296
+ >
1297
+ <div style="display: flex; align-items: center; gap: 0.75rem;">
1298
+ <div
1299
+ style="display: flex; height: 2rem; width: 2rem; align-items: center; justify-content: center; border-radius: 9999px; background-color: #E91315; box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -4px rgba(0,0,0,0.1); flex-shrink: 0;"
1300
+ >
1301
+ <CheckCircle style="height: 1rem; width: 1rem; color: #ffffff;" />
1302
+ </div>
1303
+ <div>
1304
+ <div
1305
+ style="font-size: 0.875rem; font-weight: 700; color: #E91315; font-family: 'Epilogue', sans-serif;"
1306
+ >
1307
+ Connected to Storacha
1308
+ </div>
1309
+ {#if currentSpace}
1310
+ <div style="font-size: 0.75rem; color: #0176CE; font-family: 'DM Mono', monospace;">
1311
+ Space: {formatSpaceName(currentSpace)}
1312
+ </div>
1313
+ {/if}
1314
+ </div>
1315
+ </div>
1316
+
1317
+ <button
1318
+ class="storacha-btn-icon"
1319
+ onclick={handleLogout}
1320
+ style="display: flex; align-items: center; gap: 0.25rem; padding: 0.25rem 0.75rem; font-size: 0.875rem; color: #E91315; transition: color 150ms, background-color 150ms; border: none; background: transparent; cursor: pointer; font-family: 'DM Sans', sans-serif;"
1321
+ >
1322
+ <LogOut style="height: 0.75rem; width: 0.75rem;" />
1323
+ <span>Logout</span>
1324
+ </button>
1325
+ </div>
1326
+ {/snippet}
1327
+
1105
1328
  {#snippet linkedDevicesPanel()}
1106
1329
  <div
1107
1330
  style="border-radius: 0.375rem; border: 1px solid #E91315; background: linear-gradient(to bottom right, #ffffff, #FFE4AE); padding: 0.75rem;"
@@ -1132,10 +1355,11 @@
1132
1355
  {:else}
1133
1356
  <div style="display: flex; flex-direction: column; gap: 0.375rem;">
1134
1357
  {#each devices as device, i (device.credential_id || device.ed25519_did || device.device_label || i)}
1358
+ {@const passkeyBadge = linkedDevicePasskeyLabel(device)}
1135
1359
  <div
1136
1360
  data-testid="storacha-linked-device-row"
1137
1361
  data-device-label={device.device_label || ''}
1138
- style="display: flex; align-items: center; gap: 0.625rem; padding: 0.5rem; border-radius: 0.375rem; background: rgba(255, 255, 255, 0.7); border-left: 3px solid {device.status ===
1362
+ style="display: flex; align-items: flex-start; gap: 0.625rem; padding: 0.5rem; border-radius: 0.375rem; background: rgba(255, 255, 255, 0.7); border-left: 3px solid {device.status ===
1139
1363
  'active'
1140
1364
  ? '#10b981'
1141
1365
  : '#E91315'};"
@@ -1154,17 +1378,53 @@
1154
1378
  ? device.ed25519_did.slice(0, 16) + '...' + device.ed25519_did.slice(-8)
1155
1379
  : 'N/A'}
1156
1380
  </code>
1381
+ <div
1382
+ style="display: flex; flex-wrap: wrap; gap: 0.25rem; margin-top: 0.35rem; align-items: center;"
1383
+ >
1384
+ {#if passkeyBadge}
1385
+ <span
1386
+ style="font-size: 0.55rem; font-weight: 600; padding: 0.1rem 0.35rem; border-radius: 9999px; background: #e0e7ff; color: #3730a3; font-family: 'DM Mono', monospace;"
1387
+ title="Passkey signing mode for this device"
1388
+ >
1389
+ {passkeyBadge}
1390
+ </span>
1391
+ {/if}
1392
+ {#if device.ed25519_did && (ucanCountsByDid[device.ed25519_did] ?? 0) > 0}
1393
+ <span
1394
+ style="font-size: 0.55rem; font-weight: 600; padding: 0.1rem 0.35rem; border-radius: 9999px; background: #fef3c7; color: #92400e; font-family: 'DM Sans', sans-serif;"
1395
+ title={ucanTooltipByDid[device.ed25519_did]?.trim()
1396
+ ? ucanTooltipByDid[device.ed25519_did]
1397
+ : 'UCAN delegations on this device (parsing summary…)'}
1398
+ >
1399
+ {ucanCountsByDid[device.ed25519_did]}
1400
+ {ucanCountsByDid[device.ed25519_did] === 1 ? ' UCAN' : ' UCANs'}
1401
+ </span>
1402
+ {/if}
1403
+ </div>
1157
1404
  </div>
1158
- <span
1159
- style="font-size: 0.6rem; font-weight: 600; padding: 0.125rem 0.375rem; border-radius: 9999px; flex-shrink: 0; background: {device.status ===
1160
- 'active'
1161
- ? '#dcfce7'
1162
- : '#fee2e2'}; color: {device.status === 'active'
1163
- ? '#166534'
1164
- : '#991b1b'}; font-family: 'DM Sans', sans-serif;"
1405
+ <div
1406
+ style="display: flex; flex-direction: column; align-items: flex-end; gap: 0.25rem; flex-shrink: 0;"
1165
1407
  >
1166
- {device.status}
1167
- </span>
1408
+ <span
1409
+ style="font-size: 0.6rem; font-weight: 600; padding: 0.125rem 0.375rem; border-radius: 9999px; background: {device.status ===
1410
+ 'active'
1411
+ ? '#dcfce7'
1412
+ : '#fee2e2'}; color: {device.status === 'active'
1413
+ ? '#166534'
1414
+ : '#991b1b'}; font-family: 'DM Sans', sans-serif;"
1415
+ >
1416
+ {device.status}
1417
+ </span>
1418
+ <button
1419
+ type="button"
1420
+ data-testid="storacha-linked-device-remove"
1421
+ aria-label="Remove linked device"
1422
+ onclick={() => confirmRemoveLinkedDevice(device)}
1423
+ style="font-size: 0.6rem; font-weight: 600; padding: 0.15rem 0.4rem; border-radius: 0.25rem; background: transparent; color: #b91c1c; border: 1px solid #fca5a5; cursor: pointer; font-family: 'DM Sans', sans-serif;"
1424
+ >
1425
+ Remove
1426
+ </button>
1427
+ </div>
1168
1428
  </div>
1169
1429
  {/each}
1170
1430
  </div>
@@ -1548,6 +1808,7 @@
1548
1808
 
1549
1809
  <!-- Recover from backup (IPNS / manifest) -->
1550
1810
  <button
1811
+ data-testid="storacha-recover-passkey"
1551
1812
  onclick={handleRecover}
1552
1813
  disabled={isRecovering}
1553
1814
  style="display: flex; align-items: center; justify-content: center; gap: 0.5rem; border-radius: 0.375rem; background-color: transparent; padding: 0.5rem 1.25rem; color: #E91315; border: 1px solid #E91315; cursor: pointer; font-family: 'Epilogue', sans-serif; font-weight: 600; font-size: 0.75rem; opacity: {isRecovering
@@ -1578,7 +1839,7 @@
1578
1839
  </button>
1579
1840
  </div>
1580
1841
  {:else}
1581
- <!-- Step 2: Authenticated — show DID info + delegation import -->
1842
+ <!-- Step 2: Authenticated — signing badge; DID + tab panels render below tabs -->
1582
1843
  <div
1583
1844
  data-testid="storacha-post-auth"
1584
1845
  style="display: flex; flex-direction: column; gap: 0.75rem;"
@@ -1644,116 +1905,48 @@
1644
1905
  </span>
1645
1906
  {/if}
1646
1907
  </div>
1908
+ </div>
1909
+ {/if}
1647
1910
 
1648
- <!-- DID Display -->
1649
- <div
1650
- style="border-radius: 0.375rem; border: 1px solid rgba(233, 19, 21, 0.3); background: linear-gradient(to right, #ffffff, #EFE3F3); padding: 0.625rem 0.75rem; box-shadow: 0 1px 2px 0 rgba(0,0,0,0.05);"
1911
+ {#if signingMode}
1912
+ <!-- Tab Navigation (visible after authentication) — P2P Passkeys first -->
1913
+ <div
1914
+ style="border-radius: 0.5rem; background: rgba(233, 19, 21, 0.06); padding: 0.25rem; display: flex; gap: 0.25rem;"
1915
+ >
1916
+ <button
1917
+ data-testid="storacha-tab-passkeys"
1918
+ onclick={() => handleTabSwitch('passkeys')}
1919
+ style="flex: 1; padding: 0.5rem 1rem; border-radius: 0.375rem; border: none; cursor: pointer; font-family: 'Epilogue', sans-serif; font-size: 0.8rem; font-weight: 600; transition: all 200ms; background: {activeTab ===
1920
+ 'passkeys'
1921
+ ? 'linear-gradient(135deg, #E91315, #FFC83F)'
1922
+ : 'transparent'}; color: {activeTab === 'passkeys'
1923
+ ? '#fff'
1924
+ : '#6B7280'}; box-shadow: {activeTab === 'passkeys'
1925
+ ? '0 2px 8px rgba(233, 19, 21, 0.3)'
1926
+ : 'none'};"
1651
1927
  >
1652
- <div
1653
- style="font-size: 0.625rem; font-weight: 600; color: #6b7280; font-family: 'DM Sans', sans-serif; margin-bottom: 0.25rem; text-transform: uppercase; letter-spacing: 0.05em;"
1654
- >
1655
- Your DID
1656
- </div>
1657
- <div style="display: flex; align-items: center; gap: 0.5rem;">
1658
- <code
1659
- style="flex: 1; font-size: 0.75rem; color: #374151; font-family: 'DM Mono', monospace; word-break: break-all; line-height: 1.4;"
1660
- >
1661
- {signingMode.did
1662
- ? signingMode.did.length > 40
1663
- ? signingMode.did.slice(0, 20) + '...' + signingMode.did.slice(-16)
1664
- : signingMode.did
1665
- : 'N/A'}
1666
- </code>
1667
- <button
1668
- class="storacha-btn-icon"
1669
- onclick={() => {
1670
- if (signingMode.did) {
1671
- navigator.clipboard.writeText(signingMode.did);
1672
- showMessage('DID copied to clipboard!');
1673
- }
1674
- }}
1675
- style="border-radius: 0.25rem; padding: 0.25rem; color: #0176CE; transition: all 150ms; border: none; background: transparent; cursor: pointer; flex-shrink: 0;"
1676
- title="Copy full DID"
1677
- aria-label="Copy DID to clipboard"
1678
- >
1679
- <svg
1680
- style="height: 0.875rem; width: 0.875rem;"
1681
- fill="none"
1682
- viewBox="0 0 24 24"
1683
- stroke="currentColor"
1684
- >
1685
- <path
1686
- stroke-linecap="round"
1687
- stroke-linejoin="round"
1688
- stroke-width="2"
1689
- d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
1690
- />
1691
- </svg>
1692
- </button>
1693
- </div>
1694
- </div>
1928
+ P2P Passkeys
1929
+ </button>
1930
+ <button
1931
+ data-testid="storacha-tab-storacha"
1932
+ onclick={() => handleTabSwitch('storacha')}
1933
+ style="flex: 1; padding: 0.5rem 1rem; border-radius: 0.375rem; border: none; cursor: pointer; font-family: 'Epilogue', sans-serif; font-size: 0.8rem; font-weight: 600; transition: all 200ms; background: {activeTab ===
1934
+ 'storacha'
1935
+ ? 'linear-gradient(135deg, #E91315, #FFC83F)'
1936
+ : 'transparent'}; color: {activeTab === 'storacha'
1937
+ ? '#fff'
1938
+ : '#6B7280'}; box-shadow: {activeTab === 'storacha'
1939
+ ? '0 2px 8px rgba(233, 19, 21, 0.3)'
1940
+ : 'none'};"
1941
+ >
1942
+ Storacha
1943
+ </button>
1944
+ </div>
1695
1945
 
1696
- {#if activeTab !== 'passkeys'}
1697
- <!-- Delegation Import -->
1698
- <div
1699
- style="border-radius: 0.375rem; border: 1px solid #E91315; background: linear-gradient(to bottom right, #ffffff, #EFE3F3); padding: 1rem; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -2px rgba(0,0,0,0.1);"
1700
- >
1701
- <h4
1702
- style="margin-bottom: 0.5rem; font-weight: 700; color: #E91315; font-family: 'Epilogue', sans-serif; font-size: 0.875rem;"
1703
- >
1704
- Import UCAN Delegation
1705
- </h4>
1706
- <p
1707
- style="margin-bottom: 0.75rem; font-size: 0.75rem; color: #6b7280; font-family: 'DM Sans', sans-serif; line-height: 1.4;"
1708
- >
1709
- Paste a <strong>Storacha UCAN delegation</strong> (from w3up, the CLI, or copied
1710
- from a browser that already has access) to reach your space. This is not for
1711
- linking devices over libp2p — use the <strong>P2P Passkeys</strong> tab and peer JSON
1712
- for that.
1713
- </p>
1714
- <div style="display: flex; flex-direction: column; gap: 0.75rem;">
1715
- <textarea
1716
- class="storacha-textarea"
1717
- bind:value={delegationText}
1718
- placeholder="Paste your UCAN delegation here (base64 encoded)..."
1719
- rows="4"
1720
- style="width: 100%; resize: none; border-radius: 0.375rem; border: 1px solid #E91315; background-color: #ffffff; padding: 0.5rem 0.75rem; font-size: 0.75rem; color: #111827; font-family: 'DM Mono', monospace; outline: none; box-sizing: border-box;"
1721
- ></textarea>
1722
- <button
1723
- class="storacha-btn-primary"
1724
- onclick={handleImportDelegation}
1725
- disabled={isLoading || !delegationText.trim()}
1726
- style="display: flex; width: 100%; align-items: center; justify-content: center; gap: 0.5rem; border-radius: 0.375rem; background-color: #E91315; padding: 0.5rem 1rem; color: #ffffff; box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -4px rgba(0,0,0,0.1); transition: color 150ms, background-color 150ms; border: none; cursor: pointer; font-family: 'Epilogue', sans-serif; font-weight: 600; opacity: {isLoading ||
1727
- !delegationText.trim()
1728
- ? '0.5'
1729
- : '1'}; box-sizing: border-box;"
1730
- >
1731
- {#if isLoading}
1732
- <Loader2
1733
- style="height: 1rem; width: 1rem; animation: spin 1s linear infinite;"
1734
- />
1735
- <span>Connecting...</span>
1736
- {:else}
1737
- <svg
1738
- style="height: 1rem; width: 1rem;"
1739
- fill="none"
1740
- viewBox="0 0 24 24"
1741
- stroke="currentColor"
1742
- >
1743
- <path
1744
- stroke-linecap="round"
1745
- stroke-linejoin="round"
1746
- stroke-width="2"
1747
- d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"
1748
- />
1749
- </svg>
1750
- <span>Connect</span>
1751
- {/if}
1752
- </button>
1753
- </div>
1754
- </div>
1755
- {:else}
1756
- <!-- Link Device (peer id) -->
1946
+ {#if activeTab === 'passkeys'}
1947
+ <div style="display: flex; flex-direction: column; gap: 0.75rem;">
1948
+ {@render yourDidCard()}
1949
+ <!-- Link Device (peer id) pre–Storacha-login flow -->
1757
1950
  <div
1758
1951
  style="border-radius: 0.375rem; border: 1px solid #E91315; background: linear-gradient(to bottom right, #ffffff, #FFE4AE); padding: 1rem; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -2px rgba(0,0,0,0.1);"
1759
1952
  >
@@ -1790,6 +1983,7 @@
1790
1983
  />
1791
1984
  {#if linkError}
1792
1985
  <div
1986
+ data-testid="storacha-link-error"
1793
1987
  style="font-size: 0.7rem; color: #dc2626; font-family: 'DM Sans', sans-serif;"
1794
1988
  >
1795
1989
  {linkError}
@@ -1834,47 +2028,6 @@
1834
2028
  </button>
1835
2029
  </div>
1836
2030
  </div>
1837
- {/if}
1838
- </div>
1839
- {/if}
1840
-
1841
- {#if signingMode}
1842
- <!-- Tab Navigation (visible after authentication) — P2P Passkeys first -->
1843
- <div
1844
- style="border-radius: 0.5rem; background: rgba(233, 19, 21, 0.06); padding: 0.25rem; display: flex; gap: 0.25rem;"
1845
- >
1846
- <button
1847
- data-testid="storacha-tab-passkeys"
1848
- onclick={() => handleTabSwitch('passkeys')}
1849
- style="flex: 1; padding: 0.5rem 1rem; border-radius: 0.375rem; border: none; cursor: pointer; font-family: 'Epilogue', sans-serif; font-size: 0.8rem; font-weight: 600; transition: all 200ms; background: {activeTab ===
1850
- 'passkeys'
1851
- ? 'linear-gradient(135deg, #E91315, #FFC83F)'
1852
- : 'transparent'}; color: {activeTab === 'passkeys'
1853
- ? '#fff'
1854
- : '#6B7280'}; box-shadow: {activeTab === 'passkeys'
1855
- ? '0 2px 8px rgba(233, 19, 21, 0.3)'
1856
- : 'none'};"
1857
- >
1858
- P2P Passkeys
1859
- </button>
1860
- <button
1861
- data-testid="storacha-tab-storacha"
1862
- onclick={() => handleTabSwitch('storacha')}
1863
- style="flex: 1; padding: 0.5rem 1rem; border-radius: 0.375rem; border: none; cursor: pointer; font-family: 'Epilogue', sans-serif; font-size: 0.8rem; font-weight: 600; transition: all 200ms; background: {activeTab ===
1864
- 'storacha'
1865
- ? 'linear-gradient(135deg, #E91315, #FFC83F)'
1866
- : 'transparent'}; color: {activeTab === 'storacha'
1867
- ? '#fff'
1868
- : '#6B7280'}; box-shadow: {activeTab === 'storacha'
1869
- ? '0 2px 8px rgba(233, 19, 21, 0.3)'
1870
- : 'none'};"
1871
- >
1872
- Storacha
1873
- </button>
1874
- </div>
1875
-
1876
- {#if activeTab === 'passkeys'}
1877
- <div style="display: flex; flex-direction: column; gap: 0.75rem;">
1878
2031
  <!-- Connection Status + Copy -->
1879
2032
  <div
1880
2033
  style="border-radius: 0.375rem; border: 1px solid #E91315; background: linear-gradient(to right, #ffffff, #FFE4AE); padding: 0.625rem 0.75rem;"
@@ -1945,46 +2098,74 @@
1945
2098
 
1946
2099
  {@render linkedDevicesPanel()}
1947
2100
  </div>
2101
+ {:else if activeTab === 'storacha'}
2102
+ <div style="display: flex; flex-direction: column; gap: 0.75rem;">
2103
+ <div
2104
+ style="border-radius: 0.375rem; border: 1px solid #E91315; background: linear-gradient(to bottom right, #ffffff, #EFE3F3); padding: 1rem; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -2px rgba(0,0,0,0.1);"
2105
+ >
2106
+ <h4
2107
+ style="margin-bottom: 0.5rem; font-weight: 700; color: #E91315; font-family: 'Epilogue', sans-serif; font-size: 0.875rem;"
2108
+ >
2109
+ Import UCAN Delegation
2110
+ </h4>
2111
+ <p
2112
+ style="margin-bottom: 0.75rem; font-size: 0.75rem; color: #6b7280; font-family: 'DM Sans', sans-serif; line-height: 1.4;"
2113
+ >
2114
+ Paste a <strong>Storacha UCAN delegation</strong> (from w3up, the CLI, or copied
2115
+ from a browser that already has access) to reach your space. This is not for
2116
+ linking devices over libp2p — use the <strong>P2P Passkeys</strong> tab and peer JSON
2117
+ for that.
2118
+ </p>
2119
+ <div style="display: flex; flex-direction: column; gap: 0.75rem;">
2120
+ <textarea
2121
+ class="storacha-textarea"
2122
+ data-testid="storacha-delegation-textarea"
2123
+ bind:value={delegationText}
2124
+ placeholder="Paste your UCAN delegation here (base64 encoded)..."
2125
+ rows="4"
2126
+ style="width: 100%; resize: none; border-radius: 0.375rem; border: 1px solid #E91315; background-color: #ffffff; padding: 0.5rem 0.75rem; font-size: 0.75rem; color: #111827; font-family: 'DM Mono', monospace; outline: none; box-sizing: border-box;"
2127
+ ></textarea>
2128
+ <button
2129
+ class="storacha-btn-primary"
2130
+ data-testid="storacha-delegation-import"
2131
+ onclick={handleImportDelegation}
2132
+ disabled={isLoading || !delegationText.trim()}
2133
+ style="display: flex; width: 100%; align-items: center; justify-content: center; gap: 0.5rem; border-radius: 0.375rem; background-color: #E91315; padding: 0.5rem 1rem; color: #ffffff; box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -4px rgba(0,0,0,0.1); transition: color 150ms, background-color 150ms; border: none; cursor: pointer; font-family: 'Epilogue', sans-serif; font-weight: 600; opacity: {isLoading ||
2134
+ !delegationText.trim()
2135
+ ? '0.5'
2136
+ : '1'}; box-sizing: border-box;"
2137
+ >
2138
+ {#if isLoading}
2139
+ <Loader2
2140
+ style="height: 1rem; width: 1rem; animation: spin 1s linear infinite;"
2141
+ />
2142
+ <span>Connecting...</span>
2143
+ {:else}
2144
+ <svg
2145
+ style="height: 1rem; width: 1rem;"
2146
+ fill="none"
2147
+ viewBox="0 0 24 24"
2148
+ stroke="currentColor"
2149
+ >
2150
+ <path
2151
+ stroke-linecap="round"
2152
+ stroke-linejoin="round"
2153
+ stroke-width="2"
2154
+ d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"
2155
+ />
2156
+ </svg>
2157
+ <span>Connect</span>
2158
+ {/if}
2159
+ </button>
2160
+ </div>
2161
+ </div>
2162
+ </div>
1948
2163
  {/if}
1949
2164
  {/if}
1950
2165
  </div>
1951
2166
  {:else}
1952
2167
  <!-- Logged In Section -->
1953
2168
  <div style="display: flex; flex-direction: column; gap: 1rem;">
1954
- <!-- Account Info -->
1955
- <div
1956
- style="display: flex; align-items: center; justify-content: space-between; border-radius: 0.375rem; border: 1px solid #E91315; background: linear-gradient(to right, #BDE0FF, #FFE4AE); padding: 0.75rem; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -2px rgba(0,0,0,0.1);"
1957
- >
1958
- <div style="display: flex; align-items: center; gap: 0.75rem;">
1959
- <div
1960
- style="display: flex; height: 2rem; width: 2rem; align-items: center; justify-content: center; border-radius: 9999px; background-color: #E91315; box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -4px rgba(0,0,0,0.1); flex-shrink: 0;"
1961
- >
1962
- <CheckCircle style="height: 1rem; width: 1rem; color: #ffffff;" />
1963
- </div>
1964
- <div>
1965
- <div
1966
- style="font-size: 0.875rem; font-weight: 700; color: #E91315; font-family: 'Epilogue', sans-serif;"
1967
- >
1968
- Connected to Storacha
1969
- </div>
1970
- {#if currentSpace}
1971
- <div style="font-size: 0.75rem; color: #0176CE; font-family: 'DM Mono', monospace;">
1972
- Space: {formatSpaceName(currentSpace)}
1973
- </div>
1974
- {/if}
1975
- </div>
1976
- </div>
1977
-
1978
- <button
1979
- class="storacha-btn-icon"
1980
- onclick={handleLogout}
1981
- style="display: flex; align-items: center; gap: 0.25rem; padding: 0.25rem 0.75rem; font-size: 0.875rem; color: #E91315; transition: color 150ms, background-color 150ms; border: none; background: transparent; cursor: pointer; font-family: 'DM Sans', sans-serif;"
1982
- >
1983
- <LogOut style="height: 0.75rem; width: 0.75rem;" />
1984
- <span>Logout</span>
1985
- </button>
1986
- </div>
1987
-
1988
2169
  <!-- Tab Navigation — P2P Passkeys first -->
1989
2170
  <div
1990
2171
  style="border-radius: 0.5rem; background: rgba(233, 19, 21, 0.06); padding: 0.25rem; display: flex; gap: 0.25rem;"
@@ -2020,8 +2201,10 @@
2020
2201
  </div>
2021
2202
 
2022
2203
  {#if activeTab === 'storacha'}
2023
- <!-- Action Buttons -->
2024
2204
  <div style="display: flex; flex-direction: column; gap: 0.75rem;">
2205
+ {@render storachaConnectedBanner()}
2206
+ <!-- Action Buttons -->
2207
+ <div style="display: flex; flex-direction: column; gap: 0.75rem;">
2025
2208
  <button
2026
2209
  class="storacha-btn-backup"
2027
2210
  onclick={handleBackup}
@@ -2241,10 +2424,12 @@
2241
2424
  </div>
2242
2425
  {/if}
2243
2426
  </div>
2427
+ </div>
2244
2428
  {/if}
2245
2429
 
2246
2430
  {#if activeTab === 'passkeys'}
2247
2431
  <div style="display: flex; flex-direction: column; gap: 0.75rem;">
2432
+ {@render yourDidCard()}
2248
2433
  <!-- Connection Status + Copy -->
2249
2434
  <div
2250
2435
  style="border-radius: 0.375rem; border: 1px solid #E91315; background: linear-gradient(to right, #ffffff, #FFE4AE); padding: 0.625rem 0.75rem;"
@@ -2395,6 +2580,7 @@
2395
2580
  </div>
2396
2581
  {#if linkError}
2397
2582
  <div
2583
+ data-testid="storacha-link-error"
2398
2584
  style="margin-top: 0.5rem; font-size: 0.75rem; color: #b91c1c; font-family: 'DM Sans', sans-serif;"
2399
2585
  >
2400
2586
  {linkError}