@nsite/stealthis 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -0
- package/package.json +1 -1
- package/src/nostr.ts +15 -15
- package/src/widget.ts +28 -24
package/README.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# @nsite/stealthis
|
|
2
|
+
|
|
3
|
+
A drop-in web component that adds a "Borrow this" button to any nsite. Visitors can deploy a copy of the current page to their own nostr identity with one click.
|
|
4
|
+
|
|
5
|
+
## How it works
|
|
6
|
+
|
|
7
|
+
When loaded on an nsite, `<nsite-deploy>` 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
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
Add a script tag to your nsite. The widget auto-injects a fixed-position button in the bottom-right corner.
|
|
12
|
+
|
|
13
|
+
```html
|
|
14
|
+
<script src="https://unpkg.com/@nsite/stealthis/dist/stealthis.js"></script>
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Or use the ES module:
|
|
18
|
+
|
|
19
|
+
```js
|
|
20
|
+
import '@nsite/stealthis';
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Manual placement
|
|
24
|
+
|
|
25
|
+
If you want to control where the button appears, add the element yourself (the auto-inject will skip if one already exists):
|
|
26
|
+
|
|
27
|
+
```html
|
|
28
|
+
<nsite-deploy button-text="Cop this joint" stat-text="%s npubs copped this"></nsite-deploy>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Attributes
|
|
32
|
+
|
|
33
|
+
| Attribute | Default | Description |
|
|
34
|
+
|-----------|---------|-------------|
|
|
35
|
+
| `button-text` | `"Borrow this nsite"` | Button text |
|
|
36
|
+
| `stat-text` | `"%s npubs borrowed this nsite"` | Paper trail summary. `%s` is replaced with the count. |
|
|
37
|
+
| `no-trail` | _(absent)_ | Boolean attribute. When present, hides the paper trail. |
|
|
38
|
+
|
|
39
|
+
The button's `trigger` part is exposed via `::part(trigger)` for CSS customization.
|
|
40
|
+
|
|
41
|
+
## Authentication
|
|
42
|
+
|
|
43
|
+
The widget supports three sign-in methods:
|
|
44
|
+
|
|
45
|
+
- **NIP-07** - Browser extension (e.g. nos2x, Alby)
|
|
46
|
+
- **NIP-46 Bunker URI** - Paste a `bunker://` connection string
|
|
47
|
+
- **NIP-46 QR / Nostr Connect** - Scan a QR code with a signer app
|
|
48
|
+
|
|
49
|
+
## Deploy options
|
|
50
|
+
|
|
51
|
+
- **Root site** - Publishes as the visitor's primary nsite (kind `15128`)
|
|
52
|
+
- **Named site** - Publishes as a named sub-site with a slug (kind `35128`)
|
|
53
|
+
|
|
54
|
+
If the visitor already has a root site, the form defaults to a named site. Overwriting an existing site requires explicit confirmation.
|
|
55
|
+
|
|
56
|
+
## Paper trail
|
|
57
|
+
|
|
58
|
+
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.
|
|
59
|
+
|
|
60
|
+
## Development
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pnpm install
|
|
64
|
+
pnpm run build # builds dist/stealthis.js (IIFE) and dist/stealthis.mjs (ESM)
|
|
65
|
+
pnpm run dev # watch mode
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## License
|
|
69
|
+
|
|
70
|
+
MIT
|
package/package.json
CHANGED
package/src/nostr.ts
CHANGED
|
@@ -9,7 +9,7 @@ export interface NsiteContext {
|
|
|
9
9
|
baseDomain: string;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
export interface
|
|
12
|
+
export interface Muse {
|
|
13
13
|
index: number;
|
|
14
14
|
pubkey: string;
|
|
15
15
|
relays: string[];
|
|
@@ -232,11 +232,11 @@ export async function checkExistingSite(
|
|
|
232
232
|
return events.length > 0;
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
-
const
|
|
235
|
+
const MAX_MUSE_TAGS = 9;
|
|
236
236
|
|
|
237
|
-
export function
|
|
237
|
+
export function extractMuses(event: RelayEvent): Muse[] {
|
|
238
238
|
return event.tags
|
|
239
|
-
.filter((t) => t[0] === '
|
|
239
|
+
.filter((t) => t[0] === 'muse' && t[1] && t[2])
|
|
240
240
|
.map((t) => ({ index: parseInt(t[1], 10), pubkey: t[2], relays: t.slice(3) }))
|
|
241
241
|
.sort((a, b) => a.index - b.index);
|
|
242
242
|
}
|
|
@@ -257,25 +257,25 @@ export function createDeployEvent(
|
|
|
257
257
|
if (t[0] === 'path' || t[0] === 'server') tags.push([...t]);
|
|
258
258
|
}
|
|
259
259
|
|
|
260
|
-
// Paper trail: copy
|
|
261
|
-
const
|
|
262
|
-
.filter((t) => t[0] === '
|
|
260
|
+
// Paper trail: copy muse tags, add new one, enforce max 9
|
|
261
|
+
const sourceMuses = source.tags
|
|
262
|
+
.filter((t) => t[0] === 'muse' && t[1] && t[2])
|
|
263
263
|
.map((t) => [...t])
|
|
264
264
|
.sort((a, b) => parseInt(a[1], 10) - parseInt(b[1], 10));
|
|
265
265
|
|
|
266
|
-
const maxIndex =
|
|
267
|
-
? Math.max(...
|
|
266
|
+
const maxIndex = sourceMuses.length > 0
|
|
267
|
+
? Math.max(...sourceMuses.map((t) => parseInt(t[1], 10)))
|
|
268
268
|
: -1;
|
|
269
|
-
const
|
|
270
|
-
const
|
|
269
|
+
const newMuse = ['muse', String(maxIndex + 1), options.deployerPubkey, ...options.deployerRelays];
|
|
270
|
+
const allMuses = [...sourceMuses, newMuse];
|
|
271
271
|
|
|
272
272
|
// Keep index 0 (originator) + newest, FIFO truncate the middle
|
|
273
|
-
if (
|
|
274
|
-
const originator =
|
|
275
|
-
const keep =
|
|
273
|
+
if (allMuses.length > MAX_MUSE_TAGS) {
|
|
274
|
+
const originator = allMuses[0];
|
|
275
|
+
const keep = allMuses.slice(allMuses.length - (MAX_MUSE_TAGS - 1));
|
|
276
276
|
tags.push(originator, ...keep);
|
|
277
277
|
} else {
|
|
278
|
-
for (const t of
|
|
278
|
+
for (const t of allMuses) tags.push(t);
|
|
279
279
|
}
|
|
280
280
|
|
|
281
281
|
if (options.title) tags.push(['title', options.title]);
|
package/src/widget.ts
CHANGED
|
@@ -22,7 +22,7 @@ type State =
|
|
|
22
22
|
| 'error';
|
|
23
23
|
|
|
24
24
|
export class NsiteDeployButton extends HTMLElement {
|
|
25
|
-
static observedAttributes = ['
|
|
25
|
+
static observedAttributes = ['button-text', 'stat-text', 'no-trail'];
|
|
26
26
|
|
|
27
27
|
private shadow: ShadowRoot;
|
|
28
28
|
private state: State = 'idle';
|
|
@@ -45,8 +45,8 @@ export class NsiteDeployButton extends HTMLElement {
|
|
|
45
45
|
private ncConnect: ReturnType<typeof prepareNostrConnect> | null = null;
|
|
46
46
|
private manifestPromise: Promise<nostr.SignedEvent | null> | null = null;
|
|
47
47
|
private relaysPromise: Promise<string[]> | null = null;
|
|
48
|
-
private
|
|
49
|
-
private
|
|
48
|
+
private muses: nostr.Muse[] = [];
|
|
49
|
+
private musesExpanded = false;
|
|
50
50
|
|
|
51
51
|
constructor() {
|
|
52
52
|
super();
|
|
@@ -56,8 +56,8 @@ export class NsiteDeployButton extends HTMLElement {
|
|
|
56
56
|
this.manifestPromise = nostr.fetchManifest(this.ctx);
|
|
57
57
|
this.manifestPromise.then((manifest) => {
|
|
58
58
|
if (manifest) {
|
|
59
|
-
this.
|
|
60
|
-
if (this.
|
|
59
|
+
this.muses = nostr.extractMuses(manifest);
|
|
60
|
+
if (this.muses.length > 0 && this.state === 'idle') this.render();
|
|
61
61
|
}
|
|
62
62
|
});
|
|
63
63
|
this.render();
|
|
@@ -68,8 +68,12 @@ export class NsiteDeployButton extends HTMLElement {
|
|
|
68
68
|
if (this.state === 'idle') this.render();
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
private get
|
|
72
|
-
return this.getAttribute('
|
|
71
|
+
private get buttonText(): string {
|
|
72
|
+
return this.getAttribute('button-text') || 'Borrow this nsite';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private get statText(): string {
|
|
76
|
+
return this.getAttribute('stat-text') || '%s npubs borrowed this nsite';
|
|
73
77
|
}
|
|
74
78
|
|
|
75
79
|
private esc(s: string): string {
|
|
@@ -90,12 +94,12 @@ export class NsiteDeployButton extends HTMLElement {
|
|
|
90
94
|
|
|
91
95
|
switch (this.state) {
|
|
92
96
|
case 'idle':
|
|
93
|
-
html += `<button class="nd-trigger" part="trigger">${this.esc(this.
|
|
97
|
+
html += `<button class="nd-trigger" part="trigger">${this.esc(this.buttonText)}</button>`;
|
|
94
98
|
html += this.paperTrailContent();
|
|
95
99
|
break;
|
|
96
100
|
|
|
97
101
|
case 'auth':
|
|
98
|
-
html += `<button class="nd-trigger" disabled>${this.esc(this.
|
|
102
|
+
html += `<button class="nd-trigger" disabled>${this.esc(this.buttonText)}</button>`;
|
|
99
103
|
html += this.modal(this.authContent());
|
|
100
104
|
break;
|
|
101
105
|
|
|
@@ -118,12 +122,12 @@ export class NsiteDeployButton extends HTMLElement {
|
|
|
118
122
|
break;
|
|
119
123
|
|
|
120
124
|
case 'form':
|
|
121
|
-
html += `<button class="nd-trigger" disabled>${this.esc(this.
|
|
125
|
+
html += `<button class="nd-trigger" disabled>${this.esc(this.buttonText)}</button>`;
|
|
122
126
|
html += this.modal(this.formContent());
|
|
123
127
|
break;
|
|
124
128
|
|
|
125
129
|
case 'confirm':
|
|
126
|
-
html += `<button class="nd-trigger" disabled>${this.esc(this.
|
|
130
|
+
html += `<button class="nd-trigger" disabled>${this.esc(this.buttonText)}</button>`;
|
|
127
131
|
html += this.modal(this.confirmContent());
|
|
128
132
|
break;
|
|
129
133
|
|
|
@@ -151,7 +155,7 @@ export class NsiteDeployButton extends HTMLElement {
|
|
|
151
155
|
break;
|
|
152
156
|
|
|
153
157
|
case 'error':
|
|
154
|
-
html += `<button class="nd-trigger" part="trigger">${this.esc(this.
|
|
158
|
+
html += `<button class="nd-trigger" part="trigger">${this.esc(this.buttonText)}</button>`;
|
|
155
159
|
html += this.modal(`
|
|
156
160
|
<div class="nd-header">
|
|
157
161
|
<h2 class="nd-title">Error</h2>
|
|
@@ -172,18 +176,18 @@ export class NsiteDeployButton extends HTMLElement {
|
|
|
172
176
|
// --- Content builders ---
|
|
173
177
|
|
|
174
178
|
private paperTrailContent(): string {
|
|
175
|
-
if (this.
|
|
179
|
+
if (this.hasAttribute('no-trail') || this.muses.length === 0) return '';
|
|
176
180
|
|
|
177
181
|
let html = `<div class="nd-trail">
|
|
178
|
-
<button class="nd-trail-toggle" data-action="toggle-trail"
|
|
182
|
+
<button class="nd-trail-toggle" data-action="toggle-trail">${this.esc(this.statText.replace('%s', String(this.muses.length)))}</button>`;
|
|
179
183
|
|
|
180
|
-
if (this.
|
|
184
|
+
if (this.musesExpanded) {
|
|
181
185
|
html += `<div class="nd-trail-list">`;
|
|
182
|
-
const first = this.
|
|
183
|
-
const second = this.
|
|
186
|
+
const first = this.muses[0];
|
|
187
|
+
const second = this.muses.length > 1 ? this.muses[1] : null;
|
|
184
188
|
|
|
185
189
|
// Show originator
|
|
186
|
-
html += this.
|
|
190
|
+
html += this.museItem(first);
|
|
187
191
|
|
|
188
192
|
// Show gap if indices aren't sequential (truncated middle)
|
|
189
193
|
if (second && second.index > first.index + 1) {
|
|
@@ -192,8 +196,8 @@ export class NsiteDeployButton extends HTMLElement {
|
|
|
192
196
|
}
|
|
193
197
|
|
|
194
198
|
// Show the rest
|
|
195
|
-
for (let i = 1; i < this.
|
|
196
|
-
html += this.
|
|
199
|
+
for (let i = 1; i < this.muses.length; i++) {
|
|
200
|
+
html += this.museItem(this.muses[i]);
|
|
197
201
|
}
|
|
198
202
|
html += `</div>`;
|
|
199
203
|
}
|
|
@@ -202,11 +206,11 @@ export class NsiteDeployButton extends HTMLElement {
|
|
|
202
206
|
return html;
|
|
203
207
|
}
|
|
204
208
|
|
|
205
|
-
private
|
|
206
|
-
const npub = nostr.npubEncode(
|
|
209
|
+
private museItem(muse: nostr.Muse): string {
|
|
210
|
+
const npub = nostr.npubEncode(muse.pubkey);
|
|
207
211
|
const short = npub.slice(0, 12) + '...' + npub.slice(-4);
|
|
208
212
|
return `<div class="nd-trail-item">
|
|
209
|
-
<span class="nd-trail-idx">#${
|
|
213
|
+
<span class="nd-trail-idx">#${muse.index}</span>
|
|
210
214
|
<span class="nd-trail-pk">${short}</span>
|
|
211
215
|
</div>`;
|
|
212
216
|
}
|
|
@@ -345,7 +349,7 @@ export class NsiteDeployButton extends HTMLElement {
|
|
|
345
349
|
this.shadow
|
|
346
350
|
.querySelector('[data-action="toggle-trail"]')
|
|
347
351
|
?.addEventListener('click', () => {
|
|
348
|
-
this.
|
|
352
|
+
this.musesExpanded = !this.musesExpanded;
|
|
349
353
|
this.render();
|
|
350
354
|
});
|
|
351
355
|
this.shadow
|