@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.
- package/dist/index.d.ts +6 -5
- package/dist/index.js +9 -4
- package/dist/registry/device-registry.d.ts +34 -2
- package/dist/registry/device-registry.js +70 -2
- package/dist/registry/index.d.ts +1 -1
- package/dist/registry/index.js +3 -0
- package/dist/registry/manager.d.ts +5 -0
- package/dist/registry/manager.js +13 -0
- package/dist/registry/pairing-protocol.d.ts +1 -1
- package/dist/registry/pairing-protocol.js +8 -4
- package/dist/ucan/storacha-auth.d.ts +14 -1
- package/dist/ucan/storacha-auth.js +120 -2
- package/dist/ui/StorachaFab.svelte +30 -4
- package/dist/ui/StorachaIntegration.svelte +387 -201
- package/package.json +25 -2
|
@@ -4,7 +4,14 @@
|
|
|
4
4
|
readSigningPreferenceFromStorage,
|
|
5
5
|
writeSigningPreferenceToStorage,
|
|
6
6
|
} from '../identity/signing-preference.js';
|
|
7
|
-
|
|
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(
|
|
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
|
-
|
|
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'
|
|
802
|
+
if (tab === 'passkeys') {
|
|
675
803
|
try {
|
|
676
|
-
|
|
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(
|
|
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:
|
|
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
|
-
<
|
|
1159
|
-
style="
|
|
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
|
-
|
|
1167
|
-
|
|
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 —
|
|
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
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
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
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
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
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
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}
|