@le-space/p2pass 0.1.0 → 0.2.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.
|
@@ -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: [
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
writeSigningPreferenceToStorage,
|
|
6
6
|
} from '../identity/signing-preference.js';
|
|
7
7
|
import { Upload, LogOut, Loader2, AlertCircle, CheckCircle, Download } from 'lucide-svelte';
|
|
8
|
-
import {
|
|
8
|
+
import { getSpaceUsage } from './storacha-backup.js';
|
|
9
9
|
import { OrbitDBStorachaBridge } from 'orbitdb-storacha-bridge';
|
|
10
10
|
import { IdentityService, hasLocalPasskeyHint } from '../identity/identity-service.js';
|
|
11
11
|
import { getStoredSigningMode } from '../identity/mode-detector.js';
|
|
@@ -28,7 +28,6 @@
|
|
|
28
28
|
listDelegations,
|
|
29
29
|
storeDelegationEntry,
|
|
30
30
|
} from '../registry/device-registry.js';
|
|
31
|
-
import { deriveIPNSKeyPair } from '../recovery/ipns-key.js';
|
|
32
31
|
import {
|
|
33
32
|
createManifest,
|
|
34
33
|
publishManifest,
|
|
@@ -36,7 +35,7 @@
|
|
|
36
35
|
uploadArchiveToIPFS,
|
|
37
36
|
fetchArchiveFromIPFS,
|
|
38
37
|
} from '../recovery/manifest.js';
|
|
39
|
-
import { backupRegistryDb
|
|
38
|
+
import { backupRegistryDb } from '../backup/registry-backup.js';
|
|
40
39
|
import { MultiDeviceManager } from '../registry/manager.js';
|
|
41
40
|
import { detectDeviceLabel, pairingFlow } from '../registry/pairing-protocol.js';
|
|
42
41
|
import { loadWebAuthnCredentialSafe } from '@le-space/orbitdb-identity-provider-webauthn-did/standalone';
|
|
@@ -90,7 +89,6 @@
|
|
|
90
89
|
// Component state
|
|
91
90
|
let showStoracha = $state(true);
|
|
92
91
|
let isLoading = $state(false);
|
|
93
|
-
let status = $state('');
|
|
94
92
|
let error = $state(null);
|
|
95
93
|
let success = $state(null);
|
|
96
94
|
|
|
@@ -116,7 +114,8 @@
|
|
|
116
114
|
let signingMode = $state(null); // { mode, did, algorithm, secure }
|
|
117
115
|
let delegationText = $state(''); // textarea for pasting delegation
|
|
118
116
|
let isAuthenticating = $state(false);
|
|
119
|
-
|
|
117
|
+
/** Shown when creating a new passkey; sent as WebAuthn user.id / name / displayName (worker path). */
|
|
118
|
+
let passkeyUserLabel = $state('');
|
|
120
119
|
let spaceUsage = $state(null);
|
|
121
120
|
|
|
122
121
|
// Registry DB state
|
|
@@ -299,16 +298,13 @@
|
|
|
299
298
|
}, 5000);
|
|
300
299
|
}
|
|
301
300
|
|
|
302
|
-
function clearForms() {
|
|
303
|
-
delegationText = '';
|
|
304
|
-
}
|
|
305
|
-
|
|
306
301
|
async function handleAuthenticate() {
|
|
307
302
|
isAuthenticating = true;
|
|
308
303
|
try {
|
|
309
304
|
signingMode = await identityService.initialize(undefined, {
|
|
310
305
|
preferWorkerMode,
|
|
311
306
|
signingPreference: signingPreferenceOverride ?? selectedSigningPreference,
|
|
307
|
+
...(localPasskeyDetected ? {} : { webauthnUserLabel: passkeyUserLabel }),
|
|
312
308
|
});
|
|
313
309
|
showMessage(`Authenticated! Mode: ${signingMode.algorithm} (${signingMode.mode})`);
|
|
314
310
|
|
|
@@ -890,7 +886,6 @@
|
|
|
890
886
|
isLoggedIn = false;
|
|
891
887
|
client = null;
|
|
892
888
|
currentSpace = null;
|
|
893
|
-
spaces = [];
|
|
894
889
|
spaceUsage = null;
|
|
895
890
|
signingMode = null;
|
|
896
891
|
await clearStoredDelegation(registryDb);
|
|
@@ -912,21 +907,6 @@
|
|
|
912
907
|
}
|
|
913
908
|
}
|
|
914
909
|
|
|
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
910
|
async function handleBackup() {
|
|
931
911
|
if (!bridge) {
|
|
932
912
|
showMessage('Please log in first', 'error');
|
|
@@ -943,7 +923,6 @@
|
|
|
943
923
|
|
|
944
924
|
isLoading = true;
|
|
945
925
|
resetProgress();
|
|
946
|
-
status = 'Preparing backup...';
|
|
947
926
|
|
|
948
927
|
try {
|
|
949
928
|
// Backup user database if available
|
|
@@ -979,7 +958,6 @@
|
|
|
979
958
|
showMessage(`Backup failed: ${err.message}`, 'error');
|
|
980
959
|
} finally {
|
|
981
960
|
isLoading = false;
|
|
982
|
-
status = '';
|
|
983
961
|
resetProgress();
|
|
984
962
|
}
|
|
985
963
|
}
|
|
@@ -992,12 +970,10 @@
|
|
|
992
970
|
|
|
993
971
|
isLoading = true;
|
|
994
972
|
resetProgress();
|
|
995
|
-
status = 'Preparing restore...';
|
|
996
973
|
|
|
997
974
|
try {
|
|
998
975
|
// Close existing database if provided
|
|
999
976
|
if (database) {
|
|
1000
|
-
status = 'Closing existing database...';
|
|
1001
977
|
try {
|
|
1002
978
|
await database.close();
|
|
1003
979
|
} catch {
|
|
@@ -1005,8 +981,6 @@
|
|
|
1005
981
|
}
|
|
1006
982
|
}
|
|
1007
983
|
|
|
1008
|
-
status = 'Starting restore...';
|
|
1009
|
-
|
|
1010
984
|
if (!bridge) {
|
|
1011
985
|
throw new Error('Bridge not initialized. Please connect to Storacha first.');
|
|
1012
986
|
}
|
|
@@ -1028,7 +1002,6 @@
|
|
|
1028
1002
|
showMessage(`Restore failed: ${err.message}`, 'error');
|
|
1029
1003
|
} finally {
|
|
1030
1004
|
isLoading = false;
|
|
1031
|
-
status = '';
|
|
1032
1005
|
resetProgress();
|
|
1033
1006
|
}
|
|
1034
1007
|
}
|
|
@@ -1158,7 +1131,7 @@
|
|
|
1158
1131
|
</div>
|
|
1159
1132
|
{:else}
|
|
1160
1133
|
<div style="display: flex; flex-direction: column; gap: 0.375rem;">
|
|
1161
|
-
{#each devices as device}
|
|
1134
|
+
{#each devices as device, i (device.credential_id || device.ed25519_did || device.device_label || i)}
|
|
1162
1135
|
<div
|
|
1163
1136
|
data-testid="storacha-linked-device-row"
|
|
1164
1137
|
data-device-label={device.device_label || ''}
|
|
@@ -1516,6 +1489,33 @@
|
|
|
1516
1489
|
</div>
|
|
1517
1490
|
</fieldset>
|
|
1518
1491
|
|
|
1492
|
+
{#if !localPasskeyDetected}
|
|
1493
|
+
<label
|
|
1494
|
+
style="display: flex; width: 100%; max-width: 22rem; flex-direction: column; align-items: stretch; gap: 0.35rem; text-align: left; box-sizing: border-box;"
|
|
1495
|
+
>
|
|
1496
|
+
<span
|
|
1497
|
+
style="font-size: 0.7rem; font-weight: 600; color: #374151; font-family: 'Epilogue', sans-serif;"
|
|
1498
|
+
>
|
|
1499
|
+
Passkey name (WebAuthn user ID)
|
|
1500
|
+
</span>
|
|
1501
|
+
<input
|
|
1502
|
+
type="text"
|
|
1503
|
+
data-testid="storacha-passkey-user-label"
|
|
1504
|
+
bind:value={passkeyUserLabel}
|
|
1505
|
+
disabled={isAuthenticating || signingPreferenceOverride != null}
|
|
1506
|
+
placeholder="e.g. Work laptop"
|
|
1507
|
+
autocomplete="username"
|
|
1508
|
+
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);"
|
|
1509
|
+
/>
|
|
1510
|
+
<span
|
|
1511
|
+
style="font-size: 0.65rem; color: #6b7280; font-family: 'DM Sans', sans-serif; line-height: 1.35;"
|
|
1512
|
+
>
|
|
1513
|
+
Optional. Used for user.id (and display name) when creating a new passkey. Leave blank for an
|
|
1514
|
+
anonymous default.
|
|
1515
|
+
</span>
|
|
1516
|
+
</label>
|
|
1517
|
+
{/if}
|
|
1518
|
+
|
|
1519
1519
|
<button
|
|
1520
1520
|
data-testid="storacha-passkey-primary"
|
|
1521
1521
|
class="storacha-btn-primary"
|