@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nsite/stealthis",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "main": "dist/stealthis.js",
6
6
  "module": "dist/stealthis.mjs",
package/src/nostr.ts CHANGED
@@ -9,7 +9,7 @@ export interface NsiteContext {
9
9
  baseDomain: string;
10
10
  }
11
11
 
12
- export interface Thief {
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 MAX_THIEF_TAGS = 9;
235
+ const MAX_MUSE_TAGS = 9;
236
236
 
237
- export function extractThieves(event: RelayEvent): Thief[] {
237
+ export function extractMuses(event: RelayEvent): Muse[] {
238
238
  return event.tags
239
- .filter((t) => t[0] === 'thief' && t[1] && t[2])
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 thief tags, add new one, enforce max 9
261
- const sourceThieves = source.tags
262
- .filter((t) => t[0] === 'thief' && t[1] && t[2])
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 = sourceThieves.length > 0
267
- ? Math.max(...sourceThieves.map((t) => parseInt(t[1], 10)))
266
+ const maxIndex = sourceMuses.length > 0
267
+ ? Math.max(...sourceMuses.map((t) => parseInt(t[1], 10)))
268
268
  : -1;
269
- const newThief = ['thief', String(maxIndex + 1), options.deployerPubkey, ...options.deployerRelays];
270
- const allThieves = [...sourceThieves, newThief];
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 (allThieves.length > MAX_THIEF_TAGS) {
274
- const originator = allThieves[0];
275
- const keep = allThieves.slice(allThieves.length - (MAX_THIEF_TAGS - 1));
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 allThieves) tags.push(t);
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 = ['label'];
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 thieves: nostr.Thief[] = [];
49
- private thievesExpanded = false;
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.thieves = nostr.extractThieves(manifest);
60
- if (this.thieves.length > 0 && this.state === 'idle') this.render();
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 buttonLabel(): string {
72
- return this.getAttribute('label') || 'Borrow this';
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.buttonLabel)}</button>`;
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.buttonLabel)}</button>`;
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.buttonLabel)}</button>`;
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.buttonLabel)}</button>`;
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.buttonLabel)}</button>`;
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.thieves.length === 0) return '';
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">Stolen by ${this.thieves.length} npub${this.thieves.length === 1 ? '' : 's'}</button>`;
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.thievesExpanded) {
184
+ if (this.musesExpanded) {
181
185
  html += `<div class="nd-trail-list">`;
182
- const first = this.thieves[0];
183
- const second = this.thieves.length > 1 ? this.thieves[1] : null;
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.thiefItem(first);
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.thieves.length; i++) {
196
- html += this.thiefItem(this.thieves[i]);
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 thiefItem(thief: nostr.Thief): string {
206
- const npub = nostr.npubEncode(thief.pubkey);
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">#${thief.index}</span>
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.thievesExpanded = !this.thievesExpanded;
352
+ this.musesExpanded = !this.musesExpanded;
349
353
  this.render();
350
354
  });
351
355
  this.shadow