@nsite/stealthis 0.6.0 → 0.7.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 +6 -2
- package/package.json +4 -3
- package/src/css.d.ts +4 -0
- package/src/index.ts +3 -3
- package/src/nostr.ts +201 -70
- package/src/qr.ts +2 -5
- package/src/styles.css +452 -0
- package/src/widget.ts +253 -133
- package/vite.config.ts +14 -7
- package/src/styles.ts +0 -440
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ A drop-in web component that adds a "Borrow this" button to any nsite. Visitors
|
|
|
4
4
|
|
|
5
5
|
## How it works
|
|
6
6
|
|
|
7
|
-
When loaded on an nsite, `<
|
|
7
|
+
When loaded on an nsite, `<steal-this>` detects the site owner's pubkey from the subdomain, fetches the site manifest from nostr relays, and lets authenticated visitors publish a copy under their own npub. Each deployment appends a `muse` tag to the manifest, creating a paper trail of who was inspired by the site.
|
|
8
8
|
|
|
9
9
|
## Install
|
|
10
10
|
|
|
@@ -19,7 +19,7 @@ The widget auto-injects a fixed-position button in the bottom-right corner.
|
|
|
19
19
|
If you want to control where the button appears, add the element yourself (the auto-inject will skip if one already exists):
|
|
20
20
|
|
|
21
21
|
```html
|
|
22
|
-
<
|
|
22
|
+
<steal-this button-text="Cop this joint" stat-text="%s npubs copped this"></steal-this>
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
## Attributes
|
|
@@ -29,6 +29,8 @@ If you want to control where the button appears, add the element yourself (the a
|
|
|
29
29
|
| `button-text` | `"Borrow this nsite"` | Button text |
|
|
30
30
|
| `stat-text` | `"%s npubs borrowed this nsite"` | Paper trail summary. `%s` is replaced with the count. |
|
|
31
31
|
| `no-trail` | _(absent)_ | Boolean attribute. When present, disables the paper trail entirely -- no `muse` tags are written and the trail UI is not rendered. |
|
|
32
|
+
| `obfuscate-npubs` | _(absent)_ | Boolean attribute. Shows truncated npubs with no links in the paper trail. Disables profile fetching. |
|
|
33
|
+
| `do-not-fetch-muse-data` | _(absent)_ | Boolean attribute. Skips profile enrichment but still shows full npubs linked to njump. |
|
|
32
34
|
|
|
33
35
|
The button's `trigger` part is exposed via `::part(trigger)` for CSS customization.
|
|
34
36
|
|
|
@@ -51,6 +53,8 @@ If the visitor already has a root site, the form defaults to a named site. Overw
|
|
|
51
53
|
|
|
52
54
|
Each deployment adds a `muse` tag with the deployer's pubkey and relay list. The widget shows an expandable "Inspired N npubs" counter when muse tags are present. A maximum of 9 muse tags are kept per manifest (originator + 8 most recent), with FIFO truncation of the middle.
|
|
53
55
|
|
|
56
|
+
By default, the trail streams kind-0 profiles from relays to show display names and links to each muse's nsite (or njump fallback). Use `obfuscate-npubs` to show only truncated npubs, or `do-not-fetch-muse-data` to skip profile fetching while still linking to njump.
|
|
57
|
+
|
|
54
58
|
## Development
|
|
55
59
|
|
|
56
60
|
```bash
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nsite/stealthis",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/stealthis.js",
|
|
6
6
|
"module": "dist/stealthis.mjs",
|
|
@@ -15,10 +15,11 @@
|
|
|
15
15
|
"build": "vite build"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"
|
|
19
|
-
"
|
|
18
|
+
"@libs/qrcode": "jsr:^3.0.1",
|
|
19
|
+
"nostr-tools": "^2.23.3"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
|
+
"cssnano": "^7.1.3",
|
|
22
23
|
"typescript": "^5.9.3",
|
|
23
24
|
"vite": "^7.3.1"
|
|
24
25
|
}
|
package/src/css.d.ts
ADDED
package/src/index.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { NsiteDeployButton } from './widget';
|
|
2
2
|
|
|
3
|
-
customElements.define('
|
|
3
|
+
customElements.define('steal-this', NsiteDeployButton);
|
|
4
4
|
|
|
5
5
|
function autoInject() {
|
|
6
|
-
if (!document.querySelector('
|
|
7
|
-
const el = document.createElement('
|
|
6
|
+
if (!document.querySelector('steal-this')) {
|
|
7
|
+
const el = document.createElement('steal-this');
|
|
8
8
|
el.classList.add('nd-fixed');
|
|
9
9
|
document.body.appendChild(el);
|
|
10
10
|
}
|
package/src/nostr.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { npubEncode } from
|
|
2
|
-
import type {
|
|
1
|
+
import { decode, npubEncode } from "nostr-tools/nip19";
|
|
2
|
+
import type { EventTemplate, SignedEvent } from "./signer";
|
|
3
3
|
|
|
4
|
-
export type {
|
|
4
|
+
export type { EventTemplate, SignedEvent };
|
|
5
5
|
|
|
6
6
|
export interface NsiteContext {
|
|
7
7
|
pubkey: string;
|
|
@@ -15,9 +15,24 @@ export interface Muse {
|
|
|
15
15
|
relays: string[];
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
export
|
|
18
|
+
export interface MuseProfile {
|
|
19
|
+
npub: string;
|
|
20
|
+
name?: string;
|
|
21
|
+
nsiteUrl?: string;
|
|
22
|
+
}
|
|
19
23
|
|
|
20
|
-
const BOOTSTRAP_RELAYS = [
|
|
24
|
+
const BOOTSTRAP_RELAYS = [
|
|
25
|
+
"wss://purplepag.es",
|
|
26
|
+
"wss://relay.damus.io",
|
|
27
|
+
"wss://nos.lol",
|
|
28
|
+
];
|
|
29
|
+
const PROFILE_RELAYS = [
|
|
30
|
+
"wss://purplepag.es",
|
|
31
|
+
"wss://user.kindpag.es",
|
|
32
|
+
"wss://indexer.hzrd149.com",
|
|
33
|
+
"wss://profiles.nostr1.com",
|
|
34
|
+
"wss://nos.lol",
|
|
35
|
+
];
|
|
21
36
|
const B36_LEN = 50;
|
|
22
37
|
const D_TAG_RE = /^[a-z0-9-]{1,13}$/;
|
|
23
38
|
const NAMED_LABEL_RE = /^[0-9a-z]{50}[a-z0-9-]{1,13}$/;
|
|
@@ -25,53 +40,40 @@ const NAMED_LABEL_RE = /^[0-9a-z]{50}[a-z0-9-]{1,13}$/;
|
|
|
25
40
|
// --- Base36 ---
|
|
26
41
|
|
|
27
42
|
export function pubkeyToBase36(hex: string): string {
|
|
28
|
-
return BigInt(
|
|
43
|
+
return BigInt("0x" + hex)
|
|
29
44
|
.toString(36)
|
|
30
|
-
.padStart(B36_LEN,
|
|
45
|
+
.padStart(B36_LEN, "0");
|
|
31
46
|
}
|
|
32
47
|
|
|
33
48
|
function base36ToHex(b36: string): string {
|
|
34
49
|
let n = 0n;
|
|
35
50
|
for (const c of b36) n = n * 36n + BigInt(parseInt(c, 36));
|
|
36
|
-
return n.toString(16).padStart(64,
|
|
51
|
+
return n.toString(16).padStart(64, "0");
|
|
37
52
|
}
|
|
38
53
|
|
|
39
|
-
// ---
|
|
54
|
+
// --- NIP-19 ---
|
|
40
55
|
|
|
41
56
|
function npubDecode(npub: string): string | null {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
values.push(v);
|
|
57
|
+
try {
|
|
58
|
+
const result = decode(npub);
|
|
59
|
+
if (result.type !== "npub") return null;
|
|
60
|
+
return result.data;
|
|
61
|
+
} catch (error) {
|
|
62
|
+
return null;
|
|
49
63
|
}
|
|
50
|
-
const payload = values.slice(0, -6);
|
|
51
|
-
let acc = 0,
|
|
52
|
-
bits = 0;
|
|
53
|
-
const bytes: number[] = [];
|
|
54
|
-
for (const v of payload) {
|
|
55
|
-
acc = (acc << 5) | v;
|
|
56
|
-
bits += 5;
|
|
57
|
-
while (bits >= 8) {
|
|
58
|
-
bits -= 8;
|
|
59
|
-
bytes.push((acc >> bits) & 0xff);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
if (bytes.length !== 32) return null;
|
|
63
|
-
return bytes.map((b) => b.toString(16).padStart(2, '0')).join('');
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
export { npubEncode };
|
|
67
|
+
|
|
66
68
|
// --- Context ---
|
|
67
69
|
|
|
68
70
|
export function parseContext(): NsiteContext | null {
|
|
69
|
-
const parts = window.location.hostname.split(
|
|
71
|
+
const parts = window.location.hostname.split(".");
|
|
70
72
|
|
|
71
73
|
for (let i = 0; i < parts.length; i++) {
|
|
72
|
-
if (parts[i].startsWith(
|
|
74
|
+
if (parts[i].startsWith("npub1") && parts[i].length >= 63) {
|
|
73
75
|
const pubkey = npubDecode(parts[i]);
|
|
74
|
-
if (pubkey) return { pubkey, baseDomain: parts.slice(i + 1).join(
|
|
76
|
+
if (pubkey) return { pubkey, baseDomain: parts.slice(i + 1).join(".") };
|
|
75
77
|
}
|
|
76
78
|
}
|
|
77
79
|
|
|
@@ -81,11 +83,15 @@ export function parseContext(): NsiteContext | null {
|
|
|
81
83
|
label.length > B36_LEN &&
|
|
82
84
|
label.length <= 63 &&
|
|
83
85
|
NAMED_LABEL_RE.test(label) &&
|
|
84
|
-
!label.endsWith(
|
|
86
|
+
!label.endsWith("-")
|
|
85
87
|
) {
|
|
86
88
|
try {
|
|
87
89
|
const pubkey = base36ToHex(label.slice(0, B36_LEN));
|
|
88
|
-
return {
|
|
90
|
+
return {
|
|
91
|
+
pubkey,
|
|
92
|
+
identifier: label.slice(B36_LEN),
|
|
93
|
+
baseDomain: parts.slice(1).join("."),
|
|
94
|
+
};
|
|
89
95
|
} catch {
|
|
90
96
|
/* invalid */
|
|
91
97
|
}
|
|
@@ -95,7 +101,7 @@ export function parseContext(): NsiteContext | null {
|
|
|
95
101
|
}
|
|
96
102
|
|
|
97
103
|
export function isValidDTag(s: string): boolean {
|
|
98
|
-
return D_TAG_RE.test(s) && !s.endsWith(
|
|
104
|
+
return D_TAG_RE.test(s) && !s.endsWith("-");
|
|
99
105
|
}
|
|
100
106
|
|
|
101
107
|
// --- Relay communication ---
|
|
@@ -114,23 +120,29 @@ function withSocket(
|
|
|
114
120
|
url: string,
|
|
115
121
|
sendMsg: unknown[],
|
|
116
122
|
onMsg: (data: unknown[]) => boolean,
|
|
117
|
-
timeout = 5000
|
|
123
|
+
timeout = 5000,
|
|
118
124
|
): Promise<void> {
|
|
119
125
|
return new Promise((resolve) => {
|
|
120
126
|
try {
|
|
121
127
|
const ws = new WebSocket(url);
|
|
122
128
|
const timer = setTimeout(() => {
|
|
123
|
-
try {
|
|
129
|
+
try {
|
|
130
|
+
ws.close();
|
|
131
|
+
} catch { /* */ }
|
|
124
132
|
resolve();
|
|
125
133
|
}, timeout);
|
|
126
134
|
const finish = () => {
|
|
127
135
|
clearTimeout(timer);
|
|
128
|
-
try {
|
|
136
|
+
try {
|
|
137
|
+
ws.close();
|
|
138
|
+
} catch { /* */ }
|
|
129
139
|
resolve();
|
|
130
140
|
};
|
|
131
141
|
ws.onopen = () => ws.send(JSON.stringify(sendMsg));
|
|
132
142
|
ws.onmessage = (e) => {
|
|
133
|
-
try {
|
|
143
|
+
try {
|
|
144
|
+
if (onMsg(JSON.parse(e.data))) finish();
|
|
145
|
+
} catch { /* */ }
|
|
134
146
|
};
|
|
135
147
|
ws.onerror = () => finish();
|
|
136
148
|
} catch {
|
|
@@ -139,25 +151,29 @@ function withSocket(
|
|
|
139
151
|
});
|
|
140
152
|
}
|
|
141
153
|
|
|
142
|
-
async function queryRelays(
|
|
154
|
+
async function queryRelays(
|
|
155
|
+
urls: string[],
|
|
156
|
+
filter: Record<string, unknown>,
|
|
157
|
+
): Promise<RelayEvent[]> {
|
|
143
158
|
const events = new Map<string, RelayEvent>();
|
|
144
159
|
const subId = Math.random().toString(36).slice(2, 8);
|
|
145
160
|
await Promise.allSettled(
|
|
146
161
|
urls.map((url) =>
|
|
147
|
-
withSocket(url, [
|
|
148
|
-
if (msg[0] ===
|
|
162
|
+
withSocket(url, ["REQ", subId, filter], (msg) => {
|
|
163
|
+
if (msg[0] === "EVENT" && msg[1] === subId) {
|
|
149
164
|
events.set((msg[2] as RelayEvent).id, msg[2] as RelayEvent);
|
|
150
|
-
|
|
165
|
+
}
|
|
166
|
+
return msg[0] === "EOSE" && msg[1] === subId;
|
|
151
167
|
})
|
|
152
|
-
)
|
|
168
|
+
),
|
|
153
169
|
);
|
|
154
170
|
return [...events.values()];
|
|
155
171
|
}
|
|
156
172
|
|
|
157
173
|
async function publishRelay(url: string, event: SignedEvent): Promise<boolean> {
|
|
158
174
|
let ok = false;
|
|
159
|
-
await withSocket(url, [
|
|
160
|
-
if (msg[0] ===
|
|
175
|
+
await withSocket(url, ["EVENT", event], (msg) => {
|
|
176
|
+
if (msg[0] === "OK") {
|
|
161
177
|
ok = msg[2] === true;
|
|
162
178
|
return true;
|
|
163
179
|
}
|
|
@@ -166,9 +182,14 @@ async function publishRelay(url: string, event: SignedEvent): Promise<boolean> {
|
|
|
166
182
|
return ok;
|
|
167
183
|
}
|
|
168
184
|
|
|
169
|
-
export async function publishToRelays(
|
|
170
|
-
|
|
171
|
-
|
|
185
|
+
export async function publishToRelays(
|
|
186
|
+
urls: string[],
|
|
187
|
+
event: SignedEvent,
|
|
188
|
+
): Promise<number> {
|
|
189
|
+
const results = await Promise.allSettled(
|
|
190
|
+
urls.map((url) => publishRelay(url, event)),
|
|
191
|
+
);
|
|
192
|
+
return results.filter((r) => r.status === "fulfilled" && r.value).length;
|
|
172
193
|
}
|
|
173
194
|
|
|
174
195
|
// --- High-level operations ---
|
|
@@ -177,22 +198,32 @@ function extractWriteRelays(events: RelayEvent[]): string[] {
|
|
|
177
198
|
const relays = new Set<string>();
|
|
178
199
|
for (const e of events) {
|
|
179
200
|
for (const t of e.tags) {
|
|
180
|
-
if (
|
|
201
|
+
if (
|
|
202
|
+
t[0] === "r" && t[1]?.startsWith("wss://") &&
|
|
203
|
+
(!t[2] || t[2] === "write")
|
|
204
|
+
) {
|
|
181
205
|
relays.add(t[1].trim());
|
|
206
|
+
}
|
|
182
207
|
}
|
|
183
208
|
}
|
|
184
209
|
return [...relays];
|
|
185
210
|
}
|
|
186
211
|
|
|
187
|
-
export async function fetchManifest(
|
|
212
|
+
export async function fetchManifest(
|
|
213
|
+
ctx: NsiteContext,
|
|
214
|
+
): Promise<RelayEvent | null> {
|
|
188
215
|
const manifestFilter = ctx.identifier
|
|
189
|
-
? { kinds: [35128], authors: [ctx.pubkey],
|
|
216
|
+
? { kinds: [35128], authors: [ctx.pubkey], "#d": [ctx.identifier] }
|
|
190
217
|
: { kinds: [15128], authors: [ctx.pubkey], limit: 1 };
|
|
191
218
|
|
|
192
219
|
// Query bootstrap relays for manifest AND relay list in parallel
|
|
193
220
|
const [bootstrapManifests, relayEvents] = await Promise.all([
|
|
194
221
|
queryRelays(BOOTSTRAP_RELAYS, manifestFilter),
|
|
195
|
-
queryRelays(BOOTSTRAP_RELAYS, {
|
|
222
|
+
queryRelays(BOOTSTRAP_RELAYS, {
|
|
223
|
+
kinds: [10002],
|
|
224
|
+
authors: [ctx.pubkey],
|
|
225
|
+
limit: 5,
|
|
226
|
+
}),
|
|
196
227
|
]);
|
|
197
228
|
|
|
198
229
|
// If bootstrap already found it, return immediately
|
|
@@ -202,7 +233,7 @@ export async function fetchManifest(ctx: NsiteContext): Promise<RelayEvent | nul
|
|
|
202
233
|
|
|
203
234
|
// Otherwise try the owner's relays
|
|
204
235
|
const ownerRelays = extractWriteRelays(relayEvents).filter(
|
|
205
|
-
(r) => !BOOTSTRAP_RELAYS.includes(r)
|
|
236
|
+
(r) => !BOOTSTRAP_RELAYS.includes(r),
|
|
206
237
|
);
|
|
207
238
|
if (ownerRelays.length === 0) return null;
|
|
208
239
|
|
|
@@ -214,19 +245,21 @@ export async function getWriteRelays(pubkey: string): Promise<string[]> {
|
|
|
214
245
|
const events = await queryRelays(BOOTSTRAP_RELAYS, {
|
|
215
246
|
kinds: [10002],
|
|
216
247
|
authors: [pubkey],
|
|
217
|
-
limit: 5
|
|
248
|
+
limit: 5,
|
|
218
249
|
});
|
|
219
250
|
const relays = extractWriteRelays(events);
|
|
220
|
-
return relays.length > 0
|
|
251
|
+
return relays.length > 0
|
|
252
|
+
? relays
|
|
253
|
+
: BOOTSTRAP_RELAYS.filter((r) => r !== "wss://purplepag.es");
|
|
221
254
|
}
|
|
222
255
|
|
|
223
256
|
export async function checkExistingSite(
|
|
224
257
|
relays: string[],
|
|
225
258
|
pubkey: string,
|
|
226
|
-
slug?: string
|
|
259
|
+
slug?: string,
|
|
227
260
|
): Promise<boolean> {
|
|
228
261
|
const filter = slug
|
|
229
|
-
? { kinds: [35128], authors: [pubkey],
|
|
262
|
+
? { kinds: [35128], authors: [pubkey], "#d": [slug], limit: 1 }
|
|
230
263
|
: { kinds: [15128], authors: [pubkey], limit: 1 };
|
|
231
264
|
const events = await queryRelays(relays, filter);
|
|
232
265
|
return events.length > 0;
|
|
@@ -236,8 +269,12 @@ const MAX_MUSE_TAGS = 9;
|
|
|
236
269
|
|
|
237
270
|
export function extractMuses(event: RelayEvent): Muse[] {
|
|
238
271
|
return event.tags
|
|
239
|
-
.filter((t) => t[0] ===
|
|
240
|
-
.map((t) => ({
|
|
272
|
+
.filter((t) => t[0] === "muse" && t[1] && t[2])
|
|
273
|
+
.map((t) => ({
|
|
274
|
+
index: parseInt(t[1], 10),
|
|
275
|
+
pubkey: t[2],
|
|
276
|
+
relays: t.slice(3),
|
|
277
|
+
}))
|
|
241
278
|
.sort((a, b) => a.index - b.index);
|
|
242
279
|
}
|
|
243
280
|
|
|
@@ -250,25 +287,30 @@ export function createDeployEvent(
|
|
|
250
287
|
deployerPubkey: string;
|
|
251
288
|
deployerRelays: string[];
|
|
252
289
|
noTrail?: boolean;
|
|
253
|
-
}
|
|
290
|
+
},
|
|
254
291
|
): EventTemplate {
|
|
255
292
|
const tags: string[][] = [];
|
|
256
|
-
if (options.slug) tags.push([
|
|
293
|
+
if (options.slug) tags.push(["d", options.slug]);
|
|
257
294
|
for (const t of source.tags) {
|
|
258
|
-
if (t[0] ===
|
|
295
|
+
if (t[0] === "path" || t[0] === "server") tags.push([...t]);
|
|
259
296
|
}
|
|
260
297
|
|
|
261
298
|
if (!options.noTrail) {
|
|
262
299
|
// Paper trail: copy muse tags, add new one, enforce max 9
|
|
263
300
|
const sourceMuses = source.tags
|
|
264
|
-
.filter((t) => t[0] ===
|
|
301
|
+
.filter((t) => t[0] === "muse" && t[1] && t[2])
|
|
265
302
|
.map((t) => [...t])
|
|
266
303
|
.sort((a, b) => parseInt(a[1], 10) - parseInt(b[1], 10));
|
|
267
304
|
|
|
268
305
|
const maxIndex = sourceMuses.length > 0
|
|
269
306
|
? Math.max(...sourceMuses.map((t) => parseInt(t[1], 10)))
|
|
270
307
|
: -1;
|
|
271
|
-
const newMuse = [
|
|
308
|
+
const newMuse = [
|
|
309
|
+
"muse",
|
|
310
|
+
String(maxIndex + 1),
|
|
311
|
+
options.deployerPubkey,
|
|
312
|
+
...options.deployerRelays,
|
|
313
|
+
];
|
|
272
314
|
const allMuses = [...sourceMuses, newMuse];
|
|
273
315
|
|
|
274
316
|
// Keep index 0 (originator) + newest, FIFO truncate the middle
|
|
@@ -281,17 +323,106 @@ export function createDeployEvent(
|
|
|
281
323
|
}
|
|
282
324
|
}
|
|
283
325
|
|
|
284
|
-
if (options.title) tags.push([
|
|
285
|
-
if (options.description) tags.push([
|
|
326
|
+
if (options.title) tags.push(["title", options.title]);
|
|
327
|
+
if (options.description) tags.push(["description", options.description]);
|
|
286
328
|
return {
|
|
287
329
|
kind: options.slug ? 35128 : 15128,
|
|
288
330
|
created_at: Math.floor(Date.now() / 1000),
|
|
289
331
|
tags,
|
|
290
|
-
content:
|
|
332
|
+
content: "",
|
|
291
333
|
};
|
|
292
334
|
}
|
|
293
335
|
|
|
294
|
-
|
|
336
|
+
// --- Muse profile enrichment (streaming, non-blocking) ---
|
|
337
|
+
|
|
338
|
+
export function fetchMuseProfiles(
|
|
339
|
+
muses: Muse[],
|
|
340
|
+
baseDomain: string,
|
|
341
|
+
onUpdate: (pubkey: string, profile: MuseProfile) => void,
|
|
342
|
+
): void {
|
|
343
|
+
const pubkeys = [...new Set(muses.map((m) => m.pubkey))];
|
|
344
|
+
if (pubkeys.length === 0) return;
|
|
345
|
+
|
|
346
|
+
// Track best kind-0 per pubkey (highest created_at wins)
|
|
347
|
+
const bestK0 = new Map<
|
|
348
|
+
string,
|
|
349
|
+
{ created_at: number; profile: MuseProfile }
|
|
350
|
+
>();
|
|
351
|
+
const profileResolved = new Set<string>();
|
|
352
|
+
|
|
353
|
+
const handleEvent = (event: RelayEvent) => {
|
|
354
|
+
if (event.kind === 0) {
|
|
355
|
+
const existing = bestK0.get(event.pubkey);
|
|
356
|
+
if (existing && existing.created_at >= event.created_at) return;
|
|
357
|
+
try {
|
|
358
|
+
const meta = JSON.parse(event.content);
|
|
359
|
+
const name = meta.display_name || meta.name || meta.displayName;
|
|
360
|
+
if (!name) return;
|
|
361
|
+
const profile: MuseProfile = { npub: npubEncode(event.pubkey), name };
|
|
362
|
+
bestK0.set(event.pubkey, { created_at: event.created_at, profile });
|
|
363
|
+
profileResolved.add(event.pubkey);
|
|
364
|
+
onUpdate(event.pubkey, { ...profile });
|
|
365
|
+
} catch { /* invalid json */ }
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
// Stream kind 0 from profile relays — fire all in parallel, callback per event
|
|
370
|
+
const subId = Math.random().toString(36).slice(2, 8);
|
|
371
|
+
const filter = { kinds: [0], authors: pubkeys };
|
|
372
|
+
|
|
373
|
+
Promise.allSettled(
|
|
374
|
+
PROFILE_RELAYS.map((url) =>
|
|
375
|
+
withSocket(url, ["REQ", subId, filter], (msg) => {
|
|
376
|
+
if (msg[0] === "EVENT" && msg[1] === subId) {
|
|
377
|
+
handleEvent(msg[2] as RelayEvent);
|
|
378
|
+
}
|
|
379
|
+
return msg[0] === "EOSE" && msg[1] === subId;
|
|
380
|
+
})
|
|
381
|
+
),
|
|
382
|
+
).then(() => {
|
|
383
|
+
// After profiles are gathered, check for nsites on bootstrap relays
|
|
384
|
+
const resolved = [...profileResolved];
|
|
385
|
+
if (resolved.length === 0) return;
|
|
386
|
+
|
|
387
|
+
queryRelays(BOOTSTRAP_RELAYS, {
|
|
388
|
+
kinds: [15128, 35128],
|
|
389
|
+
authors: resolved,
|
|
390
|
+
}).then((events) => {
|
|
391
|
+
// Group by pubkey — prefer root (15128), fall back to first named (35128)
|
|
392
|
+
const nsiteMap = new Map<string, string>();
|
|
393
|
+
for (const e of events) {
|
|
394
|
+
if (e.kind === 15128 && !nsiteMap.has(e.pubkey)) {
|
|
395
|
+
nsiteMap.set(e.pubkey, buildSiteUrl(baseDomain, e.pubkey));
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
for (const e of events) {
|
|
399
|
+
if (e.kind === 35128 && !nsiteMap.has(e.pubkey)) {
|
|
400
|
+
const dTag = e.tags.find((t) => t[0] === "d")?.[1];
|
|
401
|
+
if (dTag) {
|
|
402
|
+
nsiteMap.set(
|
|
403
|
+
e.pubkey,
|
|
404
|
+
buildSiteUrl(baseDomain, e.pubkey, dTag),
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
for (const [pk, url] of nsiteMap) {
|
|
411
|
+
const entry = bestK0.get(pk);
|
|
412
|
+
if (entry) {
|
|
413
|
+
entry.profile.nsiteUrl = url;
|
|
414
|
+
onUpdate(pk, { ...entry.profile });
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
export function buildSiteUrl(
|
|
422
|
+
baseDomain: string,
|
|
423
|
+
pubkey: string,
|
|
424
|
+
slug?: string,
|
|
425
|
+
): string {
|
|
295
426
|
if (slug) {
|
|
296
427
|
return `https://${pubkeyToBase36(pubkey)}${slug}.${baseDomain}`;
|
|
297
428
|
}
|
package/src/qr.ts
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
import qrcode from
|
|
1
|
+
import { qrcode } from "@libs/qrcode";
|
|
2
2
|
|
|
3
3
|
export function toSvg(text: string): string {
|
|
4
|
-
|
|
5
|
-
qr.addData(text);
|
|
6
|
-
qr.make();
|
|
7
|
-
return qr.createSvgTag({ cellSize: 3, margin: 2, scalable: true });
|
|
4
|
+
return qrcode(text, { output: "svg", ecl: "LOW" });
|
|
8
5
|
}
|