@le-space/p2pass 0.1.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.
- package/LICENSE +21 -0
- package/README.md +258 -0
- package/dist/backup/registry-backup.d.ts +26 -0
- package/dist/backup/registry-backup.js +51 -0
- package/dist/identity/identity-service.d.ts +116 -0
- package/dist/identity/identity-service.js +524 -0
- package/dist/identity/mode-detector.d.ts +29 -0
- package/dist/identity/mode-detector.js +124 -0
- package/dist/identity/signing-preference.d.ts +30 -0
- package/dist/identity/signing-preference.js +55 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +91 -0
- package/dist/p2p/setup.d.ts +48 -0
- package/dist/p2p/setup.js +283 -0
- package/dist/recovery/ipns-key.d.ts +41 -0
- package/dist/recovery/ipns-key.js +127 -0
- package/dist/recovery/manifest.d.ts +106 -0
- package/dist/recovery/manifest.js +243 -0
- package/dist/registry/device-registry.d.ts +122 -0
- package/dist/registry/device-registry.js +275 -0
- package/dist/registry/index.d.ts +3 -0
- package/dist/registry/index.js +46 -0
- package/dist/registry/manager.d.ts +76 -0
- package/dist/registry/manager.js +376 -0
- package/dist/registry/pairing-protocol.d.ts +61 -0
- package/dist/registry/pairing-protocol.js +653 -0
- package/dist/ucan/storacha-auth.d.ts +45 -0
- package/dist/ucan/storacha-auth.js +164 -0
- package/dist/ui/StorachaFab.svelte +134 -0
- package/dist/ui/StorachaFab.svelte.d.ts +23 -0
- package/dist/ui/StorachaIntegration.svelte +2467 -0
- package/dist/ui/StorachaIntegration.svelte.d.ts +23 -0
- package/dist/ui/fonts/dm-mono-400.ttf +0 -0
- package/dist/ui/fonts/dm-mono-500.ttf +0 -0
- package/dist/ui/fonts/dm-sans-400.ttf +0 -0
- package/dist/ui/fonts/dm-sans-500.ttf +0 -0
- package/dist/ui/fonts/dm-sans-600.ttf +0 -0
- package/dist/ui/fonts/dm-sans-700.ttf +0 -0
- package/dist/ui/fonts/epilogue-400.ttf +0 -0
- package/dist/ui/fonts/epilogue-500.ttf +0 -0
- package/dist/ui/fonts/epilogue-600.ttf +0 -0
- package/dist/ui/fonts/epilogue-700.ttf +0 -0
- package/dist/ui/fonts/storacha-fonts.css +152 -0
- package/dist/ui/storacha-backup.d.ts +44 -0
- package/dist/ui/storacha-backup.js +218 -0
- package/package.json +112 -0
|
@@ -0,0 +1,2467 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { onMount } from 'svelte';
|
|
3
|
+
import {
|
|
4
|
+
readSigningPreferenceFromStorage,
|
|
5
|
+
writeSigningPreferenceToStorage,
|
|
6
|
+
} from '../identity/signing-preference.js';
|
|
7
|
+
import { Upload, LogOut, Loader2, AlertCircle, CheckCircle, Download } from 'lucide-svelte';
|
|
8
|
+
import { listSpaces, getSpaceUsage } from './storacha-backup.js';
|
|
9
|
+
import { OrbitDBStorachaBridge } from 'orbitdb-storacha-bridge';
|
|
10
|
+
import { IdentityService, hasLocalPasskeyHint } from '../identity/identity-service.js';
|
|
11
|
+
import { getStoredSigningMode } from '../identity/mode-detector.js';
|
|
12
|
+
import {
|
|
13
|
+
createStorachaClient,
|
|
14
|
+
parseDelegation,
|
|
15
|
+
storeDelegation,
|
|
16
|
+
loadStoredDelegation,
|
|
17
|
+
clearStoredDelegation,
|
|
18
|
+
} from '../ucan/storacha-auth.js';
|
|
19
|
+
import {
|
|
20
|
+
openDeviceRegistry,
|
|
21
|
+
registerDevice,
|
|
22
|
+
getDeviceByDID,
|
|
23
|
+
listDevices as listRegistryDevices,
|
|
24
|
+
getArchiveEntry,
|
|
25
|
+
listKeypairs,
|
|
26
|
+
storeKeypairEntry,
|
|
27
|
+
storeArchiveEntry,
|
|
28
|
+
listDelegations,
|
|
29
|
+
storeDelegationEntry,
|
|
30
|
+
} from '../registry/device-registry.js';
|
|
31
|
+
import { deriveIPNSKeyPair } from '../recovery/ipns-key.js';
|
|
32
|
+
import {
|
|
33
|
+
createManifest,
|
|
34
|
+
publishManifest,
|
|
35
|
+
resolveManifest,
|
|
36
|
+
uploadArchiveToIPFS,
|
|
37
|
+
fetchArchiveFromIPFS,
|
|
38
|
+
} from '../recovery/manifest.js';
|
|
39
|
+
import { backupRegistryDb, restoreRegistryDb } from '../backup/registry-backup.js';
|
|
40
|
+
import { MultiDeviceManager } from '../registry/manager.js';
|
|
41
|
+
import { detectDeviceLabel, pairingFlow } from '../registry/pairing-protocol.js';
|
|
42
|
+
import { loadWebAuthnCredentialSafe } from '@le-space/orbitdb-identity-provider-webauthn-did/standalone';
|
|
43
|
+
import './fonts/storacha-fonts.css';
|
|
44
|
+
|
|
45
|
+
let {
|
|
46
|
+
orbitdb = null,
|
|
47
|
+
database = null,
|
|
48
|
+
isInitialized = false,
|
|
49
|
+
entryCount = 0,
|
|
50
|
+
databaseName = 'restored-db',
|
|
51
|
+
onRestore = () => {},
|
|
52
|
+
onBackup = () => {},
|
|
53
|
+
onAuthenticate = () => {},
|
|
54
|
+
/** Parent (e.g. StorachaFab) must show the floating panel — otherwise pairing UI is `display:none`. */
|
|
55
|
+
onPairingPromptOpen = () => {},
|
|
56
|
+
libp2p = null,
|
|
57
|
+
/** @deprecated Use {@link signingPreference} `'worker'` instead */
|
|
58
|
+
preferWorkerMode = false,
|
|
59
|
+
/** When set, overrides the in-panel signing mode selector (e.g. programmatic / tests). */
|
|
60
|
+
signingPreference: signingPreferenceOverride = null,
|
|
61
|
+
} = $props();
|
|
62
|
+
|
|
63
|
+
/** User-chosen strategy before passkey create/auth; persisted in localStorage. */
|
|
64
|
+
let selectedSigningPreference = $state(
|
|
65
|
+
/** @type {'hardware-ed25519' | 'hardware-p256' | 'worker'} */ ('hardware-ed25519')
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
onMount(() => {
|
|
69
|
+
const stored = readSigningPreferenceFromStorage();
|
|
70
|
+
if (stored) selectedSigningPreference = stored;
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
function setSigningPreference(/** @type {'hardware-ed25519' | 'hardware-p256' | 'worker'} */ p) {
|
|
74
|
+
selectedSigningPreference = p;
|
|
75
|
+
writeSigningPreferenceToStorage(p);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Emoji for linked-device row; works with {@link detectDeviceLabel} strings (OS · platform, etc.). */
|
|
79
|
+
function linkedDeviceIcon(/** @type {string | undefined} */ label) {
|
|
80
|
+
const s = (label || '').toLowerCase();
|
|
81
|
+
if (s.includes('iphone')) return '\uD83D\uDCF1';
|
|
82
|
+
if (s.includes('ipados') || s.includes('ipad')) return '\uD83D\uDCF1';
|
|
83
|
+
if (s.includes('android')) return '\uD83D\uDCF1';
|
|
84
|
+
if (s.includes('mac') || s.includes('ios')) return '\uD83D\uDCBB';
|
|
85
|
+
if (s.includes('win')) return '\uD83D\uDDA5\uFE0F';
|
|
86
|
+
if (s.includes('linux')) return '\uD83D\uDC27';
|
|
87
|
+
return '\uD83D\uDCF1';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Component state
|
|
91
|
+
let showStoracha = $state(true);
|
|
92
|
+
let isLoading = $state(false);
|
|
93
|
+
let status = $state('');
|
|
94
|
+
let error = $state(null);
|
|
95
|
+
let success = $state(null);
|
|
96
|
+
|
|
97
|
+
// Auth state
|
|
98
|
+
let isLoggedIn = $state(false);
|
|
99
|
+
let client = $state(null);
|
|
100
|
+
let currentSpace = $state(null);
|
|
101
|
+
|
|
102
|
+
// Progress tracking state
|
|
103
|
+
let showProgress = $state(false);
|
|
104
|
+
let progressType = $state('');
|
|
105
|
+
let progressCurrent = $state(0);
|
|
106
|
+
let progressTotal = $state(0);
|
|
107
|
+
let progressPercentage = $state(0);
|
|
108
|
+
let progressCurrentBlock = $state(null);
|
|
109
|
+
let progressError = $state(null);
|
|
110
|
+
|
|
111
|
+
// Bridge instance
|
|
112
|
+
let bridge = null;
|
|
113
|
+
|
|
114
|
+
// Passkey + UCAN state
|
|
115
|
+
let identityService = new IdentityService();
|
|
116
|
+
let signingMode = $state(null); // { mode, did, algorithm, secure }
|
|
117
|
+
let delegationText = $state(''); // textarea for pasting delegation
|
|
118
|
+
let isAuthenticating = $state(false);
|
|
119
|
+
let spaces = $state([]);
|
|
120
|
+
let spaceUsage = $state(null);
|
|
121
|
+
|
|
122
|
+
// Registry DB state
|
|
123
|
+
let registryDb = $state(null);
|
|
124
|
+
const REGISTRY_ADDRESS_KEY = 'p2p_passkeys_registry_address';
|
|
125
|
+
const OWNER_DID_KEY = 'p2p_passkeys_owner_did';
|
|
126
|
+
|
|
127
|
+
// Tab state
|
|
128
|
+
let activeTab = $state('passkeys'); // 'storacha' | 'passkeys' — P2P Passkeys first (primary)
|
|
129
|
+
|
|
130
|
+
// P2P Passkeys state
|
|
131
|
+
let devices = $state([]);
|
|
132
|
+
let peerInfo = $state(null);
|
|
133
|
+
let linkInput = $state('');
|
|
134
|
+
let isLinking = $state(false);
|
|
135
|
+
let linkError = $state('');
|
|
136
|
+
let deviceManager = $state(null);
|
|
137
|
+
/** Prevents duplicate concurrent init from initRegistryDb + $effect. */
|
|
138
|
+
let deviceManagerInitInProgress = false;
|
|
139
|
+
let pendingPairRequest = $state(null);
|
|
140
|
+
let pendingPairResolve = $state(null);
|
|
141
|
+
|
|
142
|
+
// Recovery state
|
|
143
|
+
let isRecovering = $state(false);
|
|
144
|
+
let recoveryStatus = $state('');
|
|
145
|
+
let ipnsKeyPair = $state(null);
|
|
146
|
+
let ipnsNameString = $state('');
|
|
147
|
+
/** UI hint: local passkey / archive / registry keypair present — primary button shows "existing" copy. */
|
|
148
|
+
let localPasskeyDetected = $state(false);
|
|
149
|
+
|
|
150
|
+
const primaryPasskeyLabel = $derived(
|
|
151
|
+
localPasskeyDetected ? 'Authenticate with existing Passkey' : 'Create new Passkey'
|
|
152
|
+
);
|
|
153
|
+
const primaryPasskeyLoadingLabel = $derived(
|
|
154
|
+
localPasskeyDetected ? 'Authenticating...' : 'Creating...'
|
|
155
|
+
);
|
|
156
|
+
const passkeyStepHint = $derived(
|
|
157
|
+
localPasskeyDetected
|
|
158
|
+
? 'Step 1: Sign in with your saved passkey, or recover from backup'
|
|
159
|
+
: 'Step 1: Create a new passkey, or recover an existing one from backup'
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
/** At least one remote peer (e.g. via bootstrap relay / circuit — not necessarily direct). */
|
|
163
|
+
let p2pHasRemotePeers = $state(false);
|
|
164
|
+
/** At least one open connection whose multiaddr uses WebRTC (browser direct path). */
|
|
165
|
+
let p2pHasDirectWebRtc = $state(false);
|
|
166
|
+
/** Count of connected remote peers (`libp2p.getPeers().length`). */
|
|
167
|
+
let p2pRemotePeerCount = $state(0);
|
|
168
|
+
|
|
169
|
+
function addrLooksLikeDirectWebRtc(maStr) {
|
|
170
|
+
const s = (maStr || '').toLowerCase();
|
|
171
|
+
return s.includes('/webrtc') || s.includes('webrtc-direct');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function syncP2pConnectionFlags(node) {
|
|
175
|
+
if (!node || typeof node.getPeers !== 'function') {
|
|
176
|
+
p2pHasRemotePeers = false;
|
|
177
|
+
p2pHasDirectWebRtc = false;
|
|
178
|
+
p2pRemotePeerCount = 0;
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
const peers = node.getPeers();
|
|
183
|
+
p2pRemotePeerCount = peers.length;
|
|
184
|
+
p2pHasRemotePeers = peers.length > 0;
|
|
185
|
+
const conns = typeof node.getConnections === 'function' ? node.getConnections() : [];
|
|
186
|
+
p2pHasDirectWebRtc = conns.some((c) => addrLooksLikeDirectWebRtc(c.remoteAddr?.toString?.()));
|
|
187
|
+
} catch {
|
|
188
|
+
p2pHasRemotePeers = false;
|
|
189
|
+
p2pHasDirectWebRtc = false;
|
|
190
|
+
p2pRemotePeerCount = 0;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
$effect(() => {
|
|
195
|
+
const db = registryDb;
|
|
196
|
+
if (!db) return;
|
|
197
|
+
void (async () => {
|
|
198
|
+
try {
|
|
199
|
+
const sm = await getStoredSigningMode(db);
|
|
200
|
+
if (sm.mode) localPasskeyDetected = true;
|
|
201
|
+
} catch {
|
|
202
|
+
/* ignore */
|
|
203
|
+
}
|
|
204
|
+
})();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
$effect(() => {
|
|
208
|
+
const node = libp2p;
|
|
209
|
+
if (!node) {
|
|
210
|
+
p2pHasRemotePeers = false;
|
|
211
|
+
p2pHasDirectWebRtc = false;
|
|
212
|
+
p2pRemotePeerCount = 0;
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
syncP2pConnectionFlags(node);
|
|
216
|
+
const onChange = () => syncP2pConnectionFlags(node);
|
|
217
|
+
node.addEventListener('peer:connect', onChange);
|
|
218
|
+
node.addEventListener('peer:disconnect', onChange);
|
|
219
|
+
node.addEventListener('connection:open', onChange);
|
|
220
|
+
node.addEventListener('connection:close', onChange);
|
|
221
|
+
return () => {
|
|
222
|
+
node.removeEventListener('peer:connect', onChange);
|
|
223
|
+
node.removeEventListener('peer:disconnect', onChange);
|
|
224
|
+
node.removeEventListener('connection:open', onChange);
|
|
225
|
+
node.removeEventListener('connection:close', onChange);
|
|
226
|
+
};
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
/** Start MultiDeviceManager once libp2p + registry exist (handles restored signingMode / late orbitdb). */
|
|
230
|
+
$effect(() => {
|
|
231
|
+
if (!signingMode?.did || !orbitdb || !libp2p || !registryDb || deviceManager) return;
|
|
232
|
+
void initDeviceManager();
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
/** Gray = no stack; red = no peers; orange = relay / non-WebRTC only; green = ≥1 WebRTC direct transport. */
|
|
236
|
+
const p2pLedDotBg = $derived(
|
|
237
|
+
!libp2p
|
|
238
|
+
? '#9ca3af'
|
|
239
|
+
: !p2pHasRemotePeers
|
|
240
|
+
? '#ef4444'
|
|
241
|
+
: p2pHasDirectWebRtc
|
|
242
|
+
? '#10b981'
|
|
243
|
+
: '#f97316'
|
|
244
|
+
);
|
|
245
|
+
const p2pLedShadow = $derived(
|
|
246
|
+
!libp2p
|
|
247
|
+
? 'none'
|
|
248
|
+
: !p2pHasRemotePeers
|
|
249
|
+
? '0 0 0 3px rgba(239, 68, 68, 0.25)'
|
|
250
|
+
: p2pHasDirectWebRtc
|
|
251
|
+
? '0 0 0 3px rgba(16, 185, 129, 0.2)'
|
|
252
|
+
: '0 0 0 3px rgba(249, 115, 22, 0.22)'
|
|
253
|
+
);
|
|
254
|
+
const p2pLedPulse = $derived(!!libp2p && p2pHasRemotePeers);
|
|
255
|
+
const p2pLedTextColor = $derived(
|
|
256
|
+
!libp2p
|
|
257
|
+
? '#6B7280'
|
|
258
|
+
: !p2pHasRemotePeers
|
|
259
|
+
? '#991b1b'
|
|
260
|
+
: p2pHasDirectWebRtc
|
|
261
|
+
? '#064e3b'
|
|
262
|
+
: '#9a3412'
|
|
263
|
+
);
|
|
264
|
+
const p2pConnectionLabel = $derived(
|
|
265
|
+
!libp2p
|
|
266
|
+
? 'P2P Offline'
|
|
267
|
+
: !p2pHasRemotePeers
|
|
268
|
+
? 'No remote peers'
|
|
269
|
+
: p2pHasDirectWebRtc
|
|
270
|
+
? 'P2P Direct (WebRTC)'
|
|
271
|
+
: 'P2P Relay'
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
/** Link Device needs MultiDeviceManager (registry DB + libp2p). */
|
|
275
|
+
const linkDeviceReady = $derived(!!deviceManager);
|
|
276
|
+
const linkDeviceDisabled = $derived(isLinking || !linkInput.trim() || !linkDeviceReady);
|
|
277
|
+
|
|
278
|
+
function resetProgress() {
|
|
279
|
+
showProgress = false;
|
|
280
|
+
progressType = '';
|
|
281
|
+
progressCurrent = 0;
|
|
282
|
+
progressTotal = 0;
|
|
283
|
+
progressPercentage = 0;
|
|
284
|
+
progressCurrentBlock = null;
|
|
285
|
+
progressError = null;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function showMessage(message, type = 'info') {
|
|
289
|
+
if (type === 'error') {
|
|
290
|
+
error = message;
|
|
291
|
+
success = null;
|
|
292
|
+
} else {
|
|
293
|
+
success = message;
|
|
294
|
+
error = null;
|
|
295
|
+
}
|
|
296
|
+
setTimeout(() => {
|
|
297
|
+
error = null;
|
|
298
|
+
success = null;
|
|
299
|
+
}, 5000);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function clearForms() {
|
|
303
|
+
delegationText = '';
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async function handleAuthenticate() {
|
|
307
|
+
isAuthenticating = true;
|
|
308
|
+
try {
|
|
309
|
+
signingMode = await identityService.initialize(undefined, {
|
|
310
|
+
preferWorkerMode,
|
|
311
|
+
signingPreference: signingPreferenceOverride ?? selectedSigningPreference,
|
|
312
|
+
});
|
|
313
|
+
showMessage(`Authenticated! Mode: ${signingMode.algorithm} (${signingMode.mode})`);
|
|
314
|
+
|
|
315
|
+
// Notify parent that authentication succeeded — await so P2P stack can init
|
|
316
|
+
await onAuthenticate(signingMode);
|
|
317
|
+
|
|
318
|
+
// Derive IPNS keypair for manifest operations
|
|
319
|
+
const kp = identityService.getIPNSKeyPair();
|
|
320
|
+
if (kp) ipnsKeyPair = kp;
|
|
321
|
+
|
|
322
|
+
// Open/create registry DB if OrbitDB is available
|
|
323
|
+
await initRegistryDb();
|
|
324
|
+
|
|
325
|
+
// Try auto-connect if delegation is stored (ignore whitespace-only junk)
|
|
326
|
+
const stored = await loadStoredDelegation(registryDb);
|
|
327
|
+
if (stored?.trim()) await handleConnectWithDelegation(stored.trim());
|
|
328
|
+
} catch (err) {
|
|
329
|
+
showMessage(`Authentication failed: ${err.message}`, 'error');
|
|
330
|
+
} finally {
|
|
331
|
+
isAuthenticating = false;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Connect Storacha using a delegation string: parse, create client, set up bridge.
|
|
337
|
+
* Optionally stores the delegation to registry or localStorage.
|
|
338
|
+
*/
|
|
339
|
+
async function connectStoracha(
|
|
340
|
+
delegationStr,
|
|
341
|
+
{ store = false, storeRegistryDb = null, storeSpaceDid = '' } = {}
|
|
342
|
+
) {
|
|
343
|
+
const [delegation, principal] = await Promise.all([
|
|
344
|
+
parseDelegation(delegationStr),
|
|
345
|
+
identityService.getPrincipal(),
|
|
346
|
+
]);
|
|
347
|
+
client = await createStorachaClient(principal, delegation);
|
|
348
|
+
|
|
349
|
+
if (store) {
|
|
350
|
+
const spaceDid = storeSpaceDid || client.currentSpace()?.did?.() || '';
|
|
351
|
+
await storeDelegation(delegationStr, storeRegistryDb, spaceDid);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
currentSpace = client.currentSpace();
|
|
355
|
+
isLoggedIn = true;
|
|
356
|
+
|
|
357
|
+
bridge = new OrbitDBStorachaBridge({ ucanClient: client });
|
|
358
|
+
if (currentSpace) bridge.spaceDID = currentSpace.did();
|
|
359
|
+
setupBridgeListeners();
|
|
360
|
+
await loadSpaceUsage();
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async function handleRecover() {
|
|
364
|
+
isRecovering = true;
|
|
365
|
+
recoveryStatus = 'Authenticating with passkey...';
|
|
366
|
+
try {
|
|
367
|
+
const recovery = await identityService.initializeFromRecovery();
|
|
368
|
+
ipnsKeyPair = recovery.ipnsKeyPair;
|
|
369
|
+
|
|
370
|
+
const storedDid = localStorage.getItem(OWNER_DID_KEY);
|
|
371
|
+
const storedAddr = localStorage.getItem(REGISTRY_ADDRESS_KEY);
|
|
372
|
+
let recoveredLocally = false;
|
|
373
|
+
|
|
374
|
+
if (storedDid && storedAddr) {
|
|
375
|
+
console.log('[recovery] Attempting local OrbitDB path — DID:', storedDid);
|
|
376
|
+
recoveredLocally = await recoverFromLocalOrbitDB(storedDid, storedAddr);
|
|
377
|
+
} else {
|
|
378
|
+
console.log('[recovery] No local DID/registry cached, using IPNS');
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (!recoveredLocally) {
|
|
382
|
+
console.log('[recovery] Using IPNS/IPFS remote path');
|
|
383
|
+
await recoverFromIPNS(recovery.ipnsKeyPair);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
recoveryStatus = '';
|
|
387
|
+
showMessage('Identity recovered successfully!');
|
|
388
|
+
} catch (err) {
|
|
389
|
+
showMessage(`Recovery failed: ${err.message}`, 'error');
|
|
390
|
+
recoveryStatus = '';
|
|
391
|
+
signingMode = null;
|
|
392
|
+
} finally {
|
|
393
|
+
isRecovering = false;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Local-first recovery: start P2P with default identity, open the local
|
|
399
|
+
* OrbitDB registry, and restore the archive without hitting the network.
|
|
400
|
+
*/
|
|
401
|
+
async function recoverFromLocalOrbitDB(storedDid, storedAddr) {
|
|
402
|
+
try {
|
|
403
|
+
recoveryStatus = 'Starting P2P stack...';
|
|
404
|
+
await onAuthenticate(null);
|
|
405
|
+
|
|
406
|
+
// Wait for Svelte prop propagation with retry
|
|
407
|
+
let waited = 0;
|
|
408
|
+
while (!orbitdb && waited < 2000) {
|
|
409
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
410
|
+
waited += 50;
|
|
411
|
+
}
|
|
412
|
+
if (!orbitdb) {
|
|
413
|
+
console.log('[recovery:local] orbitdb not available after wait');
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
recoveryStatus = 'Opening local registry...';
|
|
418
|
+
const db = await openDeviceRegistry(orbitdb, storedDid, storedAddr);
|
|
419
|
+
|
|
420
|
+
const archiveEntry = await getArchiveEntry(db, storedDid);
|
|
421
|
+
if (!archiveEntry) {
|
|
422
|
+
console.log('[recovery:local] No archive in local OrbitDB for DID:', storedDid);
|
|
423
|
+
return false;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
recoveryStatus = 'Restoring identity from local registry...';
|
|
427
|
+
await identityService.restoreFromManifest(archiveEntry, storedDid);
|
|
428
|
+
signingMode = identityService.getSigningMode();
|
|
429
|
+
console.log('[recovery:local] DID restored:', signingMode?.did);
|
|
430
|
+
|
|
431
|
+
registryDb = db;
|
|
432
|
+
await identityService.setRegistry(db);
|
|
433
|
+
await selfRegisterDevice();
|
|
434
|
+
// Local recovery bypasses initRegistryDb(), so MultiDeviceManager was never started — same as normal auth.
|
|
435
|
+
recoveryStatus = 'Initializing device linking...';
|
|
436
|
+
await initDeviceManager();
|
|
437
|
+
|
|
438
|
+
const stored = await loadStoredDelegation(db);
|
|
439
|
+
if (stored) {
|
|
440
|
+
recoveryStatus = 'Connecting to Storacha...';
|
|
441
|
+
await connectStoracha(stored);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
console.log('[recovery:local] Succeeded — no network needed');
|
|
445
|
+
return true;
|
|
446
|
+
} catch (err) {
|
|
447
|
+
console.warn('[recovery:local] Failed:', err.message);
|
|
448
|
+
return false;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* IPNS/IPFS fallback recovery: resolve the manifest from w3name,
|
|
454
|
+
* fetch the encrypted archive from the gateway, and restore.
|
|
455
|
+
* Stores the DID in localStorage for future local recoveries.
|
|
456
|
+
*/
|
|
457
|
+
async function recoverFromIPNS(ipnsKP) {
|
|
458
|
+
recoveryStatus = 'Resolving IPNS manifest...';
|
|
459
|
+
const manifest = await resolveManifest(ipnsKP.privateKey);
|
|
460
|
+
if (!manifest) {
|
|
461
|
+
throw new Error('No recovery manifest found. This identity may not have been backed up yet.');
|
|
462
|
+
}
|
|
463
|
+
console.log('[recovery:ipns] Manifest resolved — ownerDid:', manifest.ownerDid);
|
|
464
|
+
|
|
465
|
+
if (!manifest.archiveCID) {
|
|
466
|
+
throw new Error('Manifest has no archiveCID. Cannot restore identity without it.');
|
|
467
|
+
}
|
|
468
|
+
recoveryStatus = 'Fetching encrypted archive...';
|
|
469
|
+
const archiveEntry = await fetchArchiveFromIPFS(manifest.archiveCID);
|
|
470
|
+
if (!archiveEntry) {
|
|
471
|
+
throw new Error('Failed to fetch encrypted archive from IPFS.');
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
recoveryStatus = 'Restoring identity...';
|
|
475
|
+
await identityService.restoreFromManifest(archiveEntry, manifest.ownerDid);
|
|
476
|
+
signingMode = identityService.getSigningMode();
|
|
477
|
+
console.log('[recovery:ipns] DID restored:', signingMode?.did);
|
|
478
|
+
|
|
479
|
+
// Only persist DID — registry address is stored when OrbitDB is actually opened
|
|
480
|
+
localStorage.setItem(OWNER_DID_KEY, manifest.ownerDid);
|
|
481
|
+
|
|
482
|
+
recoveryStatus = 'Starting P2P stack...';
|
|
483
|
+
await onAuthenticate(signingMode);
|
|
484
|
+
|
|
485
|
+
// Wait for orbitdb prop to propagate from parent
|
|
486
|
+
let waited = 0;
|
|
487
|
+
while (!orbitdb && waited < 2000) {
|
|
488
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
489
|
+
waited += 50;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Open registry DB so address gets persisted for future local recovery
|
|
493
|
+
await initRegistryDb();
|
|
494
|
+
|
|
495
|
+
if (manifest.delegation) {
|
|
496
|
+
recoveryStatus = 'Connecting to Storacha...';
|
|
497
|
+
await connectStoracha(manifest.delegation, { store: true, storeRegistryDb: registryDb });
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
console.log(
|
|
501
|
+
'[recovery:ipns] Succeeded — DID + registry address cached for future local recovery'
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
async function handlePublishManifest() {
|
|
506
|
+
if (!client || !registryDb || !signingMode?.did) return;
|
|
507
|
+
|
|
508
|
+
// Derive IPNS keypair if not already available
|
|
509
|
+
if (!ipnsKeyPair) {
|
|
510
|
+
const kp = identityService.getIPNSKeyPair();
|
|
511
|
+
if (!kp) {
|
|
512
|
+
console.warn('[ui] No IPNS keypair available, cannot publish manifest');
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
ipnsKeyPair = kp;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
try {
|
|
519
|
+
const addr = registryDb.address?.toString?.() || registryDb.address;
|
|
520
|
+
|
|
521
|
+
// Get the current delegation string
|
|
522
|
+
const delegationStr = await loadStoredDelegation(registryDb);
|
|
523
|
+
|
|
524
|
+
// Upload encrypted archive to IPFS for auth-free recovery
|
|
525
|
+
let archiveCID = null;
|
|
526
|
+
const archiveData = await identityService.getEncryptedArchiveData();
|
|
527
|
+
if (archiveData) {
|
|
528
|
+
try {
|
|
529
|
+
archiveCID = await uploadArchiveToIPFS(client, archiveData);
|
|
530
|
+
console.log('[ui] Archive uploaded to IPFS:', archiveCID);
|
|
531
|
+
} catch (err) {
|
|
532
|
+
console.warn('[ui] Failed to upload archive to IPFS:', err.message);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const manifest = createManifest({
|
|
537
|
+
registryAddress: addr,
|
|
538
|
+
delegation: delegationStr || '',
|
|
539
|
+
ownerDid: signingMode.did,
|
|
540
|
+
archiveCID,
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
const result = await publishManifest(client, ipnsKeyPair.privateKey, manifest);
|
|
544
|
+
ipnsNameString = result.nameString;
|
|
545
|
+
|
|
546
|
+
// Persist DID for future local-first recovery
|
|
547
|
+
localStorage.setItem(OWNER_DID_KEY, signingMode.did);
|
|
548
|
+
|
|
549
|
+
console.log('[ui] Manifest published:', result.nameString);
|
|
550
|
+
} catch (err) {
|
|
551
|
+
console.warn('[ui] Failed to publish manifest:', err.message);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
async function selfRegisterDevice() {
|
|
556
|
+
if (!registryDb || !signingMode?.did) return;
|
|
557
|
+
try {
|
|
558
|
+
const existing = await getDeviceByDID(registryDb, signingMode.did);
|
|
559
|
+
if (existing) return;
|
|
560
|
+
const credential = loadWebAuthnCredentialSafe();
|
|
561
|
+
await registerDevice(registryDb, {
|
|
562
|
+
credential_id:
|
|
563
|
+
credential?.credentialId ||
|
|
564
|
+
credential?.id ||
|
|
565
|
+
libp2p?.peerId?.toString() ||
|
|
566
|
+
signingMode.did,
|
|
567
|
+
public_key:
|
|
568
|
+
credential?.publicKey?.x && credential?.publicKey?.y
|
|
569
|
+
? { kty: 'EC', crv: 'P-256', x: credential.publicKey.x, y: credential.publicKey.y }
|
|
570
|
+
: null,
|
|
571
|
+
device_label: detectDeviceLabel(),
|
|
572
|
+
created_at: Date.now(),
|
|
573
|
+
status: 'active',
|
|
574
|
+
ed25519_did: signingMode.did,
|
|
575
|
+
});
|
|
576
|
+
console.log('[ui] Self-registered device in registry');
|
|
577
|
+
} catch (err) {
|
|
578
|
+
console.warn('[ui] Failed to self-register device:', err.message);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
async function initRegistryDb() {
|
|
583
|
+
if (!orbitdb || !signingMode?.did) return;
|
|
584
|
+
try {
|
|
585
|
+
const storedAddr = localStorage.getItem(REGISTRY_ADDRESS_KEY);
|
|
586
|
+
registryDb = await openDeviceRegistry(orbitdb, signingMode.did, storedAddr);
|
|
587
|
+
|
|
588
|
+
const addr = registryDb.address?.toString?.() || registryDb.address;
|
|
589
|
+
if (addr) localStorage.setItem(REGISTRY_ADDRESS_KEY, addr);
|
|
590
|
+
localStorage.setItem(OWNER_DID_KEY, signingMode.did);
|
|
591
|
+
|
|
592
|
+
await identityService.setRegistry(registryDb);
|
|
593
|
+
await selfRegisterDevice();
|
|
594
|
+
console.log('[ui] Registry DB initialized:', addr, '— DID persisted:', signingMode.did);
|
|
595
|
+
await initDeviceManager();
|
|
596
|
+
} catch (err) {
|
|
597
|
+
// If a stored address exists but we can't write, it's from a different identity.
|
|
598
|
+
// Clear it and create a fresh registry for this identity.
|
|
599
|
+
const storedAddr = localStorage.getItem(REGISTRY_ADDRESS_KEY);
|
|
600
|
+
if (storedAddr) {
|
|
601
|
+
console.warn('[ui] Stale registry address, creating new registry:', err.message);
|
|
602
|
+
localStorage.removeItem(REGISTRY_ADDRESS_KEY);
|
|
603
|
+
try {
|
|
604
|
+
registryDb = await openDeviceRegistry(orbitdb, signingMode.did, null);
|
|
605
|
+
const addr = registryDb.address?.toString?.() || registryDb.address;
|
|
606
|
+
if (addr) localStorage.setItem(REGISTRY_ADDRESS_KEY, addr);
|
|
607
|
+
localStorage.setItem(OWNER_DID_KEY, signingMode.did);
|
|
608
|
+
await identityService.setRegistry(registryDb);
|
|
609
|
+
await selfRegisterDevice();
|
|
610
|
+
console.log('[ui] New registry DB created:', addr, '— DID persisted:', signingMode.did);
|
|
611
|
+
await initDeviceManager();
|
|
612
|
+
return;
|
|
613
|
+
} catch (retryErr) {
|
|
614
|
+
console.warn('[ui] Failed to create new registry:', retryErr.message);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
console.warn('[ui] Failed to init registry DB:', err.message);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
async function initDeviceManager() {
|
|
622
|
+
if (deviceManager) return;
|
|
623
|
+
if (deviceManagerInitInProgress) return;
|
|
624
|
+
if (!libp2p || !registryDb || !signingMode?.did) return;
|
|
625
|
+
deviceManagerInitInProgress = true;
|
|
626
|
+
try {
|
|
627
|
+
const credential = loadWebAuthnCredentialSafe();
|
|
628
|
+
deviceManager = await MultiDeviceManager.createFromExisting({
|
|
629
|
+
credential,
|
|
630
|
+
orbitdb,
|
|
631
|
+
libp2p,
|
|
632
|
+
identity: { id: signingMode.did },
|
|
633
|
+
onPairingRequest: async (request) => {
|
|
634
|
+
pairingFlow(
|
|
635
|
+
'ALICE',
|
|
636
|
+
'[UI] onPairingRequest invoked — showing Storacha panel + passkeys tab',
|
|
637
|
+
{
|
|
638
|
+
fromDid: request?.identity?.id,
|
|
639
|
+
deviceLabel: request?.identity?.deviceLabel,
|
|
640
|
+
}
|
|
641
|
+
);
|
|
642
|
+
console.log(
|
|
643
|
+
'[p2p] Pairing request from:',
|
|
644
|
+
request?.identity?.id || request?.identity?.deviceLabel
|
|
645
|
+
);
|
|
646
|
+
showStoracha = true;
|
|
647
|
+
activeTab = 'passkeys';
|
|
648
|
+
onPairingPromptOpen();
|
|
649
|
+
pairingFlow('ALICE', '[UI] waiting for user — Approve or Deny (promise pending)');
|
|
650
|
+
return new Promise((resolve) => {
|
|
651
|
+
pendingPairRequest = request;
|
|
652
|
+
pendingPairResolve = resolve;
|
|
653
|
+
});
|
|
654
|
+
},
|
|
655
|
+
onDeviceLinked: (device) => {
|
|
656
|
+
devices = devices.filter((d) => d.ed25519_did !== device.ed25519_did);
|
|
657
|
+
devices = [...devices, device];
|
|
658
|
+
},
|
|
659
|
+
onDeviceJoined: (peerId) => {
|
|
660
|
+
console.log('[p2p] Peer joined:', peerId);
|
|
661
|
+
},
|
|
662
|
+
});
|
|
663
|
+
const dbAddr = registryDb.address?.toString?.() || registryDb.address;
|
|
664
|
+
if (dbAddr) await deviceManager.openExistingDb(dbAddr);
|
|
665
|
+
|
|
666
|
+
devices = await deviceManager.listDevices();
|
|
667
|
+
peerInfo = deviceManager.getPeerInfo();
|
|
668
|
+
console.log('[ui] MultiDeviceManager initialized');
|
|
669
|
+
} catch (err) {
|
|
670
|
+
console.warn('[ui] Failed to init MultiDeviceManager:', err.message);
|
|
671
|
+
} finally {
|
|
672
|
+
deviceManagerInitInProgress = false;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
async function handleTabSwitch(tab) {
|
|
677
|
+
activeTab = tab;
|
|
678
|
+
if (tab === 'passkeys' && registryDb) {
|
|
679
|
+
try {
|
|
680
|
+
devices = await listRegistryDevices(registryDb);
|
|
681
|
+
} catch (err) {
|
|
682
|
+
console.warn('[ui] Failed to load devices:', err.message);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function handleCopyPeerInfo() {
|
|
688
|
+
let id = null;
|
|
689
|
+
if (deviceManager) {
|
|
690
|
+
peerInfo = deviceManager.getPeerInfo();
|
|
691
|
+
id = peerInfo?.peerId;
|
|
692
|
+
} else if (libp2p) {
|
|
693
|
+
id = libp2p.peerId.toString();
|
|
694
|
+
peerInfo = { peerId: id, multiaddrs: libp2p.getMultiaddrs().map((ma) => ma.toString()) };
|
|
695
|
+
}
|
|
696
|
+
if (!id) return;
|
|
697
|
+
navigator.clipboard.writeText(id);
|
|
698
|
+
showMessage('Peer ID copied — paste it on the other device after both are connected via P2P.');
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/** Plain peer id, or legacy JSON `{ "peerId", "multiaddrs"? }`. */
|
|
702
|
+
function parseLinkPeerInput(raw) {
|
|
703
|
+
const t = raw.trim();
|
|
704
|
+
if (!t) throw new Error('Enter the other device’s peer id.');
|
|
705
|
+
if (t.startsWith('{')) {
|
|
706
|
+
let o;
|
|
707
|
+
try {
|
|
708
|
+
o = JSON.parse(t);
|
|
709
|
+
} catch {
|
|
710
|
+
throw new Error('Invalid JSON. Paste a peer id, or legacy { "peerId", "multiaddrs" }.');
|
|
711
|
+
}
|
|
712
|
+
return MultiDeviceManager._normalizeLinkPayload(o);
|
|
713
|
+
}
|
|
714
|
+
return MultiDeviceManager._normalizeLinkPayload(t);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
async function migrateRegistryEntries(oldDb, newDb, did) {
|
|
718
|
+
// Read all entries from old DB once (stable — not being written to during migration)
|
|
719
|
+
const keypairs = await listKeypairs(oldDb);
|
|
720
|
+
const archive = await getArchiveEntry(oldDb, did);
|
|
721
|
+
const delegations = await listDelegations(oldDb);
|
|
722
|
+
|
|
723
|
+
if (keypairs.length === 0 && !archive && delegations.length === 0) {
|
|
724
|
+
console.log('[ui] No entries to migrate');
|
|
725
|
+
return true;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// Retry writes until ACL grant propagates from Device A
|
|
729
|
+
const maxWait = 120000;
|
|
730
|
+
const start = Date.now();
|
|
731
|
+
while (Date.now() - start < maxWait) {
|
|
732
|
+
try {
|
|
733
|
+
for (const kp of keypairs) {
|
|
734
|
+
await storeKeypairEntry(newDb, kp.did, kp.publicKey);
|
|
735
|
+
}
|
|
736
|
+
if (archive) {
|
|
737
|
+
await storeArchiveEntry(newDb, did, archive.ciphertext, archive.iv);
|
|
738
|
+
}
|
|
739
|
+
for (const d of delegations) {
|
|
740
|
+
await storeDelegationEntry(newDb, d.delegation, d.space_did);
|
|
741
|
+
}
|
|
742
|
+
console.log('[ui] Registry migration complete after', Date.now() - start, 'ms');
|
|
743
|
+
return true;
|
|
744
|
+
} catch (err) {
|
|
745
|
+
if (!err.message?.includes('not allowed to write')) {
|
|
746
|
+
console.warn('[ui] Registry migration error:', err.message);
|
|
747
|
+
return false;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
751
|
+
}
|
|
752
|
+
console.warn('[ui] Registry migration timed out waiting for write access');
|
|
753
|
+
return false;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
async function handleLinkDevice() {
|
|
757
|
+
if (!linkInput.trim()) {
|
|
758
|
+
showMessage('Enter the other device’s peer id (copy from the P2P Passkeys tab).', 'error');
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
if (!deviceManager) {
|
|
762
|
+
showMessage(
|
|
763
|
+
'Device linking is not ready yet. Wait for OrbitDB and the registry to finish initializing, and ensure this app has a libp2p instance.',
|
|
764
|
+
'error'
|
|
765
|
+
);
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
isLinking = true;
|
|
769
|
+
linkError = '';
|
|
770
|
+
try {
|
|
771
|
+
const payload = parseLinkPeerInput(linkInput);
|
|
772
|
+
const result = await deviceManager.linkToDevice(payload);
|
|
773
|
+
pairingFlow('BOB', '[UI] linkToDevice returned', {
|
|
774
|
+
type: result?.type,
|
|
775
|
+
reason: result?.reason,
|
|
776
|
+
});
|
|
777
|
+
if (result.type === 'granted') {
|
|
778
|
+
const sharedAddr = result.dbAddress?.toString?.() || result.dbAddress;
|
|
779
|
+
pairingFlow(
|
|
780
|
+
'BOB',
|
|
781
|
+
'[UI] granted — will migrate local registry entries then persist shared address',
|
|
782
|
+
{
|
|
783
|
+
sharedAddrPrefix: sharedAddr ? String(sharedAddr).slice(0, 48) + '…' : null,
|
|
784
|
+
}
|
|
785
|
+
);
|
|
786
|
+
|
|
787
|
+
// Migrate Device B's entries to the shared registry before switching
|
|
788
|
+
let migrated = false;
|
|
789
|
+
if (registryDb && deviceManager.getDevicesDb() && signingMode?.did) {
|
|
790
|
+
const sharedDb = deviceManager.getDevicesDb();
|
|
791
|
+
console.log('[ui] Migrating entries from old registry to shared registry...');
|
|
792
|
+
migrated = await migrateRegistryEntries(registryDb, sharedDb, signingMode.did);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// Only switch to shared registry if migration succeeded
|
|
796
|
+
if (migrated && sharedAddr) {
|
|
797
|
+
localStorage.setItem(REGISTRY_ADDRESS_KEY, sharedAddr);
|
|
798
|
+
console.log('[ui] Persisted shared registry address from Device A:', sharedAddr);
|
|
799
|
+
} else if (!migrated) {
|
|
800
|
+
console.warn('[ui] Keeping old registry address — migration did not complete');
|
|
801
|
+
}
|
|
802
|
+
showMessage('Device linked successfully!');
|
|
803
|
+
pairingFlow('BOB', '[UI] device link success message shown; device list refreshed');
|
|
804
|
+
linkInput = '';
|
|
805
|
+
devices = await deviceManager.listDevices();
|
|
806
|
+
} else {
|
|
807
|
+
pairingFlow('BOB', '[UI] link rejected by Alice', { reason: result.reason });
|
|
808
|
+
linkError = result.reason || 'Link request was rejected';
|
|
809
|
+
}
|
|
810
|
+
} catch (err) {
|
|
811
|
+
pairingFlow('BOB', '[UI] linkToDevice threw', { error: err?.message });
|
|
812
|
+
linkError = `Failed to link: ${err.message}`;
|
|
813
|
+
} finally {
|
|
814
|
+
isLinking = false;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function handlePairDecision(decision) {
|
|
819
|
+
if (pendingPairResolve) {
|
|
820
|
+
pairingFlow('ALICE', '[UI] user clicked pairing decision — resolving promise to handler', {
|
|
821
|
+
decision,
|
|
822
|
+
});
|
|
823
|
+
pendingPairResolve(decision);
|
|
824
|
+
pendingPairResolve = null;
|
|
825
|
+
pendingPairRequest = null;
|
|
826
|
+
} else {
|
|
827
|
+
pairingFlow('ALICE', '[UI] handlePairDecision called but no pending pairing (ignored)', {
|
|
828
|
+
decision,
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
function formatDelegationImportError(err) {
|
|
834
|
+
const msg = err?.message || String(err);
|
|
835
|
+
if (/atob|correctly encoded|base64/i.test(msg)) {
|
|
836
|
+
return (
|
|
837
|
+
'That text is not a valid UCAN delegation (base64 / CAR). ' +
|
|
838
|
+
'Export a delegation from Storacha (e.g. w3up CLI or another browser that is already logged in) and paste it here. ' +
|
|
839
|
+
'To link browsers over libp2p only, use the P2P Passkeys tab and paste the other device’s peer id — not this field.'
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
return `Delegation import failed: ${msg}`;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
async function handleImportDelegation() {
|
|
846
|
+
if (!delegationText.trim()) {
|
|
847
|
+
showMessage('Please paste a UCAN delegation', 'error');
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
isLoading = true;
|
|
851
|
+
try {
|
|
852
|
+
await handleConnectWithDelegation(delegationText.trim());
|
|
853
|
+
delegationText = '';
|
|
854
|
+
} catch (err) {
|
|
855
|
+
showMessage(formatDelegationImportError(err), 'error');
|
|
856
|
+
} finally {
|
|
857
|
+
isLoading = false;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
async function handleConnectWithDelegation(delegationStr) {
|
|
862
|
+
await connectStoracha(delegationStr, { store: true, storeRegistryDb: registryDb });
|
|
863
|
+
showMessage('Connected to Storacha via UCAN delegation!');
|
|
864
|
+
await handlePublishManifest();
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
function setupBridgeListeners() {
|
|
868
|
+
if (!bridge) return;
|
|
869
|
+
bridge.on('uploadProgress', (progress) => {
|
|
870
|
+
progressType = 'upload';
|
|
871
|
+
progressCurrent = progress.current;
|
|
872
|
+
progressTotal = progress.total;
|
|
873
|
+
progressPercentage = progress.percentage;
|
|
874
|
+
progressCurrentBlock = progress.currentBlock;
|
|
875
|
+
progressError = progress.error;
|
|
876
|
+
showProgress = true;
|
|
877
|
+
});
|
|
878
|
+
bridge.on('downloadProgress', (progress) => {
|
|
879
|
+
progressType = 'download';
|
|
880
|
+
progressCurrent = progress.current;
|
|
881
|
+
progressTotal = progress.total;
|
|
882
|
+
progressPercentage = progress.percentage;
|
|
883
|
+
progressCurrentBlock = progress.currentBlock;
|
|
884
|
+
progressError = progress.error;
|
|
885
|
+
showProgress = true;
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
async function handleLogout() {
|
|
890
|
+
isLoggedIn = false;
|
|
891
|
+
client = null;
|
|
892
|
+
currentSpace = null;
|
|
893
|
+
spaces = [];
|
|
894
|
+
spaceUsage = null;
|
|
895
|
+
signingMode = null;
|
|
896
|
+
await clearStoredDelegation(registryDb);
|
|
897
|
+
if (bridge) {
|
|
898
|
+
bridge.removeAllListeners();
|
|
899
|
+
bridge = null;
|
|
900
|
+
}
|
|
901
|
+
resetProgress();
|
|
902
|
+
showMessage('Logged out successfully');
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
async function loadSpaceUsage() {
|
|
906
|
+
if (!client) return;
|
|
907
|
+
try {
|
|
908
|
+
spaceUsage = await getSpaceUsage(client);
|
|
909
|
+
} catch (err) {
|
|
910
|
+
console.warn('Failed to load space usage info:', err.message);
|
|
911
|
+
spaceUsage = null;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
async function loadSpaces() {
|
|
916
|
+
if (!client) return;
|
|
917
|
+
isLoading = true;
|
|
918
|
+
status = 'Loading spaces...';
|
|
919
|
+
try {
|
|
920
|
+
spaces = await listSpaces(client);
|
|
921
|
+
await loadSpaceUsage();
|
|
922
|
+
} catch (err) {
|
|
923
|
+
showMessage(`Failed to load spaces: ${err.message}`, 'error');
|
|
924
|
+
} finally {
|
|
925
|
+
isLoading = false;
|
|
926
|
+
status = '';
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
async function handleBackup() {
|
|
931
|
+
if (!bridge) {
|
|
932
|
+
showMessage('Please log in first', 'error');
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
const hasDatabase = database && entryCount > 0;
|
|
937
|
+
const hasRegistry = !!registryDb;
|
|
938
|
+
|
|
939
|
+
if (!hasDatabase && !hasRegistry) {
|
|
940
|
+
showMessage('No data to backup', 'error');
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
isLoading = true;
|
|
945
|
+
resetProgress();
|
|
946
|
+
status = 'Preparing backup...';
|
|
947
|
+
|
|
948
|
+
try {
|
|
949
|
+
// Backup user database if available
|
|
950
|
+
if (hasDatabase) {
|
|
951
|
+
const result = await bridge.backup(orbitdb, database.address);
|
|
952
|
+
|
|
953
|
+
if (result.success) {
|
|
954
|
+
showMessage(
|
|
955
|
+
`Backup completed! ${result.blocksUploaded}/${result.blocksTotal} blocks uploaded`
|
|
956
|
+
);
|
|
957
|
+
onBackup(result);
|
|
958
|
+
} else {
|
|
959
|
+
showMessage(result.error, 'error');
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// Backup registry DB
|
|
965
|
+
if (hasRegistry) {
|
|
966
|
+
try {
|
|
967
|
+
const regResult = await backupRegistryDb(bridge, orbitdb, registryDb);
|
|
968
|
+
console.log('[ui] Registry DB backed up:', regResult);
|
|
969
|
+
if (!hasDatabase) {
|
|
970
|
+
showMessage('Registry backup completed!');
|
|
971
|
+
}
|
|
972
|
+
} catch (err) {
|
|
973
|
+
console.warn('[ui] Registry backup failed:', err.message);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
await handlePublishManifest();
|
|
978
|
+
} catch (err) {
|
|
979
|
+
showMessage(`Backup failed: ${err.message}`, 'error');
|
|
980
|
+
} finally {
|
|
981
|
+
isLoading = false;
|
|
982
|
+
status = '';
|
|
983
|
+
resetProgress();
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
async function restoreFromSpaceFallback() {
|
|
988
|
+
if (!orbitdb) {
|
|
989
|
+
showMessage('OrbitDB not initialized. Please wait for initialization to complete.', 'error');
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
isLoading = true;
|
|
994
|
+
resetProgress();
|
|
995
|
+
status = 'Preparing restore...';
|
|
996
|
+
|
|
997
|
+
try {
|
|
998
|
+
// Close existing database if provided
|
|
999
|
+
if (database) {
|
|
1000
|
+
status = 'Closing existing database...';
|
|
1001
|
+
try {
|
|
1002
|
+
await database.close();
|
|
1003
|
+
} catch {
|
|
1004
|
+
// Continue even if close fails
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
status = 'Starting restore...';
|
|
1009
|
+
|
|
1010
|
+
if (!bridge) {
|
|
1011
|
+
throw new Error('Bridge not initialized. Please connect to Storacha first.');
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
const result = await bridge.restoreFromSpace(orbitdb, {
|
|
1015
|
+
timeout: 120000,
|
|
1016
|
+
preferredDatabaseName: databaseName,
|
|
1017
|
+
restartAfterRestore: true,
|
|
1018
|
+
verifyIntegrity: true,
|
|
1019
|
+
});
|
|
1020
|
+
|
|
1021
|
+
if (result.success) {
|
|
1022
|
+
showMessage(`Restore completed! ${result.entriesRecovered} entries recovered.`);
|
|
1023
|
+
onRestore(result.database);
|
|
1024
|
+
} else {
|
|
1025
|
+
showMessage(`Restore failed: ${result.error}`, 'error');
|
|
1026
|
+
}
|
|
1027
|
+
} catch (err) {
|
|
1028
|
+
showMessage(`Restore failed: ${err.message}`, 'error');
|
|
1029
|
+
} finally {
|
|
1030
|
+
isLoading = false;
|
|
1031
|
+
status = '';
|
|
1032
|
+
resetProgress();
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
function formatRelativeTime(dateString) {
|
|
1037
|
+
if (!dateString) return 'Never';
|
|
1038
|
+
const date = new Date(dateString);
|
|
1039
|
+
const now = new Date();
|
|
1040
|
+
const diffInSeconds = Math.floor((now - date) / 1000);
|
|
1041
|
+
|
|
1042
|
+
if (diffInSeconds < 60) return 'Just now';
|
|
1043
|
+
if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)} minutes ago`;
|
|
1044
|
+
if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)} hours ago`;
|
|
1045
|
+
if (diffInSeconds < 2592000) return `${Math.floor(diffInSeconds / 86400)} days ago`;
|
|
1046
|
+
if (diffInSeconds < 31536000) return `${Math.floor(diffInSeconds / 2592000)} months ago`;
|
|
1047
|
+
return `${Math.floor(diffInSeconds / 31536000)} years ago`;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
function formatSpaceName(space) {
|
|
1051
|
+
return space.name === 'Unnamed Space' ? `Space ${space.did.slice(-8)}` : space.name;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
onMount(async () => {
|
|
1055
|
+
localPasskeyDetected = hasLocalPasskeyHint();
|
|
1056
|
+
|
|
1057
|
+
// Try to reopen registry DB from stored address
|
|
1058
|
+
if (orbitdb) {
|
|
1059
|
+
const storedAddr = localStorage.getItem(REGISTRY_ADDRESS_KEY);
|
|
1060
|
+
if (storedAddr) {
|
|
1061
|
+
try {
|
|
1062
|
+
registryDb = await openDeviceRegistry(orbitdb, null, storedAddr);
|
|
1063
|
+
console.log('[ui] Reopened registry DB from stored address');
|
|
1064
|
+
} catch (err) {
|
|
1065
|
+
console.warn('[ui] Failed to reopen registry:', err.message);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
// Check for stored signing mode (no biometric needed)
|
|
1071
|
+
const stored = identityService.getSigningMode();
|
|
1072
|
+
if (stored.mode) {
|
|
1073
|
+
signingMode = stored;
|
|
1074
|
+
}
|
|
1075
|
+
// Don't auto-connect — user must click "Authenticate" which may prompt biometric
|
|
1076
|
+
});
|
|
1077
|
+
</script>
|
|
1078
|
+
|
|
1079
|
+
{#snippet pairingApprovalPrompt()}
|
|
1080
|
+
{#if pendingPairRequest}
|
|
1081
|
+
<div
|
|
1082
|
+
data-testid="storacha-pairing-prompt"
|
|
1083
|
+
style="border-radius: 0.375rem; border: 2px solid #FFC83F; background: linear-gradient(to bottom right, #ffffff, #FFF8E1); padding: 1rem; box-shadow: 0 4px 12px rgba(233, 19, 21, 0.15);"
|
|
1084
|
+
>
|
|
1085
|
+
<h4
|
|
1086
|
+
style="margin: 0 0 0.5rem 0; font-weight: 700; color: #E91315; font-family: 'Epilogue', sans-serif; font-size: 0.875rem;"
|
|
1087
|
+
>
|
|
1088
|
+
Device Pairing Request
|
|
1089
|
+
</h4>
|
|
1090
|
+
<p
|
|
1091
|
+
style="margin: 0 0 0.5rem 0; font-size: 0.75rem; color: #374151; font-family: 'DM Sans', sans-serif; line-height: 1.4;"
|
|
1092
|
+
>
|
|
1093
|
+
A device wants to link to your account:
|
|
1094
|
+
</p>
|
|
1095
|
+
<div
|
|
1096
|
+
style="background: rgba(233, 19, 21, 0.04); border-radius: 0.25rem; padding: 0.5rem 0.75rem; margin-bottom: 0.75rem;"
|
|
1097
|
+
>
|
|
1098
|
+
<div style="font-size: 0.7rem; color: #6b7280; font-family: 'DM Sans', sans-serif;">
|
|
1099
|
+
<strong>Device:</strong>
|
|
1100
|
+
{pendingPairRequest.identity?.deviceLabel || 'Unknown'}
|
|
1101
|
+
</div>
|
|
1102
|
+
<div
|
|
1103
|
+
style="font-size: 0.65rem; color: #9ca3af; font-family: 'DM Mono', monospace; margin-top: 0.25rem; word-break: break-all;"
|
|
1104
|
+
>
|
|
1105
|
+
{pendingPairRequest.identity?.id
|
|
1106
|
+
? pendingPairRequest.identity.id.slice(0, 20) +
|
|
1107
|
+
'...' +
|
|
1108
|
+
pendingPairRequest.identity.id.slice(-8)
|
|
1109
|
+
: 'N/A'}
|
|
1110
|
+
</div>
|
|
1111
|
+
</div>
|
|
1112
|
+
<div style="display: flex; gap: 0.5rem;">
|
|
1113
|
+
<button
|
|
1114
|
+
data-testid="storacha-pairing-approve"
|
|
1115
|
+
onclick={() => handlePairDecision('granted')}
|
|
1116
|
+
style="flex: 1; padding: 0.5rem 1rem; border-radius: 0.375rem; background: linear-gradient(135deg, #10b981, #059669); color: #fff; border: none; cursor: pointer; font-family: 'Epilogue', sans-serif; font-weight: 700; font-size: 0.8rem;"
|
|
1117
|
+
>
|
|
1118
|
+
Approve
|
|
1119
|
+
</button>
|
|
1120
|
+
<button
|
|
1121
|
+
data-testid="storacha-pairing-deny"
|
|
1122
|
+
onclick={() => handlePairDecision('rejected')}
|
|
1123
|
+
style="flex: 1; padding: 0.5rem 1rem; border-radius: 0.375rem; background: transparent; color: #dc2626; border: 1px solid #dc2626; cursor: pointer; font-family: 'Epilogue', sans-serif; font-weight: 700; font-size: 0.8rem;"
|
|
1124
|
+
>
|
|
1125
|
+
Deny
|
|
1126
|
+
</button>
|
|
1127
|
+
</div>
|
|
1128
|
+
</div>
|
|
1129
|
+
{/if}
|
|
1130
|
+
{/snippet}
|
|
1131
|
+
|
|
1132
|
+
{#snippet linkedDevicesPanel()}
|
|
1133
|
+
<div
|
|
1134
|
+
style="border-radius: 0.375rem; border: 1px solid #E91315; background: linear-gradient(to bottom right, #ffffff, #FFE4AE); padding: 0.75rem;"
|
|
1135
|
+
>
|
|
1136
|
+
<div
|
|
1137
|
+
style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.5rem;"
|
|
1138
|
+
>
|
|
1139
|
+
<div
|
|
1140
|
+
style="font-size: 0.65rem; font-weight: 700; color: #E91315; text-transform: uppercase; letter-spacing: 0.05em; font-family: 'DM Sans', sans-serif;"
|
|
1141
|
+
>
|
|
1142
|
+
Linked Devices
|
|
1143
|
+
</div>
|
|
1144
|
+
<div
|
|
1145
|
+
style="display: flex; align-items: center; gap: 0.25rem; background: #FFC83F; padding: 0.125rem 0.5rem; border-radius: 9999px;"
|
|
1146
|
+
>
|
|
1147
|
+
<span
|
|
1148
|
+
style="font-size: 0.7rem; font-weight: 700; color: #111827; font-family: 'DM Mono', monospace;"
|
|
1149
|
+
>{devices.length}</span
|
|
1150
|
+
>
|
|
1151
|
+
</div>
|
|
1152
|
+
</div>
|
|
1153
|
+
{#if devices.length === 0}
|
|
1154
|
+
<div
|
|
1155
|
+
style="text-align: center; padding: 1rem; font-size: 0.8rem; color: #9ca3af; font-family: 'DM Sans', sans-serif;"
|
|
1156
|
+
>
|
|
1157
|
+
No devices linked yet
|
|
1158
|
+
</div>
|
|
1159
|
+
{:else}
|
|
1160
|
+
<div style="display: flex; flex-direction: column; gap: 0.375rem;">
|
|
1161
|
+
{#each devices as device}
|
|
1162
|
+
<div
|
|
1163
|
+
data-testid="storacha-linked-device-row"
|
|
1164
|
+
data-device-label={device.device_label || ''}
|
|
1165
|
+
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 ===
|
|
1166
|
+
'active'
|
|
1167
|
+
? '#10b981'
|
|
1168
|
+
: '#E91315'};"
|
|
1169
|
+
>
|
|
1170
|
+
<div style="font-size: 1rem; flex-shrink: 0;">
|
|
1171
|
+
{linkedDeviceIcon(device.device_label)}
|
|
1172
|
+
</div>
|
|
1173
|
+
<div style="flex: 1; min-width: 0;">
|
|
1174
|
+
<div
|
|
1175
|
+
style="font-size: 0.8rem; font-weight: 600; color: #1f2937; font-family: 'DM Sans', sans-serif;"
|
|
1176
|
+
>
|
|
1177
|
+
{device.device_label || 'Unknown Device'}
|
|
1178
|
+
</div>
|
|
1179
|
+
<code style="font-size: 0.625rem; color: #6B7280; font-family: 'DM Mono', monospace;">
|
|
1180
|
+
{device.ed25519_did
|
|
1181
|
+
? device.ed25519_did.slice(0, 16) + '...' + device.ed25519_did.slice(-8)
|
|
1182
|
+
: 'N/A'}
|
|
1183
|
+
</code>
|
|
1184
|
+
</div>
|
|
1185
|
+
<span
|
|
1186
|
+
style="font-size: 0.6rem; font-weight: 600; padding: 0.125rem 0.375rem; border-radius: 9999px; flex-shrink: 0; background: {device.status ===
|
|
1187
|
+
'active'
|
|
1188
|
+
? '#dcfce7'
|
|
1189
|
+
: '#fee2e2'}; color: {device.status === 'active'
|
|
1190
|
+
? '#166534'
|
|
1191
|
+
: '#991b1b'}; font-family: 'DM Sans', sans-serif;"
|
|
1192
|
+
>
|
|
1193
|
+
{device.status}
|
|
1194
|
+
</span>
|
|
1195
|
+
</div>
|
|
1196
|
+
{/each}
|
|
1197
|
+
</div>
|
|
1198
|
+
{/if}
|
|
1199
|
+
</div>
|
|
1200
|
+
{/snippet}
|
|
1201
|
+
|
|
1202
|
+
<div
|
|
1203
|
+
data-testid="storacha-panel"
|
|
1204
|
+
class="storacha-panel"
|
|
1205
|
+
style="max-height: 70vh; overflow-y: auto; border-radius: 0.75rem; border: 1px solid #E91315; background: linear-gradient(to bottom right, #FFE4AE, #EFE3F3); padding: 1rem; box-shadow: 0 25px 50px -12px rgba(0,0,0,0.25); font-family: 'DM Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;"
|
|
1206
|
+
>
|
|
1207
|
+
<!-- Header -->
|
|
1208
|
+
<div
|
|
1209
|
+
style="margin-bottom: 1rem; display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid rgba(233, 19, 21, 0.2); padding-bottom: 0.75rem;"
|
|
1210
|
+
>
|
|
1211
|
+
<div style="display: flex; align-items: center; gap: 0.75rem;">
|
|
1212
|
+
<div
|
|
1213
|
+
style="border-radius: 0.5rem; border: 1px solid rgba(233, 19, 21, 0.2); background-color: #ffffff; padding: 0.5rem; box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -4px rgba(0,0,0,0.1);"
|
|
1214
|
+
>
|
|
1215
|
+
<svg
|
|
1216
|
+
width="20"
|
|
1217
|
+
height="22"
|
|
1218
|
+
viewBox="0 0 154 172"
|
|
1219
|
+
fill="none"
|
|
1220
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
1221
|
+
>
|
|
1222
|
+
<path
|
|
1223
|
+
d="M110.999 41.5313H71.4081C70.2881 41.5313 69.334 42.4869 69.334 43.6087V154.359C69.334 159.461 69.1847 164.596 69.334 169.698C69.334 169.773 69.334 169.839 69.334 169.914C69.334 171.036 70.2881 171.992 71.4081 171.992H111.646C112.766 171.992 113.72 171.036 113.72 169.914V129.613L111.646 131.69H151.884C153.004 131.69 153.959 130.735 153.959 129.613V95.7513C153.959 91.6796 154.041 87.5996 153.942 83.5362C153.685 72.9996 149.512 62.8038 142.318 55.1091C135.125 47.4144 125.319 42.7029 114.907 41.7141C113.604 41.5894 112.302 41.5313 110.991 41.5313C108.319 41.523 108.319 45.6777 110.991 45.6861C120.772 45.7193 130.305 49.4171 137.457 56.1229C144.608 62.8287 149.022 71.9443 149.702 81.6416C149.993 85.813 149.802 90.0592 149.802 94.2306V124.677C149.802 126.231 149.694 127.826 149.802 129.38C149.802 129.455 149.802 129.53 149.802 129.604L151.876 127.527H111.638C110.518 127.527 109.564 128.483 109.564 129.604V169.906L111.638 167.829H71.3998L73.474 169.906V48.7689C73.474 47.1319 73.5818 45.4617 73.474 43.8247C73.474 43.7499 73.474 43.6834 73.474 43.6087L71.3998 45.6861H110.991C113.662 45.6861 113.662 41.5313 110.991 41.5313H110.999Z"
|
|
1224
|
+
fill="#E91315"
|
|
1225
|
+
/>
|
|
1226
|
+
<path
|
|
1227
|
+
d="M108.519 68.9694C108.452 62.9532 104.727 57.66 99.1103 55.5494C93.4935 53.4387 87.0886 55.2669 83.3718 59.779C79.5554 64.4157 78.9165 71.0966 82.0277 76.2901C85.1389 81.4836 91.2037 84.0762 97.1025 82.9544C103.723 81.6996 108.444 75.617 108.527 68.9694C108.56 66.2937 104.412 66.2937 104.379 68.9694C104.329 73.1325 101.749 77.0878 97.7579 78.4838C93.7673 79.8798 89.03 78.6749 86.3087 75.2265C83.5875 71.778 83.4879 67.2077 85.6865 63.6346C87.8851 60.0615 92.2076 58.1752 96.2811 59.0477C100.985 60.0532 104.32 64.1664 104.379 68.9777C104.412 71.6533 108.56 71.6533 108.527 68.9777L108.519 68.9694Z"
|
|
1228
|
+
fill="#E91315"
|
|
1229
|
+
/>
|
|
1230
|
+
<path
|
|
1231
|
+
d="M94.265 73.3237C96.666 73.3237 98.6124 71.3742 98.6124 68.9695C98.6124 66.5647 96.666 64.6152 94.265 64.6152C91.8641 64.6152 89.9177 66.5647 89.9177 68.9695C89.9177 71.3742 91.8641 73.3237 94.265 73.3237Z"
|
|
1232
|
+
fill="#E91315"
|
|
1233
|
+
/>
|
|
1234
|
+
<path
|
|
1235
|
+
d="M71.4081 36.8029H132.429C144.642 36.8029 150.64 28.5764 151.752 23.8981C152.863 19.2281 147.263 7.43685 133.624 22.1199C133.624 22.1199 141.754 6.32336 130.869 2.76686C119.984 -0.789637 107.473 10.1042 102.512 20.5577C102.512 20.5577 103.109 7.6529 91.8923 10.769C80.6754 13.8851 71.4081 36.7946 71.4081 36.7946V36.8029Z"
|
|
1236
|
+
fill="#E91315"
|
|
1237
|
+
/>
|
|
1238
|
+
<path
|
|
1239
|
+
d="M18.186 66.1195C17.879 66.0531 17.8707 65.6126 18.1694 65.5212C31.6927 61.4246 42.2376 70.7895 46.0457 76.6312C48.3189 80.1212 51.6956 83.3868 54.1182 85.5058C55.4042 86.6276 55.0889 88.7216 53.5292 89.4113C52.4589 89.8849 50.7498 90.9402 49.2316 91.846C46.3859 93.5495 42.4699 100.554 33.0948 101.884C26.1921 102.856 17.6716 98.7014 13.6561 96.4329C13.3408 96.2584 13.5399 95.793 13.8884 95.8761C19.8536 97.3137 24.2673 94.8291 22.4753 91.5302C21.1395 89.0706 17.5223 88.1482 12.2789 90.2339C7.61621 92.087 2.07414 86.0376 0.597357 84.2843C0.439724 84.1015 0.555875 83.8106 0.788177 83.7857C5.16044 83.3453 9.41656 78.8664 12.2291 74.1715C14.801 69.8755 20.5837 69.4849 22.4255 69.4683C22.6744 69.4683 22.8154 69.1858 22.6661 68.9863C22.0605 68.1886 20.6169 66.6513 18.186 66.1112V66.1195ZM30.1413 87.9571C29.7264 87.9322 29.4692 88.3975 29.7181 88.7299C30.7967 90.1342 33.5345 92.5855 38.7448 90.9818C45.8134 88.8047 46.1038 84.3175 40.9516 80.3455C36.4798 76.9054 29.2204 77.5618 24.8647 79.8968C24.4084 80.1461 24.5992 80.8441 25.1136 80.8026C26.8641 80.6696 30.133 80.8607 32.0827 82.2401C34.7126 84.0932 35.617 88.331 30.1413 87.9654V87.9571Z"
|
|
1240
|
+
fill="#E91315"
|
|
1241
|
+
/>
|
|
1242
|
+
</svg>
|
|
1243
|
+
</div>
|
|
1244
|
+
<div>
|
|
1245
|
+
<h3
|
|
1246
|
+
style="font-size: 1.125rem; font-weight: 700; color: #E91315; font-family: 'Epilogue', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;"
|
|
1247
|
+
>
|
|
1248
|
+
P2Pass
|
|
1249
|
+
</h3>
|
|
1250
|
+
<p
|
|
1251
|
+
style="font-size: 0.6875rem; color: #555; font-family: 'DM Mono', monospace; line-height: 1.35; max-width: 18rem;"
|
|
1252
|
+
>
|
|
1253
|
+
peer-to-peer passkeys and ucans
|
|
1254
|
+
</p>
|
|
1255
|
+
</div>
|
|
1256
|
+
</div>
|
|
1257
|
+
|
|
1258
|
+
<button
|
|
1259
|
+
class="storacha-toggle"
|
|
1260
|
+
onclick={() => (showStoracha = !showStoracha)}
|
|
1261
|
+
style="border-radius: 0.5rem; padding: 0.5rem; color: #E91315; transition: color 150ms, background-color 150ms; border: none; background: transparent; cursor: pointer;"
|
|
1262
|
+
title={showStoracha ? 'Collapse' : 'Expand'}
|
|
1263
|
+
aria-label={showStoracha ? 'Collapse P2Pass panel' : 'Expand P2Pass panel'}
|
|
1264
|
+
>
|
|
1265
|
+
<svg
|
|
1266
|
+
style="height: 1rem; width: 1rem; transition: transform 200ms; transform: {showStoracha
|
|
1267
|
+
? 'rotate(180deg)'
|
|
1268
|
+
: 'rotate(0deg)'};"
|
|
1269
|
+
fill="none"
|
|
1270
|
+
viewBox="0 0 24 24"
|
|
1271
|
+
stroke="currentColor"
|
|
1272
|
+
>
|
|
1273
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
|
1274
|
+
</svg>
|
|
1275
|
+
</button>
|
|
1276
|
+
</div>
|
|
1277
|
+
|
|
1278
|
+
{#if showStoracha}
|
|
1279
|
+
<!-- OrbitDB Initialization Status -->
|
|
1280
|
+
{#if !isInitialized}
|
|
1281
|
+
<div
|
|
1282
|
+
style="margin-bottom: 1rem; border-radius: 0.5rem; border: 1px solid rgba(217, 169, 56, 0.4); background: linear-gradient(to right, #fff8e1, #fffde7, #fff3e0); padding: 1rem; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -2px rgba(0,0,0,0.1);"
|
|
1283
|
+
>
|
|
1284
|
+
<div style="display: flex; align-items: flex-start; gap: 0.75rem;">
|
|
1285
|
+
<div
|
|
1286
|
+
style="display: flex; height: 2rem; width: 2rem; align-items: center; justify-content: center; border-radius: 9999px; background: linear-gradient(to bottom right, #FFC83F, #e67e22); 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;"
|
|
1287
|
+
>
|
|
1288
|
+
<Loader2
|
|
1289
|
+
style="height: 1rem; width: 1rem; color: #ffffff; animation: spin 1s linear infinite;"
|
|
1290
|
+
/>
|
|
1291
|
+
</div>
|
|
1292
|
+
<div style="flex: 1; font-size: 0.875rem;">
|
|
1293
|
+
<div style="font-weight: 600; color: #78350f; font-family: 'Epilogue', sans-serif;">
|
|
1294
|
+
Database Initializing
|
|
1295
|
+
</div>
|
|
1296
|
+
<div
|
|
1297
|
+
style="margin-top: 0.25rem; color: rgba(120, 53, 15, 0.9); font-family: 'DM Sans', sans-serif;"
|
|
1298
|
+
>
|
|
1299
|
+
OrbitDB is still setting up. You can login to Storacha now, but backup & restore will
|
|
1300
|
+
be available once initialization completes.
|
|
1301
|
+
</div>
|
|
1302
|
+
<div style="margin-top: 0.5rem; display: flex; align-items: center; gap: 0.5rem;">
|
|
1303
|
+
<div
|
|
1304
|
+
style="height: 0.375rem; width: 6rem; border-radius: 9999px; background-color: #fde68a;"
|
|
1305
|
+
>
|
|
1306
|
+
<div
|
|
1307
|
+
style="height: 100%; width: 75%; border-radius: 9999px; background: linear-gradient(to right, #FFC83F, #e67e22); animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;"
|
|
1308
|
+
></div>
|
|
1309
|
+
</div>
|
|
1310
|
+
<span style="font-size: 0.75rem; color: #92400e; font-family: 'DM Mono', monospace;"
|
|
1311
|
+
>Please wait...</span
|
|
1312
|
+
>
|
|
1313
|
+
</div>
|
|
1314
|
+
</div>
|
|
1315
|
+
</div>
|
|
1316
|
+
</div>
|
|
1317
|
+
{/if}
|
|
1318
|
+
|
|
1319
|
+
<!-- Status Messages -->
|
|
1320
|
+
{#if error}
|
|
1321
|
+
<div
|
|
1322
|
+
style="margin-bottom: 1rem; border-radius: 0.5rem; border: 1px solid rgba(233, 19, 21, 0.4); background: linear-gradient(to right, #fef2f2, #fdf2f8, #fff1f2); padding: 1rem; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -2px rgba(0,0,0,0.1);"
|
|
1323
|
+
>
|
|
1324
|
+
<div style="display: flex; align-items: flex-start; gap: 0.75rem;">
|
|
1325
|
+
<div
|
|
1326
|
+
style="display: flex; height: 2rem; width: 2rem; align-items: center; justify-content: center; border-radius: 9999px; background: linear-gradient(to bottom right, #E91315, #be123c); 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;"
|
|
1327
|
+
>
|
|
1328
|
+
<AlertCircle style="height: 1rem; width: 1rem; color: #ffffff;" />
|
|
1329
|
+
</div>
|
|
1330
|
+
<div style="flex: 1;">
|
|
1331
|
+
<div style="font-weight: 600; color: #7f1d1d; font-family: 'Epilogue', sans-serif;">
|
|
1332
|
+
Error
|
|
1333
|
+
</div>
|
|
1334
|
+
<div
|
|
1335
|
+
style="margin-top: 0.25rem; font-size: 0.875rem; color: rgba(127, 29, 29, 0.9); font-family: 'DM Sans', sans-serif;"
|
|
1336
|
+
>
|
|
1337
|
+
{error}
|
|
1338
|
+
</div>
|
|
1339
|
+
</div>
|
|
1340
|
+
</div>
|
|
1341
|
+
</div>
|
|
1342
|
+
{/if}
|
|
1343
|
+
|
|
1344
|
+
{#if success}
|
|
1345
|
+
<div
|
|
1346
|
+
style="margin-bottom: 1rem; border-radius: 0.5rem; border: 1px solid rgba(16, 185, 129, 0.4); background: linear-gradient(to right, #ecfdf5, #f0fdf4, #f0fdfa); padding: 1rem; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -2px rgba(0,0,0,0.1);"
|
|
1347
|
+
>
|
|
1348
|
+
<div style="display: flex; align-items: flex-start; gap: 0.75rem;">
|
|
1349
|
+
<div
|
|
1350
|
+
style="display: flex; height: 2rem; width: 2rem; align-items: center; justify-content: center; border-radius: 9999px; background: linear-gradient(to bottom right, #10b981, #14b8a6); 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;"
|
|
1351
|
+
>
|
|
1352
|
+
<CheckCircle style="height: 1rem; width: 1rem; color: #ffffff;" />
|
|
1353
|
+
</div>
|
|
1354
|
+
<div style="flex: 1;">
|
|
1355
|
+
<div style="font-weight: 600; color: #064e3b; font-family: 'Epilogue', sans-serif;">
|
|
1356
|
+
Success
|
|
1357
|
+
</div>
|
|
1358
|
+
<div
|
|
1359
|
+
style="margin-top: 0.25rem; font-size: 0.875rem; color: rgba(6, 78, 59, 0.9); font-family: 'DM Sans', sans-serif;"
|
|
1360
|
+
>
|
|
1361
|
+
{success}
|
|
1362
|
+
</div>
|
|
1363
|
+
</div>
|
|
1364
|
+
</div>
|
|
1365
|
+
</div>
|
|
1366
|
+
{/if}
|
|
1367
|
+
|
|
1368
|
+
<!-- Progress Bar -->
|
|
1369
|
+
{#if showProgress}
|
|
1370
|
+
<div
|
|
1371
|
+
style="margin-bottom: 1rem; border-radius: 0.5rem; border: 1px solid rgba(233, 19, 21, 0.3); background: linear-gradient(to right, #FFF5E6, #FFF0F0, #FFF5E6); padding: 1rem; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -2px rgba(0,0,0,0.1);"
|
|
1372
|
+
>
|
|
1373
|
+
<div style="display: flex; align-items: flex-start; gap: 0.75rem;">
|
|
1374
|
+
<div
|
|
1375
|
+
style="display: flex; height: 2rem; width: 2rem; align-items: center; justify-content: center; border-radius: 9999px; background: linear-gradient(to bottom right, #E91315, #FFC83F); 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;"
|
|
1376
|
+
>
|
|
1377
|
+
<svg
|
|
1378
|
+
style="height: 1rem; width: 1rem; color: #ffffff; animation: spin 1s linear infinite;"
|
|
1379
|
+
fill="none"
|
|
1380
|
+
viewBox="0 0 24 24"
|
|
1381
|
+
stroke="currentColor"
|
|
1382
|
+
>
|
|
1383
|
+
<path
|
|
1384
|
+
stroke-linecap="round"
|
|
1385
|
+
stroke-linejoin="round"
|
|
1386
|
+
stroke-width="2"
|
|
1387
|
+
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
|
1388
|
+
/>
|
|
1389
|
+
</svg>
|
|
1390
|
+
</div>
|
|
1391
|
+
<div style="flex: 1;">
|
|
1392
|
+
<div
|
|
1393
|
+
style="margin-bottom: 0.5rem; display: flex; align-items: center; justify-content: space-between;"
|
|
1394
|
+
>
|
|
1395
|
+
<span style="font-weight: 600; color: #7A1518; font-family: 'Epilogue', sans-serif;">
|
|
1396
|
+
{progressType === 'upload' ? 'Uploading' : 'Downloading'} Progress
|
|
1397
|
+
</span>
|
|
1398
|
+
<span
|
|
1399
|
+
style="font-size: 0.875rem; font-weight: 500; color: #E91315; font-family: 'DM Mono', monospace;"
|
|
1400
|
+
>
|
|
1401
|
+
{progressPercentage}% ({progressCurrent}/{progressTotal})
|
|
1402
|
+
</span>
|
|
1403
|
+
</div>
|
|
1404
|
+
<div
|
|
1405
|
+
style="height: 0.75rem; width: 100%; border-radius: 9999px; background-color: #FFE4AE; box-shadow: inset 0 2px 4px 0 rgba(0,0,0,0.05);"
|
|
1406
|
+
>
|
|
1407
|
+
<div
|
|
1408
|
+
style="height: 0.75rem; border-radius: 9999px; background: linear-gradient(to right, #E91315, #FFC83F, #E91315); box-shadow: 0 1px 2px 0 rgba(0,0,0,0.05); transition: all 500ms ease-out; width: {progressPercentage}%"
|
|
1409
|
+
></div>
|
|
1410
|
+
</div>
|
|
1411
|
+
{#if progressCurrentBlock}
|
|
1412
|
+
<div
|
|
1413
|
+
style="margin-top: 0.5rem; font-size: 0.75rem; color: rgba(122, 21, 24, 0.8); font-family: 'DM Mono', monospace;"
|
|
1414
|
+
>
|
|
1415
|
+
{progressType === 'upload' ? 'Current block:' : 'Current CID:'}
|
|
1416
|
+
<span style="font-weight: 500;">
|
|
1417
|
+
{progressType === 'upload'
|
|
1418
|
+
? progressCurrentBlock.hash?.slice(0, 16)
|
|
1419
|
+
: progressCurrentBlock.storachaCID?.slice(0, 16)}...
|
|
1420
|
+
</span>
|
|
1421
|
+
</div>
|
|
1422
|
+
{/if}
|
|
1423
|
+
{#if progressError}
|
|
1424
|
+
<div
|
|
1425
|
+
style="margin-top: 0.5rem; border-radius: 0.375rem; background-color: #fee2e2; padding: 0.25rem 0.5rem; font-size: 0.75rem; color: #b91c1c; font-family: 'DM Sans', sans-serif;"
|
|
1426
|
+
>
|
|
1427
|
+
Error: {progressError.message}
|
|
1428
|
+
</div>
|
|
1429
|
+
{/if}
|
|
1430
|
+
</div>
|
|
1431
|
+
</div>
|
|
1432
|
+
</div>
|
|
1433
|
+
{/if}
|
|
1434
|
+
|
|
1435
|
+
<!-- Pairing prompt: mounted outside tab panels so it always shows (snippet renders nothing when idle). -->
|
|
1436
|
+
{@render pairingApprovalPrompt()}
|
|
1437
|
+
|
|
1438
|
+
{#if !isLoggedIn}
|
|
1439
|
+
<!-- Login Section -->
|
|
1440
|
+
<div style="display: flex; flex-direction: column; gap: 1rem;">
|
|
1441
|
+
<div
|
|
1442
|
+
style="text-align: center; font-size: 0.875rem; color: #374151; font-family: 'DM Sans', sans-serif;"
|
|
1443
|
+
>
|
|
1444
|
+
Create, use and replicate your passkeys and UCANs between your devices - recover them from
|
|
1445
|
+
decentralised Filecoin/Storacha storage
|
|
1446
|
+
</div>
|
|
1447
|
+
|
|
1448
|
+
{#if !signingMode}
|
|
1449
|
+
<!-- Step 1: passkey — labels follow hasLocalPasskeyHint() + registry -->
|
|
1450
|
+
<div style="display: flex; flex-direction: column; align-items: center; gap: 0.75rem;">
|
|
1451
|
+
<div
|
|
1452
|
+
style="text-align: center; font-size: 0.75rem; color: #6b7280; font-family: 'DM Sans', sans-serif;"
|
|
1453
|
+
>
|
|
1454
|
+
{passkeyStepHint}
|
|
1455
|
+
</div>
|
|
1456
|
+
|
|
1457
|
+
<fieldset
|
|
1458
|
+
disabled={isAuthenticating || signingPreferenceOverride != null}
|
|
1459
|
+
data-testid="storacha-signing-preference-group"
|
|
1460
|
+
style="margin: 0; width: 100%; max-width: 22rem; border-radius: 0.5rem; border: 1px solid rgba(233, 19, 21, 0.25); padding: 0.625rem 0.75rem; background: rgba(255, 255, 255, 0.6); box-sizing: border-box;"
|
|
1461
|
+
>
|
|
1462
|
+
<legend
|
|
1463
|
+
style="font-size: 0.7rem; font-weight: 600; color: #374151; font-family: 'Epilogue', sans-serif; padding: 0 0.25rem;"
|
|
1464
|
+
>
|
|
1465
|
+
Signing mode
|
|
1466
|
+
</legend>
|
|
1467
|
+
<div style="display: flex; flex-direction: column; gap: 0.4rem;">
|
|
1468
|
+
<label
|
|
1469
|
+
style="display: flex; align-items: flex-start; gap: 0.5rem; cursor: pointer; font-size: 0.68rem; color: #374151; font-family: 'DM Sans', sans-serif; line-height: 1.35;"
|
|
1470
|
+
>
|
|
1471
|
+
<input
|
|
1472
|
+
type="radio"
|
|
1473
|
+
name="storacha-signing-pref"
|
|
1474
|
+
data-testid="storacha-signing-pref-hardware-ed25519"
|
|
1475
|
+
checked={selectedSigningPreference === 'hardware-ed25519'}
|
|
1476
|
+
onchange={() => setSigningPreference('hardware-ed25519')}
|
|
1477
|
+
style="margin-top: 0.15rem; accent-color: #E91315;"
|
|
1478
|
+
/>
|
|
1479
|
+
<span
|
|
1480
|
+
><strong>Hardware Ed25519</strong> (default) — passkey signatures in the secure
|
|
1481
|
+
element; Ed25519 preferred, <strong>P-256 fallback</strong> if the authenticator does
|
|
1482
|
+
not support Ed25519.</span
|
|
1483
|
+
>
|
|
1484
|
+
</label>
|
|
1485
|
+
<label
|
|
1486
|
+
style="display: flex; align-items: flex-start; gap: 0.5rem; cursor: pointer; font-size: 0.68rem; color: #374151; font-family: 'DM Sans', sans-serif; line-height: 1.35;"
|
|
1487
|
+
>
|
|
1488
|
+
<input
|
|
1489
|
+
type="radio"
|
|
1490
|
+
name="storacha-signing-pref"
|
|
1491
|
+
data-testid="storacha-signing-pref-hardware-p256"
|
|
1492
|
+
checked={selectedSigningPreference === 'hardware-p256'}
|
|
1493
|
+
onchange={() => setSigningPreference('hardware-p256')}
|
|
1494
|
+
style="margin-top: 0.15rem; accent-color: #E91315;"
|
|
1495
|
+
/>
|
|
1496
|
+
<span
|
|
1497
|
+
><strong>Hardware P-256</strong> — WebAuthn ES256 only (no Ed25519 on this passkey).</span
|
|
1498
|
+
>
|
|
1499
|
+
</label>
|
|
1500
|
+
<label
|
|
1501
|
+
style="display: flex; align-items: flex-start; gap: 0.5rem; cursor: pointer; font-size: 0.68rem; color: #374151; font-family: 'DM Sans', sans-serif; line-height: 1.35;"
|
|
1502
|
+
>
|
|
1503
|
+
<input
|
|
1504
|
+
type="radio"
|
|
1505
|
+
name="storacha-signing-pref"
|
|
1506
|
+
data-testid="storacha-signing-pref-worker"
|
|
1507
|
+
checked={selectedSigningPreference === 'worker'}
|
|
1508
|
+
onchange={() => setSigningPreference('worker')}
|
|
1509
|
+
style="margin-top: 0.15rem; accent-color: #E91315;"
|
|
1510
|
+
/>
|
|
1511
|
+
<span
|
|
1512
|
+
><strong>Worker Ed25519</strong> — signing key in a web worker; WebAuthn used for
|
|
1513
|
+
PRF / user verification (recommended for OrbitDB multi-device).</span
|
|
1514
|
+
>
|
|
1515
|
+
</label>
|
|
1516
|
+
</div>
|
|
1517
|
+
</fieldset>
|
|
1518
|
+
|
|
1519
|
+
<button
|
|
1520
|
+
data-testid="storacha-passkey-primary"
|
|
1521
|
+
class="storacha-btn-primary"
|
|
1522
|
+
onclick={handleAuthenticate}
|
|
1523
|
+
disabled={isAuthenticating}
|
|
1524
|
+
style="display: flex; align-items: center; justify-content: center; gap: 0.5rem; border-radius: 0.375rem; background-color: #E91315; padding: 0.625rem 1.5rem; 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; font-size: 0.875rem; opacity: {isAuthenticating
|
|
1525
|
+
? '0.5'
|
|
1526
|
+
: '1'};"
|
|
1527
|
+
>
|
|
1528
|
+
{#if isAuthenticating}
|
|
1529
|
+
<Loader2 style="height: 1rem; width: 1rem; animation: spin 1s linear infinite;" />
|
|
1530
|
+
<span>{primaryPasskeyLoadingLabel}</span>
|
|
1531
|
+
{:else}
|
|
1532
|
+
<svg
|
|
1533
|
+
style="height: 1rem; width: 1rem;"
|
|
1534
|
+
fill="none"
|
|
1535
|
+
viewBox="0 0 24 24"
|
|
1536
|
+
stroke="currentColor"
|
|
1537
|
+
>
|
|
1538
|
+
<path
|
|
1539
|
+
stroke-linecap="round"
|
|
1540
|
+
stroke-linejoin="round"
|
|
1541
|
+
stroke-width="2"
|
|
1542
|
+
d="M12 11c0 3.517-1.009 6.799-2.753 9.571m-3.44-2.04l.054-.09A13.916 13.916 0 008 11a4 4 0 118 0c0 1.017-.07 2.019-.203 3m-2.118 6.844A21.88 21.88 0 0015.171 17m3.839 1.132c.645-2.266.99-4.659.99-7.132A8 8 0 008 4.07M3 15.364c.64-1.319 1-2.8 1-4.364 0-1.457.39-2.823 1.07-4"
|
|
1543
|
+
/>
|
|
1544
|
+
</svg>
|
|
1545
|
+
<span>{primaryPasskeyLabel}</span>
|
|
1546
|
+
{/if}
|
|
1547
|
+
</button>
|
|
1548
|
+
|
|
1549
|
+
<!-- Recover from backup (IPNS / manifest) -->
|
|
1550
|
+
<button
|
|
1551
|
+
onclick={handleRecover}
|
|
1552
|
+
disabled={isRecovering}
|
|
1553
|
+
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
|
|
1554
|
+
? '0.5'
|
|
1555
|
+
: '1'}; transition: background-color 150ms;"
|
|
1556
|
+
>
|
|
1557
|
+
{#if isRecovering}
|
|
1558
|
+
<Loader2
|
|
1559
|
+
style="height: 0.875rem; width: 0.875rem; animation: spin 1s linear infinite;"
|
|
1560
|
+
/>
|
|
1561
|
+
<span>{recoveryStatus || 'Recovering...'}</span>
|
|
1562
|
+
{:else}
|
|
1563
|
+
<svg
|
|
1564
|
+
style="height: 0.875rem; width: 0.875rem;"
|
|
1565
|
+
fill="none"
|
|
1566
|
+
viewBox="0 0 24 24"
|
|
1567
|
+
stroke="currentColor"
|
|
1568
|
+
>
|
|
1569
|
+
<path
|
|
1570
|
+
stroke-linecap="round"
|
|
1571
|
+
stroke-linejoin="round"
|
|
1572
|
+
stroke-width="2"
|
|
1573
|
+
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
|
1574
|
+
/>
|
|
1575
|
+
</svg>
|
|
1576
|
+
<span>Recover Passkey</span>
|
|
1577
|
+
{/if}
|
|
1578
|
+
</button>
|
|
1579
|
+
</div>
|
|
1580
|
+
{:else}
|
|
1581
|
+
<!-- Step 2: Authenticated — show DID info + delegation import -->
|
|
1582
|
+
<div
|
|
1583
|
+
data-testid="storacha-post-auth"
|
|
1584
|
+
style="display: flex; flex-direction: column; gap: 0.75rem;"
|
|
1585
|
+
>
|
|
1586
|
+
<!-- Signing Mode Badge -->
|
|
1587
|
+
<div
|
|
1588
|
+
style="display: flex; align-items: center; justify-content: center; gap: 0.5rem; flex-wrap: wrap;"
|
|
1589
|
+
>
|
|
1590
|
+
{#if signingMode.algorithm === 'Ed25519' && signingMode.mode === 'hardware'}
|
|
1591
|
+
<span
|
|
1592
|
+
style="display: inline-flex; align-items: center; gap: 0.25rem; border-radius: 9999px; background-color: #dcfce7; border: 1px solid #86efac; padding: 0.25rem 0.75rem; font-size: 0.75rem; font-weight: 600; color: #166534; font-family: 'DM Sans', sans-serif;"
|
|
1593
|
+
>
|
|
1594
|
+
<svg
|
|
1595
|
+
style="height: 0.625rem; width: 0.625rem;"
|
|
1596
|
+
fill="currentColor"
|
|
1597
|
+
viewBox="0 0 20 20"
|
|
1598
|
+
><path
|
|
1599
|
+
fill-rule="evenodd"
|
|
1600
|
+
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
|
|
1601
|
+
clip-rule="evenodd"
|
|
1602
|
+
/></svg
|
|
1603
|
+
>
|
|
1604
|
+
Hardware Ed25519
|
|
1605
|
+
</span>
|
|
1606
|
+
{:else if signingMode.algorithm === 'P-256' && signingMode.mode === 'hardware'}
|
|
1607
|
+
<span
|
|
1608
|
+
style="display: inline-flex; align-items: center; gap: 0.25rem; border-radius: 9999px; background-color: #BDE0FF; border: 1px solid #0176CE; padding: 0.25rem 0.75rem; font-size: 0.75rem; font-weight: 600; color: #0176CE; font-family: 'DM Sans', sans-serif;"
|
|
1609
|
+
>
|
|
1610
|
+
<svg
|
|
1611
|
+
style="height: 0.625rem; width: 0.625rem;"
|
|
1612
|
+
fill="currentColor"
|
|
1613
|
+
viewBox="0 0 20 20"
|
|
1614
|
+
><path
|
|
1615
|
+
fill-rule="evenodd"
|
|
1616
|
+
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
|
|
1617
|
+
clip-rule="evenodd"
|
|
1618
|
+
/></svg
|
|
1619
|
+
>
|
|
1620
|
+
Hardware P-256
|
|
1621
|
+
</span>
|
|
1622
|
+
{:else}
|
|
1623
|
+
<span
|
|
1624
|
+
style="display: inline-flex; align-items: center; gap: 0.25rem; border-radius: 9999px; background-color: #FFE4AE; border: 1px solid #FFC83F; padding: 0.25rem 0.75rem; font-size: 0.75rem; font-weight: 600; color: #92400e; font-family: 'DM Sans', sans-serif;"
|
|
1625
|
+
>
|
|
1626
|
+
<svg
|
|
1627
|
+
style="height: 0.625rem; width: 0.625rem;"
|
|
1628
|
+
fill="currentColor"
|
|
1629
|
+
viewBox="0 0 20 20"
|
|
1630
|
+
><path
|
|
1631
|
+
fill-rule="evenodd"
|
|
1632
|
+
d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z"
|
|
1633
|
+
clip-rule="evenodd"
|
|
1634
|
+
/></svg
|
|
1635
|
+
>
|
|
1636
|
+
Worker Ed25519
|
|
1637
|
+
</span>
|
|
1638
|
+
{/if}
|
|
1639
|
+
{#if signingMode.secure}
|
|
1640
|
+
<span
|
|
1641
|
+
style="display: inline-flex; align-items: center; gap: 0.25rem; border-radius: 9999px; background-color: #dcfce7; border: 1px solid #86efac; padding: 0.25rem 0.5rem; font-size: 0.625rem; font-weight: 500; color: #166534; font-family: 'DM Mono', monospace;"
|
|
1642
|
+
>
|
|
1643
|
+
Secure
|
|
1644
|
+
</span>
|
|
1645
|
+
{/if}
|
|
1646
|
+
</div>
|
|
1647
|
+
|
|
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);"
|
|
1651
|
+
>
|
|
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>
|
|
1695
|
+
|
|
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) -->
|
|
1757
|
+
<div
|
|
1758
|
+
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
|
+
>
|
|
1760
|
+
<h4
|
|
1761
|
+
style="margin-bottom: 0.5rem; font-weight: 700; color: #E91315; font-family: 'Epilogue', sans-serif; font-size: 0.875rem;"
|
|
1762
|
+
>
|
|
1763
|
+
Link Another Device
|
|
1764
|
+
</h4>
|
|
1765
|
+
<p
|
|
1766
|
+
style="margin-bottom: 0.75rem; font-size: 0.75rem; color: #6b7280; font-family: 'DM Sans', sans-serif; line-height: 1.4;"
|
|
1767
|
+
>
|
|
1768
|
+
After both browsers are on the same app and P2P has discovered peers (pubsub),
|
|
1769
|
+
paste the other device’s <strong>peer id</strong> (copy from its Passkeys tab). Linking
|
|
1770
|
+
uses addresses libp2p already learned — no JSON.
|
|
1771
|
+
</p>
|
|
1772
|
+
{#if !linkDeviceReady}
|
|
1773
|
+
<div
|
|
1774
|
+
style="margin-bottom: 0.5rem; font-size: 0.7rem; color: #b45309; font-family: 'DM Sans', sans-serif; line-height: 1.35; border-radius: 0.375rem; background: rgba(254, 243, 199, 0.9); padding: 0.5rem 0.65rem; border: 1px solid #fbbf24;"
|
|
1775
|
+
>
|
|
1776
|
+
Linking is unavailable until the device registry and P2P stack finish loading
|
|
1777
|
+
(MultiDeviceManager). If this stays stuck, check the browser console and confirm
|
|
1778
|
+
OrbitDB initialized after passkey auth.
|
|
1779
|
+
</div>
|
|
1780
|
+
{/if}
|
|
1781
|
+
<div style="display: flex; flex-direction: column; gap: 0.75rem;">
|
|
1782
|
+
<input
|
|
1783
|
+
type="text"
|
|
1784
|
+
data-testid="storacha-link-peer-input"
|
|
1785
|
+
bind:value={linkInput}
|
|
1786
|
+
placeholder="Other device’s libp2p peer id (12D3KooW…)"
|
|
1787
|
+
autocomplete="off"
|
|
1788
|
+
spellcheck="false"
|
|
1789
|
+
style="width: 100%; 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;"
|
|
1790
|
+
/>
|
|
1791
|
+
{#if linkError}
|
|
1792
|
+
<div
|
|
1793
|
+
style="font-size: 0.7rem; color: #dc2626; font-family: 'DM Sans', sans-serif;"
|
|
1794
|
+
>
|
|
1795
|
+
{linkError}
|
|
1796
|
+
</div>
|
|
1797
|
+
{/if}
|
|
1798
|
+
<button
|
|
1799
|
+
data-testid="storacha-link-device-submit"
|
|
1800
|
+
data-mdm-ready={linkDeviceReady ? 'true' : 'false'}
|
|
1801
|
+
type="button"
|
|
1802
|
+
onclick={handleLinkDevice}
|
|
1803
|
+
disabled={linkDeviceDisabled}
|
|
1804
|
+
title={!linkDeviceReady
|
|
1805
|
+
? 'Waiting for device registry / MultiDeviceManager to initialize'
|
|
1806
|
+
: 'Link using the other device’s peer id'}
|
|
1807
|
+
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: {linkDeviceDisabled
|
|
1808
|
+
? 'not-allowed'
|
|
1809
|
+
: 'pointer'}; font-family: 'Epilogue', sans-serif; font-weight: 600; opacity: {linkDeviceDisabled
|
|
1810
|
+
? '0.5'
|
|
1811
|
+
: '1'}; box-sizing: border-box;"
|
|
1812
|
+
>
|
|
1813
|
+
{#if isLinking}
|
|
1814
|
+
<Loader2
|
|
1815
|
+
style="height: 1rem; width: 1rem; animation: spin 1s linear infinite;"
|
|
1816
|
+
/>
|
|
1817
|
+
<span>Linking...</span>
|
|
1818
|
+
{:else}
|
|
1819
|
+
<svg
|
|
1820
|
+
style="height: 1rem; width: 1rem;"
|
|
1821
|
+
fill="none"
|
|
1822
|
+
viewBox="0 0 24 24"
|
|
1823
|
+
stroke="currentColor"
|
|
1824
|
+
>
|
|
1825
|
+
<path
|
|
1826
|
+
stroke-linecap="round"
|
|
1827
|
+
stroke-linejoin="round"
|
|
1828
|
+
stroke-width="2"
|
|
1829
|
+
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"
|
|
1830
|
+
/>
|
|
1831
|
+
</svg>
|
|
1832
|
+
<span>Link Device</span>
|
|
1833
|
+
{/if}
|
|
1834
|
+
</button>
|
|
1835
|
+
</div>
|
|
1836
|
+
</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
|
+
<!-- Connection Status + Copy -->
|
|
1879
|
+
<div
|
|
1880
|
+
style="border-radius: 0.375rem; border: 1px solid #E91315; background: linear-gradient(to right, #ffffff, #FFE4AE); padding: 0.625rem 0.75rem;"
|
|
1881
|
+
>
|
|
1882
|
+
<div style="display: flex; align-items: center; justify-content: space-between;">
|
|
1883
|
+
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
|
1884
|
+
<div style="display: flex; align-items: center; gap: 0.25rem;">
|
|
1885
|
+
<div
|
|
1886
|
+
style="height: 0.5rem; width: 0.5rem; border-radius: 9999px; background: {p2pLedDotBg}; box-shadow: {p2pLedShadow}; animation: {p2pLedPulse
|
|
1887
|
+
? 'pulse 2s infinite'
|
|
1888
|
+
: 'none'};"
|
|
1889
|
+
></div>
|
|
1890
|
+
{#if libp2p}
|
|
1891
|
+
<span
|
|
1892
|
+
data-testid="storacha-p2p-remote-peer-count"
|
|
1893
|
+
title="Connected libp2p peers"
|
|
1894
|
+
style="font-size: 0.65rem; font-weight: 700; font-family: 'DM Mono', monospace; color: {p2pLedTextColor}; line-height: 1; min-width: 0.65rem; text-align: center;"
|
|
1895
|
+
>{p2pRemotePeerCount}</span
|
|
1896
|
+
>
|
|
1897
|
+
{/if}
|
|
1898
|
+
</div>
|
|
1899
|
+
<span
|
|
1900
|
+
style="font-size: 0.75rem; font-weight: 600; color: {p2pLedTextColor}; font-family: 'Epilogue', sans-serif;"
|
|
1901
|
+
>
|
|
1902
|
+
{p2pConnectionLabel}
|
|
1903
|
+
</span>
|
|
1904
|
+
</div>
|
|
1905
|
+
<div style="display: flex; align-items: center; gap: 0.375rem;">
|
|
1906
|
+
{#if libp2p}
|
|
1907
|
+
<code
|
|
1908
|
+
style="font-size: 0.6rem; color: #E91315; font-family: 'DM Mono', monospace; background: rgba(233, 19, 21, 0.06); padding: 0.125rem 0.375rem; border-radius: 0.25rem;"
|
|
1909
|
+
>
|
|
1910
|
+
{libp2p.peerId.toString().slice(0, 8)}...{libp2p.peerId
|
|
1911
|
+
.toString()
|
|
1912
|
+
.slice(-4)}
|
|
1913
|
+
</code>
|
|
1914
|
+
<button
|
|
1915
|
+
data-testid="storacha-copy-peer-info"
|
|
1916
|
+
onclick={handleCopyPeerInfo}
|
|
1917
|
+
title="Copy your libp2p peer id"
|
|
1918
|
+
style="display: flex; align-items: center; justify-content: center; height: 1.25rem; width: 1.25rem; border-radius: 0.25rem; border: none; background: transparent; cursor: pointer; color: #E91315; padding: 0; transition: all 150ms;"
|
|
1919
|
+
>
|
|
1920
|
+
<svg
|
|
1921
|
+
style="height: 0.7rem; width: 0.7rem;"
|
|
1922
|
+
fill="none"
|
|
1923
|
+
viewBox="0 0 24 24"
|
|
1924
|
+
stroke="currentColor"
|
|
1925
|
+
>
|
|
1926
|
+
<path
|
|
1927
|
+
stroke-linecap="round"
|
|
1928
|
+
stroke-linejoin="round"
|
|
1929
|
+
stroke-width="2"
|
|
1930
|
+
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"
|
|
1931
|
+
/>
|
|
1932
|
+
</svg>
|
|
1933
|
+
</button>
|
|
1934
|
+
{/if}
|
|
1935
|
+
</div>
|
|
1936
|
+
</div>
|
|
1937
|
+
{#if ipnsNameString}
|
|
1938
|
+
<div
|
|
1939
|
+
style="font-size: 0.7rem; color: #6b7280; font-family: 'DM Mono', monospace; margin-top: 0.25rem;"
|
|
1940
|
+
>
|
|
1941
|
+
IPNS: {ipnsNameString.slice(0, 20)}...
|
|
1942
|
+
</div>
|
|
1943
|
+
{/if}
|
|
1944
|
+
</div>
|
|
1945
|
+
|
|
1946
|
+
{@render linkedDevicesPanel()}
|
|
1947
|
+
</div>
|
|
1948
|
+
{/if}
|
|
1949
|
+
{/if}
|
|
1950
|
+
</div>
|
|
1951
|
+
{:else}
|
|
1952
|
+
<!-- Logged In Section -->
|
|
1953
|
+
<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
|
+
<!-- Tab Navigation — P2P Passkeys first -->
|
|
1989
|
+
<div
|
|
1990
|
+
style="border-radius: 0.5rem; background: rgba(233, 19, 21, 0.06); padding: 0.25rem; display: flex; gap: 0.25rem;"
|
|
1991
|
+
>
|
|
1992
|
+
<button
|
|
1993
|
+
data-testid="storacha-tab-passkeys"
|
|
1994
|
+
onclick={() => handleTabSwitch('passkeys')}
|
|
1995
|
+
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 ===
|
|
1996
|
+
'passkeys'
|
|
1997
|
+
? 'linear-gradient(135deg, #E91315, #FFC83F)'
|
|
1998
|
+
: 'transparent'}; color: {activeTab === 'passkeys'
|
|
1999
|
+
? '#fff'
|
|
2000
|
+
: '#6B7280'}; box-shadow: {activeTab === 'passkeys'
|
|
2001
|
+
? '0 2px 8px rgba(233, 19, 21, 0.3)'
|
|
2002
|
+
: 'none'};"
|
|
2003
|
+
>
|
|
2004
|
+
P2P Passkeys
|
|
2005
|
+
</button>
|
|
2006
|
+
<button
|
|
2007
|
+
data-testid="storacha-tab-storacha"
|
|
2008
|
+
onclick={() => handleTabSwitch('storacha')}
|
|
2009
|
+
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 ===
|
|
2010
|
+
'storacha'
|
|
2011
|
+
? 'linear-gradient(135deg, #E91315, #FFC83F)'
|
|
2012
|
+
: 'transparent'}; color: {activeTab === 'storacha'
|
|
2013
|
+
? '#fff'
|
|
2014
|
+
: '#6B7280'}; box-shadow: {activeTab === 'storacha'
|
|
2015
|
+
? '0 2px 8px rgba(233, 19, 21, 0.3)'
|
|
2016
|
+
: 'none'};"
|
|
2017
|
+
>
|
|
2018
|
+
Storacha
|
|
2019
|
+
</button>
|
|
2020
|
+
</div>
|
|
2021
|
+
|
|
2022
|
+
{#if activeTab === 'storacha'}
|
|
2023
|
+
<!-- Action Buttons -->
|
|
2024
|
+
<div style="display: flex; flex-direction: column; gap: 0.75rem;">
|
|
2025
|
+
<button
|
|
2026
|
+
class="storacha-btn-backup"
|
|
2027
|
+
onclick={handleBackup}
|
|
2028
|
+
disabled={isLoading || (!registryDb && entryCount === 0)}
|
|
2029
|
+
style="display: flex; width: 100%; align-items: center; justify-content: center; gap: 0.5rem; border-radius: 0.375rem; background-color: #FFC83F; padding: 0.5rem 1rem; font-weight: 700; color: #111827; 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; opacity: {isLoading ||
|
|
2030
|
+
(!registryDb && entryCount === 0)
|
|
2031
|
+
? '0.5'
|
|
2032
|
+
: '1'}; box-sizing: border-box;"
|
|
2033
|
+
>
|
|
2034
|
+
<Upload style="height: 1rem; width: 1rem;" />
|
|
2035
|
+
<span>Backup to Storacha</span>
|
|
2036
|
+
</button>
|
|
2037
|
+
|
|
2038
|
+
<button
|
|
2039
|
+
class="storacha-btn-restore"
|
|
2040
|
+
onclick={restoreFromSpaceFallback}
|
|
2041
|
+
disabled={isLoading || !isInitialized}
|
|
2042
|
+
style="display: flex; width: 100%; align-items: center; justify-content: center; gap: 0.5rem; border-radius: 0.375rem; background-color: #0176CE; padding: 0.5rem 1rem; font-weight: 700; 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; opacity: {isLoading ||
|
|
2043
|
+
!isInitialized
|
|
2044
|
+
? '0.5'
|
|
2045
|
+
: '1'}; box-sizing: border-box;"
|
|
2046
|
+
title="Restore database from Storacha backup"
|
|
2047
|
+
>
|
|
2048
|
+
<Download style="height: 1rem; width: 1rem;" />
|
|
2049
|
+
<span>Restore from Storacha</span>
|
|
2050
|
+
</button>
|
|
2051
|
+
</div>
|
|
2052
|
+
|
|
2053
|
+
<!-- Space Usage Information -->
|
|
2054
|
+
<div
|
|
2055
|
+
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);"
|
|
2056
|
+
>
|
|
2057
|
+
<div
|
|
2058
|
+
style="margin-bottom: 0.75rem; display: flex; align-items: center; justify-content: space-between;"
|
|
2059
|
+
>
|
|
2060
|
+
<h4
|
|
2061
|
+
style="display: flex; align-items: center; gap: 0.5rem; font-weight: 700; color: #E91315; font-family: 'Epilogue', sans-serif; margin: 0;"
|
|
2062
|
+
>
|
|
2063
|
+
<svg
|
|
2064
|
+
style="height: 1rem; width: 1rem;"
|
|
2065
|
+
fill="none"
|
|
2066
|
+
viewBox="0 0 24 24"
|
|
2067
|
+
stroke="currentColor"
|
|
2068
|
+
>
|
|
2069
|
+
<path
|
|
2070
|
+
stroke-linecap="round"
|
|
2071
|
+
stroke-linejoin="round"
|
|
2072
|
+
stroke-width="2"
|
|
2073
|
+
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
|
|
2074
|
+
/>
|
|
2075
|
+
</svg>
|
|
2076
|
+
<span>Storage Analytics</span>
|
|
2077
|
+
</h4>
|
|
2078
|
+
<div style="display: flex; align-items: center; gap: 0.25rem;">
|
|
2079
|
+
<button
|
|
2080
|
+
class="storacha-btn-icon"
|
|
2081
|
+
onclick={loadSpaceUsage}
|
|
2082
|
+
disabled={isLoading}
|
|
2083
|
+
style="border-radius: 0.375rem; padding: 0.5rem; color: #E91315; transition: all 300ms; border: none; background: transparent; cursor: pointer; opacity: {isLoading
|
|
2084
|
+
? '0.5'
|
|
2085
|
+
: '1'};"
|
|
2086
|
+
title="Refresh space usage"
|
|
2087
|
+
aria-label="Refresh space usage"
|
|
2088
|
+
>
|
|
2089
|
+
<svg
|
|
2090
|
+
style="height: 1rem; width: 1rem;"
|
|
2091
|
+
fill="none"
|
|
2092
|
+
viewBox="0 0 24 24"
|
|
2093
|
+
stroke="currentColor"
|
|
2094
|
+
>
|
|
2095
|
+
<path
|
|
2096
|
+
stroke-linecap="round"
|
|
2097
|
+
stroke-linejoin="round"
|
|
2098
|
+
stroke-width="2"
|
|
2099
|
+
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
|
2100
|
+
/>
|
|
2101
|
+
</svg>
|
|
2102
|
+
</button>
|
|
2103
|
+
{#if spaceUsage && spaceUsage.totalFiles <= 50 && !spaceUsage.analyzed}
|
|
2104
|
+
<button
|
|
2105
|
+
class="storacha-btn-icon"
|
|
2106
|
+
onclick={async () => {
|
|
2107
|
+
spaceUsage = await getSpaceUsage(client, true);
|
|
2108
|
+
}}
|
|
2109
|
+
disabled={isLoading}
|
|
2110
|
+
style="border-radius: 0.375rem; padding: 0.5rem; color: #0176CE; transition: all 300ms; border: none; background: transparent; cursor: pointer; opacity: {isLoading
|
|
2111
|
+
? '0.5'
|
|
2112
|
+
: '1'};"
|
|
2113
|
+
title="Analyze file types"
|
|
2114
|
+
aria-label="Analyze file types"
|
|
2115
|
+
>
|
|
2116
|
+
<svg
|
|
2117
|
+
style="height: 1rem; width: 1rem;"
|
|
2118
|
+
fill="none"
|
|
2119
|
+
viewBox="0 0 24 24"
|
|
2120
|
+
stroke="currentColor"
|
|
2121
|
+
>
|
|
2122
|
+
<path
|
|
2123
|
+
stroke-linecap="round"
|
|
2124
|
+
stroke-linejoin="round"
|
|
2125
|
+
stroke-width="2"
|
|
2126
|
+
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
|
2127
|
+
/>
|
|
2128
|
+
</svg>
|
|
2129
|
+
</button>
|
|
2130
|
+
{/if}
|
|
2131
|
+
</div>
|
|
2132
|
+
</div>
|
|
2133
|
+
|
|
2134
|
+
{#if spaceUsage}
|
|
2135
|
+
<div
|
|
2136
|
+
style="margin-bottom: 1rem; border-radius: 0.25rem; border: 1px solid #E91315; background: linear-gradient(to right, #EFE3F3, #ffffff); padding: 0.75rem; box-shadow: 0 1px 2px 0 rgba(0,0,0,0.05);"
|
|
2137
|
+
>
|
|
2138
|
+
<div
|
|
2139
|
+
style="display: flex; align-items: center; justify-content: space-between; font-size: 0.875rem;"
|
|
2140
|
+
>
|
|
2141
|
+
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
|
2142
|
+
<div
|
|
2143
|
+
style="display: flex; height: 1.5rem; width: 1.5rem; align-items: center; justify-content: center; border-radius: 9999px; background-color: #E91315; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -2px rgba(0,0,0,0.1);"
|
|
2144
|
+
>
|
|
2145
|
+
<span
|
|
2146
|
+
style="font-size: 0.75rem; font-weight: 700; color: #ffffff; font-family: 'DM Mono', monospace;"
|
|
2147
|
+
>{spaceUsage.totalFiles}</span
|
|
2148
|
+
>
|
|
2149
|
+
</div>
|
|
2150
|
+
<span
|
|
2151
|
+
style="font-weight: 500; color: #1f2937; font-family: 'DM Sans', sans-serif;"
|
|
2152
|
+
>
|
|
2153
|
+
file{spaceUsage.totalFiles !== 1 ? 's' : ''} stored
|
|
2154
|
+
</span>
|
|
2155
|
+
</div>
|
|
2156
|
+
{#if spaceUsage.lastUploadDate}
|
|
2157
|
+
<div
|
|
2158
|
+
style="color: #0176CE; font-family: 'DM Mono', monospace; font-size: 0.75rem;"
|
|
2159
|
+
>
|
|
2160
|
+
Last upload: {formatRelativeTime(spaceUsage.lastUploadDate)}
|
|
2161
|
+
</div>
|
|
2162
|
+
{/if}
|
|
2163
|
+
</div>
|
|
2164
|
+
|
|
2165
|
+
{#if spaceUsage.totalFiles > 0}
|
|
2166
|
+
<div
|
|
2167
|
+
style="margin-top: 0.5rem; display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.5rem; font-size: 0.75rem;"
|
|
2168
|
+
>
|
|
2169
|
+
{#if spaceUsage.backupFiles > 0}
|
|
2170
|
+
<div style="display: flex; align-items: center; gap: 0.25rem;">
|
|
2171
|
+
<div
|
|
2172
|
+
style="height: 0.5rem; width: 0.5rem; border-radius: 9999px; background-color: #FFC83F;"
|
|
2173
|
+
></div>
|
|
2174
|
+
<span style="color: #374151; font-family: 'DM Sans', sans-serif;">
|
|
2175
|
+
{spaceUsage.backupFiles} backup{spaceUsage.backupFiles !== 1 ? 's' : ''}
|
|
2176
|
+
</span>
|
|
2177
|
+
</div>
|
|
2178
|
+
{/if}
|
|
2179
|
+
{#if spaceUsage.blockFiles > 0}
|
|
2180
|
+
<div style="display: flex; align-items: center; gap: 0.25rem;">
|
|
2181
|
+
<div
|
|
2182
|
+
style="height: 0.5rem; width: 0.5rem; border-radius: 9999px; background-color: #0176CE;"
|
|
2183
|
+
></div>
|
|
2184
|
+
<span style="color: #374151; font-family: 'DM Sans', sans-serif;">
|
|
2185
|
+
{spaceUsage.blockFiles} data block{spaceUsage.blockFiles !== 1 ? 's' : ''}
|
|
2186
|
+
</span>
|
|
2187
|
+
</div>
|
|
2188
|
+
{/if}
|
|
2189
|
+
{#if spaceUsage.otherFiles > 0}
|
|
2190
|
+
<div style="display: flex; align-items: center; gap: 0.25rem;">
|
|
2191
|
+
<div
|
|
2192
|
+
style="height: 0.5rem; width: 0.5rem; border-radius: 9999px; background-color: #E91315;"
|
|
2193
|
+
></div>
|
|
2194
|
+
<span style="color: #374151; font-family: 'DM Sans', sans-serif;">
|
|
2195
|
+
{spaceUsage.otherFiles} other
|
|
2196
|
+
</span>
|
|
2197
|
+
</div>
|
|
2198
|
+
{/if}
|
|
2199
|
+
</div>
|
|
2200
|
+
|
|
2201
|
+
<div
|
|
2202
|
+
style="margin-top: 0.5rem; font-size: 0.75rem; color: #4b5563; font-family: 'DM Sans', sans-serif;"
|
|
2203
|
+
>
|
|
2204
|
+
{#if spaceUsage.oldestUploadDate && spaceUsage.oldestUploadDate !== spaceUsage.lastUploadDate}
|
|
2205
|
+
<div style="color: #0176CE; font-family: 'DM Mono', monospace;">
|
|
2206
|
+
Oldest upload: {formatRelativeTime(spaceUsage.oldestUploadDate)}
|
|
2207
|
+
</div>
|
|
2208
|
+
{/if}
|
|
2209
|
+
<em style="color: #6b7280;">Note: Each backup creates many data blocks</em>
|
|
2210
|
+
</div>
|
|
2211
|
+
{/if}
|
|
2212
|
+
</div>
|
|
2213
|
+
{:else if spaceUsage === null && isLoggedIn}
|
|
2214
|
+
<div
|
|
2215
|
+
style="margin-bottom: 1rem; border-radius: 0.25rem; border: 1px solid #E91315; background: linear-gradient(to right, #EFE3F3, #FFE4AE); padding: 0.75rem; box-shadow: 0 1px 2px 0 rgba(0,0,0,0.05);"
|
|
2216
|
+
>
|
|
2217
|
+
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
|
2218
|
+
<div
|
|
2219
|
+
style="display: flex; height: 1.25rem; width: 1.25rem; align-items: center; justify-content: center; border-radius: 9999px; background-color: #E91315; flex-shrink: 0;"
|
|
2220
|
+
>
|
|
2221
|
+
<svg
|
|
2222
|
+
style="height: 0.75rem; width: 0.75rem; color: #ffffff;"
|
|
2223
|
+
fill="none"
|
|
2224
|
+
viewBox="0 0 24 24"
|
|
2225
|
+
stroke="currentColor"
|
|
2226
|
+
>
|
|
2227
|
+
<path
|
|
2228
|
+
stroke-linecap="round"
|
|
2229
|
+
stroke-linejoin="round"
|
|
2230
|
+
stroke-width="2"
|
|
2231
|
+
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
2232
|
+
/>
|
|
2233
|
+
</svg>
|
|
2234
|
+
</div>
|
|
2235
|
+
<div
|
|
2236
|
+
style="font-size: 0.875rem; font-weight: 500; color: #E91315; font-family: 'DM Sans', sans-serif;"
|
|
2237
|
+
>
|
|
2238
|
+
Space usage information unavailable
|
|
2239
|
+
</div>
|
|
2240
|
+
</div>
|
|
2241
|
+
</div>
|
|
2242
|
+
{/if}
|
|
2243
|
+
</div>
|
|
2244
|
+
{/if}
|
|
2245
|
+
|
|
2246
|
+
{#if activeTab === 'passkeys'}
|
|
2247
|
+
<div style="display: flex; flex-direction: column; gap: 0.75rem;">
|
|
2248
|
+
<!-- Connection Status + Copy -->
|
|
2249
|
+
<div
|
|
2250
|
+
style="border-radius: 0.375rem; border: 1px solid #E91315; background: linear-gradient(to right, #ffffff, #FFE4AE); padding: 0.625rem 0.75rem;"
|
|
2251
|
+
>
|
|
2252
|
+
<div style="display: flex; align-items: center; justify-content: space-between;">
|
|
2253
|
+
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
|
2254
|
+
<div style="display: flex; align-items: center; gap: 0.25rem;">
|
|
2255
|
+
<div
|
|
2256
|
+
style="height: 0.5rem; width: 0.5rem; border-radius: 9999px; background: {p2pLedDotBg}; box-shadow: {p2pLedShadow}; animation: {p2pLedPulse
|
|
2257
|
+
? 'pulse 2s infinite'
|
|
2258
|
+
: 'none'};"
|
|
2259
|
+
></div>
|
|
2260
|
+
{#if libp2p}
|
|
2261
|
+
<span
|
|
2262
|
+
data-testid="storacha-p2p-remote-peer-count"
|
|
2263
|
+
title="Connected libp2p peers"
|
|
2264
|
+
style="font-size: 0.65rem; font-weight: 700; font-family: 'DM Mono', monospace; color: {p2pLedTextColor}; line-height: 1; min-width: 0.65rem; text-align: center;"
|
|
2265
|
+
>{p2pRemotePeerCount}</span
|
|
2266
|
+
>
|
|
2267
|
+
{/if}
|
|
2268
|
+
</div>
|
|
2269
|
+
<span
|
|
2270
|
+
style="font-size: 0.75rem; font-weight: 600; color: {p2pLedTextColor}; font-family: 'Epilogue', sans-serif;"
|
|
2271
|
+
>
|
|
2272
|
+
{p2pConnectionLabel}
|
|
2273
|
+
</span>
|
|
2274
|
+
</div>
|
|
2275
|
+
<div style="display: flex; align-items: center; gap: 0.375rem;">
|
|
2276
|
+
{#if libp2p}
|
|
2277
|
+
<code
|
|
2278
|
+
style="font-size: 0.6rem; color: #E91315; font-family: 'DM Mono', monospace; background: rgba(233, 19, 21, 0.06); padding: 0.125rem 0.375rem; border-radius: 0.25rem;"
|
|
2279
|
+
>
|
|
2280
|
+
{libp2p.peerId.toString().slice(0, 8)}...{libp2p.peerId.toString().slice(-4)}
|
|
2281
|
+
</code>
|
|
2282
|
+
<button
|
|
2283
|
+
data-testid="storacha-copy-peer-info"
|
|
2284
|
+
onclick={handleCopyPeerInfo}
|
|
2285
|
+
title="Copy your libp2p peer id"
|
|
2286
|
+
style="display: flex; align-items: center; justify-content: center; height: 1.25rem; width: 1.25rem; border-radius: 0.25rem; border: none; background: transparent; cursor: pointer; color: #E91315; padding: 0; transition: all 150ms;"
|
|
2287
|
+
>
|
|
2288
|
+
<svg
|
|
2289
|
+
style="height: 0.7rem; width: 0.7rem;"
|
|
2290
|
+
fill="none"
|
|
2291
|
+
viewBox="0 0 24 24"
|
|
2292
|
+
stroke="currentColor"
|
|
2293
|
+
>
|
|
2294
|
+
<path
|
|
2295
|
+
stroke-linecap="round"
|
|
2296
|
+
stroke-linejoin="round"
|
|
2297
|
+
stroke-width="2"
|
|
2298
|
+
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"
|
|
2299
|
+
/>
|
|
2300
|
+
</svg>
|
|
2301
|
+
</button>
|
|
2302
|
+
{/if}
|
|
2303
|
+
</div>
|
|
2304
|
+
</div>
|
|
2305
|
+
{#if ipnsNameString}
|
|
2306
|
+
<div
|
|
2307
|
+
style="font-size: 0.7rem; color: #6b7280; font-family: 'DM Mono', monospace; margin-top: 0.25rem;"
|
|
2308
|
+
>
|
|
2309
|
+
IPNS: {ipnsNameString.slice(0, 20)}...
|
|
2310
|
+
</div>
|
|
2311
|
+
{/if}
|
|
2312
|
+
</div>
|
|
2313
|
+
|
|
2314
|
+
{#if !libp2p}
|
|
2315
|
+
<div
|
|
2316
|
+
style="border-radius: 0.375rem; border: 1px dashed #E91315; background: rgba(233, 19, 21, 0.03); padding: 1.25rem; text-align: center;"
|
|
2317
|
+
>
|
|
2318
|
+
<div
|
|
2319
|
+
style="font-size: 0.8rem; font-weight: 700; color: #E91315; font-family: 'Epilogue', sans-serif; margin-bottom: 0.25rem;"
|
|
2320
|
+
>
|
|
2321
|
+
P2P Networking Not Available
|
|
2322
|
+
</div>
|
|
2323
|
+
<div
|
|
2324
|
+
style="font-size: 0.75rem; color: #6B7280; font-family: 'DM Sans', sans-serif; line-height: 1.4;"
|
|
2325
|
+
>
|
|
2326
|
+
Provide a libp2p instance to enable device linking and peer-to-peer sync.
|
|
2327
|
+
</div>
|
|
2328
|
+
</div>
|
|
2329
|
+
{:else}
|
|
2330
|
+
<!-- Link Device -->
|
|
2331
|
+
<div
|
|
2332
|
+
style="border-radius: 0.375rem; border: 1px solid #E91315; background: linear-gradient(to bottom right, #ffffff, #FFE4AE); padding: 0.75rem;"
|
|
2333
|
+
>
|
|
2334
|
+
<div
|
|
2335
|
+
style="font-size: 0.65rem; font-weight: 700; color: #E91315; text-transform: uppercase; letter-spacing: 0.05em; font-family: 'DM Sans', sans-serif; margin-bottom: 0.5rem;"
|
|
2336
|
+
>
|
|
2337
|
+
Link Another Device
|
|
2338
|
+
</div>
|
|
2339
|
+
{#if !linkDeviceReady}
|
|
2340
|
+
<div
|
|
2341
|
+
style="margin-bottom: 0.5rem; font-size: 0.65rem; color: #b45309; font-family: 'DM Sans', sans-serif; line-height: 1.35; border-radius: 0.375rem; background: rgba(254, 243, 199, 0.9); padding: 0.45rem 0.55rem; border: 1px solid #fbbf24;"
|
|
2342
|
+
>
|
|
2343
|
+
Registry / MultiDeviceManager not ready — button stays disabled until linking
|
|
2344
|
+
can run.
|
|
2345
|
+
</div>
|
|
2346
|
+
{/if}
|
|
2347
|
+
<div style="display: flex; flex-direction: column; gap: 0.5rem;">
|
|
2348
|
+
<input
|
|
2349
|
+
type="text"
|
|
2350
|
+
data-testid="storacha-link-peer-input"
|
|
2351
|
+
class="storacha-link-peer-id-input"
|
|
2352
|
+
bind:value={linkInput}
|
|
2353
|
+
placeholder="Other device’s peer id (12D3KooW…)"
|
|
2354
|
+
autocomplete="off"
|
|
2355
|
+
spellcheck="false"
|
|
2356
|
+
style="width: 100%; border-radius: 0.375rem; border: 1px solid #E91315; background: #ffffff; padding: 0.5rem 0.75rem; font-size: 0.75rem; color: #111827; font-family: 'DM Mono', monospace; outline: none; box-sizing: border-box;"
|
|
2357
|
+
/>
|
|
2358
|
+
<button
|
|
2359
|
+
data-testid="storacha-link-device-submit"
|
|
2360
|
+
data-mdm-ready={linkDeviceReady ? 'true' : 'false'}
|
|
2361
|
+
type="button"
|
|
2362
|
+
onclick={handleLinkDevice}
|
|
2363
|
+
disabled={linkDeviceDisabled}
|
|
2364
|
+
title={!linkDeviceReady
|
|
2365
|
+
? 'Waiting for device registry / MultiDeviceManager'
|
|
2366
|
+
: 'Link using the other device’s peer id'}
|
|
2367
|
+
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; border: none; cursor: {linkDeviceDisabled
|
|
2368
|
+
? 'not-allowed'
|
|
2369
|
+
: 'pointer'}; font-family: 'Epilogue', sans-serif; font-weight: 700; font-size: 0.8rem; box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -4px rgba(0,0,0,0.1); opacity: {linkDeviceDisabled
|
|
2370
|
+
? '0.5'
|
|
2371
|
+
: '1'}; box-sizing: border-box;"
|
|
2372
|
+
>
|
|
2373
|
+
{#if isLinking}
|
|
2374
|
+
<Loader2
|
|
2375
|
+
style="height: 0.875rem; width: 0.875rem; animation: spin 1s linear infinite;"
|
|
2376
|
+
/>
|
|
2377
|
+
Linking...
|
|
2378
|
+
{:else}
|
|
2379
|
+
<svg
|
|
2380
|
+
style="height: 0.875rem; width: 0.875rem;"
|
|
2381
|
+
fill="none"
|
|
2382
|
+
viewBox="0 0 24 24"
|
|
2383
|
+
stroke="currentColor"
|
|
2384
|
+
>
|
|
2385
|
+
<path
|
|
2386
|
+
stroke-linecap="round"
|
|
2387
|
+
stroke-linejoin="round"
|
|
2388
|
+
stroke-width="2"
|
|
2389
|
+
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"
|
|
2390
|
+
/>
|
|
2391
|
+
</svg>
|
|
2392
|
+
Link Device
|
|
2393
|
+
{/if}
|
|
2394
|
+
</button>
|
|
2395
|
+
</div>
|
|
2396
|
+
{#if linkError}
|
|
2397
|
+
<div
|
|
2398
|
+
style="margin-top: 0.5rem; font-size: 0.75rem; color: #b91c1c; font-family: 'DM Sans', sans-serif;"
|
|
2399
|
+
>
|
|
2400
|
+
{linkError}
|
|
2401
|
+
</div>
|
|
2402
|
+
{/if}
|
|
2403
|
+
</div>
|
|
2404
|
+
{/if}
|
|
2405
|
+
|
|
2406
|
+
{@render linkedDevicesPanel()}
|
|
2407
|
+
</div>
|
|
2408
|
+
{/if}
|
|
2409
|
+
</div>
|
|
2410
|
+
{/if}
|
|
2411
|
+
{/if}
|
|
2412
|
+
</div>
|
|
2413
|
+
|
|
2414
|
+
<style>
|
|
2415
|
+
@keyframes spin {
|
|
2416
|
+
from {
|
|
2417
|
+
transform: rotate(0deg);
|
|
2418
|
+
}
|
|
2419
|
+
to {
|
|
2420
|
+
transform: rotate(360deg);
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
@keyframes pulse {
|
|
2424
|
+
0%,
|
|
2425
|
+
100% {
|
|
2426
|
+
opacity: 1;
|
|
2427
|
+
}
|
|
2428
|
+
50% {
|
|
2429
|
+
opacity: 0.5;
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2433
|
+
.storacha-panel::-webkit-scrollbar {
|
|
2434
|
+
width: 4px;
|
|
2435
|
+
}
|
|
2436
|
+
.storacha-panel::-webkit-scrollbar-track {
|
|
2437
|
+
background: rgba(0, 0, 0, 0.1);
|
|
2438
|
+
border-radius: 2px;
|
|
2439
|
+
}
|
|
2440
|
+
.storacha-panel::-webkit-scrollbar-thumb {
|
|
2441
|
+
background: rgba(0, 0, 0, 0.3);
|
|
2442
|
+
border-radius: 2px;
|
|
2443
|
+
}
|
|
2444
|
+
.storacha-panel::-webkit-scrollbar-thumb:hover {
|
|
2445
|
+
background: rgba(0, 0, 0, 0.4);
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2448
|
+
.storacha-btn-primary:hover:not(:disabled) {
|
|
2449
|
+
background-color: #b91c1c;
|
|
2450
|
+
}
|
|
2451
|
+
.storacha-btn-backup:hover:not(:disabled) {
|
|
2452
|
+
background-color: #eab308;
|
|
2453
|
+
}
|
|
2454
|
+
.storacha-btn-restore:hover:not(:disabled) {
|
|
2455
|
+
background-color: #1d4ed8;
|
|
2456
|
+
}
|
|
2457
|
+
.storacha-btn-icon:hover:not(:disabled) {
|
|
2458
|
+
background-color: rgba(233, 19, 21, 0.1);
|
|
2459
|
+
}
|
|
2460
|
+
.storacha-toggle:hover {
|
|
2461
|
+
background-color: rgba(233, 19, 21, 0.08);
|
|
2462
|
+
}
|
|
2463
|
+
.storacha-textarea:focus {
|
|
2464
|
+
border-color: transparent;
|
|
2465
|
+
box-shadow: 0 0 0 2px #e91315;
|
|
2466
|
+
}
|
|
2467
|
+
</style>
|