diff --git a/src/help/nwc.md b/src/help/nwc.md new file mode 100644 index 0000000..990e5d7 --- /dev/null +++ b/src/help/nwc.md @@ -0,0 +1,98 @@ +# NWC — Nostr Wallet Connect setup + +NWC (NIP-47) lets Artstr send Lightning payments through your +wallet without leaving the app. Required for one-click zapping + +unlocking premium designs. + +## Quick start (CoinOS, recommended) + +CoinOS is a free web-based wallet that supports NWC. Easiest path +if you don't already have a Lightning wallet: + +1. Sign up at . +2. Open **Settings → Connect** in the CoinOS app. +3. Create a new Nostr Wallet Connect link. Set a **budget** that + comfortably covers the zaps + unlocks you expect (a few + thousand sats is plenty to start). +4. Copy the connection string (starts with `nostr+walletconnect://`). +5. In Artstr, open **Settings → Lightning wallet**. +6. Paste the connection string + click **Connect**. +7. The status line confirms the connection + lists the wallet's + advertised capabilities (`pay_invoice` is the required one). + +## Other supported wallets + +Any NIP-47 wallet works: + +- **Alby Hub** — self-hosted; nostr-native; great for power users. +- **Mutiny** — browser-based, open-source. +- **Phoenix** — mobile-first, advertises NWC. +- **Cashu.me** — ecash custodial. +- **LNbits** — self-hosted; the NWC extension exposes a + connection string per wallet. + +In every case the pattern is the same: the wallet generates a +`nostr+walletconnect://...` URI that you paste into Artstr. + +## Multi-device sync + +The **Sync this wallet to my Nostr account** checkbox (appears +once connected) publishes your NWC URI as a kind-30078 event, +encrypted to your own pubkey via NIP-44. On any other device where +you sign in with the same Nostr key, Artstr finds the synced event, +decrypts it, and auto-reconnects without you pasting the URI +again. + +Privacy: the synced event is NIP-44-encrypted to **only your own +pubkey**. Relays + other clients can't read the URI — only an +extension holding your nsec can decrypt it. + +## Sending a zap with NWC connected + +1. Click the **⚡ Zap** button on any design / page / chapter (or + the **Unlock with ⚡ zap** on a premium row). +2. Amount picker opens with presets. +3. Artstr builds a zap request, gets the BOLT11 invoice from the + recipient's LSP, and sends `pay_invoice` through your NWC + connection. +4. The wallet pays; the LSP issues the zap receipt; the UI + refreshes. + +End-to-end usually completes in under 3 seconds. + +## Test connection + +The **Test connection** button (next to Disconnect) runs a +zero-amount pay_invoice round-trip to verify the wallet is +reachable + capable. Useful before assuming the integration is +broken. + +## Capability warnings + +If the wallet doesn't advertise `pay_invoice`, the UI shows an +⚠ banner — unlocks will fail. Reconnect via a different wallet's +URI or enable `pay_invoice` in the source wallet (most wallets +ship it on; some self-hosted setups disable it by default). + +## Security model + +- The connection string contains the wallet's relay URL + a + client-side secret key Artstr uses to sign NIP-04 messages to + the wallet. **Treat it like a password** — anyone with the URI + can send `pay_invoice` up to the configured budget. +- Set a **budget** on the wallet side that caps total spending. + Most NWC wallets let you renew the budget periodically. +- Setting a low per-payment maximum (e.g., 1 000 sats) is a good + default for unlocking designs. +- Revoking NWC is a one-click action on the wallet side — the URI + becomes inert immediately. + +## Caveats + +- NWC over relays adds latency (signed event → relay → wallet → + signed receipt → relay → client). Expect 1-3 seconds per zap on + a healthy connection. +- If the wallet's relay is down, zaps queue but won't complete. + The wallet usually has multiple relays as fallbacks. +- Mobile browsers sometimes throttle background WebSockets; if + zaps time out, foreground the Artstr tab + retry. diff --git a/src/help/panel-print-save.md b/src/help/panel-print-save.md index 8e1f62a..20ee43c 100644 --- a/src/help/panel-print-save.md +++ b/src/help/panel-print-save.md @@ -33,8 +33,9 @@ publishing to Nostr lives in the top-right Publish flow, not here. ## Export - **Export JPEG** — direct rasterised export at the artboard's - natural pixel size, JPEG-compressed (~0.92 quality). One file - per artboard. + natural pixel size, JPEG-compressed. The resolution dropdown + picks between **Low (96 DPI)**, **Standard (150 DPI)**, and + **Print (300 DPI)**. - **Export PNG** — same but as PNG (lossless). Sometimes available only for modes that support transparent backgrounds. - Pixel-art mode has its own dedicated exports (PNG at multiple @@ -43,6 +44,30 @@ publishing to Nostr lives in the top-right Publish flow, not here. - Book mode has its own dedicated **Export PDF…** flow (page range, bleed, crop marks, split output) from the Pages overview toolbar. +### Why JPEG sometimes routes through PDF + +The direct-JPEG path renders every layer onto a hidden canvas, then +extracts the canvas as a JPEG. That fails in three cases: + +1. **CORS-blocked images** — if any image layer's source server + doesn't send `Access-Control-Allow-Origin: *`, the browser + refuses to embed the pixels in the canvas. The "Render canvas" + path can't bypass this; the print engine can. +2. **Tainted canvas** — the canvas accepted the image but flagged it + "tainted" once it tried to read pixels. Same root cause as #1 + but caught at a different stage. +3. **Unsupported modes** — pixel-art (uses its own dedicated + exporter) and standalone decks (which have a multi-slide + exporter elsewhere) don't go through this path. + +In any of those cases, the JPEG button automatically opens the +**Print → PDF** flow: the browser's print engine has a separate +image pipeline that handles cross-origin embeds the canvas can't +touch. You'll see a toast explaining the switch ("N image(s) +couldn't be embedded directly — switching to the PDF route"). Save +the resulting PDF, then re-export it as JPEG from a PDF viewer if +you specifically need a flat JPEG. + ## Designer tab The Designer tab has a separate `designerPrintSaveFieldset` with diff --git a/src/help/publishing-premium.md b/src/help/publishing-premium.md new file mode 100644 index 0000000..6874d0c --- /dev/null +++ b/src/help/publishing-premium.md @@ -0,0 +1,101 @@ +# Premium publishing (zap-gated) + +Sell designs for sats. The published event ships a watermarked +preview anyone can browse + an encrypted payload only paying +viewers can decrypt. Discovery + browsing is public; the design +itself is gated. + +## How it works + +1. The full design payload is encrypted via the same AES-256-GCM + pipeline as a private publish, but with a special **softgate** + key derivation. +2. The key derives from public event material (event id, pubkey, + d-tag) plus an obfuscated per-epoch pepper baked into the + static Artstr client. +3. A watermarked JPEG preview (max 720 px long axis, ~50 KB + budget) ships in the unencrypted envelope so buyers can see + what they'd be unlocking. +4. The event publishes with: + - `['t', 'casewrap']`, `['t', 'casewrap-']` — discovery. + - `['title', …]`, `['category', …]` — public metadata. + - `['premium', 'true']`. + - `['zap-gate', 'true']`, `['zap-min', '']`, `['zap-split', 'required']`. + - `['zap', , , '']` — creator share. + - `['zap', , , '']` — platform fee. +5. When a buyer's zap receipt for this event lands on relays, the + client computes the soft-gate key + decrypts. + +## Soft-gate is "soft" + +Honest framing: this raises friction against drive-by scraping but +a determined reverse-engineer can recover the client pepper and +decrypt without paying. The mode is for low-trust commercial +distribution, not DRM-grade gating. Stronger DRM can swap in later +without changing the buyer side. + +## What you need before publishing premium + +- A **lightning address** (`lud16`) on your kind-0 Nostr profile. + Set this on any Nostr client that edits your profile metadata + (Damus, Amethyst, nos2x, etc). Without it the platform can't + route the buyer's payment to you. +- A non-empty title + category (same as any publish). +- A working artboard preview — the modal renders the watermarked + JPEG live + refuses to publish if rendering fails (CORS images + etc). + +## Publishing flow + +1. Set Title + Category. +2. Click **Publish to Nostr**. +3. In the confirm modal: + - Keep visibility on **Public**. + - Toggle **Zap-gated (premium)** on. + - Set the **Minimum zap** in sats (default 100). + - Read the split note (creator: 70 %, platform: 30 % by default; + see the per-event `zap` tags for the exact split that ships). +4. Click **Publish**. + +## How buyers unlock + +In the community browser's **Premium** tab, a locked design shows +the watermarked preview + an **Unlock with ⚡ zap** button. Clicking +it: + +1. Opens the buyer's connected NWC wallet (see + [NWC setup](#/help/nwc)). +2. Builds a NIP-57 zap request for the minimum amount. +3. Sends `pay_invoice` through the NWC connection. +4. On a successful zap receipt, derives the soft-gate key + decrypts + the design in-place. +5. Caches the decrypted payload in the **My Purchases** vault so + later re-visits skip the re-zap. + +## My Purchases vault + +Your unlocks live in a **vault** encrypted to your own pubkey (so +they sync across devices the same way private publishes do). +Capped at a fixed size; if the vault overflows, the oldest items +get evicted with a visible failure rather than silently lost. + +## Editing a premium event + +The same NIP-33 replaceability applies — re-publishing updates the +event in place. Existing buyers' vault entries continue working +against the new event since the soft-gate key derives from the +d-tag, which stays stable. + +## Caveats + +- Decks ship a per-slide watermarked preview (up to 6 slides) so + buyers can flip through what they'd unlock; non-deck modes get a + single preview JPEG. +- The platform fee is currently a fixed 30/70 split on the event's + zap tags. Future updates may make this configurable. +- The buyer's NWC wallet must support `pay_invoice` (almost all + do). + +## Deeper reference + +- Spec: `docs/PREMIUM_DESIGNS_FEATURE.md` in the GitHub repo. diff --git a/src/help/publishing-private.md b/src/help/publishing-private.md new file mode 100644 index 0000000..4d7012e --- /dev/null +++ b/src/help/publishing-private.md @@ -0,0 +1,71 @@ +# Private publishing + +Encrypted-to-self designs. Same Nostr event format as a public +design, but `content` is an encrypted envelope and the metadata +tags are stripped down so the design doesn't appear in any +community feed. + +## How the encryption works + +1. The full design payload is JSON-serialised. +2. Gzipped (when the browser supports `CompressionStream`). +3. Encrypted with a fresh AES-256-GCM key. +4. The AES key is wrapped to **your own Nostr pubkey** via either + NIP-44 (preferred) or NIP-04 (fallback). +5. The wrapped key + iv + ciphertext go into an `envelope` JSON + object that becomes the event's `content` field. + +Only someone signing with your Nostr key can derive the AES key +and decrypt the payload. The event is technically broadcast to all +relays, but it's mathematically opaque to everyone else. + +## What's still visible + +- Your **pubkey** (every Nostr event is signed and the pubkey is + part of the signature). +- The event's `kind`, `created_at`, `d` tag, and `encrypted` tag. +- The fact that you published *something* at this time. + +What's **not** visible: title, category, IDs, layers, dimensions, +images, or any other payload content. + +## When to use it + +- Working drafts you want backed up across devices but not shared. +- Personal projects you don't want indexed. +- Sensitive content where even the title shouldn't leak. + +## Editing across devices + +Open Artstr on another device, sign in with the same Nostr +extension, and click **Mine** in the community browser. Your +private events appear (decrypted lazily as you click them). The +**Private** tab specifically filters to your encrypted events. + +## Caveats + +- Requires a Nostr extension that supports NIP-44 or NIP-04 + encryption. Most do; alby and nos2x both work. +- The d-tag for a private design is a random opaque string + (`artstr-private-`) so it can't be guessed. Don't lose + the d-tag if you intentionally want to overwrite the event + later — it's stored in your project's autosave under + `state.privateDTag`. +- **Sharing privately with one specific other person** is *not* + supported in v1. The current encryption only wraps the AES key + to your own pubkey. Multi-recipient envelopes are a future arc. + +## Books + +The book sidebar has its own **🔒 Private** chip. Flipping it +encrypts the manifest **and** every sidecar event (pages, +masters, cover, chapters) on subsequent publishes. See +[Book Designer](#/help/book) for the full sidecar flow. + +A private book's chapter events are kind-30078 (encrypted), not +kind-30023 NIP-23 articles, so external NIP-23 readers won't see +them — which is the desired behaviour for a private book. + +## Deeper reference + +- Spec: `docs/PRIVATE_PUBLISHING_FEATURE.md` in the GitHub repo. diff --git a/src/help/publishing-public.md b/src/help/publishing-public.md new file mode 100644 index 0000000..368e04d --- /dev/null +++ b/src/help/publishing-public.md @@ -0,0 +1,79 @@ +# Public publishing + +The default tier. Plain-text event the relays distribute to anyone +who asks for it. Discoverable in the community browser, forkable, +embeddable. + +## What the event looks like + +- `kind: 30078` (or `30023` for book chapters). +- `tags`: + - `['d', ':']` — the addressable identifier. + Replaceable per NIP-33: re-publishing under the same d-tag + replaces the event in place. + - `['t', 'casewrap']` + `['t', '']` (e.g. + `casewrap-cover`, `casewrap-deck`, `casewrap-book`). + - `['title', '']`, `['category', '<…>']`, + `['language', '<…>']`. + - External ID tags when you've filled them in: `['i', 'imdb:…']`, + `['i', 'upc:…']`, `['i', 'tvdb:…']`, etc. + - `['client', 'Artstr Studio']`. + - For books / linked designs: `['p', '', …, + 'design-contributor']` + `['link', '', …]`. +- `content`: JSON-serialised design payload. Every layer, every + mode-specific block, every reference. + +## Discoverability + +- Listed in the **community browser** under the New / Popular / + Zapped tabs, filterable by category + language. +- Searchable via the Search tab + Nostr search relays that index + `casewrap` content. +- Linked from share URLs: `/#/share/` resolves to a + read-only view + Fork button. + +## Publishing flow + +1. Set **Title** + **Category** in Project Metadata. +2. Click the **Publish to Nostr** button (top right). +3. Confirm modal opens with: + - A preview pane. + - Metadata list (Type / Title / Category / Author / etc). + - **Public / Private** radio (defaults to Public). + - **Zap-gated (premium)** toggle (defaults off → public free). +4. Click **Publish**. +5. The event signs through your Nostr extension + broadcasts to + the relay list in **Settings → Relays**. + +## Editing a published design + +Loading a public design from the community browser drops you in +**edit mode** — the publish button now says "Publish update" and +preserves the original d-tag. Republishing replaces the event in +place via NIP-33. + +To **fork** instead of edit (so your version becomes a new event +under your pubkey), use the community browser's **Fork** action on +the design row — it clears the editing state so your next publish +mints a fresh d-tag. + +## Forking + attribution + +Linked-design layers from other authors auto-emit: + +- `p` tags for each contributor (so they get notifications + can + be discovered as design contributors). +- `link` tags so external clients can resolve the sub-designs. + +See the [Credits panel](#/help/panel-credits) for what gets surfaced +in-app + at publish time. + +## Caveats + +- Anything you upload as an image layer is embedded in the event + payload as a `data:` URL — Nostr relays may reject very large + events. For high-res photo layers, prefer publishing the photo + somewhere else and linking via an `https://` URL. +- Even with private toggles off, the metadata tags (title, + category) are public. The encryption only protects the `content` + field. diff --git a/src/help/publishing.md b/src/help/publishing.md new file mode 100644 index 0000000..bc389ed --- /dev/null +++ b/src/help/publishing.md @@ -0,0 +1,66 @@ +# Publishing — overview + +Artstr Studio publishes designs to Nostr instead of a private +server. Every published design becomes a kind-30078 event (or +kind-30023 for book chapters) that anyone can fetch, fork, or +embed. + +There are three visibility tiers: + +| Tier | Who can open it | Use case | +|------|-----------------|----------| +| **[Public](#/help/publishing-public)** | Anyone with the naddr / nevent. | Default — discoverable in the community browser, forkable. | +| **[Private](#/help/publishing-private)** | Only you, via the same Nostr key (any device). | Working drafts, personal projects, archives. End-to-end encrypted. | +| **[Premium](#/help/publishing-premium)** | Anyone who pays a one-time zap (sats). | Paid templates / books. Watermarked preview + an unlock CTA. | + +## Choosing a tier + +The choice is made in the **Publish to Nostr** confirm modal: + +- Top of the modal shows a **Public / Private** radio. +- A **Zap-gated (premium)** toggle below it (hidden when Private is + selected — encrypted designs can't be feed-listed, so gating them + for sale doesn't make sense). +- The book sidebar has its own **🔒 Private** chip that flips + visibility for the whole book + every sidecar event. + +## How the publish event differs per tier + +| | Public | Private | Premium | +|---|---|---|---| +| Event kind | 30078 | 30078 | 30078 | +| `t` tag | `casewrap` + mode-specific | `encrypted` | `casewrap` + `premium` | +| `content` field | JSON payload | encrypted envelope | encrypted envelope | +| Preview JPEG | none | none | watermarked, embedded | +| Discovery | community browser + naddr share | naddr share only | community browser (premium tab) | +| Lightning | `zap` tags for tipping | none | `zap` tags split between creator + platform | + +## Tipping vs unlocking (NIP-57 zaps) + +Zaps are NIP-57 lightning payments. Two flavours: + +- **Tipping** — any user can zap any design / page / chapter. Goes + 100% to the creator's lightning address (no platform share). + Works on public, private (for you only), and premium designs. +- **Unlocking** — a premium design's zap requirement. The buyer zaps + the required minimum; on receipt of a valid zap receipt, the + client decrypts the design. Split: creator share + platform fee. + +See [Zaps](#/help/zaps) for the full mechanics + how to enable +zapping in your profile. + +## Sending zaps + +You need a **NIP-47 wallet** (Nostr Wallet Connect, or NWC) connected +to Artstr so it can sign + send invoice payments without leaving the +app. See [NWC setup](#/help/nwc). + +## Editing + republishing + +All published events are **replaceable** (NIP-33). Re-publishing under +the same d-tag updates the event in place — old `naddr` share links +keep working; `nevent` (snapshot) share links point at the old +version. + +The community browser's **Mine** tab shows every event you've +published with a "Republish" + "Delete" action per row. diff --git a/src/help/zaps.md b/src/help/zaps.md new file mode 100644 index 0000000..c9ee5e1 --- /dev/null +++ b/src/help/zaps.md @@ -0,0 +1,93 @@ +# Zaps — NIP-57 lightning payments + +Artstr uses Nostr-native lightning zaps for both tipping creators +and unlocking premium designs. Same protocol either way; the +difference is who the zap goes to and what the client does on +receipt. + +## How zaps work in two paragraphs + +A zap is a kind-9735 receipt event proving that a Lightning +payment happened. The flow: + +1. The user (you) signs a kind-9734 **zap request** event with the + amount + recipient + intent. +2. The recipient's lightning address (`lud16` on their profile) + resolves to a LNURL endpoint; the request becomes a BOLT11 + invoice. +3. You pay the invoice from your wallet. The wallet's LSP issues + a **zap receipt** event back to the relays. +4. Clients that see the receipt update UI (zap-totals, unlock + gated content, etc). + +## Tipping any design / page / chapter + +Every design preview pane has a **⚡ Zap** button (visible when +the author has a lightning address on their profile). Clicking: + +1. Opens an amount picker (default presets 21 / 100 / 1000 sats + + custom). +2. Builds + signs the zap request via your Nostr extension. +3. Pays through your connected **NWC wallet** (see + [NWC setup](#/help/nwc)). If you don't have NWC connected, it + falls back to copying the invoice for you to pay manually in + any Lightning wallet. + +The zap goes 100% to the author. No platform share. + +## Unlocking a premium design + +Same plumbing, different outcome. The locked design row shows an +**Unlock with ⚡ zap** button. Clicking sends the minimum required +amount, split between the creator + the platform per the event's +`zap` tags (currently 70/30). On receipt: + +1. The client verifies the receipt's amount + recipient match the + event's `zap-min` + `zap` tags. +2. Derives the soft-gate key from the event metadata. +3. Decrypts the payload + saves it to your **My Purchases** + vault. + +See [Premium publishing](#/help/publishing-premium) for the publish +side. + +## Receiving zaps on your designs + +To get tipped or sold-to: + +1. Sign up at any LN-address service. **CoinOS** (free, web-based) + is the easiest: . +2. Edit your Nostr kind-0 profile to add `lud16` = your LN address + (e.g. `you@coinos.io`). Any Nostr client that edits profiles + works (Damus, Amethyst, primal.net, nos2x options page). +3. Wait a few minutes for your profile to propagate to the relays + you care about. + +Once `lud16` is set, every published design of yours becomes +zap-able automatically — Artstr discovers the lightning address +from your profile. + +## Sending zaps without an NWC wallet + +If you don't have NWC connected, the zap modal will: + +- Build the BOLT11 invoice. +- Show a QR code + a copy button. +- Wait for the invoice to be paid externally (you can pay from any + Lightning wallet — Phoenix, Wallet of Satoshi, etc). +- Verify the receipt on relays + update the UI. + +This is slower than NWC (you switch apps to pay) but works +universally. + +## Tips + +- The **Zapped** tab in the community browser sorts designs by + total zap volume — a quick way to find what people are paying + for. +- Zaps are public on Nostr (the receipt event is signed by the + LSP and broadcast). If you want anonymous payment, you'll need a + separate Lightning identity. +- Failed zaps (invoice paid but receipt never arrives) can be + diagnosed via the wallet's transaction log. The LSP issued the + receipt; the relays may have dropped it. diff --git a/src/index.html b/src/index.html index 38830ac..ba34c7f 100644 --- a/src/index.html +++ b/src/index.html @@ -12074,6 +12074,12 @@ { slug: 'panel-print-save', title: 'Save / Print', group: 'Sidebar panels', file: 'help/panel-print-save.md' }, { slug: 'panel-credits', title: 'Credits', group: 'Sidebar panels', file: 'help/panel-credits.md' }, { slug: 'panel-tool-options', title: 'Tool Options pane', group: 'Sidebar panels', file: 'help/panel-tool-options.md' }, + { slug: 'publishing', title: 'Overview', group: 'Publishing & payments', file: 'help/publishing.md' }, + { slug: 'publishing-public', title: 'Public publishing', group: 'Publishing & payments', file: 'help/publishing-public.md' }, + { slug: 'publishing-private', title: 'Private publishing', group: 'Publishing & payments', file: 'help/publishing-private.md' }, + { slug: 'publishing-premium', title: 'Premium (zap-gated)', group: 'Publishing & payments', file: 'help/publishing-premium.md' }, + { slug: 'zaps', title: 'Zaps (NIP-57)', group: 'Publishing & payments', file: 'help/zaps.md' }, + { slug: 'nwc', title: 'NWC wallet setup', group: 'Publishing & payments', file: 'help/nwc.md' }, ]; const _docsCache = new Map(); // slug → rendered HTML let _docsActiveSlug = 'index';