From 421104257e26a768a6933d2665b23da4aa0637e9 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sun, 31 May 2026 22:02:24 -0400 Subject: [PATCH] Linked Designs P2c: private (visibility-gated) linked designs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LINKED_DESIGNS_FEATURE.md ยง12.1 โ private sources carry a NIP-44 / NIP-04 envelope addressed to specific pubkeys. Phase 1 treated them as unparseable junk; Phase 2 detects them, attempts decryption, and either renders the decrypted payload (if the viewer is in the envelope's recipients list) or paints a ๐ placeholder otherwise. Resolver changes (_fetchPayload): - After the premium branch, detect isEncryptedEnvelope(parsed) and await decryptProjectEnvelope. On success return the decrypted payload like a public source; on any failure (not in recipients, no signing extension, malformed envelope) return { payload: null, private: { state: 'locked' } } so the renderer can paint the locked UI without re-fetching. Renderer changes: - New renderLockedPrivate helper paints a ๐ + "Private design" + "Not shared with this account" on a dark gray background. No unlock CTA โ access to a private design is granted at the source's publish time by adding the viewer to envelope.recipients, there's no buyer-side action to take. - Sync peek hit handles cached.private.state === 'locked'; async path handles entry.private.state === 'locked'. Source authors are always in their own envelope.recipients, so the existing self-decrypt path works for them without special-casing. --- src/index.html | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/src/index.html b/src/index.html index c859ad4..dd902f6 100644 --- a/src/index.html +++ b/src/index.html @@ -6612,6 +6612,39 @@ premium: { state: 'locked', envelope, addressCoord }, }; } + // Private (visibility) sources carry a NIP-44/04-wrapped + // envelope addressed to specific recipients. The viewer can + // see the source iff their pubkey is in envelope.recipients + // โ the source author is always self-addressed, so they + // always succeed. decryptProjectEnvelope throws when the + // viewer isn't in the recipients list (or has no signing + // extension); we catch and surface as locked. + if (typeof isEncryptedEnvelope === 'function' && isEncryptedEnvelope(parsed)) { + if (typeof decryptProjectEnvelope === 'function') { + try { + const decrypted = await decryptProjectEnvelope(parsed); + if (decrypted) { + return { + payload: decrypted, + sourceEvent: ev, + contentBytes: ev.content.length, + private: { state: 'unlocked' }, + }; + } + } catch (err) { + // Fall through to the locked branch โ the most common + // path is "not in recipients" but we also land here if + // the user isn't signed in or the extension can't + // decrypt for any other reason. + } + } + return { + payload: null, + sourceEvent: ev, + contentBytes: ev.content.length, + private: { state: 'locked' }, + }; + } return { payload: parsed, sourceEvent: ev, contentBytes: ev.content.length }; } @@ -11058,6 +11091,19 @@ } }; + // Locked-private paint helper. No unlock CTA โ access to a + // private design is granted at the source's publish time + // (the author adds the viewer to envelope.recipients), not + // via a buyer-side action. The host just shows that the + // layer exists but isn't readable from this account. + const renderLockedPrivate = () => { + fitWrap.innerHTML = ''; + const wrap = document.createElement('div'); + wrap.style.cssText = 'position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;background:#1f2937;color:#e5e7eb;padding:8px;text-align:center;gap:4px;font-size:.74rem;line-height:1.3'; + wrap.innerHTML = '