diff --git a/src/index.html b/src/index.html index 0b19ae4..c859ad4 100644 --- a/src/index.html +++ b/src/index.html @@ -6549,10 +6549,70 @@ if (!ev?.content || ev.content.length > LINKED_DESIGN_MAX_EVENT_BYTES) { return null; } - let payload; - try { payload = JSON.parse(ev.content); } + let parsed; + try { parsed = JSON.parse(ev.content); } catch { return null; } - return { payload, sourceEvent: ev, contentBytes: ev.content.length }; + // Premium sources (LINKED_DESIGNS_FEATURE.md ยง12.1) carry an + // encrypted envelope instead of a raw payload. Access is + // gated by the buyer's purchase-vault entry โ NOT the + // decryption itself, since the soft-gate KDF is deterministic + // from public material (any browser could technically + // decrypt). Check the vault first; if there's no entry, + // try a self-authored decrypt; else return a locked entry + // the renderer can show with an unlock CTA. + if (typeof isPremiumEvent === 'function' && isPremiumEvent(ev)) { + const envelope = parsed; + const addressCoord = typeof premiumAddressCoord === 'function' ? premiumAddressCoord(ev) : ''; + const vaultEntry = typeof findUnlockedEntry === 'function' ? findUnlockedEntry(ev.id, addressCoord) : null; + if (vaultEntry?.payload) { + return { + payload: vaultEntry.payload, + sourceEvent: ev, + contentBytes: ev.content.length, + premium: { state: 'unlocked', envelope, addressCoord }, + }; + } + // Self-authored sources auto-decrypt โ soft-gate KDF is + // deterministic from public material, so the host author + // can always decrypt their own premium designs. + const myPubkey = state.community?.myPubkey || ''; + if (myPubkey && ev.pubkey === myPubkey && typeof Premium?.decryptPayload === 'function') { + try { + const decrypted = await Premium.decryptPayload(envelope, { + creatorPubkey: ev.pubkey, + dTag: typeof premiumEventDTag === 'function' ? premiumEventDTag(ev) : '', + schemaVersion: envelope?.payloadSchemaVersion || 5, + }); + if (decrypted) { + // Mirror the unlock into the vault so the rest of + // the app sees it too (My Designs, feed, etc). + if (typeof markUnlocked === 'function') { + markUnlocked(ev.id, { + source: 'self-authored-link', + address: addressCoord, + payload: decrypted, + previewJpegDataUrl: envelope?.preview?.dataUrl || undefined, + }); + } + return { + payload: decrypted, + sourceEvent: ev, + contentBytes: ev.content.length, + premium: { state: 'unlocked', envelope, addressCoord }, + }; + } + } catch (err) { + console.warn('[linked-designs] self-authored premium decrypt failed', err); + } + } + return { + payload: null, + sourceEvent: ev, + contentBytes: ev.content.length, + premium: { state: 'locked', envelope, addressCoord }, + }; + } + return { payload: parsed, sourceEvent: ev, contentBytes: ev.content.length }; } // Public: resolve a single linked layer's spec ({ ref, pinnedEventId }). @@ -10998,6 +11058,67 @@ } }; + // Locked-premium paint helper (LINKED_DESIGNS_FEATURE.md ยง12.1). + // Shows the watermarked preview JPEG from the envelope + a + // semi-transparent lock overlay + an inline unlock CTA. After + // a successful unlock the vault has an entry; we clear the + // resolver cache and trigger a global render so the layer + // re-resolves into the decrypted payload. + const renderLockedPremium = (entry) => { + 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:#111'; + const previewUrl = entry?.premium?.envelope?.preview?.dataUrl; + if (previewUrl) { + const img = document.createElement('img'); + img.src = previewUrl; + img.alt = 'Linked premium preview'; + img.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;object-fit:contain;opacity:.45;pointer-events:none'; + wrap.appendChild(img); + } + const overlay = document.createElement('div'); + overlay.style.cssText = 'position:relative;z-index:1;display:flex;flex-direction:column;align-items:center;gap:6px;padding:8px;text-align:center;font-size:.74rem;line-height:1.3;color:#fff;background:rgba(0,0,0,.55);border-radius:6px;max-width:90%;pointer-events:auto'; + overlay.innerHTML = '