From b980215f15ee4efd40862e009229a1e149017599 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sat, 4 Jul 2026 16:39:42 -0400 Subject: [PATCH] Add creator premium epoch refresh action --- docs/PREMIUM_2_0_UPGRADE.md | 26 ++++- src/index.html | 95 +++++++++++++++++++ .../premium2-index-integration.test.mjs | 12 +++ 3 files changed, 129 insertions(+), 4 deletions(-) diff --git a/docs/PREMIUM_2_0_UPGRADE.md b/docs/PREMIUM_2_0_UPGRADE.md index 3fbad71..2f72775 100644 --- a/docs/PREMIUM_2_0_UPGRADE.md +++ b/docs/PREMIUM_2_0_UPGRADE.md @@ -547,10 +547,28 @@ Recommended defaults: Claim windows should never be described as cryptographic expiry. If an epoch is manually closed and a creator wants an old public listing to -be purchasable again, the creator should update/republish the listing under -the current active policy. The new public event gets the new active epoch and -becomes a fresh storefront. Existing buyers keep their older private purchase -copies as snapshots of what they bought. +be purchasable again, the creator should use the owner-only `Refresh epoch` +action or otherwise update/republish the listing under the current active +policy. The new public event reuses the same replaceable-event `d` tag, gets +the new active epoch, and becomes the current storefront for that address. +Existing buyers keep their older private purchase copies as snapshots of what +they bought. + +The `Refresh epoch` action does not silently publish. It: + +1. decrypts the creator's existing premium payload; +2. loads it into the editor without requiring design edits; +3. enters normal edit/update mode for the original `d` tag; +4. enables Premium with the existing price; +5. opens the standard publish confirmation; +6. publishes through the normal Premium path, which stamps the current admin + policy and encrypts with the current soft-gate epoch. + +If old epoch crypto has been reverse-engineered, that old derivation should +not unlock the refreshed listing because the refreshed event has a newly +encrypted payload under the active epoch. This is still a static-client +soft gate, not hard DRM, but it prevents old-epoch bypass tooling from +automatically carrying forward to the refreshed storefront. --- diff --git a/src/index.html b/src/index.html index 2b3c784..358947f 100644 --- a/src/index.html +++ b/src/index.html @@ -31413,6 +31413,77 @@ }); } + async function resolvePremiumPayloadForRepublish(row) { + if (!row?.premium && !isPremiumEvent(row?.e)) { + throw new Error('This is not a Premium 2.0 design.'); + } + const address = row?.premium?.address || premiumAddressCoord(row.e); + const cached = findUnlockedEntry(row.e?.id, address); + if (cached?.payload) return cached.payload; + const envelope = row?.premium?.envelope || JSON.parse(row.e?.content || '{}'); + return await Premium.decryptPayload(envelope, { + creatorPubkey: row.e?.pubkey || '', + dTag: row?.premium?.dTag || premiumEventDTag(row.e), + schemaVersion: envelope?.payloadSchemaVersion || 5, + }); + } + + async function republishPremiumUnderCurrentEpoch(row) { + if (!rowIsOwnedByMe(row) || !rowDTag(row)) { + toastError('Only the creator can refresh a premium listing epoch.'); + return; + } + if (!row?.premium && !isPremiumEvent(row?.e)) { + toastError('This is not a premium listing.'); + return; + } + const ok = confirm( + 'Republish this premium listing under the current Artstr premium epoch?\n\n' + + 'This replaces the public storefront event using the latest admin policy. Existing buyers keep their private purchased snapshots.' + ); + if (!ok) return; + let payload; + try { + payload = await resolvePremiumPayloadForRepublish(row); + } catch (err) { + toastError(`Could not decrypt this premium design for republish: ${err?.message || err}`); + return; + } + const mode = normalizeTemplateMode(payload); + try { + if (mode === 'disc-design') { + importDiscDesignFromPayload(payload, 'a', { openDesigner: true }); + } else { + loadProjectFromText(JSON.stringify(payload)); + } + } catch (err) { + toastError(`Could not load this design for republish: ${err?.message || err}`); + return; + } + enterEditMode({ + dTag: rowDTag(row), + kind: row.e?.kind || 30078, + eventId: row.e?.id || '', + title: row.title || payload?.meta?.title || 'Untitled design', + }); + state.visibility = 'public'; + state.publish = { + ...(state.publish || {}), + zapGate: true, + zapMin: getZapMin(row.e) || state.publish?.zapMin || 100, + }; + $('nostrModal')?.classList.remove('open'); + const profileModalEl = $('profileModal'); + if (profileModalEl && profileModalEl.getAttribute('aria-hidden') === 'false') { + closeProfilePage({ popState: false }); + } + toast('Premium listing loaded. Review and publish the epoch refresh.', { duration: 5000 }); + setTimeout(() => { + const publishBtn = mode === 'disc-design' ? $('designerPublishBtn') : $('communityPublishBtn'); + publishBtn?.click(); + }, 0); + } + // ---- Publish-confirm modal: preview + metadata review before posting ---- const MODE_LABELS_PRETTY = { @@ -35187,9 +35258,11 @@ } const tipBtn = makeFeedCardTipButton(row); const editBtn = makeFeedCardEditButton(row); + const refreshEpochBtn = makeFeedCardRefreshEpochButton(row); const isMine = rowIsOwnedByMe(row); actions.append(makeReactionBtn(row), btn); if (editBtn) actions.appendChild(editBtn); + if (refreshEpochBtn) actions.appendChild(refreshEpochBtn); const moreMenu = makeActionMenu([ blockAuthorAction(row, 'card'), reportTemplateAction(row) @@ -35225,6 +35298,19 @@ return btn; } + function makeFeedCardRefreshEpochButton(row) { + if (!rowIsOwnedByMe(row) || !rowDTag(row) || !row?.premium) return null; + const btn = document.createElement('button'); + btn.type = 'button'; + btn.textContent = 'Refresh epoch'; + btn.title = 'Republish this premium listing under the current Artstr premium policy without changing the design'; + btn.addEventListener('click', e => { + e.stopPropagation(); + republishPremiumUnderCurrentEpoch(row); + }); + return btn; + } + function makeFeedCardTipButton(row) { const pk = row?.e?.pubkey; if (!pk) return null; @@ -36078,6 +36164,15 @@ editBtn.addEventListener('click', () => startEditFromRow(row)); actionsDiv.insertBefore(editBtn, saveJsonBtn); + if (row.premium) { + const refreshEpochBtn = document.createElement('button'); + refreshEpochBtn.type = 'button'; + refreshEpochBtn.textContent = 'Refresh epoch'; + refreshEpochBtn.title = 'Republish this premium listing under the current Artstr premium policy without changing the design'; + refreshEpochBtn.addEventListener('click', () => republishPremiumUnderCurrentEpoch(row)); + actionsDiv.insertBefore(refreshEpochBtn, saveJsonBtn); + } + // Owner-only Visibility toggle — flip a published design between // public (feed-listed) and private (encrypted) in place. const makePrivate = !row.private; diff --git a/tests/premium/premium2-index-integration.test.mjs b/tests/premium/premium2-index-integration.test.mjs index 8dd848c..26649ce 100644 --- a/tests/premium/premium2-index-integration.test.mjs +++ b/tests/premium/premium2-index-integration.test.mjs @@ -153,6 +153,18 @@ test('index page refreshes the Purchased tab after a successful Premium 2.0 purc assert.match(html, /refreshPurchasedTabIfVisible\(\{ force: false \}\)/); }); +test('index page exposes creator premium epoch refresh for existing listings', async () => { + const html = await indexHtml(); + + assert.match(html, /async function republishPremiumUnderCurrentEpoch\(row\)/); + assert.match(html, /async function resolvePremiumPayloadForRepublish\(row\)/); + assert.match(html, /makeFeedCardRefreshEpochButton\(row\)/); + assert.match(html, /btn\.textContent = 'Refresh epoch'/); + assert.match(html, /state\.publish = \{\n\s+\.\.\.\(state\.publish \|\| \{\}\),\n\s+zapGate: true,/); + assert.match(html, /publishBtn\?\.click\(\)/); + assert.match(html, /Existing buyers keep their private purchased snapshots/); +}); + test('index page exposes Premium 2.0 purchase repair workflow', async () => { const html = await indexHtml();