ns-auth-sdk 1.2.6 → 1.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/README.md +33 -16
- package/dist/index.cjs +1397 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.css +115 -16
- package/dist/index.css.map +1 -1
- package/dist/index.d.cts +289 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +232 -157
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +1342 -1206
- package/dist/index.mjs.map +1 -1
- package/dist/utils-BR2TqhEA.cjs +3 -0
- package/dist/utils-BVvtCpMQ.mjs +3 -0
- package/dist/utils-CV_lg5Ir.mjs +492 -0
- package/dist/utils-CV_lg5Ir.mjs.map +1 -0
- package/dist/utils-CxLeFzNn.cjs +528 -0
- package/dist/utils-CxLeFzNn.cjs.map +1 -0
- package/package.json +18 -17
- package/dist/index.d.ts +0 -214
- package/dist/index.js +0 -1234
- package/dist/index.js.map +0 -1
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1397 @@
|
|
|
1
|
+
//#region rolldown:runtime
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
10
|
+
for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
11
|
+
key = keys[i];
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except) {
|
|
13
|
+
__defProp(to, key, {
|
|
14
|
+
get: ((k) => from[k]).bind(null, key),
|
|
15
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return to;
|
|
21
|
+
};
|
|
22
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
23
|
+
value: mod,
|
|
24
|
+
enumerable: true
|
|
25
|
+
}) : target, mod));
|
|
26
|
+
|
|
27
|
+
//#endregion
|
|
28
|
+
const require_utils = require('./utils-CxLeFzNn.cjs');
|
|
29
|
+
let react = require("react");
|
|
30
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
31
|
+
let isomorphic_dompurify = require("isomorphic-dompurify");
|
|
32
|
+
isomorphic_dompurify = __toESM(isomorphic_dompurify);
|
|
33
|
+
let react_zxing = require("react-zxing");
|
|
34
|
+
let zustand = require("zustand");
|
|
35
|
+
|
|
36
|
+
//#region src/services/auth.service.ts
|
|
37
|
+
/**
|
|
38
|
+
* Service wrapper around NosskeyManager
|
|
39
|
+
* Handles WebAuthn/Passkey integration with Nostr
|
|
40
|
+
*/
|
|
41
|
+
var AuthService = class {
|
|
42
|
+
constructor(config = {}) {
|
|
43
|
+
this.manager = null;
|
|
44
|
+
this.config = {
|
|
45
|
+
rpId: config.rpId || (typeof window !== "undefined" ? window.location.hostname.replace(/^www\./, "") : "localhost"),
|
|
46
|
+
rpName: config.rpName || this.getDefaultRpName(),
|
|
47
|
+
storageKey: config.storageKey || "nsauth_keyinfo",
|
|
48
|
+
cacheTimeoutMs: config.cacheTimeoutMs || 1800 * 1e3
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
getDefaultRpName() {
|
|
52
|
+
if (typeof window === "undefined") return "localhost";
|
|
53
|
+
const hostname = window.location.hostname;
|
|
54
|
+
if (hostname.includes("nosskey.app")) return "nosskey.app";
|
|
55
|
+
return hostname.replace(/^www\./, "");
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Initialize the NosskeyManager instance
|
|
59
|
+
*/
|
|
60
|
+
getManager() {
|
|
61
|
+
if (!this.manager) this.manager = new require_utils.NosskeyManager({
|
|
62
|
+
cacheOptions: {
|
|
63
|
+
enabled: true,
|
|
64
|
+
timeoutMs: this.config.cacheTimeoutMs
|
|
65
|
+
},
|
|
66
|
+
storageOptions: {
|
|
67
|
+
enabled: true,
|
|
68
|
+
storageKey: this.config.storageKey
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
return this.manager;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Create a new passkey
|
|
75
|
+
* Uses platform authenticator only (Touch ID, Face ID, Windows Hello)
|
|
76
|
+
*/
|
|
77
|
+
async createPasskey(username) {
|
|
78
|
+
const manager = this.getManager();
|
|
79
|
+
const rpId = this.config.rpId === "localhost" ? "localhost" : this.config.rpId;
|
|
80
|
+
const rpName = this.config.rpName;
|
|
81
|
+
const trimmedUsername = username?.trim();
|
|
82
|
+
const uniqueUsername = trimmedUsername ? trimmedUsername : `user-${Date.now()}@example.com`;
|
|
83
|
+
const options = {
|
|
84
|
+
rp: {
|
|
85
|
+
id: rpId,
|
|
86
|
+
name: rpName
|
|
87
|
+
},
|
|
88
|
+
user: {
|
|
89
|
+
name: uniqueUsername,
|
|
90
|
+
displayName: trimmedUsername || "User"
|
|
91
|
+
},
|
|
92
|
+
authenticatorSelection: {
|
|
93
|
+
authenticatorAttachment: "platform",
|
|
94
|
+
residentKey: "preferred",
|
|
95
|
+
userVerification: "preferred"
|
|
96
|
+
},
|
|
97
|
+
extensions: { prf: {} }
|
|
98
|
+
};
|
|
99
|
+
return await manager.createPasskey(options);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Create a new Nostr key from a credential ID
|
|
103
|
+
*/
|
|
104
|
+
async createNostrKey(credentialId) {
|
|
105
|
+
return await this.getManager().createNostrKey(credentialId);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Get the current public key
|
|
109
|
+
*/
|
|
110
|
+
async getPublicKey() {
|
|
111
|
+
return await this.getManager().getPublicKey();
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Sign a Nostr event
|
|
115
|
+
*/
|
|
116
|
+
async signEvent(event) {
|
|
117
|
+
return await this.getManager().signEvent(event);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Get current key info
|
|
121
|
+
*/
|
|
122
|
+
getCurrentKeyInfo() {
|
|
123
|
+
return this.getManager().getCurrentKeyInfo();
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Set current key info
|
|
127
|
+
*/
|
|
128
|
+
setCurrentKeyInfo(keyInfo) {
|
|
129
|
+
this.getManager().setCurrentKeyInfo(keyInfo);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Check if key info exists
|
|
133
|
+
*/
|
|
134
|
+
hasKeyInfo() {
|
|
135
|
+
return this.getManager().hasKeyInfo();
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Clear stored key info
|
|
139
|
+
*/
|
|
140
|
+
clearStoredKeyInfo() {
|
|
141
|
+
this.getManager().clearStoredKeyInfo();
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Check if PRF is supported
|
|
145
|
+
*/
|
|
146
|
+
async isPrfSupported() {
|
|
147
|
+
const { isPrfSupported } = await Promise.resolve().then(() => require("./utils-BR2TqhEA.cjs"));
|
|
148
|
+
return await isPrfSupported();
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
//#endregion
|
|
153
|
+
//#region src/utils/validation.ts
|
|
154
|
+
const MAX_ROLE_LENGTH = 100;
|
|
155
|
+
const ROLE_PATTERN = /^[a-zA-Z0-9\s\-_]+$/;
|
|
156
|
+
const isValidRoleTag = (role) => {
|
|
157
|
+
const trimmed = role.trim();
|
|
158
|
+
return trimmed.length > 0 && trimmed.length <= MAX_ROLE_LENGTH && ROLE_PATTERN.test(trimmed);
|
|
159
|
+
};
|
|
160
|
+
const isValidPubkey = (pubkey) => pubkey.length === 64 && /^[0-9a-fA-F]+$/.test(pubkey);
|
|
161
|
+
const isValidHttpUrl = (url) => {
|
|
162
|
+
try {
|
|
163
|
+
const parsed = new URL(url);
|
|
164
|
+
return ["http:", "https:"].includes(parsed.protocol);
|
|
165
|
+
} catch {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
const isValidRelayUrl = (url) => {
|
|
170
|
+
try {
|
|
171
|
+
const parsed = new URL(url);
|
|
172
|
+
return ["ws:", "wss:"].includes(parsed.protocol) && parsed.hostname.length > 0;
|
|
173
|
+
} catch {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
//#endregion
|
|
179
|
+
//#region src/services/relay.service.ts
|
|
180
|
+
/**
|
|
181
|
+
* Service for communicating with Nostr relays using applesauce-core
|
|
182
|
+
*/
|
|
183
|
+
var RelayService = class {
|
|
184
|
+
constructor(config = {}) {
|
|
185
|
+
this.eventStore = null;
|
|
186
|
+
this.defaultRelays = ["wss://relay.damus.io"];
|
|
187
|
+
this.maxProfileContentSize = 1e4;
|
|
188
|
+
this.minProfileQueryIntervalMs = 300;
|
|
189
|
+
this.minPublishIntervalMs = 750;
|
|
190
|
+
this.lastActionAt = /* @__PURE__ */ new Map();
|
|
191
|
+
this.relayUrls = this.validateRelayUrls(config.relayUrls ?? this.defaultRelays);
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Initialize with applesauce EventStore
|
|
195
|
+
*/
|
|
196
|
+
initialize(eventStore) {
|
|
197
|
+
this.eventStore = eventStore;
|
|
198
|
+
if (eventStore && "setRelays" in eventStore && typeof eventStore.setRelays === "function") eventStore.setRelays(this.relayUrls);
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Set relay URLs
|
|
202
|
+
*/
|
|
203
|
+
setRelays(urls) {
|
|
204
|
+
this.relayUrls = this.validateRelayUrls(urls);
|
|
205
|
+
if (this.eventStore && "setRelays" in this.eventStore && typeof this.eventStore.setRelays === "function") this.eventStore.setRelays(this.relayUrls);
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Get current relay URLs
|
|
209
|
+
*/
|
|
210
|
+
getRelays() {
|
|
211
|
+
return [...this.relayUrls];
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Publish an event to relays
|
|
215
|
+
*/
|
|
216
|
+
async publishEvent(event, timeoutMs = 1e3) {
|
|
217
|
+
if (!this.eventStore) throw new Error("RelayService not initialized. Call initialize() with an EventStore instance.");
|
|
218
|
+
await this.enforceRateLimit("publish", this.minPublishIntervalMs);
|
|
219
|
+
return new Promise((resolve, reject) => {
|
|
220
|
+
if (this.relayUrls.length === 0) {
|
|
221
|
+
reject(/* @__PURE__ */ new Error("No relays configured"));
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const eventStore = this.eventStore;
|
|
225
|
+
if (!eventStore || typeof eventStore.publish !== "function") {
|
|
226
|
+
reject(/* @__PURE__ */ new Error("EventStore does not support publish method"));
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const subscription = eventStore.publish(event).subscribe({
|
|
230
|
+
next: (response) => {
|
|
231
|
+
if (response?.type === "OK") {
|
|
232
|
+
subscription.unsubscribe();
|
|
233
|
+
resolve(true);
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
error: (error) => {
|
|
237
|
+
subscription.unsubscribe();
|
|
238
|
+
reject(error);
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
setTimeout(() => {
|
|
242
|
+
subscription.unsubscribe();
|
|
243
|
+
resolve(false);
|
|
244
|
+
}, timeoutMs);
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Fetch a profile (Kind 0 event)
|
|
249
|
+
*/
|
|
250
|
+
async fetchProfile(pubkey) {
|
|
251
|
+
if (!this.eventStore) throw new Error("RelayService not initialized. Call initialize() with an EventStore instance.");
|
|
252
|
+
await this.enforceRateLimit("fetch-profile", this.minProfileQueryIntervalMs);
|
|
253
|
+
return new Promise((resolve) => {
|
|
254
|
+
const filter = {
|
|
255
|
+
kinds: [0],
|
|
256
|
+
authors: [pubkey],
|
|
257
|
+
limit: 1
|
|
258
|
+
};
|
|
259
|
+
const eventStore = this.eventStore;
|
|
260
|
+
if (!eventStore || typeof eventStore.query !== "function") {
|
|
261
|
+
resolve(null);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
const subscription = eventStore.query(filter).subscribe({
|
|
265
|
+
next: (packet) => {
|
|
266
|
+
if (packet?.event && packet.event.kind === 0) {
|
|
267
|
+
const metadata = this.parseProfileMetadata(packet.event.content);
|
|
268
|
+
if (metadata) {
|
|
269
|
+
subscription.unsubscribe();
|
|
270
|
+
resolve(metadata);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
console.error("Failed to parse profile metadata");
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
complete: () => {
|
|
277
|
+
subscription.unsubscribe();
|
|
278
|
+
resolve(null);
|
|
279
|
+
},
|
|
280
|
+
error: (error) => {
|
|
281
|
+
console.error("Error fetching profile:", error);
|
|
282
|
+
subscription.unsubscribe();
|
|
283
|
+
resolve(null);
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
setTimeout(() => {
|
|
287
|
+
subscription.unsubscribe();
|
|
288
|
+
resolve(null);
|
|
289
|
+
}, 5e3);
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Fetch role tag from profile event (Kind 0)
|
|
294
|
+
*/
|
|
295
|
+
async fetchProfileRoleTag(pubkey) {
|
|
296
|
+
if (!this.eventStore) throw new Error("RelayService not initialized. Call initialize() with an EventStore instance.");
|
|
297
|
+
await this.enforceRateLimit("fetch-role-tag", this.minProfileQueryIntervalMs);
|
|
298
|
+
return new Promise((resolve) => {
|
|
299
|
+
const filter = {
|
|
300
|
+
kinds: [0],
|
|
301
|
+
authors: [pubkey],
|
|
302
|
+
limit: 1
|
|
303
|
+
};
|
|
304
|
+
const eventStore = this.eventStore;
|
|
305
|
+
if (!eventStore || typeof eventStore.query !== "function") {
|
|
306
|
+
resolve(null);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
const subscription = eventStore.query(filter).subscribe({
|
|
310
|
+
next: (packet) => {
|
|
311
|
+
if (packet?.event && packet.event.kind === 0) {
|
|
312
|
+
const tags = packet.event.tags || [];
|
|
313
|
+
for (const tag of tags) if (tag[0] === "role" && tag[1]) {
|
|
314
|
+
const candidate = tag[1].trim();
|
|
315
|
+
if (isValidRoleTag(candidate)) {
|
|
316
|
+
subscription.unsubscribe();
|
|
317
|
+
resolve(candidate);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
subscription.unsubscribe();
|
|
322
|
+
resolve(null);
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
complete: () => {
|
|
326
|
+
subscription.unsubscribe();
|
|
327
|
+
resolve(null);
|
|
328
|
+
},
|
|
329
|
+
error: (error) => {
|
|
330
|
+
console.error("Error fetching profile role tag:", error);
|
|
331
|
+
subscription.unsubscribe();
|
|
332
|
+
resolve(null);
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
setTimeout(() => {
|
|
336
|
+
subscription.unsubscribe();
|
|
337
|
+
resolve(null);
|
|
338
|
+
}, 5e3);
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Fetch a follow list (Kind 3 event)
|
|
343
|
+
*/
|
|
344
|
+
async fetchFollowList(pubkey) {
|
|
345
|
+
if (!this.eventStore) throw new Error("RelayService not initialized. Call initialize() with an EventStore instance.");
|
|
346
|
+
await this.enforceRateLimit("fetch-follow-list", this.minProfileQueryIntervalMs);
|
|
347
|
+
return new Promise((resolve) => {
|
|
348
|
+
const filter = {
|
|
349
|
+
kinds: [3],
|
|
350
|
+
authors: [pubkey],
|
|
351
|
+
limit: 1
|
|
352
|
+
};
|
|
353
|
+
const eventStore = this.eventStore;
|
|
354
|
+
if (!eventStore || typeof eventStore.query !== "function") {
|
|
355
|
+
resolve([]);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
const subscription = eventStore.query(filter).subscribe({
|
|
359
|
+
next: (packet) => {
|
|
360
|
+
if (packet?.event && packet.event.kind === 3) {
|
|
361
|
+
const followList = [];
|
|
362
|
+
const tags = packet.event.tags || [];
|
|
363
|
+
for (const tag of tags) if (tag[0] === "p" && tag[1]) followList.push({
|
|
364
|
+
pubkey: tag[1],
|
|
365
|
+
relay: tag[2] || void 0,
|
|
366
|
+
petname: tag[3] || void 0
|
|
367
|
+
});
|
|
368
|
+
subscription.unsubscribe();
|
|
369
|
+
resolve(followList);
|
|
370
|
+
}
|
|
371
|
+
},
|
|
372
|
+
complete: () => {
|
|
373
|
+
subscription.unsubscribe();
|
|
374
|
+
resolve([]);
|
|
375
|
+
},
|
|
376
|
+
error: (error) => {
|
|
377
|
+
console.error("Error fetching follow list:", error);
|
|
378
|
+
subscription.unsubscribe();
|
|
379
|
+
resolve([]);
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
setTimeout(() => {
|
|
383
|
+
subscription.unsubscribe();
|
|
384
|
+
resolve([]);
|
|
385
|
+
}, 1e4);
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Fetch multiple profiles in batch
|
|
390
|
+
*/
|
|
391
|
+
async fetchMultipleProfiles(pubkeys) {
|
|
392
|
+
if (!this.eventStore) throw new Error("RelayService not initialized. Call initialize() with an EventStore instance.");
|
|
393
|
+
if (pubkeys.length === 0) return /* @__PURE__ */ new Map();
|
|
394
|
+
await this.enforceRateLimit("fetch-multiple-profiles", this.minProfileQueryIntervalMs);
|
|
395
|
+
return new Promise((resolve) => {
|
|
396
|
+
const profiles = /* @__PURE__ */ new Map();
|
|
397
|
+
const filter = {
|
|
398
|
+
kinds: [0],
|
|
399
|
+
authors: pubkeys
|
|
400
|
+
};
|
|
401
|
+
const eventStore = this.eventStore;
|
|
402
|
+
if (!eventStore || typeof eventStore.query !== "function") {
|
|
403
|
+
resolve(/* @__PURE__ */ new Map());
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
const subscription = eventStore.query(filter).subscribe({
|
|
407
|
+
next: (packet) => {
|
|
408
|
+
if (packet?.event && packet.event.kind === 0 && packet.event.pubkey) {
|
|
409
|
+
const metadata = this.parseProfileMetadata(packet.event.content);
|
|
410
|
+
if (metadata) profiles.set(packet.event.pubkey, metadata);
|
|
411
|
+
else console.error("Failed to parse profile metadata");
|
|
412
|
+
}
|
|
413
|
+
},
|
|
414
|
+
complete: () => {
|
|
415
|
+
subscription.unsubscribe();
|
|
416
|
+
resolve(profiles);
|
|
417
|
+
},
|
|
418
|
+
error: (error) => {
|
|
419
|
+
console.error("Error fetching profiles:", error);
|
|
420
|
+
subscription.unsubscribe();
|
|
421
|
+
resolve(profiles);
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
setTimeout(() => {
|
|
425
|
+
subscription.unsubscribe();
|
|
426
|
+
resolve(profiles);
|
|
427
|
+
}, 1e3);
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Query kind 0 events (profiles) by pubkey
|
|
432
|
+
* If pubkeys array is empty, fetches recent kind 0 events
|
|
433
|
+
*/
|
|
434
|
+
async queryProfiles(pubkeys = [], limit = 100) {
|
|
435
|
+
if (!this.eventStore) throw new Error("RelayService not initialized. Call initialize() with an EventStore instance.");
|
|
436
|
+
await this.enforceRateLimit("query-profiles", this.minProfileQueryIntervalMs);
|
|
437
|
+
return new Promise((resolve) => {
|
|
438
|
+
const profiles = /* @__PURE__ */ new Map();
|
|
439
|
+
const filter = {
|
|
440
|
+
kinds: [0],
|
|
441
|
+
limit
|
|
442
|
+
};
|
|
443
|
+
if (pubkeys.length > 0) filter.authors = pubkeys;
|
|
444
|
+
const eventStore = this.eventStore;
|
|
445
|
+
if (!eventStore || typeof eventStore.query !== "function") {
|
|
446
|
+
resolve(/* @__PURE__ */ new Map());
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
const subscription = eventStore.query(filter).subscribe({
|
|
450
|
+
next: (packet) => {
|
|
451
|
+
if (packet?.event && packet.event.kind === 0 && packet.event.pubkey) {
|
|
452
|
+
const metadata = this.parseProfileMetadata(packet.event.content);
|
|
453
|
+
if (metadata) {
|
|
454
|
+
const timestamp = packet.event.created_at || 0;
|
|
455
|
+
const existing = profiles.get(packet.event.pubkey);
|
|
456
|
+
if (!existing || timestamp > existing.timestamp) profiles.set(packet.event.pubkey, {
|
|
457
|
+
metadata,
|
|
458
|
+
timestamp
|
|
459
|
+
});
|
|
460
|
+
} else console.error("Failed to parse profile metadata");
|
|
461
|
+
}
|
|
462
|
+
},
|
|
463
|
+
complete: () => {
|
|
464
|
+
subscription.unsubscribe();
|
|
465
|
+
const result = /* @__PURE__ */ new Map();
|
|
466
|
+
profiles.forEach((value, pubkey) => {
|
|
467
|
+
result.set(pubkey, value.metadata);
|
|
468
|
+
});
|
|
469
|
+
resolve(result);
|
|
470
|
+
},
|
|
471
|
+
error: (error) => {
|
|
472
|
+
console.error("Error querying profiles:", error);
|
|
473
|
+
subscription.unsubscribe();
|
|
474
|
+
const result = /* @__PURE__ */ new Map();
|
|
475
|
+
profiles.forEach((value, pubkey) => {
|
|
476
|
+
result.set(pubkey, value.metadata);
|
|
477
|
+
});
|
|
478
|
+
resolve(result);
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
setTimeout(() => {
|
|
482
|
+
subscription.unsubscribe();
|
|
483
|
+
const result = /* @__PURE__ */ new Map();
|
|
484
|
+
profiles.forEach((value, pubkey) => {
|
|
485
|
+
result.set(pubkey, value.metadata);
|
|
486
|
+
});
|
|
487
|
+
resolve(result);
|
|
488
|
+
}, 1e4);
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Publish or update a kind 3 event (follow list/contacts)
|
|
493
|
+
*/
|
|
494
|
+
async publishFollowList(pubkey, followList, signEvent) {
|
|
495
|
+
const tags = followList.map((entry) => {
|
|
496
|
+
if (!isValidPubkey(entry.pubkey)) throw new Error("Invalid pubkey format for follow list entry.");
|
|
497
|
+
if (entry.relay && !isValidRelayUrl(entry.relay)) throw new Error("Invalid relay URL format for follow list entry.");
|
|
498
|
+
const tag = ["p", entry.pubkey];
|
|
499
|
+
if (entry.relay) tag.push(entry.relay);
|
|
500
|
+
if (entry.petname) tag.push(entry.petname);
|
|
501
|
+
return tag;
|
|
502
|
+
});
|
|
503
|
+
const signedEvent = await signEvent({
|
|
504
|
+
kind: 3,
|
|
505
|
+
content: "",
|
|
506
|
+
created_at: Math.floor(Date.now() / 1e3),
|
|
507
|
+
tags
|
|
508
|
+
});
|
|
509
|
+
return await this.publishEvent(signedEvent);
|
|
510
|
+
}
|
|
511
|
+
validateRelayUrls(urls) {
|
|
512
|
+
if (urls.length === 0) return [];
|
|
513
|
+
const validUrls = urls.filter((url) => isValidRelayUrl(url));
|
|
514
|
+
if (validUrls.length !== urls.length) throw new Error("Invalid relay URL format");
|
|
515
|
+
return validUrls;
|
|
516
|
+
}
|
|
517
|
+
parseProfileMetadata(content) {
|
|
518
|
+
if (content.length > this.maxProfileContentSize) return null;
|
|
519
|
+
try {
|
|
520
|
+
const parsed = JSON.parse(content);
|
|
521
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
522
|
+
const metadata = {};
|
|
523
|
+
if (typeof parsed.name === "string") metadata.name = parsed.name;
|
|
524
|
+
if (typeof parsed.display_name === "string") metadata.display_name = parsed.display_name;
|
|
525
|
+
if (typeof parsed.about === "string") metadata.about = parsed.about;
|
|
526
|
+
if (typeof parsed.picture === "string") metadata.picture = parsed.picture;
|
|
527
|
+
if (typeof parsed.website === "string") metadata.website = parsed.website;
|
|
528
|
+
return metadata;
|
|
529
|
+
} catch (error) {
|
|
530
|
+
console.error("Failed to parse profile metadata:", error);
|
|
531
|
+
return null;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
async enforceRateLimit(action, minIntervalMs) {
|
|
535
|
+
const lastAt = this.lastActionAt.get(action) ?? 0;
|
|
536
|
+
const waitMs = minIntervalMs - (Date.now() - lastAt);
|
|
537
|
+
if (waitMs > 0) await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
538
|
+
this.lastActionAt.set(action, Date.now());
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
//#endregion
|
|
543
|
+
//#region src/components/auth/LoginButton.tsx
|
|
544
|
+
function LoginButton({ authService, setAuthenticated, setLoginError, onSuccess }) {
|
|
545
|
+
const [isLoading, setIsLoading] = (0, react.useState)(false);
|
|
546
|
+
const handleLogin = async () => {
|
|
547
|
+
setIsLoading(true);
|
|
548
|
+
setLoginError(null);
|
|
549
|
+
try {
|
|
550
|
+
if (!authService.hasKeyInfo()) throw new Error("No account found. Please register first.");
|
|
551
|
+
const keyInfo = authService.getCurrentKeyInfo();
|
|
552
|
+
if (!keyInfo) throw new Error("Failed to load account information.");
|
|
553
|
+
await authService.getPublicKey();
|
|
554
|
+
setAuthenticated(keyInfo);
|
|
555
|
+
if (onSuccess) onSuccess();
|
|
556
|
+
} catch (err) {
|
|
557
|
+
console.error("Login error:", err);
|
|
558
|
+
setLoginError(err instanceof Error ? err.message : "Failed to login");
|
|
559
|
+
setIsLoading(false);
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
563
|
+
className: "login-button-container",
|
|
564
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
565
|
+
className: "auth-button secondary",
|
|
566
|
+
onClick: handleLogin,
|
|
567
|
+
disabled: isLoading,
|
|
568
|
+
children: isLoading ? "Logging in..." : "Login"
|
|
569
|
+
})
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
//#endregion
|
|
574
|
+
//#region src/components/auth/RegistrationFlow.tsx
|
|
575
|
+
function RegistrationFlow({ authService, setAuthenticated, onSuccess }) {
|
|
576
|
+
const [isLoading, setIsLoading] = (0, react.useState)(false);
|
|
577
|
+
const [error, setError] = (0, react.useState)(null);
|
|
578
|
+
const [step, setStep] = (0, react.useState)("info");
|
|
579
|
+
const [username, setUsername] = (0, react.useState)("");
|
|
580
|
+
const maxUsernameLength = 100;
|
|
581
|
+
const handleRegister = async () => {
|
|
582
|
+
setIsLoading(true);
|
|
583
|
+
setError(null);
|
|
584
|
+
setStep("creating");
|
|
585
|
+
try {
|
|
586
|
+
authService.clearStoredKeyInfo();
|
|
587
|
+
let credentialId;
|
|
588
|
+
const passkeyUsername = username.trim() || void 0;
|
|
589
|
+
try {
|
|
590
|
+
credentialId = await authService.createPasskey(passkeyUsername);
|
|
591
|
+
} catch (passkeyError) {
|
|
592
|
+
console.error("[RegistrationFlow] Passkey creation failed:", passkeyError);
|
|
593
|
+
throw new Error("Unable to create passkey. Please try again.");
|
|
594
|
+
}
|
|
595
|
+
let keyInfo;
|
|
596
|
+
try {
|
|
597
|
+
keyInfo = await authService.createNostrKey(credentialId);
|
|
598
|
+
} catch (nostrKeyError) {
|
|
599
|
+
console.error("[RegistrationFlow] Failed to create Nostr key from passkey:", nostrKeyError);
|
|
600
|
+
throw new Error("Unable to finalize account. Please try again.");
|
|
601
|
+
}
|
|
602
|
+
authService.setCurrentKeyInfo(keyInfo);
|
|
603
|
+
setAuthenticated(keyInfo);
|
|
604
|
+
setStep("success");
|
|
605
|
+
if (onSuccess) setTimeout(() => {
|
|
606
|
+
onSuccess();
|
|
607
|
+
}, 1500);
|
|
608
|
+
} catch (err) {
|
|
609
|
+
console.error("[RegistrationFlow] Registration error:", err);
|
|
610
|
+
setError("Registration failed. Please try again.");
|
|
611
|
+
setStep("info");
|
|
612
|
+
setIsLoading(false);
|
|
613
|
+
}
|
|
614
|
+
};
|
|
615
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
616
|
+
className: "auth-container",
|
|
617
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
618
|
+
className: "auth-card",
|
|
619
|
+
children: [
|
|
620
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h1", { children: "Create Account" }),
|
|
621
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
622
|
+
className: "auth-description",
|
|
623
|
+
children: "Create a new decentralized Identity. Your identity will be securely derived from a passkey."
|
|
624
|
+
}),
|
|
625
|
+
step === "info" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
626
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
627
|
+
className: "auth-features",
|
|
628
|
+
children: [
|
|
629
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
630
|
+
className: "feature-item",
|
|
631
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
632
|
+
className: "feature-icon",
|
|
633
|
+
children: "🔐"
|
|
634
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "Phishing-resistant authentication" })]
|
|
635
|
+
}),
|
|
636
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
637
|
+
className: "feature-item",
|
|
638
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
639
|
+
className: "feature-icon",
|
|
640
|
+
children: "📱"
|
|
641
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "Biometric authentication support" })]
|
|
642
|
+
}),
|
|
643
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
644
|
+
className: "feature-item",
|
|
645
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
646
|
+
className: "feature-icon",
|
|
647
|
+
children: "🌐"
|
|
648
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "Cross-device synchronization" })]
|
|
649
|
+
})
|
|
650
|
+
]
|
|
651
|
+
}),
|
|
652
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
653
|
+
className: "username-section",
|
|
654
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
655
|
+
htmlFor: "username",
|
|
656
|
+
className: "username-label",
|
|
657
|
+
children: "Name (Optional)"
|
|
658
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
659
|
+
id: "username",
|
|
660
|
+
type: "text",
|
|
661
|
+
className: "username-input",
|
|
662
|
+
placeholder: "Enter a name for this passkey",
|
|
663
|
+
value: username,
|
|
664
|
+
onChange: (e) => setUsername(e.target.value),
|
|
665
|
+
disabled: isLoading,
|
|
666
|
+
maxLength: maxUsernameLength
|
|
667
|
+
})]
|
|
668
|
+
}),
|
|
669
|
+
error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
670
|
+
className: "error-message",
|
|
671
|
+
children: error
|
|
672
|
+
}),
|
|
673
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
674
|
+
className: "auth-button primary",
|
|
675
|
+
onClick: handleRegister,
|
|
676
|
+
disabled: isLoading,
|
|
677
|
+
children: isLoading ? "Creating..." : "Create Account"
|
|
678
|
+
})
|
|
679
|
+
] }),
|
|
680
|
+
step === "creating" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
681
|
+
className: "loading-state",
|
|
682
|
+
children: [
|
|
683
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "spinner" }),
|
|
684
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: "Creating your passkey..." }),
|
|
685
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
686
|
+
className: "loading-hint",
|
|
687
|
+
children: "Please follow your browser's authentication prompt"
|
|
688
|
+
}),
|
|
689
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
690
|
+
className: "loading-hint-small",
|
|
691
|
+
children: "💡 Using your system's native passkey manager (Touch ID, Face ID, Windows Hello, etc.)"
|
|
692
|
+
})
|
|
693
|
+
]
|
|
694
|
+
}),
|
|
695
|
+
step === "success" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
696
|
+
className: "success-state",
|
|
697
|
+
children: [
|
|
698
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
699
|
+
className: "success-icon",
|
|
700
|
+
children: "✓"
|
|
701
|
+
}),
|
|
702
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: "Account created successfully!" }),
|
|
703
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
704
|
+
className: "success-hint",
|
|
705
|
+
children: "Redirecting to profile setup..."
|
|
706
|
+
})
|
|
707
|
+
]
|
|
708
|
+
})
|
|
709
|
+
]
|
|
710
|
+
})
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
//#endregion
|
|
715
|
+
//#region src/utils/sanitize.ts
|
|
716
|
+
const sanitizeOptions = {
|
|
717
|
+
ALLOWED_TAGS: [],
|
|
718
|
+
ALLOWED_ATTR: []
|
|
719
|
+
};
|
|
720
|
+
const sanitizeText = (value) => isomorphic_dompurify.default.sanitize(value, sanitizeOptions);
|
|
721
|
+
|
|
722
|
+
//#endregion
|
|
723
|
+
//#region src/components/membership/BarcodeScanner.tsx
|
|
724
|
+
const BarcodeScanner = ({ onDecode, active = true }) => {
|
|
725
|
+
const [error, setError] = (0, react.useState)(null);
|
|
726
|
+
const { ref } = (0, react_zxing.useZxing)({
|
|
727
|
+
onDecodeResult: (result) => {
|
|
728
|
+
onDecode(result.getText());
|
|
729
|
+
},
|
|
730
|
+
onError: (e) => {
|
|
731
|
+
setError(e instanceof Error ? e.message : "Camera error");
|
|
732
|
+
}
|
|
733
|
+
});
|
|
734
|
+
if (!active) return null;
|
|
735
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
736
|
+
style: {
|
|
737
|
+
position: "relative",
|
|
738
|
+
width: "100%",
|
|
739
|
+
maxWidth: "400px"
|
|
740
|
+
},
|
|
741
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("video", {
|
|
742
|
+
ref,
|
|
743
|
+
style: {
|
|
744
|
+
width: "100%",
|
|
745
|
+
borderRadius: "8px"
|
|
746
|
+
},
|
|
747
|
+
playsInline: true,
|
|
748
|
+
muted: true
|
|
749
|
+
}), error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
750
|
+
style: {
|
|
751
|
+
color: "red",
|
|
752
|
+
marginTop: "0.5rem"
|
|
753
|
+
},
|
|
754
|
+
children: error
|
|
755
|
+
})]
|
|
756
|
+
});
|
|
757
|
+
};
|
|
758
|
+
|
|
759
|
+
//#endregion
|
|
760
|
+
//#region src/components/membership/MembershipPage.tsx
|
|
761
|
+
const TrustInfo = () => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
762
|
+
className: "trust-info",
|
|
763
|
+
children: [
|
|
764
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", { children: "Building Trust with Verifiable Relationship Credentials" }),
|
|
765
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", { children: [
|
|
766
|
+
"Beyond the Personhood Credential, members can create ",
|
|
767
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "Verifiable Relationship Credentials (VRCs)" }),
|
|
768
|
+
". A VRC is issued directly between two members – for example, by scanning a QR‑code at a meetup – and certifies a first‑hand trust link."
|
|
769
|
+
] }),
|
|
770
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", { children: [
|
|
771
|
+
"Each VRC becomes a node in a decentralized trust graph. When you add a member, the system records a ",
|
|
772
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "role" }),
|
|
773
|
+
" tag that ties the new participant to your existing graph, enabling permissionless access to network‑state resources while keeping the underlying data private."
|
|
774
|
+
] }),
|
|
775
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: "The graph grows organically: trusted authorities issue PHCs, and members continuously enrich the network with peer‑generated VRCs, creating a resilient, scalable web of verified participants." })
|
|
776
|
+
]
|
|
777
|
+
});
|
|
778
|
+
function MembershipPage({ authService, relayService, publicKey, onUnauthenticated }) {
|
|
779
|
+
const [isLoading, setIsLoading] = (0, react.useState)(false);
|
|
780
|
+
const [isSaving, setIsSaving] = (0, react.useState)(false);
|
|
781
|
+
const [saveMessage, setSaveMessage] = (0, react.useState)(null);
|
|
782
|
+
const [searchQuery, setSearchQuery] = (0, react.useState)("");
|
|
783
|
+
const [profiles, setProfiles] = (0, react.useState)([]);
|
|
784
|
+
const [members, setMembers] = (0, react.useState)([]);
|
|
785
|
+
const [memberProfiles, setMemberProfiles] = (0, react.useState)(/* @__PURE__ */ new Map());
|
|
786
|
+
const [showScanner, setShowScanner] = (0, react.useState)(false);
|
|
787
|
+
(0, react.useEffect)(() => {
|
|
788
|
+
if (!publicKey) {
|
|
789
|
+
if (onUnauthenticated) onUnauthenticated();
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
loadFollowList();
|
|
793
|
+
}, [publicKey]);
|
|
794
|
+
(0, react.useEffect)(() => {
|
|
795
|
+
if (members.length > 0) loadMemberProfiles();
|
|
796
|
+
}, [members]);
|
|
797
|
+
const handleDecoded = (decoded) => {
|
|
798
|
+
setSearchQuery(decoded);
|
|
799
|
+
setShowScanner(false);
|
|
800
|
+
handleSearch();
|
|
801
|
+
};
|
|
802
|
+
const loadFollowList = async () => {
|
|
803
|
+
if (!publicKey) return;
|
|
804
|
+
setIsLoading(true);
|
|
805
|
+
try {
|
|
806
|
+
setMembers(await relayService.fetchFollowList(publicKey));
|
|
807
|
+
} catch (error) {
|
|
808
|
+
console.error("Failed to load follow list:", error);
|
|
809
|
+
setSaveMessage("Failed to load membership list");
|
|
810
|
+
} finally {
|
|
811
|
+
setIsLoading(false);
|
|
812
|
+
}
|
|
813
|
+
};
|
|
814
|
+
const loadMemberProfiles = async () => {
|
|
815
|
+
if (members.length === 0) return;
|
|
816
|
+
try {
|
|
817
|
+
const pubkeys = members.map((m) => m.pubkey);
|
|
818
|
+
setMemberProfiles(await relayService.fetchMultipleProfiles(pubkeys));
|
|
819
|
+
} catch (error) {
|
|
820
|
+
console.error("Failed to load member profiles:", error);
|
|
821
|
+
}
|
|
822
|
+
};
|
|
823
|
+
const handleSearch = async () => {
|
|
824
|
+
if (!searchQuery.trim()) {
|
|
825
|
+
setIsLoading(true);
|
|
826
|
+
try {
|
|
827
|
+
const profilesMap = await relayService.queryProfiles([], 50);
|
|
828
|
+
const profilesList = [];
|
|
829
|
+
profilesMap.forEach((profile, pubkey) => {
|
|
830
|
+
profilesList.push({
|
|
831
|
+
...profile,
|
|
832
|
+
pubkey
|
|
833
|
+
});
|
|
834
|
+
});
|
|
835
|
+
setProfiles(profilesList);
|
|
836
|
+
} catch (error) {
|
|
837
|
+
console.error("Failed to query profiles:", error);
|
|
838
|
+
setSaveMessage("Failed to search profiles");
|
|
839
|
+
} finally {
|
|
840
|
+
setIsLoading(false);
|
|
841
|
+
}
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
const trimmedQuery = searchQuery.trim();
|
|
845
|
+
if (trimmedQuery.length !== 64 || !/^[0-9a-fA-F]+$/.test(trimmedQuery)) {
|
|
846
|
+
setSaveMessage("Invalid pubkey format. Must be 64 hex characters.");
|
|
847
|
+
setTimeout(() => setSaveMessage(null), 3e3);
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
setIsLoading(true);
|
|
851
|
+
try {
|
|
852
|
+
const profilesMap = await relayService.queryProfiles([trimmedQuery], 1);
|
|
853
|
+
const profilesList = [];
|
|
854
|
+
profilesMap.forEach((profile, pubkey) => {
|
|
855
|
+
profilesList.push({
|
|
856
|
+
...profile,
|
|
857
|
+
pubkey
|
|
858
|
+
});
|
|
859
|
+
});
|
|
860
|
+
setProfiles(profilesList);
|
|
861
|
+
if (profilesList.length === 0) {
|
|
862
|
+
setSaveMessage("No profile found for this pubkey");
|
|
863
|
+
setTimeout(() => setSaveMessage(null), 3e3);
|
|
864
|
+
}
|
|
865
|
+
} catch (error) {
|
|
866
|
+
console.error("Failed to query profile:", error);
|
|
867
|
+
setSaveMessage("Failed to search profile");
|
|
868
|
+
} finally {
|
|
869
|
+
setIsLoading(false);
|
|
870
|
+
}
|
|
871
|
+
};
|
|
872
|
+
const handleAddMember = async (pubkey) => {
|
|
873
|
+
if (!publicKey) return;
|
|
874
|
+
if (members.some((m) => m.pubkey === pubkey)) {
|
|
875
|
+
setSaveMessage("User is already a member");
|
|
876
|
+
setTimeout(() => setSaveMessage(null), 3e3);
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
setIsSaving(true);
|
|
880
|
+
try {
|
|
881
|
+
const newMembers = [...members, { pubkey }];
|
|
882
|
+
await relayService.publishFollowList(publicKey, newMembers, (event) => authService.signEvent(event));
|
|
883
|
+
setMembers(newMembers);
|
|
884
|
+
setSaveMessage("Member added successfully!");
|
|
885
|
+
setTimeout(() => setSaveMessage(null), 3e3);
|
|
886
|
+
} catch (error) {
|
|
887
|
+
console.error("Failed to add member:", error);
|
|
888
|
+
setSaveMessage("Failed to add member. Please try again.");
|
|
889
|
+
} finally {
|
|
890
|
+
setIsSaving(false);
|
|
891
|
+
}
|
|
892
|
+
};
|
|
893
|
+
const handleRemoveMember = async (pubkey) => {
|
|
894
|
+
if (!publicKey) return;
|
|
895
|
+
setIsSaving(true);
|
|
896
|
+
try {
|
|
897
|
+
const newMembers = members.filter((m) => m.pubkey !== pubkey);
|
|
898
|
+
await relayService.publishFollowList(publicKey, newMembers, (event) => authService.signEvent(event));
|
|
899
|
+
setMembers(newMembers);
|
|
900
|
+
setSaveMessage("Member removed successfully!");
|
|
901
|
+
setTimeout(() => setSaveMessage(null), 3e3);
|
|
902
|
+
} catch (error) {
|
|
903
|
+
console.error("Failed to remove member:", error);
|
|
904
|
+
setSaveMessage("Failed to remove member. Please try again.");
|
|
905
|
+
} finally {
|
|
906
|
+
setIsSaving(false);
|
|
907
|
+
}
|
|
908
|
+
};
|
|
909
|
+
const getProfileDisplayName = (profile, pubkey) => {
|
|
910
|
+
return profile.display_name || profile.name || pubkey.slice(0, 16) + "...";
|
|
911
|
+
};
|
|
912
|
+
const formatPubkey = (pubkey) => {
|
|
913
|
+
return `${pubkey.slice(0, 8)}...${pubkey.slice(-8)}`;
|
|
914
|
+
};
|
|
915
|
+
if (isLoading && members.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
916
|
+
className: "membership-container",
|
|
917
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
918
|
+
className: "loading-state",
|
|
919
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "spinner" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: "Loading membership list..." })]
|
|
920
|
+
})
|
|
921
|
+
});
|
|
922
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
923
|
+
className: "membership-container",
|
|
924
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
925
|
+
className: "membership-card",
|
|
926
|
+
children: [
|
|
927
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h1", { children: "Membership Management" }),
|
|
928
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TrustInfo, {}),
|
|
929
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
930
|
+
className: "membership-description",
|
|
931
|
+
children: "Query applicants and manage the membership list."
|
|
932
|
+
}),
|
|
933
|
+
saveMessage && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
934
|
+
className: `save-message ${saveMessage.includes("Error") || saveMessage.includes("Failed") ? "error" : "success"}`,
|
|
935
|
+
children: saveMessage
|
|
936
|
+
}),
|
|
937
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
938
|
+
className: "search-section",
|
|
939
|
+
children: [
|
|
940
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", { children: "Add Member" }),
|
|
941
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
942
|
+
className: "search-form",
|
|
943
|
+
children: [
|
|
944
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
945
|
+
type: "text",
|
|
946
|
+
value: searchQuery,
|
|
947
|
+
onChange: (e) => setSearchQuery(e.target.value),
|
|
948
|
+
onKeyPress: (e) => e.key === "Enter" && handleSearch(),
|
|
949
|
+
placeholder: "Enter pubkey or leave empty for recent profiles",
|
|
950
|
+
className: "search-input",
|
|
951
|
+
disabled: isLoading
|
|
952
|
+
}),
|
|
953
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
954
|
+
onClick: handleSearch,
|
|
955
|
+
className: "search-button",
|
|
956
|
+
disabled: isLoading || isSaving,
|
|
957
|
+
children: isLoading ? "Searching..." : "Search"
|
|
958
|
+
}),
|
|
959
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
960
|
+
onClick: () => setShowScanner((prev) => !prev),
|
|
961
|
+
className: "scanner-toggle",
|
|
962
|
+
disabled: isLoading || isSaving,
|
|
963
|
+
children: showScanner ? "Close Scanner" : "Scan QR"
|
|
964
|
+
})
|
|
965
|
+
]
|
|
966
|
+
}),
|
|
967
|
+
showScanner && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
968
|
+
style: { marginTop: "1rem" },
|
|
969
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(BarcodeScanner, {
|
|
970
|
+
onDecode: handleDecoded,
|
|
971
|
+
active: true
|
|
972
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
973
|
+
style: {
|
|
974
|
+
fontSize: "0.85rem",
|
|
975
|
+
marginTop: "0.5rem"
|
|
976
|
+
},
|
|
977
|
+
children: "Point at QR‑code containing a pubkey"
|
|
978
|
+
})]
|
|
979
|
+
}),
|
|
980
|
+
profiles.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
981
|
+
className: "profiles-list",
|
|
982
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: "Search Results" }), profiles.map((profile) => {
|
|
983
|
+
const isMember = members.some((m) => m.pubkey === profile.pubkey);
|
|
984
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
985
|
+
className: "profile-item",
|
|
986
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
987
|
+
className: "profile-info",
|
|
988
|
+
children: [profile.picture && isValidHttpUrl(profile.picture) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
989
|
+
src: profile.picture,
|
|
990
|
+
alt: sanitizeText(getProfileDisplayName(profile, profile.pubkey)),
|
|
991
|
+
className: "profile-avatar",
|
|
992
|
+
loading: "lazy",
|
|
993
|
+
referrerPolicy: "no-referrer",
|
|
994
|
+
onError: (event) => {
|
|
995
|
+
event.currentTarget.style.display = "none";
|
|
996
|
+
}
|
|
997
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
998
|
+
className: "profile-details",
|
|
999
|
+
children: [
|
|
1000
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1001
|
+
className: "profile-name",
|
|
1002
|
+
children: sanitizeText(getProfileDisplayName(profile, profile.pubkey))
|
|
1003
|
+
}),
|
|
1004
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1005
|
+
className: "profile-pubkey",
|
|
1006
|
+
children: formatPubkey(profile.pubkey)
|
|
1007
|
+
}),
|
|
1008
|
+
profile.about && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1009
|
+
className: "profile-about",
|
|
1010
|
+
children: sanitizeText(profile.about)
|
|
1011
|
+
})
|
|
1012
|
+
]
|
|
1013
|
+
})]
|
|
1014
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1015
|
+
onClick: () => isMember ? handleRemoveMember(profile.pubkey) : handleAddMember(profile.pubkey),
|
|
1016
|
+
className: `member-button ${isMember ? "remove" : "add"}`,
|
|
1017
|
+
disabled: isSaving,
|
|
1018
|
+
children: isMember ? "Remove" : "Add Member"
|
|
1019
|
+
})]
|
|
1020
|
+
}, profile.pubkey);
|
|
1021
|
+
})]
|
|
1022
|
+
})
|
|
1023
|
+
]
|
|
1024
|
+
}),
|
|
1025
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1026
|
+
className: "members-section",
|
|
1027
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("h2", { children: [
|
|
1028
|
+
"Current Members (",
|
|
1029
|
+
members.length,
|
|
1030
|
+
")"
|
|
1031
|
+
] }), members.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1032
|
+
className: "empty-message",
|
|
1033
|
+
children: "No members yet. Add members from search results above."
|
|
1034
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1035
|
+
className: "members-list",
|
|
1036
|
+
children: members.map((member) => {
|
|
1037
|
+
const profile = memberProfiles.get(member.pubkey);
|
|
1038
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1039
|
+
className: "member-item",
|
|
1040
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1041
|
+
className: "profile-info",
|
|
1042
|
+
children: [profile?.picture && isValidHttpUrl(profile.picture) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
1043
|
+
src: profile.picture,
|
|
1044
|
+
alt: sanitizeText(getProfileDisplayName(profile || {}, member.pubkey)),
|
|
1045
|
+
className: "profile-avatar",
|
|
1046
|
+
loading: "lazy",
|
|
1047
|
+
referrerPolicy: "no-referrer",
|
|
1048
|
+
onError: (event) => {
|
|
1049
|
+
event.currentTarget.style.display = "none";
|
|
1050
|
+
}
|
|
1051
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1052
|
+
className: "profile-details",
|
|
1053
|
+
children: [
|
|
1054
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1055
|
+
className: "profile-name",
|
|
1056
|
+
children: profile ? sanitizeText(getProfileDisplayName(profile, member.pubkey)) : formatPubkey(member.pubkey)
|
|
1057
|
+
}),
|
|
1058
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1059
|
+
className: "profile-pubkey",
|
|
1060
|
+
children: formatPubkey(member.pubkey)
|
|
1061
|
+
}),
|
|
1062
|
+
profile?.about && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1063
|
+
className: "profile-about",
|
|
1064
|
+
children: sanitizeText(profile.about)
|
|
1065
|
+
}),
|
|
1066
|
+
member.petname && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1067
|
+
className: "profile-petname",
|
|
1068
|
+
children: ["Name: ", sanitizeText(member.petname)]
|
|
1069
|
+
})
|
|
1070
|
+
]
|
|
1071
|
+
})]
|
|
1072
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1073
|
+
onClick: () => handleRemoveMember(member.pubkey),
|
|
1074
|
+
className: "member-button remove",
|
|
1075
|
+
disabled: isSaving,
|
|
1076
|
+
children: "Remove"
|
|
1077
|
+
})]
|
|
1078
|
+
}, member.pubkey);
|
|
1079
|
+
})
|
|
1080
|
+
})]
|
|
1081
|
+
})
|
|
1082
|
+
]
|
|
1083
|
+
})
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
//#endregion
|
|
1088
|
+
//#region src/components/profile/ProfilePage.tsx
|
|
1089
|
+
const PersonhoodInfo = () => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1090
|
+
className: "personhood-info",
|
|
1091
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", { children: [
|
|
1092
|
+
"Network‑state members receive a ",
|
|
1093
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "Personhood Credential (PHC)" }),
|
|
1094
|
+
" from a trusted authority. The PHC attests that the holder is a unique, real individual. Because the credential lives in your passport, you can present a ",
|
|
1095
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("em", { children: "zero‑knowledge proof" }),
|
|
1096
|
+
" that you are verified."
|
|
1097
|
+
] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: "In this form we compare the name you enter with the name disclosed by your verified passport proof. If the two match, the profile will be saved as your business card credential together with a passport tag that references the PHC's unique identifier." })]
|
|
1098
|
+
});
|
|
1099
|
+
const MAX_NAME_LENGTH = 100;
|
|
1100
|
+
const MAX_ABOUT_LENGTH = 1e3;
|
|
1101
|
+
const MAX_URL_LENGTH = 2048;
|
|
1102
|
+
function ProfilePage({ authService, relayService, publicKey, onUnauthenticated, onSuccess, onRoleSuggestion }) {
|
|
1103
|
+
const [isLoading, setIsLoading] = (0, react.useState)(false);
|
|
1104
|
+
const [isSaving, setIsSaving] = (0, react.useState)(false);
|
|
1105
|
+
const [saveMessage, setSaveMessage] = (0, react.useState)(null);
|
|
1106
|
+
const [suggestedRole, setSuggestedRole] = (0, react.useState)(null);
|
|
1107
|
+
const [isGettingRole, setIsGettingRole] = (0, react.useState)(false);
|
|
1108
|
+
const [formData, setFormData] = (0, react.useState)({
|
|
1109
|
+
name: "",
|
|
1110
|
+
display_name: "",
|
|
1111
|
+
about: "",
|
|
1112
|
+
picture: "",
|
|
1113
|
+
website: ""
|
|
1114
|
+
});
|
|
1115
|
+
(0, react.useEffect)(() => {
|
|
1116
|
+
if (!publicKey) {
|
|
1117
|
+
if (onUnauthenticated) onUnauthenticated();
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
loadProfile();
|
|
1121
|
+
}, [publicKey]);
|
|
1122
|
+
const loadProfile = async () => {
|
|
1123
|
+
if (!publicKey) return;
|
|
1124
|
+
setIsLoading(true);
|
|
1125
|
+
try {
|
|
1126
|
+
const [profile, roleTag] = await Promise.all([relayService.fetchProfile(publicKey), relayService.fetchProfileRoleTag(publicKey)]);
|
|
1127
|
+
if (profile) setFormData({
|
|
1128
|
+
name: sanitizeText(profile.name || ""),
|
|
1129
|
+
display_name: sanitizeText(profile.display_name || ""),
|
|
1130
|
+
about: sanitizeText(profile.about || ""),
|
|
1131
|
+
picture: profile.picture || "",
|
|
1132
|
+
website: profile.website || ""
|
|
1133
|
+
});
|
|
1134
|
+
if (roleTag) setSuggestedRole(roleTag);
|
|
1135
|
+
} catch (error) {
|
|
1136
|
+
console.error("Failed to load profile:", error);
|
|
1137
|
+
} finally {
|
|
1138
|
+
setIsLoading(false);
|
|
1139
|
+
}
|
|
1140
|
+
};
|
|
1141
|
+
const handleSubmit = async (e) => {
|
|
1142
|
+
e.preventDefault();
|
|
1143
|
+
if (!publicKey) return;
|
|
1144
|
+
setIsSaving(true);
|
|
1145
|
+
setSaveMessage(null);
|
|
1146
|
+
try {
|
|
1147
|
+
const tags = [];
|
|
1148
|
+
if (!isValidPubkey(publicKey)) throw new Error("Invalid public key format");
|
|
1149
|
+
if (formData.about && formData.about.trim().length > 0 && onRoleSuggestion) {
|
|
1150
|
+
setIsGettingRole(true);
|
|
1151
|
+
try {
|
|
1152
|
+
const roleSuggestion = await onRoleSuggestion(formData.about);
|
|
1153
|
+
const candidate = roleSuggestion ? roleSuggestion.trim() : "";
|
|
1154
|
+
if (candidate && isValidRoleTag(candidate)) {
|
|
1155
|
+
tags.push(["role", candidate]);
|
|
1156
|
+
setSuggestedRole(candidate);
|
|
1157
|
+
} else setSuggestedRole(null);
|
|
1158
|
+
} catch (error) {
|
|
1159
|
+
console.error("Failed to get role suggestion:", error);
|
|
1160
|
+
setSuggestedRole(null);
|
|
1161
|
+
} finally {
|
|
1162
|
+
setIsGettingRole(false);
|
|
1163
|
+
}
|
|
1164
|
+
} else setSuggestedRole(null);
|
|
1165
|
+
const profileEvent = {
|
|
1166
|
+
kind: 0,
|
|
1167
|
+
content: JSON.stringify({
|
|
1168
|
+
...formData,
|
|
1169
|
+
name: sanitizeText(formData.name || ""),
|
|
1170
|
+
display_name: sanitizeText(formData.display_name || ""),
|
|
1171
|
+
about: sanitizeText(formData.about || "")
|
|
1172
|
+
}),
|
|
1173
|
+
created_at: Math.floor(Date.now() / 1e3),
|
|
1174
|
+
tags
|
|
1175
|
+
};
|
|
1176
|
+
const follows = {
|
|
1177
|
+
kind: 3,
|
|
1178
|
+
content: "",
|
|
1179
|
+
created_at: Math.floor(Date.now() / 1e3),
|
|
1180
|
+
tags: [(() => {
|
|
1181
|
+
const followTag = ["p", publicKey];
|
|
1182
|
+
const relayUrl = relayService.getRelays()[0];
|
|
1183
|
+
if (relayUrl) followTag.push(relayUrl);
|
|
1184
|
+
if (formData.name) followTag.push(formData.name);
|
|
1185
|
+
return followTag;
|
|
1186
|
+
})()]
|
|
1187
|
+
};
|
|
1188
|
+
const signedProfile = await authService.signEvent(profileEvent);
|
|
1189
|
+
const signedFollows = await authService.signEvent(follows);
|
|
1190
|
+
await relayService.publishEvent(signedProfile);
|
|
1191
|
+
await relayService.publishEvent(signedFollows);
|
|
1192
|
+
setSaveMessage("Profile saved successfully!");
|
|
1193
|
+
setTimeout(() => {
|
|
1194
|
+
setSaveMessage(null);
|
|
1195
|
+
}, 3e3);
|
|
1196
|
+
if (onSuccess) onSuccess();
|
|
1197
|
+
} catch (error) {
|
|
1198
|
+
console.error("Failed to save profile:", error);
|
|
1199
|
+
setSaveMessage("Failed to save profile. Please try again.");
|
|
1200
|
+
} finally {
|
|
1201
|
+
setIsSaving(false);
|
|
1202
|
+
}
|
|
1203
|
+
};
|
|
1204
|
+
const handleChange = (e) => {
|
|
1205
|
+
const { name, value } = e.target;
|
|
1206
|
+
setFormData((prev) => ({
|
|
1207
|
+
...prev,
|
|
1208
|
+
[name]: value
|
|
1209
|
+
}));
|
|
1210
|
+
};
|
|
1211
|
+
if (isLoading) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1212
|
+
className: "profile-container",
|
|
1213
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1214
|
+
className: "loading-state",
|
|
1215
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "spinner" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: "Loading profile..." })]
|
|
1216
|
+
})
|
|
1217
|
+
});
|
|
1218
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1219
|
+
className: "profile-container",
|
|
1220
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1221
|
+
className: "profile-card",
|
|
1222
|
+
children: [
|
|
1223
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h1", { children: "Profile Setup" }),
|
|
1224
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PersonhoodInfo, {}),
|
|
1225
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
|
|
1226
|
+
onSubmit: handleSubmit,
|
|
1227
|
+
className: "profile-form",
|
|
1228
|
+
children: [
|
|
1229
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1230
|
+
className: "form-group",
|
|
1231
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
1232
|
+
htmlFor: "name",
|
|
1233
|
+
children: "First Name"
|
|
1234
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1235
|
+
type: "text",
|
|
1236
|
+
id: "name",
|
|
1237
|
+
name: "name",
|
|
1238
|
+
value: formData.name || "",
|
|
1239
|
+
onChange: handleChange,
|
|
1240
|
+
placeholder: "Your username",
|
|
1241
|
+
maxLength: MAX_NAME_LENGTH
|
|
1242
|
+
})]
|
|
1243
|
+
}),
|
|
1244
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1245
|
+
className: "form-group",
|
|
1246
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
1247
|
+
htmlFor: "display_name",
|
|
1248
|
+
children: "Last Name"
|
|
1249
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1250
|
+
type: "text",
|
|
1251
|
+
id: "display_name",
|
|
1252
|
+
name: "display_name",
|
|
1253
|
+
value: formData.display_name || "",
|
|
1254
|
+
onChange: handleChange,
|
|
1255
|
+
placeholder: "Your Last Name",
|
|
1256
|
+
maxLength: MAX_NAME_LENGTH
|
|
1257
|
+
})]
|
|
1258
|
+
}),
|
|
1259
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1260
|
+
className: "form-group",
|
|
1261
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
1262
|
+
htmlFor: "about",
|
|
1263
|
+
children: "About"
|
|
1264
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
|
|
1265
|
+
id: "about",
|
|
1266
|
+
name: "about",
|
|
1267
|
+
value: formData.about || "",
|
|
1268
|
+
onChange: handleChange,
|
|
1269
|
+
placeholder: "Tell us about yourself",
|
|
1270
|
+
rows: 4,
|
|
1271
|
+
maxLength: MAX_ABOUT_LENGTH
|
|
1272
|
+
})]
|
|
1273
|
+
}),
|
|
1274
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1275
|
+
className: "form-group",
|
|
1276
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
1277
|
+
htmlFor: "picture",
|
|
1278
|
+
children: "Profile Picture URL"
|
|
1279
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1280
|
+
type: "url",
|
|
1281
|
+
id: "picture",
|
|
1282
|
+
name: "picture",
|
|
1283
|
+
value: formData.picture || "",
|
|
1284
|
+
onChange: handleChange,
|
|
1285
|
+
placeholder: "https://example.com/avatar.jpg",
|
|
1286
|
+
maxLength: MAX_URL_LENGTH
|
|
1287
|
+
})]
|
|
1288
|
+
}),
|
|
1289
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1290
|
+
className: "form-group",
|
|
1291
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
1292
|
+
htmlFor: "website",
|
|
1293
|
+
children: "LinkedIn"
|
|
1294
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1295
|
+
type: "url",
|
|
1296
|
+
id: "website",
|
|
1297
|
+
name: "website",
|
|
1298
|
+
value: formData.website || "",
|
|
1299
|
+
onChange: handleChange,
|
|
1300
|
+
placeholder: "https://linkedin.com/example",
|
|
1301
|
+
maxLength: MAX_URL_LENGTH
|
|
1302
|
+
})]
|
|
1303
|
+
}),
|
|
1304
|
+
saveMessage && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1305
|
+
className: `save-message ${saveMessage.includes("Error") ? "error" : "success"}`,
|
|
1306
|
+
children: saveMessage
|
|
1307
|
+
}),
|
|
1308
|
+
(isGettingRole || suggestedRole) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1309
|
+
className: "role-tag-container",
|
|
1310
|
+
children: isGettingRole ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1311
|
+
className: "role-tag-label",
|
|
1312
|
+
children: "Getting AI suggestion..."
|
|
1313
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1314
|
+
className: "role-tag-loading",
|
|
1315
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "role-tag-spinner" })
|
|
1316
|
+
})] }) : suggestedRole ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1317
|
+
className: "role-tag-label",
|
|
1318
|
+
children: "AI Suggested Role:"
|
|
1319
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1320
|
+
className: "role-tag",
|
|
1321
|
+
children: sanitizeText(suggestedRole)
|
|
1322
|
+
})] }) : null
|
|
1323
|
+
}),
|
|
1324
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1325
|
+
className: "form-actions",
|
|
1326
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1327
|
+
type: "submit",
|
|
1328
|
+
className: "save-button",
|
|
1329
|
+
disabled: isSaving,
|
|
1330
|
+
children: isSaving ? "Saving..." : "Save Profile"
|
|
1331
|
+
})
|
|
1332
|
+
})
|
|
1333
|
+
]
|
|
1334
|
+
})
|
|
1335
|
+
]
|
|
1336
|
+
})
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
//#endregion
|
|
1341
|
+
//#region src/hooks/useAuth.ts
|
|
1342
|
+
/**
|
|
1343
|
+
* Hook to initialize auth state on app load
|
|
1344
|
+
*/
|
|
1345
|
+
function useAuthInit(authService, setAuthenticated) {
|
|
1346
|
+
(0, react.useEffect)(() => {
|
|
1347
|
+
if (authService.hasKeyInfo()) {
|
|
1348
|
+
const keyInfo = authService.getCurrentKeyInfo();
|
|
1349
|
+
if (keyInfo) setAuthenticated(keyInfo);
|
|
1350
|
+
}
|
|
1351
|
+
}, [authService, setAuthenticated]);
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
//#endregion
|
|
1355
|
+
//#region src/store/authStore.ts
|
|
1356
|
+
const createAuthStore = () => {
|
|
1357
|
+
return (0, zustand.create)((set) => ({
|
|
1358
|
+
isAuthenticated: false,
|
|
1359
|
+
publicKey: null,
|
|
1360
|
+
keyInfo: null,
|
|
1361
|
+
loginError: null,
|
|
1362
|
+
setAuthenticated: (keyInfo) => {
|
|
1363
|
+
set({
|
|
1364
|
+
isAuthenticated: !!keyInfo,
|
|
1365
|
+
publicKey: keyInfo?.pubkey || null,
|
|
1366
|
+
keyInfo,
|
|
1367
|
+
loginError: null
|
|
1368
|
+
});
|
|
1369
|
+
},
|
|
1370
|
+
setLoginError: (error) => {
|
|
1371
|
+
set({ loginError: error });
|
|
1372
|
+
},
|
|
1373
|
+
logout: () => {
|
|
1374
|
+
set({
|
|
1375
|
+
isAuthenticated: false,
|
|
1376
|
+
publicKey: null,
|
|
1377
|
+
keyInfo: null,
|
|
1378
|
+
loginError: null
|
|
1379
|
+
});
|
|
1380
|
+
}
|
|
1381
|
+
}));
|
|
1382
|
+
};
|
|
1383
|
+
const useAuthStore = createAuthStore();
|
|
1384
|
+
|
|
1385
|
+
//#endregion
|
|
1386
|
+
exports.AuthService = AuthService;
|
|
1387
|
+
exports.BarcodeScanner = BarcodeScanner;
|
|
1388
|
+
exports.LoginButton = LoginButton;
|
|
1389
|
+
exports.MembershipPage = MembershipPage;
|
|
1390
|
+
exports.ProfilePage = ProfilePage;
|
|
1391
|
+
exports.RegistrationFlow = RegistrationFlow;
|
|
1392
|
+
exports.RelayService = RelayService;
|
|
1393
|
+
exports.__toESM = __toESM;
|
|
1394
|
+
exports.createAuthStore = createAuthStore;
|
|
1395
|
+
exports.useAuthInit = useAuthInit;
|
|
1396
|
+
exports.useAuthStore = useAuthStore;
|
|
1397
|
+
//# sourceMappingURL=index.cjs.map
|