Add Premium 2.0 admin policy publishing tool

This commit is contained in:
thePR0M3TH3AN committed 2026-07-04 15:44:16 -04:00
1 parent d04a24d2e2
commit c9a8f63335
4 files changed
+177

No files matched your search

+116
View File
@@ -5631,6 +5631,21 @@
<p class="hint">Blocked authors are hidden in all community feed views.</p>
<div id="blockListContainer" class="hint">No blocked authors yet.</div>
</details>
<details id="premiumPolicyAdminDetails" style="margin-top:12px;border-top:1px solid var(--line);padding-top:10px;display:none">
<summary style="font-weight:700;font-size:.82rem;cursor:pointer;color:var(--muted)">Premium policy admin</summary>
<p class="hint" style="margin:8px 0 6px">Publish the signed Premium 2.0 policy event used for epoch and claim-window stamping.</p>
<div style="display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px">
<div><label for="premiumPolicySoftgateEpoch">Softgate epoch</label><input id="premiumPolicySoftgateEpoch" type="text" value="2026-07" /></div>
<div><label for="premiumPolicyClaimEpoch">Claim epoch</label><input id="premiumPolicyClaimEpoch" type="text" value="2026-07" /></div>
<div><label for="premiumPolicyDefaultDays">Claim days</label><input id="premiumPolicyDefaultDays" type="number" min="7" max="180" step="1" value="90" /></div>
<div><label for="premiumPolicyEpochStatus">Epoch status</label><select id="premiumPolicyEpochStatus"><option value="active">Active</option><option value="legacy">Legacy</option><option value="closed">Closed</option></select></div>
</div>
<div class="buttonRow" style="margin:8px 0 0">
<button type="button" id="premiumPolicyRefreshBtn">Refresh policy</button>
<button type="button" id="premiumPolicyPublishBtn" class="primary">Publish policy</button>
</div>
<p class="hint" id="premiumPolicyStatus" style="margin:8px 0 0;min-height:1.2em;color:var(--muted)"></p>
</details>
<details id="walletDetails" style="margin-top:12px;border-top:1px solid var(--line);padding-top:10px">
<summary style="font-weight:700;font-size:.82rem;cursor:pointer;color:var(--muted)">⚡ Lightning wallet <span class="docsHelpIcon" data-help="nwc" role="button" aria-label="Open help: NWC wallet setup" title="Open help: NWC setup">ⓘ</span><span id="walletStatusBadge" style="font-weight:500;color:var(--muted);font-size:.74rem;margin-left:6px"></span></summary>
<div id="walletDisconnectedView">
@@ -26183,6 +26198,86 @@
throw new Error('No valid Artstr premium policy was found on your relays. Ask the Artstr admin to publish the Premium 2.0 policy event, then try publishing again.');
}
function isPremiumPolicyAdmin() {
return !!state.community?.myPubkey && normalizePubkey(state.community.myPubkey) === normalizePubkey(platform.getPubkeyHex());
}
function updatePremiumPolicyAdminVisibility() {
const details = $('premiumPolicyAdminDetails');
if (!details) return;
const visible = isPremiumPolicyAdmin();
details.style.display = visible ? '' : 'none';
if (visible && !details.dataset.initialized) {
details.dataset.initialized = 'true';
refreshPremiumPolicyAdminStatus().catch(() => {});
}
}
function buildPremiumPolicyFromAdminForm() {
const softgateEpoch = ($('premiumPolicySoftgateEpoch')?.value || '2026-07').trim();
const claimEpoch = ($('premiumPolicyClaimEpoch')?.value || softgateEpoch).trim();
const defaultClaimDays = Math.max(7, Math.min(180, Number($('premiumPolicyDefaultDays')?.value) || 90));
const status = ($('premiumPolicyEpochStatus')?.value || 'active').trim();
return {
v: 1,
activeSoftgateEpoch: softgateEpoch,
activeClaimEpoch: claimEpoch,
defaultClaimDays,
minClaimDays: 7,
maxClaimDays: 180,
privateCopies: 'required',
firstUnlockDefault: 'allowed',
issuedAt: Math.floor(Date.now() / 1000),
epochs: {
'2026-05': {
status: softgateEpoch === '2026-05' ? status : 'legacy',
firstUnlock: 'allowed',
privateCopies: 'allowed',
softgateKdf: 'artstr-softgate-v1',
minClientPolicy: 1,
},
[softgateEpoch]: {
status,
firstUnlock: status === 'closed' ? 'closed' : 'allowed',
privateCopies: 'required',
softgateKdf: softgateEpoch === '2026-05' ? 'artstr-softgate-v1' : 'artstr-softgate-v1.5',
workFactor: 1,
},
},
};
}
async function refreshPremiumPolicyAdminStatus() {
const statusEl = $('premiumPolicyStatus');
if (!statusEl || !isPremiumPolicyAdmin()) return;
statusEl.textContent = 'Checking relays…';
try {
const loaded = await _loadPremiumPublishPolicy();
const policy = loaded.policy || {};
if ($('premiumPolicySoftgateEpoch')) $('premiumPolicySoftgateEpoch').value = policy.activeSoftgateEpoch || '2026-07';
if ($('premiumPolicyClaimEpoch')) $('premiumPolicyClaimEpoch').value = policy.activeClaimEpoch || policy.activeSoftgateEpoch || '2026-07';
if ($('premiumPolicyDefaultDays')) $('premiumPolicyDefaultDays').value = String(policy.defaultClaimDays || 90);
if ($('premiumPolicyEpochStatus')) $('premiumPolicyEpochStatus').value = policy.epochs?.[policy.activeSoftgateEpoch]?.status || 'active';
statusEl.textContent = `Active policy: ${policy.activeSoftgateEpoch || 'unknown'} / claim ${policy.activeClaimEpoch || 'unknown'}.`;
} catch (err) {
statusEl.textContent = err?.message || 'No valid premium policy found.';
}
}
async function publishPremiumPolicyFromSettings() {
if (!isPremiumPolicyAdmin()) throw new Error('Only the Artstr platform admin key can publish premium policy.');
const helpers = window.ArtstrPremium2;
if (!helpers?.buildPremiumPolicyEventDraft || !helpers?.validatePremiumPolicyEvent) {
throw new Error('Premium 2.0 policy helper module did not load.');
}
const policy = buildPremiumPolicyFromAdminForm();
const draft = helpers.buildPremiumPolicyEventDraft(policy);
const signed = await publishCommunityEvent(draft);
const validated = helpers.validatePremiumPolicyEvent(signed, { platformPubkey: platform.getPubkeyHex() });
if (!validated.ok) throw new Error(`Published policy did not validate: ${validated.reason}`);
return signed;
}
async function _publishPremiumDesign(payload, mode, typeTag, attributionTags = []) {
// Pre-flight: lud16 on creator (already enforced at the modal level),
// and resolve the platform lud16 so the split-zap actually works for
@@ -35090,6 +35185,7 @@
profileBtn.style.display = 'none';
}
updateTrustToggles();
if (typeof updatePremiumPolicyAdminVisibility === 'function') updatePremiumPolicyAdminVisibility();
requestAnimationFrame(updateNostrWidgetSpace);
}
@@ -39108,6 +39204,26 @@
on('profileMenuSettings', 'click', openSettingsModal);
on('profileMenuLogout', 'click', nostrLogout);
on('settingsCloseBtn', 'click', closeSettingsModal);
on('premiumPolicyRefreshBtn', 'click', () => refreshPremiumPolicyAdminStatus().catch((err) => {
const el = $('premiumPolicyStatus');
if (el) el.textContent = err?.message || 'Policy refresh failed.';
}));
on('premiumPolicyPublishBtn', 'click', async () => {
const btn = $('premiumPolicyPublishBtn');
const statusEl = $('premiumPolicyStatus');
if (btn) btn.disabled = true;
if (statusEl) statusEl.textContent = 'Publishing premium policy…';
try {
const signed = await publishPremiumPolicyFromSettings();
if (statusEl) statusEl.textContent = `Published policy ${shortPubkey(signed.id)}.`;
toastSuccess('Premium policy published ✓');
} catch (err) {
if (statusEl) statusEl.textContent = err?.message || 'Policy publish failed.';
toastError(err?.message || 'Policy publish failed.');
} finally {
if (btn) btn.disabled = false;
}
});
const settingsModalEl = $('settingsModal');
if (settingsModalEl) settingsModalEl.addEventListener('click', (e) => { if (e.target === settingsModalEl) closeSettingsModal(); });
// Escape key closes both the menu and the settings modal.
+24
View File
@@ -86,6 +86,30 @@ export function buildPremiumPublishStamp(policy, {
};
}
export function buildPremiumPolicyEventDraft(policy, {
createdAt = Math.floor(Date.now() / 1000),
} = {}) {
if (!policy || typeof policy !== 'object' || policy.v !== 1) {
throw new TypeError('Premium policy v1 is required.');
}
if (!policy.activeSoftgateEpoch || !policy.activeClaimEpoch || !policy.epochs?.[policy.activeSoftgateEpoch]) {
throw new Error('Premium policy is missing active epoch fields.');
}
return {
kind: 30078,
created_at: createdAt,
tags: [
['d', PREMIUM_POLICY_D_TAG],
['client', 'Artstr Studio'],
['policy', 'premium-policy-v1'],
],
content: JSON.stringify({
...policy,
issuedAt: Number(policy.issuedAt) || createdAt,
}),
};
}
function bytesToBase64Url(bytes) {
let binary = '';
for (const byte of bytes) binary += String.fromCharCode(byte);
+22
View File
@@ -9,6 +9,7 @@ import {
buildPurchaseCopyCoordinate,
buildPurchaseCopyPlaintext,
buildPremiumPublishStamp,
buildPremiumPolicyEventDraft,
buildVaultV2Item,
claimStatus,
normalizeReceiptEvidence,
@@ -92,6 +93,27 @@ test('rejects premium publish stamps for unsupported or closed policy epochs', a
);
});
test('builds admin premium policy event drafts with signed-data-only content', async () => {
const valid = await fixture('policy-valid-active.json');
const { policy } = validatePremiumPolicyEvent(valid, { platformPubkey });
const draft = buildPremiumPolicyEventDraft(policy, { createdAt: 1783200000 });
assert.equal(draft.kind, 30078);
assert.equal(draft.created_at, 1783200000);
assert.deepEqual(draft.tags, [
['d', 'artstr:premium-policy:v1'],
['client', 'Artstr Studio'],
['policy', 'premium-policy-v1'],
]);
const content = JSON.parse(draft.content);
assert.equal(content.v, 1);
assert.equal(content.activeSoftgateEpoch, '2026-07');
assert.equal(content.activeClaimEpoch, '2026-07');
assert.equal(content.issuedAt, 1783200000);
assert.doesNotMatch(draft.content, /function|eval|script|wasm/i);
});
test('resolves vault v2 private copies before legacy inline payloads', async () => {
const v2 = await fixture('vault-v2-private-copy-pointer.json');
assert.deepEqual(resolveVaultItem(v2), {
@@ -41,6 +41,21 @@ test('index page wires admin policy into premium publish stamping', async () =>
assert.match(html, /\['encrypted', premiumPolicy\.stamp\.encryptedTag\]/);
});
test('index page exposes admin-only premium policy publishing controls', async () => {
const html = await indexHtml();
assert.match(html, /id="premiumPolicyAdminDetails"/);
assert.match(html, /id="premiumPolicyPublishBtn"/);
assert.match(html, /function isPremiumPolicyAdmin\(\)/);
assert.match(html, /normalizePubkey\(state\.community\.myPubkey\) === normalizePubkey\(platform\.getPubkeyHex\(\)\)/);
assert.match(html, /function buildPremiumPolicyFromAdminForm\(\)/);
assert.match(html, /async function publishPremiumPolicyFromSettings\(\)/);
assert.match(html, /helpers\.buildPremiumPolicyEventDraft\(policy\)/);
assert.match(html, /helpers\.validatePremiumPolicyEvent\(signed, \{ platformPubkey: platform\.getPubkeyHex\(\) \}\)/);
assert.match(html, /on\('premiumPolicyPublishBtn', 'click'/);
assert.match(html, /Premium policy published/);
});
test('soft-gate crypto supports the active Premium 2.0 policy epoch', async () => {
const html = await indexHtml();