From 2aed156912d3ad98e7147d288712ae456356cef3 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Thu, 28 May 2026 21:26:09 -0400 Subject: [PATCH] =?UTF-8?q?Premium=20step=203=20=E2=80=94=20encrypted=20pu?= =?UTF-8?q?blish=20+=20premium=20consume?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PREMIUM_DESIGNS_FEATURE.md §19 item 3 + the consumer changes needed to actually display, gate, and decrypt the new event shape. Closes the architectural gap that Adam called out in testing: premium templates were being published with the design payload in plaintext on relays, with only a client-side tag asking nicely not to import. Publish side - _publishPremiumDesign(payload, mode, typeTag) — new helper that runs when state.publish.zapGate && visibility === 'public'. Steps: render the watermarked preview (bails with a precise error if the design is too large or has CORS images), pick a `premium::` d-tag (or reuse state.editingDTag on edit-in-place), encrypt the payload via Premium.encryptPayload with the event coordinate as AAD, embed the preview JPEG in envelope.preview, then publish a single kind-30078 event with the §5.1 tag set (premium / premium-mode=softgate-v1 / softgate-epoch / encrypted / zap-gate / zap-min / zap-split / two `zap` tags with weights from PLATFORM_FEE_BPS / discovery tags / etc.). - publishCurrentTemplateToNostr branches into the premium path before the existing public path, after the private path. The label in the publish toast says "Premium" so the user knows which path ran. - getTemplateTags no longer emits zap-gate / zap-min from state.publish — that tag set is owned by the premium publish helper now, so non-premium publishes don't accidentally tag. Consume side - isPremiumEvent(event), getPremiumEnvelope(event), premiumEventDTag, premiumAddressCoord, premiumEventMode, parsePremiumZapSplits — pure tag/content readers. - _stubPayloadFromPremiumEvent — synthesises a payload shape from public tags (title / category / language / mode) so the existing feed-card readers don't choke on a locked row whose real payload is encrypted. - buildRowsFromEvents: detects premium events; if the user has already unlocked (cached payload in UNLOCK_CACHE_KEY) we use the real decrypted payload, otherwise the stub. Annotates the row with row.premium = { envelope, previewJpegDataUrl, decrypted, address, dTag } AND row.zapGate so the existing lock-CTA logic keeps working. - Older tag-only events (zap-gate=true with no `premium` tag) are still surfaced as locked, but they're now a "delete + republish" nag — their plaintext payload is already on relays. - buildFeedCard miniPreviewWrap: for locked premium rows, renders the watermarked JPEG from envelope.preview.dataUrl as an instead of trying to renderTemplatePreviewDOM on the stub payload. fitMiniPreview is skipped (image fits via CSS). - openDesignPreview (the preview pane): same gate. The preview body shows the watermarked JPEG + an honest "Soft-gate, not strong DRM" note. The Use/Fork button is replaced with a "⚡ Unlock for N sats" CTA that runs the existing openUnlockConfirm flow. The Save JSON button is disabled with a "Locked — pay to unlock the design data" tooltip. (Previously Save JSON downloaded the full payload — a flat bypass.) Unlock decryption - openUnlockConfirm: after payZapSplit succeeds, if the row is premium, call Premium.decryptPayload with the row's envelope + coord. Cache the decrypted payload in markUnlocked alongside creator/platform preimages + the address coord. If decrypt fails after a successful payment (epoch mismatch, corrupted envelope), the payment proof is still cached so the user doesn't re-pay; the message asks them to refresh. - Once decrypted is in the cache, buildRowsFromEvents picks it up on the next rerender and the row's Use/Fork / Save JSON work normally (the existing import path reads row.payload). Receipt fast-path by address - relayFastPathUnlockCheck now takes full events (not just ids). It runs two relay queries in parallel: kind-9735 with #a for premium events (address-coord scope so edit-in-place doesn't invalidate prior purchases — §13.2), and kind-9735 with #e for legacy tag-only events. Marks unlocked off whichever matches. - The call site in fetchFeedAndReactions now filters the template events to (premium || zap-gated) and passes them in full. Copy - Publish modal toggle: "Premium encrypted — viewers see a watermarked preview and must zap to unlock the full design. Soft-gate, not strong DRM — once unlocked, buyers can still export or share." Reflects the spec's §13.5 honesty rule. Tested via CDP with a stubbed publishCommunityEvent: - All 17 spec §5.1 tags emitted in the right shapes including the two `zap` split tags with weights pulled from PLATFORM_FEE_BPS (70 / 30). - Envelope has aes-256-gcm + IV + salt + ciphertext + softgate metadata + embedded preview (~18 KB for a 1920x1080 customart). - Direct string search against the serialized envelope for the layer text "Locked!" and color hex "7c3aed" → not present. Plaintext does NOT leak through the envelope. - buildRowsFromEvents on the captured event produces a row with row.premium.decrypted = false, full address coord, mode and title derived from public tags. - Premium.decryptPayload(envelope, coord) returns the exact original payload via deep-equal JSON comparison. - Regression smoke (existing editor + soft-gate self-test) still passes. Next step is the buyer-side purchase vault (§8) — NIP-44 self- encrypted kind-30078 vault so unlocked keys sync across browsers / devices, and the "My Purchases" tab in the Nostr modal. Co-Authored-By: Claude Opus 4.7 --- src/index.html | 414 +++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 352 insertions(+), 62 deletions(-) diff --git a/src/index.html b/src/index.html index 72408fd..f434679 100644 --- a/src/index.html +++ b/src/index.html @@ -4289,7 +4289,7 @@