@le-space/p2pass 0.1.0 → 0.3.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/dist/identity/identity-service.d.ts +4 -2
- package/dist/identity/identity-service.js +52 -14
- package/dist/index.d.ts +5 -4
- package/dist/index.js +8 -3
- package/dist/registry/device-registry.d.ts +34 -2
- package/dist/registry/device-registry.js +70 -2
- package/dist/registry/index.d.ts +1 -1
- package/dist/registry/index.js +3 -0
- package/dist/registry/manager.d.ts +5 -0
- package/dist/registry/manager.js +13 -0
- package/dist/registry/pairing-protocol.d.ts +1 -1
- package/dist/registry/pairing-protocol.js +8 -4
- package/dist/ucan/storacha-auth.d.ts +14 -1
- package/dist/ucan/storacha-auth.js +120 -2
- package/dist/ui/StorachaFab.svelte +30 -4
- package/dist/ui/StorachaIntegration.svelte +215 -48
- package/package.json +23 -2
|
@@ -24,12 +24,13 @@ export class IdentityService {
|
|
|
24
24
|
* If no credentials, creates new passkey.
|
|
25
25
|
*
|
|
26
26
|
* @param {'platform'|'cross-platform'} [authenticatorType]
|
|
27
|
-
* @param {{ preferWorkerMode?: boolean, signingPreference?: import('./signing-preference.js').SigningPreference }} [options]
|
|
27
|
+
* @param {{ preferWorkerMode?: boolean, signingPreference?: import('./signing-preference.js').SigningPreference, webauthnUserLabel?: string }} [options]
|
|
28
28
|
* @returns {Promise<{ mode: string, did: string, algorithm: string }>}
|
|
29
29
|
*/
|
|
30
30
|
initialize(authenticatorType?: "platform" | "cross-platform", options?: {
|
|
31
31
|
preferWorkerMode?: boolean;
|
|
32
32
|
signingPreference?: import("./signing-preference.js").SigningPreference;
|
|
33
|
+
webauthnUserLabel?: string;
|
|
33
34
|
}): Promise<{
|
|
34
35
|
mode: string;
|
|
35
36
|
did: string;
|
|
@@ -38,12 +39,13 @@ export class IdentityService {
|
|
|
38
39
|
/**
|
|
39
40
|
* Force create a new identity (discards existing).
|
|
40
41
|
* @param {'platform'|'cross-platform'} [authenticatorType]
|
|
41
|
-
* @param {{ preferWorkerMode?: boolean, signingPreference?: import('./signing-preference.js').SigningPreference }} [options]
|
|
42
|
+
* @param {{ preferWorkerMode?: boolean, signingPreference?: import('./signing-preference.js').SigningPreference, webauthnUserLabel?: string }} [options]
|
|
42
43
|
* @returns {Promise<{ mode: string, did: string, algorithm: string }>}
|
|
43
44
|
*/
|
|
44
45
|
createNewIdentity(authenticatorType?: "platform" | "cross-platform", options?: {
|
|
45
46
|
preferWorkerMode?: boolean;
|
|
46
47
|
signingPreference?: import("./signing-preference.js").SigningPreference;
|
|
48
|
+
webauthnUserLabel?: string;
|
|
47
49
|
}): Promise<{
|
|
48
50
|
mode: string;
|
|
49
51
|
did: string;
|
|
@@ -20,7 +20,6 @@ import {
|
|
|
20
20
|
|
|
21
21
|
import {
|
|
22
22
|
storeKeypairEntry,
|
|
23
|
-
getKeypairEntry,
|
|
24
23
|
storeArchiveEntry,
|
|
25
24
|
getArchiveEntry,
|
|
26
25
|
listKeypairs,
|
|
@@ -36,6 +35,38 @@ import { resolveSigningPreference } from './signing-preference.js';
|
|
|
36
35
|
|
|
37
36
|
const ARCHIVE_CACHE_KEY = 'p2p_passkeys_worker_archive';
|
|
38
37
|
|
|
38
|
+
/** WebAuthn user.id must be at most 64 bytes (UTF-8). */
|
|
39
|
+
const WEBAUTHN_USER_ID_MAX_BYTES = 64;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Build {@link https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialuserentity PublicKeyCredentialUserEntity}.
|
|
43
|
+
* Empty label keeps prior defaults (random opaque user.id).
|
|
44
|
+
*
|
|
45
|
+
* @param {string} label
|
|
46
|
+
* @returns {Promise<{ id: Uint8Array, name: string, displayName: string }>}
|
|
47
|
+
*/
|
|
48
|
+
async function publicKeyCredentialUserFromLabel(label) {
|
|
49
|
+
const trimmed = (typeof label === 'string' ? label : '').trim();
|
|
50
|
+
if (!trimmed) {
|
|
51
|
+
return {
|
|
52
|
+
id: crypto.getRandomValues(new Uint8Array(16)),
|
|
53
|
+
name: 'p2p-user',
|
|
54
|
+
displayName: 'P2P User',
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const encoder = new TextEncoder();
|
|
58
|
+
let idBytes = encoder.encode(trimmed);
|
|
59
|
+
if (idBytes.length > WEBAUTHN_USER_ID_MAX_BYTES) {
|
|
60
|
+
const digest = await crypto.subtle.digest('SHA-256', idBytes);
|
|
61
|
+
idBytes = new Uint8Array(digest);
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
id: idBytes,
|
|
65
|
+
name: trimmed,
|
|
66
|
+
displayName: trimmed,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
39
70
|
/**
|
|
40
71
|
* Best-effort: this origin likely already has passkey / identity material (no WebAuthn prompt).
|
|
41
72
|
* Uses the same signals as restore paths — false negatives are OK (same handlers still apply).
|
|
@@ -118,11 +149,11 @@ export class IdentityService {
|
|
|
118
149
|
* If no credentials, creates new passkey.
|
|
119
150
|
*
|
|
120
151
|
* @param {'platform'|'cross-platform'} [authenticatorType]
|
|
121
|
-
* @param {{ preferWorkerMode?: boolean, signingPreference?: import('./signing-preference.js').SigningPreference }} [options]
|
|
152
|
+
* @param {{ preferWorkerMode?: boolean, signingPreference?: import('./signing-preference.js').SigningPreference, webauthnUserLabel?: string }} [options]
|
|
122
153
|
* @returns {Promise<{ mode: string, did: string, algorithm: string }>}
|
|
123
154
|
*/
|
|
124
155
|
async initialize(authenticatorType, options = {}) {
|
|
125
|
-
const { preferWorkerMode = false, signingPreference = null } = options;
|
|
156
|
+
const { preferWorkerMode = false, signingPreference = null, webauthnUserLabel = '' } = options;
|
|
126
157
|
const pref = resolveSigningPreference({ preferWorkerMode, signingPreference });
|
|
127
158
|
const preferWorker = pref === 'worker';
|
|
128
159
|
const forceP256Hardware = pref === 'hardware-p256';
|
|
@@ -135,10 +166,16 @@ export class IdentityService {
|
|
|
135
166
|
// Try hardware mode first (unless worker mode is selected)
|
|
136
167
|
if (!preferWorker) {
|
|
137
168
|
try {
|
|
138
|
-
const
|
|
169
|
+
const trimmedLabel = webauthnUserLabel.trim();
|
|
170
|
+
const hwOpts = {
|
|
139
171
|
authenticatorType,
|
|
140
172
|
forceP256: forceP256Hardware,
|
|
141
|
-
}
|
|
173
|
+
};
|
|
174
|
+
if (trimmedLabel) {
|
|
175
|
+
hwOpts.userId = trimmedLabel;
|
|
176
|
+
hwOpts.displayName = trimmedLabel;
|
|
177
|
+
}
|
|
178
|
+
const signer = await this.#hardwareService.initialize(hwOpts);
|
|
142
179
|
|
|
143
180
|
if (signer) {
|
|
144
181
|
this.#mode = 'hardware';
|
|
@@ -161,7 +198,7 @@ export class IdentityService {
|
|
|
161
198
|
}
|
|
162
199
|
|
|
163
200
|
// No existing identity — create new worker identity
|
|
164
|
-
await this.#createWorkerIdentity(authenticatorType);
|
|
201
|
+
await this.#createWorkerIdentity(authenticatorType, webauthnUserLabel);
|
|
165
202
|
console.log(`[identity] Created new worker identity, DID: ${this.#did}`);
|
|
166
203
|
return this.getSigningMode();
|
|
167
204
|
}
|
|
@@ -169,7 +206,7 @@ export class IdentityService {
|
|
|
169
206
|
/**
|
|
170
207
|
* Force create a new identity (discards existing).
|
|
171
208
|
* @param {'platform'|'cross-platform'} [authenticatorType]
|
|
172
|
-
* @param {{ preferWorkerMode?: boolean, signingPreference?: import('./signing-preference.js').SigningPreference }} [options]
|
|
209
|
+
* @param {{ preferWorkerMode?: boolean, signingPreference?: import('./signing-preference.js').SigningPreference, webauthnUserLabel?: string }} [options]
|
|
173
210
|
* @returns {Promise<{ mode: string, did: string, algorithm: string }>}
|
|
174
211
|
*/
|
|
175
212
|
async createNewIdentity(authenticatorType, options = {}) {
|
|
@@ -190,7 +227,7 @@ export class IdentityService {
|
|
|
190
227
|
*/
|
|
191
228
|
async initializeFromRecovery() {
|
|
192
229
|
console.log('[identity] Starting recovery via discoverable credential...');
|
|
193
|
-
const { prfSeed, rawCredentialId
|
|
230
|
+
const { prfSeed, rawCredentialId } = await recoverPrfSeed();
|
|
194
231
|
|
|
195
232
|
this.#prfSeed = prfSeed;
|
|
196
233
|
this.#ipnsKeyPair = await deriveIPNSKeyPair(prfSeed);
|
|
@@ -415,9 +452,9 @@ export class IdentityService {
|
|
|
415
452
|
/**
|
|
416
453
|
* Create a new worker-mode Ed25519 identity.
|
|
417
454
|
*/
|
|
418
|
-
async #createWorkerIdentity(authenticatorType) {
|
|
455
|
+
async #createWorkerIdentity(authenticatorType, webauthnUserLabel) {
|
|
419
456
|
// Create WebAuthn credential with PRF
|
|
420
|
-
const credential = await this.#createWebAuthnCredential(authenticatorType);
|
|
457
|
+
const credential = await this.#createWebAuthnCredential(authenticatorType, webauthnUserLabel);
|
|
421
458
|
|
|
422
459
|
// Extract PRF seed
|
|
423
460
|
const { seed: prfSeed } = await extractPrfSeedFromCredential(credential);
|
|
@@ -468,9 +505,10 @@ export class IdentityService {
|
|
|
468
505
|
/**
|
|
469
506
|
* Create a WebAuthn credential with PRF extension.
|
|
470
507
|
*/
|
|
471
|
-
async #createWebAuthnCredential(authenticatorType) {
|
|
508
|
+
async #createWebAuthnCredential(authenticatorType, webauthnUserLabel) {
|
|
472
509
|
const challenge = crypto.getRandomValues(new Uint8Array(32));
|
|
473
510
|
const prfSalt = await computeDeterministicPrfSalt();
|
|
511
|
+
const userEntity = await publicKeyCredentialUserFromLabel(webauthnUserLabel);
|
|
474
512
|
|
|
475
513
|
const createOptions = {
|
|
476
514
|
publicKey: {
|
|
@@ -479,9 +517,9 @@ export class IdentityService {
|
|
|
479
517
|
id: globalThis.location?.hostname || 'localhost',
|
|
480
518
|
},
|
|
481
519
|
user: {
|
|
482
|
-
id:
|
|
483
|
-
name:
|
|
484
|
-
displayName:
|
|
520
|
+
id: userEntity.id,
|
|
521
|
+
name: userEntity.name,
|
|
522
|
+
displayName: userEntity.displayName,
|
|
485
523
|
},
|
|
486
524
|
challenge,
|
|
487
525
|
pubKeyCredParams: [
|
package/dist/index.d.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
export const VERSION: "0.1.0";
|
|
2
|
-
export { default as StorachaIntegration } from "./ui/StorachaIntegration.svelte";
|
|
3
|
-
export { default as StorachaFab } from "./ui/StorachaFab.svelte";
|
|
4
2
|
export { MultiDeviceManager } from "./registry/manager.js";
|
|
3
|
+
import StorachaIntegration from './ui/StorachaIntegration.svelte';
|
|
4
|
+
import StorachaFab from './ui/StorachaFab.svelte';
|
|
5
|
+
export { StorachaIntegration, StorachaFab };
|
|
5
6
|
export { IdentityService, hasLocalPasskeyHint } from "./identity/identity-service.js";
|
|
6
7
|
export { detectSigningMode, getStoredSigningMode } from "./identity/mode-detector.js";
|
|
7
8
|
export { SIGNING_PREFERENCE_STORAGE_KEY, SIGNING_PREFERENCE_LIST, isSigningPreference, readSigningPreferenceFromStorage, writeSigningPreferenceToStorage, resolveSigningPreference } from "./identity/signing-preference.js";
|
|
8
|
-
export { createStorachaClient, parseDelegation, storeDelegation, loadStoredDelegation, clearStoredDelegation } from "./ucan/storacha-auth.js";
|
|
9
|
-
export { openDeviceRegistry, registerDevice, listDevices, getDeviceByCredentialId, getDeviceByDID, grantDeviceWriteAccess, revokeDeviceAccess, hashCredentialId, coseToJwk, storeDelegationEntry, listDelegations, getDelegation, removeDelegation, storeArchiveEntry, getArchiveEntry, storeKeypairEntry, getKeypairEntry, listKeypairs } from "./registry/device-registry.js";
|
|
9
|
+
export { createStorachaClient, parseDelegation, storeDelegation, loadStoredDelegation, clearStoredDelegation, formatDelegationsTooltipSummary } from "./ucan/storacha-auth.js";
|
|
10
|
+
export { openDeviceRegistry, registerDevice, listDevices, getDeviceByCredentialId, getDeviceByDID, grantDeviceWriteAccess, revokeDeviceAccess, removeDeviceEntry, delegationCountForDevice, delegationsEntriesForDevice, hashCredentialId, coseToJwk, storeDelegationEntry, listDelegations, getDelegation, removeDelegation, storeArchiveEntry, getArchiveEntry, storeKeypairEntry, getKeypairEntry, listKeypairs } from "./registry/device-registry.js";
|
|
10
11
|
export { LINK_DEVICE_PROTOCOL, registerLinkDeviceHandler, unregisterLinkDeviceHandler, sendPairingRequest, detectDeviceLabel, sortPairingMultiaddrs, filterPairingDialMultiaddrs, pairingFlow, PAIRING_HINT_ADDR_CAP } from "./registry/pairing-protocol.js";
|
|
11
12
|
export { setupP2PStack, createLibp2pInstance, createHeliaInstance, cleanupP2PStack } from "./p2p/setup.js";
|
|
12
13
|
export { listSpaces, getSpaceUsage, listStorachaFiles } from "./ui/storacha-backup.js";
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// @le-space/p2pass — public API
|
|
2
2
|
export const VERSION = '0.1.0';
|
|
3
3
|
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
// Explicit default bindings (not `export { default as X } from`) — avoids star-export / default resolution errors in Vite.
|
|
5
|
+
import StorachaIntegration from './ui/StorachaIntegration.svelte';
|
|
6
|
+
import StorachaFab from './ui/StorachaFab.svelte';
|
|
7
|
+
export { StorachaIntegration, StorachaFab };
|
|
7
8
|
|
|
8
9
|
// Identity
|
|
9
10
|
export { IdentityService, hasLocalPasskeyHint } from './identity/identity-service.js';
|
|
@@ -24,6 +25,7 @@ export {
|
|
|
24
25
|
storeDelegation,
|
|
25
26
|
loadStoredDelegation,
|
|
26
27
|
clearStoredDelegation,
|
|
28
|
+
formatDelegationsTooltipSummary,
|
|
27
29
|
} from './ucan/storacha-auth.js';
|
|
28
30
|
|
|
29
31
|
// Registry (multi-device + credential storage)
|
|
@@ -36,6 +38,9 @@ export {
|
|
|
36
38
|
getDeviceByDID,
|
|
37
39
|
grantDeviceWriteAccess,
|
|
38
40
|
revokeDeviceAccess,
|
|
41
|
+
removeDeviceEntry,
|
|
42
|
+
delegationCountForDevice,
|
|
43
|
+
delegationsEntriesForDevice,
|
|
39
44
|
hashCredentialId,
|
|
40
45
|
coseToJwk,
|
|
41
46
|
storeDelegationEntry,
|
|
@@ -23,7 +23,7 @@ export function openDeviceRegistry(orbitdb: Object, ownerIdentityId: string, add
|
|
|
23
23
|
/**
|
|
24
24
|
* Register a device entry in the registry.
|
|
25
25
|
* @param {Object} db - OrbitDB KV database
|
|
26
|
-
* @param {Object} entry - { credential_id, public_key, device_label, created_at, status, ed25519_did }
|
|
26
|
+
* @param {Object} entry - { credential_id, public_key, device_label, created_at, status, ed25519_did, passkey_kind? }
|
|
27
27
|
*/
|
|
28
28
|
export function registerDevice(db: Object, entry: Object): Promise<void>;
|
|
29
29
|
/**
|
|
@@ -58,14 +58,46 @@ export function grantDeviceWriteAccess(db: Object, did: string): Promise<void>;
|
|
|
58
58
|
* @param {string} did - Ed25519 DID to revoke
|
|
59
59
|
*/
|
|
60
60
|
export function revokeDeviceAccess(db: Object, did: string): Promise<void>;
|
|
61
|
+
/**
|
|
62
|
+
* Revoke a device's write access and remove its registry row (OrbitDB KV key).
|
|
63
|
+
*
|
|
64
|
+
* @param {Object} db - OrbitDB KV database
|
|
65
|
+
* @param {string} credentialId - device entry credential_id (same string used at registration)
|
|
66
|
+
* @returns {Promise<boolean>} true if an entry was removed
|
|
67
|
+
*/
|
|
68
|
+
export function removeDeviceEntry(db: Object, credentialId: string): Promise<boolean>;
|
|
69
|
+
/**
|
|
70
|
+
* UCAN delegations attributed to a device (stored_by_did). Entries without stored_by_did count toward ownerDidForLegacy only.
|
|
71
|
+
*
|
|
72
|
+
* @param {Array<{ stored_by_did?: string }>} delegations
|
|
73
|
+
* @param {string} deviceDid
|
|
74
|
+
* @param {string} [ownerDidForLegacy]
|
|
75
|
+
* @returns {number}
|
|
76
|
+
*/
|
|
77
|
+
export function delegationCountForDevice(delegations: Array<{
|
|
78
|
+
stored_by_did?: string;
|
|
79
|
+
}>, deviceDid: string, ownerDidForLegacy?: string): number;
|
|
80
|
+
/**
|
|
81
|
+
* Registry delegation rows attributed to a device (same rules as {@link delegationCountForDevice}).
|
|
82
|
+
*
|
|
83
|
+
* @param {Array<{ stored_by_did?: string, delegation?: string }>} delegations
|
|
84
|
+
* @param {string} deviceDid
|
|
85
|
+
* @param {string} [ownerDidForLegacy]
|
|
86
|
+
* @returns {Array<Record<string, unknown>>}
|
|
87
|
+
*/
|
|
88
|
+
export function delegationsEntriesForDevice(delegations: Array<{
|
|
89
|
+
stored_by_did?: string;
|
|
90
|
+
delegation?: string;
|
|
91
|
+
}>, deviceDid: string, ownerDidForLegacy?: string): Array<Record<string, unknown>>;
|
|
61
92
|
/**
|
|
62
93
|
* Store a UCAN delegation in the registry.
|
|
63
94
|
* @param {Object} db - OrbitDB KV database
|
|
64
95
|
* @param {string} delegationBase64 - raw delegation string
|
|
65
96
|
* @param {string} [spaceDid] - Storacha space DID
|
|
66
97
|
* @param {string} [label] - human-readable label
|
|
98
|
+
* @param {string} [storedByDid] - Ed25519 DID of the device that stored this delegation (for per-device UI)
|
|
67
99
|
*/
|
|
68
|
-
export function storeDelegationEntry(db: Object, delegationBase64: string, spaceDid?: string, label?: string): Promise<void>;
|
|
100
|
+
export function storeDelegationEntry(db: Object, delegationBase64: string, spaceDid?: string, label?: string, storedByDid?: string): Promise<void>;
|
|
69
101
|
/**
|
|
70
102
|
* List all stored UCAN delegations.
|
|
71
103
|
* @param {Object} db - OrbitDB KV database
|
|
@@ -81,7 +81,7 @@ export async function openDeviceRegistry(orbitdb, ownerIdentityId, address = nul
|
|
|
81
81
|
/**
|
|
82
82
|
* Register a device entry in the registry.
|
|
83
83
|
* @param {Object} db - OrbitDB KV database
|
|
84
|
-
* @param {Object} entry - { credential_id, public_key, device_label, created_at, status, ed25519_did }
|
|
84
|
+
* @param {Object} entry - { credential_id, public_key, device_label, created_at, status, ed25519_did, passkey_kind? }
|
|
85
85
|
*/
|
|
86
86
|
export async function registerDevice(db, entry) {
|
|
87
87
|
const key = await hashCredentialId(entry.credential_id);
|
|
@@ -153,6 +153,72 @@ export async function revokeDeviceAccess(db, did) {
|
|
|
153
153
|
}
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
+
/**
|
|
157
|
+
* Revoke a device's write access and remove its registry row (OrbitDB KV key).
|
|
158
|
+
*
|
|
159
|
+
* @param {Object} db - OrbitDB KV database
|
|
160
|
+
* @param {string} credentialId - device entry credential_id (same string used at registration)
|
|
161
|
+
* @returns {Promise<boolean>} true if an entry was removed
|
|
162
|
+
*/
|
|
163
|
+
export async function removeDeviceEntry(db, credentialId) {
|
|
164
|
+
const key = await hashCredentialId(credentialId);
|
|
165
|
+
const entry = await db.get(key);
|
|
166
|
+
if (!entry) return false;
|
|
167
|
+
const did = entry.ed25519_did;
|
|
168
|
+
try {
|
|
169
|
+
if (did) await db.access.revoke('write', did);
|
|
170
|
+
} catch (err) {
|
|
171
|
+
console.warn('[device-registry] revoke write before del:', err?.message);
|
|
172
|
+
}
|
|
173
|
+
await db.del(key);
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* UCAN delegations attributed to a device (stored_by_did). Entries without stored_by_did count toward ownerDidForLegacy only.
|
|
179
|
+
*
|
|
180
|
+
* @param {Array<{ stored_by_did?: string }>} delegations
|
|
181
|
+
* @param {string} deviceDid
|
|
182
|
+
* @param {string} [ownerDidForLegacy]
|
|
183
|
+
* @returns {number}
|
|
184
|
+
*/
|
|
185
|
+
export function delegationCountForDevice(delegations, deviceDid, ownerDidForLegacy) {
|
|
186
|
+
if (!deviceDid) return 0;
|
|
187
|
+
let n = 0;
|
|
188
|
+
for (const d of delegations) {
|
|
189
|
+
if (d.stored_by_did === deviceDid) n++;
|
|
190
|
+
}
|
|
191
|
+
if (ownerDidForLegacy && deviceDid === ownerDidForLegacy) {
|
|
192
|
+
for (const d of delegations) {
|
|
193
|
+
if (!d.stored_by_did) n++;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return n;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Registry delegation rows attributed to a device (same rules as {@link delegationCountForDevice}).
|
|
201
|
+
*
|
|
202
|
+
* @param {Array<{ stored_by_did?: string, delegation?: string }>} delegations
|
|
203
|
+
* @param {string} deviceDid
|
|
204
|
+
* @param {string} [ownerDidForLegacy]
|
|
205
|
+
* @returns {Array<Record<string, unknown>>}
|
|
206
|
+
*/
|
|
207
|
+
export function delegationsEntriesForDevice(delegations, deviceDid, ownerDidForLegacy) {
|
|
208
|
+
if (!deviceDid) return [];
|
|
209
|
+
/** @type {Array<Record<string, unknown>>} */
|
|
210
|
+
const out = [];
|
|
211
|
+
for (const d of delegations) {
|
|
212
|
+
if (d.stored_by_did === deviceDid) out.push(d);
|
|
213
|
+
}
|
|
214
|
+
if (ownerDidForLegacy && deviceDid === ownerDidForLegacy) {
|
|
215
|
+
for (const d of delegations) {
|
|
216
|
+
if (!d.stored_by_did) out.push(d);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return out;
|
|
220
|
+
}
|
|
221
|
+
|
|
156
222
|
// ---------------------------------------------------------------------------
|
|
157
223
|
// UCAN delegation entries
|
|
158
224
|
// ---------------------------------------------------------------------------
|
|
@@ -163,8 +229,9 @@ export async function revokeDeviceAccess(db, did) {
|
|
|
163
229
|
* @param {string} delegationBase64 - raw delegation string
|
|
164
230
|
* @param {string} [spaceDid] - Storacha space DID
|
|
165
231
|
* @param {string} [label] - human-readable label
|
|
232
|
+
* @param {string} [storedByDid] - Ed25519 DID of the device that stored this delegation (for per-device UI)
|
|
166
233
|
*/
|
|
167
|
-
export async function storeDelegationEntry(db, delegationBase64, spaceDid, label) {
|
|
234
|
+
export async function storeDelegationEntry(db, delegationBase64, spaceDid, label, storedByDid) {
|
|
168
235
|
const hash = await hashCredentialId(delegationBase64);
|
|
169
236
|
const key = `delegation:${hash}`;
|
|
170
237
|
await db.put(key, {
|
|
@@ -172,6 +239,7 @@ export async function storeDelegationEntry(db, delegationBase64, spaceDid, label
|
|
|
172
239
|
space_did: spaceDid || '',
|
|
173
240
|
label: label || 'default',
|
|
174
241
|
created_at: Date.now(),
|
|
242
|
+
stored_by_did: storedByDid || '',
|
|
175
243
|
});
|
|
176
244
|
}
|
|
177
245
|
|
package/dist/registry/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { MultiDeviceManager } from "./manager.js";
|
|
2
|
-
export { openDeviceRegistry, registerDevice, listDevices, getDeviceByCredentialId, getDeviceByDID, grantDeviceWriteAccess, revokeDeviceAccess, hashCredentialId, coseToJwk, storeDelegationEntry, listDelegations, getDelegation, removeDelegation, storeArchiveEntry, getArchiveEntry, storeKeypairEntry, getKeypairEntry, listKeypairs } from "./device-registry.js";
|
|
2
|
+
export { openDeviceRegistry, registerDevice, listDevices, getDeviceByCredentialId, getDeviceByDID, grantDeviceWriteAccess, revokeDeviceAccess, removeDeviceEntry, delegationCountForDevice, delegationsEntriesForDevice, hashCredentialId, coseToJwk, storeDelegationEntry, listDelegations, getDelegation, removeDelegation, storeArchiveEntry, getArchiveEntry, storeKeypairEntry, getKeypairEntry, listKeypairs } from "./device-registry.js";
|
|
3
3
|
export { LINK_DEVICE_PROTOCOL, registerLinkDeviceHandler, unregisterLinkDeviceHandler, sendPairingRequest, detectDeviceLabel, sortPairingMultiaddrs, filterPairingDialMultiaddrs, pairingFlow, PAIRING_HINT_ADDR_CAP } from "./pairing-protocol.js";
|
package/dist/registry/index.js
CHANGED
|
@@ -59,6 +59,11 @@ export class MultiDeviceManager {
|
|
|
59
59
|
listDevices(): Promise<any[]>;
|
|
60
60
|
syncDevices(): Promise<void>;
|
|
61
61
|
revokeDevice(did: any): Promise<void>;
|
|
62
|
+
/**
|
|
63
|
+
* Remove a device row from the registry and revoke its OrbitDB write access.
|
|
64
|
+
* @param {string} credentialId - value stored as device entry `credential_id`
|
|
65
|
+
*/
|
|
66
|
+
removeLinkedDevice(credentialId: string): Promise<boolean>;
|
|
62
67
|
processIncomingPairingRequest(requestMsg: any): Promise<{
|
|
63
68
|
type: string;
|
|
64
69
|
orbitdbAddress: any;
|
package/dist/registry/manager.js
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
getDeviceByDID,
|
|
14
14
|
grantDeviceWriteAccess,
|
|
15
15
|
revokeDeviceAccess,
|
|
16
|
+
removeDeviceEntry,
|
|
16
17
|
coseToJwk,
|
|
17
18
|
} from './device-registry.js';
|
|
18
19
|
|
|
@@ -140,6 +141,7 @@ export class MultiDeviceManager {
|
|
|
140
141
|
created_at: Date.now(),
|
|
141
142
|
status: 'active',
|
|
142
143
|
ed25519_did: this._identity.id,
|
|
144
|
+
passkey_kind: this._identity.passkeyKind || null,
|
|
143
145
|
});
|
|
144
146
|
|
|
145
147
|
await this.syncDevices();
|
|
@@ -247,6 +249,7 @@ export class MultiDeviceManager {
|
|
|
247
249
|
this._credential?.credentialId || this._credential?.id || this._libp2p.peerId.toString(),
|
|
248
250
|
publicKey: null,
|
|
249
251
|
deviceLabel: detectDeviceLabel(),
|
|
252
|
+
passkeyKind: this._identity.passkeyKind || null,
|
|
250
253
|
},
|
|
251
254
|
multiaddrs
|
|
252
255
|
);
|
|
@@ -325,6 +328,15 @@ export class MultiDeviceManager {
|
|
|
325
328
|
await revokeDeviceAccess(this._devicesDb, did);
|
|
326
329
|
}
|
|
327
330
|
|
|
331
|
+
/**
|
|
332
|
+
* Remove a device row from the registry and revoke its OrbitDB write access.
|
|
333
|
+
* @param {string} credentialId - value stored as device entry `credential_id`
|
|
334
|
+
*/
|
|
335
|
+
async removeLinkedDevice(credentialId) {
|
|
336
|
+
if (!this._devicesDb) throw new Error('Device registry not initialized');
|
|
337
|
+
return removeDeviceEntry(this._devicesDb, credentialId);
|
|
338
|
+
}
|
|
339
|
+
|
|
328
340
|
async processIncomingPairingRequest(requestMsg) {
|
|
329
341
|
if (!this._devicesDb) throw new Error('Device registry not initialized');
|
|
330
342
|
const { identity } = requestMsg;
|
|
@@ -348,6 +360,7 @@ export class MultiDeviceManager {
|
|
|
348
360
|
created_at: Date.now(),
|
|
349
361
|
status: 'active',
|
|
350
362
|
ed25519_did: identity.id,
|
|
363
|
+
passkey_kind: identity.passkeyKind || null,
|
|
351
364
|
});
|
|
352
365
|
return { type: 'granted', orbitdbAddress: this._dbAddress };
|
|
353
366
|
}
|
|
@@ -42,7 +42,7 @@ export function detectDeviceLabel(): string;
|
|
|
42
42
|
*
|
|
43
43
|
* @param {Object} libp2p - libp2p instance (Device B)
|
|
44
44
|
* @param {string|Object} deviceAPeerId - peerId string or PeerId object of Device A
|
|
45
|
-
* @param {Object} identity - { id, credentialId, publicKey?, deviceLabel? }
|
|
45
|
+
* @param {Object} identity - { id, credentialId, publicKey?, deviceLabel?, passkeyKind? }
|
|
46
46
|
* @param {string[]} [hintMultiaddrs] - Known multiaddrs for Device A (from QR payload)
|
|
47
47
|
* @returns {Promise<{type: 'granted', orbitdbAddress: string}|{type: 'rejected', reason: string}>}
|
|
48
48
|
*/
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Protocol: /orbitdb/link-device/1.0.0
|
|
5
5
|
*
|
|
6
6
|
* Message flow:
|
|
7
|
-
* Device B → Device A: { type: 'request', identity: { id, credentialId, deviceLabel } }
|
|
7
|
+
* Device B → Device A: { type: 'request', identity: { id, credentialId, deviceLabel, passkeyKind?, ... } }
|
|
8
8
|
* Device A → Device B: { type: 'granted', orbitdbAddress } | { type: 'rejected', reason }
|
|
9
9
|
*
|
|
10
10
|
* Copied from orbitdb-identity-provider-webauthn-did/src/multi-device/pairing-protocol.js
|
|
@@ -330,12 +330,14 @@ export async function registerLinkDeviceHandler(libp2p, db, onRequest, onDeviceL
|
|
|
330
330
|
result = { type: 'granted', orbitdbAddress: db.address };
|
|
331
331
|
if (isKnown && onDeviceLinked) {
|
|
332
332
|
onDeviceLinked({
|
|
333
|
+
...isKnown,
|
|
333
334
|
credential_id: identity.credentialId,
|
|
334
|
-
public_key: identity.publicKey
|
|
335
|
-
device_label: identity.deviceLabel || 'Linked Device',
|
|
335
|
+
public_key: identity.publicKey ?? isKnown.public_key ?? null,
|
|
336
|
+
device_label: identity.deviceLabel || isKnown.device_label || 'Linked Device',
|
|
336
337
|
created_at: isKnown.created_at || Date.now(),
|
|
337
338
|
status: 'active',
|
|
338
339
|
ed25519_did: identity.id,
|
|
340
|
+
passkey_kind: identity.passkeyKind || isKnown.passkey_kind || null,
|
|
339
341
|
});
|
|
340
342
|
}
|
|
341
343
|
} else {
|
|
@@ -365,6 +367,7 @@ export async function registerLinkDeviceHandler(libp2p, db, onRequest, onDeviceL
|
|
|
365
367
|
created_at: Date.now(),
|
|
366
368
|
status: 'active',
|
|
367
369
|
ed25519_did: identity.id,
|
|
370
|
+
passkey_kind: identity.passkeyKind || null,
|
|
368
371
|
};
|
|
369
372
|
try {
|
|
370
373
|
await registerDevice(db, deviceEntry);
|
|
@@ -528,7 +531,7 @@ export function detectDeviceLabel() {
|
|
|
528
531
|
*
|
|
529
532
|
* @param {Object} libp2p - libp2p instance (Device B)
|
|
530
533
|
* @param {string|Object} deviceAPeerId - peerId string or PeerId object of Device A
|
|
531
|
-
* @param {Object} identity - { id, credentialId, publicKey?, deviceLabel? }
|
|
534
|
+
* @param {Object} identity - { id, credentialId, publicKey?, deviceLabel?, passkeyKind? }
|
|
532
535
|
* @param {string[]} [hintMultiaddrs] - Known multiaddrs for Device A (from QR payload)
|
|
533
536
|
* @returns {Promise<{type: 'granted', orbitdbAddress: string}|{type: 'rejected', reason: string}>}
|
|
534
537
|
*/
|
|
@@ -628,6 +631,7 @@ export async function sendPairingRequest(libp2p, deviceAPeerId, identity, hintMu
|
|
|
628
631
|
credentialId: identity.credentialId,
|
|
629
632
|
publicKey: identity.publicKey || null,
|
|
630
633
|
deviceLabel: identity.deviceLabel || detectDeviceLabel(),
|
|
634
|
+
passkeyKind: identity.passkeyKind || null,
|
|
631
635
|
},
|
|
632
636
|
};
|
|
633
637
|
pairingFlow('BOB', 'sending link-device REQUEST (length-prefixed JSON) to Alice', {
|
|
@@ -24,8 +24,9 @@ export function parseDelegation(proofString: string): Promise<any>;
|
|
|
24
24
|
* @param {string} delegationBase64
|
|
25
25
|
* @param {Object} [registryDb] - OrbitDB registry database
|
|
26
26
|
* @param {string} [spaceDid] - Storacha space DID (for registry metadata)
|
|
27
|
+
* @param {string} [storedByDid] - device DID that imported the delegation (per-device counts in UI)
|
|
27
28
|
*/
|
|
28
|
-
export function storeDelegation(delegationBase64: string, registryDb?: Object, spaceDid?: string): Promise<void>;
|
|
29
|
+
export function storeDelegation(delegationBase64: string, registryDb?: Object, spaceDid?: string, storedByDid?: string): Promise<void>;
|
|
29
30
|
/**
|
|
30
31
|
* Load a stored delegation string.
|
|
31
32
|
* Reads from registry DB if provided, otherwise localStorage.
|
|
@@ -43,3 +44,15 @@ export function loadStoredDelegation(registryDb?: Object): Promise<string | null
|
|
|
43
44
|
* @param {string} [delegationBase64] - specific delegation to remove (if omitted, removes all)
|
|
44
45
|
*/
|
|
45
46
|
export function clearStoredDelegation(registryDb?: Object, delegationBase64?: string): Promise<void>;
|
|
47
|
+
/**
|
|
48
|
+
* Multi-line text suitable for a native HTML `title` on the linked-device UCAN badge.
|
|
49
|
+
* Parses each stored delegation via {@link parseDelegation}.
|
|
50
|
+
*
|
|
51
|
+
* @param {Array<{ delegation?: string, space_did?: string, label?: string }>} entries
|
|
52
|
+
* @returns {Promise<string>}
|
|
53
|
+
*/
|
|
54
|
+
export function formatDelegationsTooltipSummary(entries: Array<{
|
|
55
|
+
delegation?: string;
|
|
56
|
+
space_did?: string;
|
|
57
|
+
label?: string;
|
|
58
|
+
}>): Promise<string>;
|
|
@@ -98,11 +98,12 @@ export async function parseDelegation(proofString) {
|
|
|
98
98
|
* @param {string} delegationBase64
|
|
99
99
|
* @param {Object} [registryDb] - OrbitDB registry database
|
|
100
100
|
* @param {string} [spaceDid] - Storacha space DID (for registry metadata)
|
|
101
|
+
* @param {string} [storedByDid] - device DID that imported the delegation (per-device counts in UI)
|
|
101
102
|
*/
|
|
102
|
-
export async function storeDelegation(delegationBase64, registryDb, spaceDid) {
|
|
103
|
+
export async function storeDelegation(delegationBase64, registryDb, spaceDid, storedByDid) {
|
|
103
104
|
if (registryDb) {
|
|
104
105
|
try {
|
|
105
|
-
await storeDelegationEntry(registryDb, delegationBase64, spaceDid);
|
|
106
|
+
await storeDelegationEntry(registryDb, delegationBase64, spaceDid, undefined, storedByDid);
|
|
106
107
|
console.log('[storacha] Delegation stored in registry DB');
|
|
107
108
|
return;
|
|
108
109
|
} catch (err) {
|
|
@@ -162,3 +163,120 @@ export async function clearStoredDelegation(registryDb, delegationBase64) {
|
|
|
162
163
|
localStorage.removeItem(STORAGE_KEY_DELEGATION);
|
|
163
164
|
}
|
|
164
165
|
}
|
|
166
|
+
|
|
167
|
+
/** Max characters for native `title` tooltips (browser-dependent display). */
|
|
168
|
+
const DELEGATION_TOOLTIP_MAX = 1200;
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* @param {string} s
|
|
172
|
+
* @param {number} max
|
|
173
|
+
*/
|
|
174
|
+
function truncateMiddle(s, max) {
|
|
175
|
+
if (s.length <= max) return s;
|
|
176
|
+
const half = Math.floor((max - 1) / 2);
|
|
177
|
+
return `${s.slice(0, half)}…${s.slice(s.length - half)}`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* @param {unknown} p - UCANTO principal or string
|
|
182
|
+
*/
|
|
183
|
+
function principalDid(p) {
|
|
184
|
+
if (p == null) return '—';
|
|
185
|
+
if (typeof p === 'string') return p;
|
|
186
|
+
if (typeof p === 'object' && p !== null && 'did' in p && typeof p.did === 'function') {
|
|
187
|
+
try {
|
|
188
|
+
return /** @type {{ did: () => string }} */ (p).did();
|
|
189
|
+
} catch {
|
|
190
|
+
return String(p);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return String(p);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* @param {unknown} exp - UCAN expiration (seconds or ms since epoch)
|
|
198
|
+
*/
|
|
199
|
+
function formatUcanExpiration(exp) {
|
|
200
|
+
if (exp == null) return null;
|
|
201
|
+
const n = typeof exp === 'bigint' ? Number(exp) : Number(exp);
|
|
202
|
+
if (!Number.isFinite(n) || n <= 0) return null;
|
|
203
|
+
const ms = n > 1e12 ? n : n * 1000;
|
|
204
|
+
try {
|
|
205
|
+
return new Date(ms).toUTCString();
|
|
206
|
+
} catch {
|
|
207
|
+
return String(exp);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* @param {any} d - Parsed delegation from {@link parseDelegation}
|
|
213
|
+
* @returns {string}
|
|
214
|
+
*/
|
|
215
|
+
function summarizeParsedDelegationForTooltip(d) {
|
|
216
|
+
const lines = [];
|
|
217
|
+
lines.push(`Issuer: ${truncateMiddle(principalDid(d?.issuer), 64)}`);
|
|
218
|
+
lines.push(`Audience: ${truncateMiddle(principalDid(d?.audience), 64)}`);
|
|
219
|
+
const expStr = formatUcanExpiration(d?.expiration);
|
|
220
|
+
if (expStr) lines.push(`Expires: ${expStr}`);
|
|
221
|
+
const nbStr = formatUcanExpiration(d?.notBefore);
|
|
222
|
+
if (nbStr) lines.push(`Not before: ${nbStr}`);
|
|
223
|
+
const caps = d?.capabilities;
|
|
224
|
+
if (Array.isArray(caps) && caps.length > 0) {
|
|
225
|
+
const shown = caps.slice(0, 6).map((c) => {
|
|
226
|
+
const can = c?.can ?? '?';
|
|
227
|
+
const w = c?.with != null ? String(c.with) : '—';
|
|
228
|
+
return `${can} → ${truncateMiddle(w, 48)}`;
|
|
229
|
+
});
|
|
230
|
+
let capBlock = shown.join('\n');
|
|
231
|
+
if (caps.length > 6) capBlock += `\n… +${caps.length - 6} more`;
|
|
232
|
+
lines.push(`Capabilities:\n${capBlock}`);
|
|
233
|
+
}
|
|
234
|
+
const facts = d?.facts;
|
|
235
|
+
if (Array.isArray(facts) && facts.length > 0) {
|
|
236
|
+
lines.push(`Facts: ${facts.length} attached`);
|
|
237
|
+
}
|
|
238
|
+
try {
|
|
239
|
+
if (d?.cid != null) lines.push(`Root CID: ${truncateMiddle(String(d.cid), 72)}`);
|
|
240
|
+
} catch {
|
|
241
|
+
/* ignore */
|
|
242
|
+
}
|
|
243
|
+
return lines.join('\n');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Multi-line text suitable for a native HTML `title` on the linked-device UCAN badge.
|
|
248
|
+
* Parses each stored delegation via {@link parseDelegation}.
|
|
249
|
+
*
|
|
250
|
+
* @param {Array<{ delegation?: string, space_did?: string, label?: string }>} entries
|
|
251
|
+
* @returns {Promise<string>}
|
|
252
|
+
*/
|
|
253
|
+
export async function formatDelegationsTooltipSummary(entries) {
|
|
254
|
+
if (!Array.isArray(entries) || entries.length === 0) {
|
|
255
|
+
return 'No UCAN delegations';
|
|
256
|
+
}
|
|
257
|
+
const blocks = [];
|
|
258
|
+
for (let i = 0; i < entries.length; i++) {
|
|
259
|
+
const e = entries[i];
|
|
260
|
+
const raw = e?.delegation;
|
|
261
|
+
if (typeof raw !== 'string' || !raw.trim()) continue;
|
|
262
|
+
let head =
|
|
263
|
+
entries.length > 1 ? `Delegation ${i + 1} of ${entries.length}` : 'UCAN delegation';
|
|
264
|
+
if (e.space_did) head += ` · Space ${truncateMiddle(e.space_did, 40)}`;
|
|
265
|
+
if (e.label && e.label !== 'default') head += ` · ${e.label}`;
|
|
266
|
+
try {
|
|
267
|
+
const parsed = await parseDelegation(raw);
|
|
268
|
+
blocks.push(`${head}\n${summarizeParsedDelegationForTooltip(parsed)}`);
|
|
269
|
+
} catch (err) {
|
|
270
|
+
const msg =
|
|
271
|
+
err && typeof err === 'object' && 'message' in err
|
|
272
|
+
? String(/** @type {{ message: string }} */ (err).message)
|
|
273
|
+
: String(err);
|
|
274
|
+
blocks.push(`${head}\n(parse failed: ${truncateMiddle(msg, 100)})`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (blocks.length === 0) return 'No valid delegation payloads';
|
|
278
|
+
const joined = blocks.join('\n\n────────\n\n');
|
|
279
|
+
return joined.length > DELEGATION_TOOLTIP_MAX
|
|
280
|
+
? truncateMiddle(joined, DELEGATION_TOOLTIP_MAX)
|
|
281
|
+
: joined;
|
|
282
|
+
}
|
|
@@ -13,16 +13,41 @@
|
|
|
13
13
|
libp2p = null,
|
|
14
14
|
preferWorkerMode = false,
|
|
15
15
|
signingPreference = null,
|
|
16
|
+
/** When false, hide the floating FAB (e.g. host app opens the panel from the footer). */
|
|
17
|
+
fabVisible = true,
|
|
18
|
+
/**
|
|
19
|
+
* When set, panel open state is synced with this store so the host can toggle from a footer control.
|
|
20
|
+
* @type {import('svelte/store').Writable<boolean> | null}
|
|
21
|
+
*/
|
|
22
|
+
panelOpenStore = null,
|
|
16
23
|
} = $props();
|
|
17
24
|
|
|
18
25
|
let showPanel = $state(false);
|
|
19
26
|
let isHovered = $state(false);
|
|
27
|
+
|
|
28
|
+
function setPanelOpen(/** @type {boolean} */ v) {
|
|
29
|
+
showPanel = v;
|
|
30
|
+
panelOpenStore?.set(v);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function togglePanel() {
|
|
34
|
+
setPanelOpen(!showPanel);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
$effect(() => {
|
|
38
|
+
if (!panelOpenStore) return;
|
|
39
|
+
const unsub = panelOpenStore.subscribe((v) => {
|
|
40
|
+
showPanel = v;
|
|
41
|
+
});
|
|
42
|
+
return unsub;
|
|
43
|
+
});
|
|
20
44
|
</script>
|
|
21
45
|
|
|
22
46
|
<!-- Floating Storacha FAB Button -->
|
|
47
|
+
{#if fabVisible}
|
|
23
48
|
<button
|
|
24
49
|
data-testid="storacha-fab-toggle"
|
|
25
|
-
onclick={() => (
|
|
50
|
+
onclick={() => togglePanel()}
|
|
26
51
|
onmouseenter={() => (isHovered = true)}
|
|
27
52
|
onmouseleave={() => (isHovered = false)}
|
|
28
53
|
title={showPanel ? 'Hide P2Pass panel' : 'Open P2Pass — passkey & UCAN, peer-to-peer'}
|
|
@@ -84,13 +109,14 @@
|
|
|
84
109
|
/>
|
|
85
110
|
</svg>
|
|
86
111
|
</button>
|
|
112
|
+
{/if}
|
|
87
113
|
|
|
88
114
|
<!-- Backdrop overlay -->
|
|
89
115
|
{#if showPanel}
|
|
90
116
|
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
91
117
|
<div
|
|
92
|
-
onclick={() => (
|
|
93
|
-
onkeydown={(e) => e.key === 'Escape' && (
|
|
118
|
+
onclick={() => setPanelOpen(false)}
|
|
119
|
+
onkeydown={(e) => e.key === 'Escape' && setPanelOpen(false)}
|
|
94
120
|
role="presentation"
|
|
95
121
|
style="
|
|
96
122
|
position: fixed;
|
|
@@ -125,7 +151,7 @@
|
|
|
125
151
|
{onBackup}
|
|
126
152
|
{onAuthenticate}
|
|
127
153
|
onPairingPromptOpen={() => {
|
|
128
|
-
|
|
154
|
+
setPanelOpen(true);
|
|
129
155
|
}}
|
|
130
156
|
{libp2p}
|
|
131
157
|
{preferWorkerMode}
|
|
@@ -4,8 +4,15 @@
|
|
|
4
4
|
readSigningPreferenceFromStorage,
|
|
5
5
|
writeSigningPreferenceToStorage,
|
|
6
6
|
} from '../identity/signing-preference.js';
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
// Per-icon entrypoints avoid the root `lucide-svelte` barrel (`export *`), which can trigger
|
|
8
|
+
// "Importing binding name 'default' cannot be resolved by star export entries" in strict ESM.
|
|
9
|
+
import Upload from 'lucide-svelte/icons/upload';
|
|
10
|
+
import LogOut from 'lucide-svelte/icons/log-out';
|
|
11
|
+
import Loader2 from 'lucide-svelte/icons/loader-2';
|
|
12
|
+
import AlertCircle from 'lucide-svelte/icons/alert-circle';
|
|
13
|
+
import CheckCircle from 'lucide-svelte/icons/check-circle';
|
|
14
|
+
import Download from 'lucide-svelte/icons/download';
|
|
15
|
+
import { getSpaceUsage } from './storacha-backup.js';
|
|
9
16
|
import { OrbitDBStorachaBridge } from 'orbitdb-storacha-bridge';
|
|
10
17
|
import { IdentityService, hasLocalPasskeyHint } from '../identity/identity-service.js';
|
|
11
18
|
import { getStoredSigningMode } from '../identity/mode-detector.js';
|
|
@@ -15,6 +22,7 @@
|
|
|
15
22
|
storeDelegation,
|
|
16
23
|
loadStoredDelegation,
|
|
17
24
|
clearStoredDelegation,
|
|
25
|
+
formatDelegationsTooltipSummary,
|
|
18
26
|
} from '../ucan/storacha-auth.js';
|
|
19
27
|
import {
|
|
20
28
|
openDeviceRegistry,
|
|
@@ -27,8 +35,10 @@
|
|
|
27
35
|
storeArchiveEntry,
|
|
28
36
|
listDelegations,
|
|
29
37
|
storeDelegationEntry,
|
|
38
|
+
removeDeviceEntry,
|
|
39
|
+
delegationsEntriesForDevice,
|
|
40
|
+
hashCredentialId,
|
|
30
41
|
} from '../registry/device-registry.js';
|
|
31
|
-
import { deriveIPNSKeyPair } from '../recovery/ipns-key.js';
|
|
32
42
|
import {
|
|
33
43
|
createManifest,
|
|
34
44
|
publishManifest,
|
|
@@ -36,7 +46,7 @@
|
|
|
36
46
|
uploadArchiveToIPFS,
|
|
37
47
|
fetchArchiveFromIPFS,
|
|
38
48
|
} from '../recovery/manifest.js';
|
|
39
|
-
import { backupRegistryDb
|
|
49
|
+
import { backupRegistryDb } from '../backup/registry-backup.js';
|
|
40
50
|
import { MultiDeviceManager } from '../registry/manager.js';
|
|
41
51
|
import { detectDeviceLabel, pairingFlow } from '../registry/pairing-protocol.js';
|
|
42
52
|
import { loadWebAuthnCredentialSafe } from '@le-space/orbitdb-identity-provider-webauthn-did/standalone';
|
|
@@ -87,10 +97,95 @@
|
|
|
87
97
|
return '\uD83D\uDCF1';
|
|
88
98
|
}
|
|
89
99
|
|
|
100
|
+
/** @param {{ mode?: string, algorithm?: string } | null} sm */
|
|
101
|
+
function passkeyKindFromSigningMode(sm) {
|
|
102
|
+
if (!sm) return null;
|
|
103
|
+
if (sm.mode === 'worker') return 'worker-ed25519';
|
|
104
|
+
if (sm.algorithm === 'P-256') return 'hardware-p256';
|
|
105
|
+
return 'hardware-ed25519';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Passkey kind label for a registry device row (stored passkey_kind, else local session). */
|
|
109
|
+
function linkedDevicePasskeyLabel(/** @type {Record<string, unknown>} */ device) {
|
|
110
|
+
const k = device.passkey_kind;
|
|
111
|
+
if (typeof k === 'string' && k) return k;
|
|
112
|
+
if (signingMode?.did && device.ed25519_did === signingMode.did) {
|
|
113
|
+
return passkeyKindFromSigningMode(signingMode);
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** UCAN delegation counts keyed by device DID (see {@link delegationsEntriesForDevice}). */
|
|
119
|
+
let ucanCountsByDid = $state(/** @type {Record<string, number>} */ ({}));
|
|
120
|
+
/** Parsed UCAN summary for each device row’s badge `title`. */
|
|
121
|
+
let ucanTooltipByDid = $state(/** @type {Record<string, string>} */ ({}));
|
|
122
|
+
|
|
123
|
+
async function refreshLinkedDeviceDelegationCounts() {
|
|
124
|
+
if (!registryDb) {
|
|
125
|
+
ucanCountsByDid = {};
|
|
126
|
+
ucanTooltipByDid = {};
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
const delegations = await listDelegations(registryDb);
|
|
131
|
+
const ownerDid = localStorage.getItem(OWNER_DID_KEY) || signingMode?.did || '';
|
|
132
|
+
/** @type {Record<string, number>} */
|
|
133
|
+
const next = {};
|
|
134
|
+
/** @type {Record<string, string>} */
|
|
135
|
+
const tips = {};
|
|
136
|
+
for (const dev of devices) {
|
|
137
|
+
const did = dev.ed25519_did;
|
|
138
|
+
if (typeof did !== 'string' || !did) continue;
|
|
139
|
+
const entries = delegationsEntriesForDevice(delegations, did, ownerDid);
|
|
140
|
+
next[did] = entries.length;
|
|
141
|
+
if (entries.length > 0) {
|
|
142
|
+
tips[did] = await formatDelegationsTooltipSummary(
|
|
143
|
+
/** @type {Array<{ delegation?: string, space_did?: string, label?: string }>} */ (
|
|
144
|
+
entries
|
|
145
|
+
)
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
ucanCountsByDid = next;
|
|
150
|
+
ucanTooltipByDid = tips;
|
|
151
|
+
} catch {
|
|
152
|
+
ucanCountsByDid = {};
|
|
153
|
+
ucanTooltipByDid = {};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function confirmRemoveLinkedDevice(/** @type {Record<string, unknown>} */ device) {
|
|
158
|
+
const label = String(device.device_label || device.ed25519_did || 'this device');
|
|
159
|
+
const credId = device.credential_id;
|
|
160
|
+
if (typeof credId !== 'string' || !credId) {
|
|
161
|
+
showMessage('Cannot remove device: missing credential id.', 'error');
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (
|
|
165
|
+
!confirm(
|
|
166
|
+
`Remove linked device "${label}" from the registry? Its OrbitDB write access will be revoked.`
|
|
167
|
+
)
|
|
168
|
+
) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const db = deviceManager?.getRegistryDb?.() ?? registryDb;
|
|
172
|
+
if (!db) {
|
|
173
|
+
showMessage('Cannot remove device: registry not ready.', 'error');
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
try {
|
|
177
|
+
await removeDeviceEntry(db, credId);
|
|
178
|
+
devices = devices.filter((d) => d.ed25519_did !== device.ed25519_did);
|
|
179
|
+
await refreshLinkedDeviceDelegationCounts();
|
|
180
|
+
showMessage('Device removed from linked devices.');
|
|
181
|
+
} catch (err) {
|
|
182
|
+
showMessage(`Failed to remove device: ${err?.message || err}`, 'error');
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
90
186
|
// Component state
|
|
91
187
|
let showStoracha = $state(true);
|
|
92
188
|
let isLoading = $state(false);
|
|
93
|
-
let status = $state('');
|
|
94
189
|
let error = $state(null);
|
|
95
190
|
let success = $state(null);
|
|
96
191
|
|
|
@@ -116,7 +211,8 @@
|
|
|
116
211
|
let signingMode = $state(null); // { mode, did, algorithm, secure }
|
|
117
212
|
let delegationText = $state(''); // textarea for pasting delegation
|
|
118
213
|
let isAuthenticating = $state(false);
|
|
119
|
-
|
|
214
|
+
/** Shown when creating a new passkey; sent as WebAuthn user.id / name / displayName (worker path). */
|
|
215
|
+
let passkeyUserLabel = $state('');
|
|
120
216
|
let spaceUsage = $state(null);
|
|
121
217
|
|
|
122
218
|
// Registry DB state
|
|
@@ -226,6 +322,15 @@
|
|
|
226
322
|
};
|
|
227
323
|
});
|
|
228
324
|
|
|
325
|
+
/** Per-device UCAN delegation counts for linked-device badges. */
|
|
326
|
+
$effect(() => {
|
|
327
|
+
registryDb;
|
|
328
|
+
devices;
|
|
329
|
+
signingMode?.did;
|
|
330
|
+
isLoggedIn;
|
|
331
|
+
void refreshLinkedDeviceDelegationCounts();
|
|
332
|
+
});
|
|
333
|
+
|
|
229
334
|
/** Start MultiDeviceManager once libp2p + registry exist (handles restored signingMode / late orbitdb). */
|
|
230
335
|
$effect(() => {
|
|
231
336
|
if (!signingMode?.did || !orbitdb || !libp2p || !registryDb || deviceManager) return;
|
|
@@ -299,16 +404,13 @@
|
|
|
299
404
|
}, 5000);
|
|
300
405
|
}
|
|
301
406
|
|
|
302
|
-
function clearForms() {
|
|
303
|
-
delegationText = '';
|
|
304
|
-
}
|
|
305
|
-
|
|
306
407
|
async function handleAuthenticate() {
|
|
307
408
|
isAuthenticating = true;
|
|
308
409
|
try {
|
|
309
410
|
signingMode = await identityService.initialize(undefined, {
|
|
310
411
|
preferWorkerMode,
|
|
311
412
|
signingPreference: signingPreferenceOverride ?? selectedSigningPreference,
|
|
413
|
+
...(localPasskeyDetected ? {} : { webauthnUserLabel: passkeyUserLabel }),
|
|
312
414
|
});
|
|
313
415
|
showMessage(`Authenticated! Mode: ${signingMode.algorithm} (${signingMode.mode})`);
|
|
314
416
|
|
|
@@ -348,7 +450,12 @@
|
|
|
348
450
|
|
|
349
451
|
if (store) {
|
|
350
452
|
const spaceDid = storeSpaceDid || client.currentSpace()?.did?.() || '';
|
|
351
|
-
await storeDelegation(
|
|
453
|
+
await storeDelegation(
|
|
454
|
+
delegationStr,
|
|
455
|
+
storeRegistryDb,
|
|
456
|
+
spaceDid,
|
|
457
|
+
signingMode?.did || ''
|
|
458
|
+
);
|
|
352
459
|
}
|
|
353
460
|
|
|
354
461
|
currentSpace = client.currentSpace();
|
|
@@ -556,7 +663,14 @@
|
|
|
556
663
|
if (!registryDb || !signingMode?.did) return;
|
|
557
664
|
try {
|
|
558
665
|
const existing = await getDeviceByDID(registryDb, signingMode.did);
|
|
559
|
-
|
|
666
|
+
const kind = passkeyKindFromSigningMode(signingMode);
|
|
667
|
+
if (existing) {
|
|
668
|
+
if (kind && !existing.passkey_kind) {
|
|
669
|
+
const k = await hashCredentialId(existing.credential_id);
|
|
670
|
+
await registryDb.put(k, { ...existing, passkey_kind: kind });
|
|
671
|
+
}
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
560
674
|
const credential = loadWebAuthnCredentialSafe();
|
|
561
675
|
await registerDevice(registryDb, {
|
|
562
676
|
credential_id:
|
|
@@ -572,6 +686,7 @@
|
|
|
572
686
|
created_at: Date.now(),
|
|
573
687
|
status: 'active',
|
|
574
688
|
ed25519_did: signingMode.did,
|
|
689
|
+
passkey_kind: kind,
|
|
575
690
|
});
|
|
576
691
|
console.log('[ui] Self-registered device in registry');
|
|
577
692
|
} catch (err) {
|
|
@@ -629,7 +744,7 @@
|
|
|
629
744
|
credential,
|
|
630
745
|
orbitdb,
|
|
631
746
|
libp2p,
|
|
632
|
-
identity: { id: signingMode.did },
|
|
747
|
+
identity: { id: signingMode.did, passkeyKind: passkeyKindFromSigningMode(signingMode) },
|
|
633
748
|
onPairingRequest: async (request) => {
|
|
634
749
|
pairingFlow(
|
|
635
750
|
'ALICE',
|
|
@@ -737,7 +852,13 @@
|
|
|
737
852
|
await storeArchiveEntry(newDb, did, archive.ciphertext, archive.iv);
|
|
738
853
|
}
|
|
739
854
|
for (const d of delegations) {
|
|
740
|
-
await storeDelegationEntry(
|
|
855
|
+
await storeDelegationEntry(
|
|
856
|
+
newDb,
|
|
857
|
+
d.delegation,
|
|
858
|
+
d.space_did,
|
|
859
|
+
d.label,
|
|
860
|
+
d.stored_by_did
|
|
861
|
+
);
|
|
741
862
|
}
|
|
742
863
|
console.log('[ui] Registry migration complete after', Date.now() - start, 'ms');
|
|
743
864
|
return true;
|
|
@@ -890,7 +1011,6 @@
|
|
|
890
1011
|
isLoggedIn = false;
|
|
891
1012
|
client = null;
|
|
892
1013
|
currentSpace = null;
|
|
893
|
-
spaces = [];
|
|
894
1014
|
spaceUsage = null;
|
|
895
1015
|
signingMode = null;
|
|
896
1016
|
await clearStoredDelegation(registryDb);
|
|
@@ -912,21 +1032,6 @@
|
|
|
912
1032
|
}
|
|
913
1033
|
}
|
|
914
1034
|
|
|
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
1035
|
async function handleBackup() {
|
|
931
1036
|
if (!bridge) {
|
|
932
1037
|
showMessage('Please log in first', 'error');
|
|
@@ -943,7 +1048,6 @@
|
|
|
943
1048
|
|
|
944
1049
|
isLoading = true;
|
|
945
1050
|
resetProgress();
|
|
946
|
-
status = 'Preparing backup...';
|
|
947
1051
|
|
|
948
1052
|
try {
|
|
949
1053
|
// Backup user database if available
|
|
@@ -979,7 +1083,6 @@
|
|
|
979
1083
|
showMessage(`Backup failed: ${err.message}`, 'error');
|
|
980
1084
|
} finally {
|
|
981
1085
|
isLoading = false;
|
|
982
|
-
status = '';
|
|
983
1086
|
resetProgress();
|
|
984
1087
|
}
|
|
985
1088
|
}
|
|
@@ -992,12 +1095,10 @@
|
|
|
992
1095
|
|
|
993
1096
|
isLoading = true;
|
|
994
1097
|
resetProgress();
|
|
995
|
-
status = 'Preparing restore...';
|
|
996
1098
|
|
|
997
1099
|
try {
|
|
998
1100
|
// Close existing database if provided
|
|
999
1101
|
if (database) {
|
|
1000
|
-
status = 'Closing existing database...';
|
|
1001
1102
|
try {
|
|
1002
1103
|
await database.close();
|
|
1003
1104
|
} catch {
|
|
@@ -1005,8 +1106,6 @@
|
|
|
1005
1106
|
}
|
|
1006
1107
|
}
|
|
1007
1108
|
|
|
1008
|
-
status = 'Starting restore...';
|
|
1009
|
-
|
|
1010
1109
|
if (!bridge) {
|
|
1011
1110
|
throw new Error('Bridge not initialized. Please connect to Storacha first.');
|
|
1012
1111
|
}
|
|
@@ -1028,7 +1127,6 @@
|
|
|
1028
1127
|
showMessage(`Restore failed: ${err.message}`, 'error');
|
|
1029
1128
|
} finally {
|
|
1030
1129
|
isLoading = false;
|
|
1031
|
-
status = '';
|
|
1032
1130
|
resetProgress();
|
|
1033
1131
|
}
|
|
1034
1132
|
}
|
|
@@ -1158,11 +1256,12 @@
|
|
|
1158
1256
|
</div>
|
|
1159
1257
|
{:else}
|
|
1160
1258
|
<div style="display: flex; flex-direction: column; gap: 0.375rem;">
|
|
1161
|
-
{#each devices as device}
|
|
1259
|
+
{#each devices as device, i (device.credential_id || device.ed25519_did || device.device_label || i)}
|
|
1260
|
+
{@const passkeyBadge = linkedDevicePasskeyLabel(device)}
|
|
1162
1261
|
<div
|
|
1163
1262
|
data-testid="storacha-linked-device-row"
|
|
1164
1263
|
data-device-label={device.device_label || ''}
|
|
1165
|
-
style="display: flex; align-items:
|
|
1264
|
+
style="display: flex; align-items: flex-start; gap: 0.625rem; padding: 0.5rem; border-radius: 0.375rem; background: rgba(255, 255, 255, 0.7); border-left: 3px solid {device.status ===
|
|
1166
1265
|
'active'
|
|
1167
1266
|
? '#10b981'
|
|
1168
1267
|
: '#E91315'};"
|
|
@@ -1181,17 +1280,53 @@
|
|
|
1181
1280
|
? device.ed25519_did.slice(0, 16) + '...' + device.ed25519_did.slice(-8)
|
|
1182
1281
|
: 'N/A'}
|
|
1183
1282
|
</code>
|
|
1283
|
+
<div
|
|
1284
|
+
style="display: flex; flex-wrap: wrap; gap: 0.25rem; margin-top: 0.35rem; align-items: center;"
|
|
1285
|
+
>
|
|
1286
|
+
{#if passkeyBadge}
|
|
1287
|
+
<span
|
|
1288
|
+
style="font-size: 0.55rem; font-weight: 600; padding: 0.1rem 0.35rem; border-radius: 9999px; background: #e0e7ff; color: #3730a3; font-family: 'DM Mono', monospace;"
|
|
1289
|
+
title="Passkey signing mode for this device"
|
|
1290
|
+
>
|
|
1291
|
+
{passkeyBadge}
|
|
1292
|
+
</span>
|
|
1293
|
+
{/if}
|
|
1294
|
+
{#if device.ed25519_did && (ucanCountsByDid[device.ed25519_did] ?? 0) > 0}
|
|
1295
|
+
<span
|
|
1296
|
+
style="font-size: 0.55rem; font-weight: 600; padding: 0.1rem 0.35rem; border-radius: 9999px; background: #fef3c7; color: #92400e; font-family: 'DM Sans', sans-serif;"
|
|
1297
|
+
title={ucanTooltipByDid[device.ed25519_did]?.trim()
|
|
1298
|
+
? ucanTooltipByDid[device.ed25519_did]
|
|
1299
|
+
: 'UCAN delegations on this device (parsing summary…)'}
|
|
1300
|
+
>
|
|
1301
|
+
{ucanCountsByDid[device.ed25519_did]}
|
|
1302
|
+
{ucanCountsByDid[device.ed25519_did] === 1 ? ' UCAN' : ' UCANs'}
|
|
1303
|
+
</span>
|
|
1304
|
+
{/if}
|
|
1305
|
+
</div>
|
|
1184
1306
|
</div>
|
|
1185
|
-
<
|
|
1186
|
-
style="
|
|
1187
|
-
'active'
|
|
1188
|
-
? '#dcfce7'
|
|
1189
|
-
: '#fee2e2'}; color: {device.status === 'active'
|
|
1190
|
-
? '#166534'
|
|
1191
|
-
: '#991b1b'}; font-family: 'DM Sans', sans-serif;"
|
|
1307
|
+
<div
|
|
1308
|
+
style="display: flex; flex-direction: column; align-items: flex-end; gap: 0.25rem; flex-shrink: 0;"
|
|
1192
1309
|
>
|
|
1193
|
-
|
|
1194
|
-
|
|
1310
|
+
<span
|
|
1311
|
+
style="font-size: 0.6rem; font-weight: 600; padding: 0.125rem 0.375rem; border-radius: 9999px; background: {device.status ===
|
|
1312
|
+
'active'
|
|
1313
|
+
? '#dcfce7'
|
|
1314
|
+
: '#fee2e2'}; color: {device.status === 'active'
|
|
1315
|
+
? '#166534'
|
|
1316
|
+
: '#991b1b'}; font-family: 'DM Sans', sans-serif;"
|
|
1317
|
+
>
|
|
1318
|
+
{device.status}
|
|
1319
|
+
</span>
|
|
1320
|
+
<button
|
|
1321
|
+
type="button"
|
|
1322
|
+
data-testid="storacha-linked-device-remove"
|
|
1323
|
+
aria-label="Remove linked device"
|
|
1324
|
+
onclick={() => confirmRemoveLinkedDevice(device)}
|
|
1325
|
+
style="font-size: 0.6rem; font-weight: 600; padding: 0.15rem 0.4rem; border-radius: 0.25rem; background: transparent; color: #b91c1c; border: 1px solid #fca5a5; cursor: pointer; font-family: 'DM Sans', sans-serif;"
|
|
1326
|
+
>
|
|
1327
|
+
Remove
|
|
1328
|
+
</button>
|
|
1329
|
+
</div>
|
|
1195
1330
|
</div>
|
|
1196
1331
|
{/each}
|
|
1197
1332
|
</div>
|
|
@@ -1516,6 +1651,33 @@
|
|
|
1516
1651
|
</div>
|
|
1517
1652
|
</fieldset>
|
|
1518
1653
|
|
|
1654
|
+
{#if !localPasskeyDetected}
|
|
1655
|
+
<label
|
|
1656
|
+
style="display: flex; width: 100%; max-width: 22rem; flex-direction: column; align-items: stretch; gap: 0.35rem; text-align: left; box-sizing: border-box;"
|
|
1657
|
+
>
|
|
1658
|
+
<span
|
|
1659
|
+
style="font-size: 0.7rem; font-weight: 600; color: #374151; font-family: 'Epilogue', sans-serif;"
|
|
1660
|
+
>
|
|
1661
|
+
Passkey name (WebAuthn user ID)
|
|
1662
|
+
</span>
|
|
1663
|
+
<input
|
|
1664
|
+
type="text"
|
|
1665
|
+
data-testid="storacha-passkey-user-label"
|
|
1666
|
+
bind:value={passkeyUserLabel}
|
|
1667
|
+
disabled={isAuthenticating || signingPreferenceOverride != null}
|
|
1668
|
+
placeholder="e.g. Work laptop"
|
|
1669
|
+
autocomplete="username"
|
|
1670
|
+
style="width: 100%; box-sizing: border-box; border-radius: 0.375rem; border: 1px solid rgba(233, 19, 21, 0.25); padding: 0.5rem 0.625rem; font-size: 0.8rem; font-family: 'DM Sans', sans-serif; color: #111827; background: rgba(255, 255, 255, 0.9);"
|
|
1671
|
+
/>
|
|
1672
|
+
<span
|
|
1673
|
+
style="font-size: 0.65rem; color: #6b7280; font-family: 'DM Sans', sans-serif; line-height: 1.35;"
|
|
1674
|
+
>
|
|
1675
|
+
Optional. Used for user.id (and display name) when creating a new passkey. Leave blank for an
|
|
1676
|
+
anonymous default.
|
|
1677
|
+
</span>
|
|
1678
|
+
</label>
|
|
1679
|
+
{/if}
|
|
1680
|
+
|
|
1519
1681
|
<button
|
|
1520
1682
|
data-testid="storacha-passkey-primary"
|
|
1521
1683
|
class="storacha-btn-primary"
|
|
@@ -1548,6 +1710,7 @@
|
|
|
1548
1710
|
|
|
1549
1711
|
<!-- Recover from backup (IPNS / manifest) -->
|
|
1550
1712
|
<button
|
|
1713
|
+
data-testid="storacha-recover-passkey"
|
|
1551
1714
|
onclick={handleRecover}
|
|
1552
1715
|
disabled={isRecovering}
|
|
1553
1716
|
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
|
|
@@ -1714,6 +1877,7 @@
|
|
|
1714
1877
|
<div style="display: flex; flex-direction: column; gap: 0.75rem;">
|
|
1715
1878
|
<textarea
|
|
1716
1879
|
class="storacha-textarea"
|
|
1880
|
+
data-testid="storacha-delegation-textarea"
|
|
1717
1881
|
bind:value={delegationText}
|
|
1718
1882
|
placeholder="Paste your UCAN delegation here (base64 encoded)..."
|
|
1719
1883
|
rows="4"
|
|
@@ -1721,6 +1885,7 @@
|
|
|
1721
1885
|
></textarea>
|
|
1722
1886
|
<button
|
|
1723
1887
|
class="storacha-btn-primary"
|
|
1888
|
+
data-testid="storacha-delegation-import"
|
|
1724
1889
|
onclick={handleImportDelegation}
|
|
1725
1890
|
disabled={isLoading || !delegationText.trim()}
|
|
1726
1891
|
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 ||
|
|
@@ -1790,6 +1955,7 @@
|
|
|
1790
1955
|
/>
|
|
1791
1956
|
{#if linkError}
|
|
1792
1957
|
<div
|
|
1958
|
+
data-testid="storacha-link-error"
|
|
1793
1959
|
style="font-size: 0.7rem; color: #dc2626; font-family: 'DM Sans', sans-serif;"
|
|
1794
1960
|
>
|
|
1795
1961
|
{linkError}
|
|
@@ -2395,6 +2561,7 @@
|
|
|
2395
2561
|
</div>
|
|
2396
2562
|
{#if linkError}
|
|
2397
2563
|
<div
|
|
2564
|
+
data-testid="storacha-link-error"
|
|
2398
2565
|
style="margin-top: 0.5rem; font-size: 0.75rem; color: #b91c1c; font-family: 'DM Sans', sans-serif;"
|
|
2399
2566
|
>
|
|
2400
2567
|
{linkError}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@le-space/p2pass",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "P2Pass — peer-to-peer passkeys, UCANs, OrbitDB registry sync, and Storacha backup (Svelte)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"scripts": {
|
|
19
19
|
"dev:example": "vite --config example/vite.config.js",
|
|
20
20
|
"build:example": "vite build --config example/vite.config.js",
|
|
21
|
+
"kill:dev": "bash scripts/kill-relay-and-vite.sh",
|
|
21
22
|
"test:e2e": "playwright test",
|
|
22
23
|
"test:e2e:ui": "playwright test --ui",
|
|
23
24
|
"test": "vitest run",
|
|
@@ -34,7 +35,27 @@
|
|
|
34
35
|
"exports": {
|
|
35
36
|
".": {
|
|
36
37
|
"types": "./dist/index.d.ts",
|
|
37
|
-
"svelte": "./dist/index.js"
|
|
38
|
+
"svelte": "./dist/index.js",
|
|
39
|
+
"browser": "./dist/index.js",
|
|
40
|
+
"import": "./dist/index.js",
|
|
41
|
+
"default": "./dist/index.js"
|
|
42
|
+
},
|
|
43
|
+
"./signing-preference": {
|
|
44
|
+
"types": "./dist/identity/signing-preference.d.ts",
|
|
45
|
+
"import": "./dist/identity/signing-preference.js",
|
|
46
|
+
"default": "./dist/identity/signing-preference.js"
|
|
47
|
+
},
|
|
48
|
+
"./ui/StorachaFab": {
|
|
49
|
+
"types": "./dist/ui/StorachaFab.svelte.d.ts",
|
|
50
|
+
"svelte": "./dist/ui/StorachaFab.svelte",
|
|
51
|
+
"import": "./dist/ui/StorachaFab.svelte",
|
|
52
|
+
"default": "./dist/ui/StorachaFab.svelte"
|
|
53
|
+
},
|
|
54
|
+
"./ui/StorachaFab.svelte": {
|
|
55
|
+
"types": "./dist/ui/StorachaFab.svelte.d.ts",
|
|
56
|
+
"svelte": "./dist/ui/StorachaFab.svelte",
|
|
57
|
+
"import": "./dist/ui/StorachaFab.svelte",
|
|
58
|
+
"default": "./dist/ui/StorachaFab.svelte"
|
|
38
59
|
}
|
|
39
60
|
},
|
|
40
61
|
"files": [
|