Add Premium 2.0 admin policy publish stamping

This commit is contained in:
thePR0M3TH3AN committed 2026-07-04 15:14:35 -04:00
1 parent 170756f722
commit 9c079dc402
4 files changed
+176 -7

No files matched your search

+49 -7
View File
@@ -6488,9 +6488,15 @@
new Uint8Array([0xa5, 0x71, 0x4e, 0x2c, 0x9b, 0x88, 0x33, 0xd0]), // palette seed III
new Uint8Array([0x62, 0x09, 0xfd, 0x18, 0x4a, 0xe7, 0xc4, 0x95]), // palette seed IV
],
'2026-07': [
new Uint8Array([0x3f, 0x92, 0x0d, 0xbb, 0x51, 0x7a, 0xe4, 0x26]), // palette seed I
new Uint8Array([0xd6, 0x44, 0x83, 0x19, 0xac, 0x05, 0x7e, 0xf1]), // palette seed II
new Uint8Array([0x28, 0xcd, 0x70, 0x5a, 0x9f, 0x31, 0xb8, 0x64]), // palette seed III
new Uint8Array([0xea, 0x16, 0x4b, 0xc7, 0x02, 0xdd, 0x90, 0x38]), // palette seed IV
],
};
const SUPPORTED_EPOCHS = Object.freeze(Object.keys(_softGatePepperParts));
const CURRENT_EPOCH = '2026-05';
const CURRENT_EPOCH = '2026-07';
const PREMIUM_KDF_INFO = 'artstr-softgate-v1';
function pepperBytesForEpoch(epoch) {
@@ -25776,12 +25782,41 @@
// Encrypts the payload via Premium.encryptPayload, renders the
// watermarked preview JPEG, builds the §5.1 tag set including the
// platform-fee zap split, and publishes a single kind-30078 event.
async function _loadPremiumPublishPolicy() {
const helpers = window.ArtstrPremium2;
if (!helpers?.validatePremiumPolicyEvent || !helpers?.buildPremiumPublishStamp) {
throw new Error('Premium 2.0 policy helper module did not load.');
}
const platformPubkey = platform.getPubkeyHex();
const relays = getActiveRelays(splitRelayList(state.community?.relays || ''), { includeCoolingDown: true });
const events = await queryRelays(relays, {
kinds: [30078],
authors: [platformPubkey],
'#d': [helpers.PREMIUM_POLICY_D_TAG || 'artstr:premium-policy:v1'],
limit: 5,
}, { timeoutMs: 4500 });
const latest = (events || [])
.filter((event) => event?.content)
.sort((a, b) => (b.created_at || 0) - (a.created_at || 0));
for (const event of latest) {
if (!NWC.verifyEvent(event)) continue;
const validated = helpers.validatePremiumPolicyEvent(event, { platformPubkey });
if (!validated.ok) continue;
const stamp = helpers.buildPremiumPublishStamp(validated.policy, {
supportedSoftgateEpochs: Premium.SUPPORTED_EPOCHS,
});
return { event, policy: validated.policy, stamp };
}
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.');
}
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
// buyers later.
const myPubkey = state.community?.myPubkey;
if (!myPubkey) throw new Error('Sign in with Nostr before publishing premium.');
const premiumPolicy = await _loadPremiumPublishPolicy();
// Generate the preview FIRST so a too-large artboard bails before we
// build any event. Decks get one watermarked JPEG per slide (capped)
// so buyers can flip through what they'd unlock; everything else gets
@@ -25809,9 +25844,18 @@
const coord = { creatorPubkey: myPubkey, dTag, schemaVersion: 5 };
// Encrypt the payload. The decrypted shape is the existing
// loadProjectFromText-compatible JSON.
const envelope = await Premium.encryptPayload(payload, coord);
const envelope = await Premium.encryptPayload(payload, coord, { epoch: premiumPolicy.stamp.softgateEpoch });
envelope.payloadSchemaVersion = 5;
envelope.premiumMode = 'softgate-v1';
envelope.premiumMode = premiumPolicy.stamp.premiumMode;
envelope.policy = {
eventId: premiumPolicy.event.id || '',
pubkey: premiumPolicy.event.pubkey || '',
d: window.ArtstrPremium2?.PREMIUM_POLICY_D_TAG || 'artstr:premium-policy:v1',
};
envelope.softgate = {
...(envelope.softgate || {}),
...(premiumPolicy.stamp.envelopeSoftgate || {}),
};
// Single-image envelope for non-decks; multi-slide envelope for
// decks. Buyers detect via `kind` and either render a paginated
// viewer (decks) or the single watermarked JPEG (everything else).
@@ -25840,7 +25884,6 @@
const zapMin = Math.max(10, Math.floor(Number(state.publish?.zapMin) || 100));
const platformPubkey = platform.getPubkeyHex();
const relayHint = (splitRelayList(state.community?.relays || '')[0]) || '';
const epoch = envelope.softgate?.epoch || Premium.CURRENT_EPOCH;
const tags = [
['d', dTag],
['t', 'casewrap'],
@@ -25851,9 +25894,8 @@
['language', state.meta?.language || 'en'],
['client', 'Artstr Studio'],
['premium', 'true'],
['premium-mode', 'softgate-v1'],
['softgate-epoch', epoch],
['encrypted', 'artstr-softgate-v1'],
...premiumPolicy.stamp.tags,
['encrypted', premiumPolicy.stamp.encryptedTag],
['zap-gate', 'true'],
['zap-min', String(zapMin)],
['zap-split', 'required'],
+52
View File
@@ -1,5 +1,7 @@
export const PREMIUM_POLICY_D_TAG = 'artstr:premium-policy:v1';
export const PURCHASE_COPY_PREFIX = 'artstr:purchase-copy:';
export const PREMIUM_MODE_V15 = 'softgate-v1.5';
export const PRIVATE_SNAPSHOT_ACTION = 'private-snapshot-required';
export function tagValue(event, name) {
const tag = (event?.tags || []).find((t) => Array.isArray(t) && t[0] === name);
@@ -34,6 +36,56 @@ export function validatePremiumPolicyEvent(event, { platformPubkey } = {}) {
return { ok: true, policy };
}
export function buildPremiumPublishStamp(policy, {
now = Math.floor(Date.now() / 1000),
supportedSoftgateEpochs = [],
} = {}) {
if (!policy || typeof policy !== 'object' || policy.v !== 1) {
throw new TypeError('Premium policy v1 is required.');
}
const softgateEpoch = policy.activeSoftgateEpoch || '';
const claimEpoch = policy.activeClaimEpoch || '';
const epochConfig = policy.epochs?.[softgateEpoch];
if (!softgateEpoch || !claimEpoch || !epochConfig) {
throw new Error('Premium policy is missing the active epoch configuration.');
}
if (epochConfig.status === 'closed') {
throw new Error(`Premium policy epoch ${softgateEpoch} is closed for new publishes.`);
}
if (supportedSoftgateEpochs.length && !supportedSoftgateEpochs.includes(softgateEpoch)) {
throw new Error(`Premium policy epoch ${softgateEpoch} is not supported by this client.`);
}
const minDays = Math.max(1, Number(policy.minClaimDays) || 1);
const maxDays = Math.max(minDays, Number(policy.maxClaimDays) || 180);
const requestedDays = Number(policy.defaultClaimDays) || 90;
const claimDays = Math.min(maxDays, Math.max(minDays, requestedDays));
const claimUntil = Number(now) + (claimDays * 86400);
const premiumMode = PREMIUM_MODE_V15;
const postPurchaseAction = PRIVATE_SNAPSHOT_ACTION;
return {
premiumMode,
softgateEpoch,
claimEpoch,
claimUntil,
claimDays,
postPurchaseAction,
encryptedTag: epochConfig.softgateKdf || 'artstr-softgate-v1.5',
tags: [
['premium-mode', premiumMode],
['softgate-epoch', softgateEpoch],
['claim-epoch', claimEpoch],
['claim-until', String(claimUntil)],
['claim-policy', postPurchaseAction],
['post-purchase-action', postPurchaseAction],
],
envelopeSoftgate: {
claimEpoch,
claimUntil,
postPurchaseAction,
},
};
}
function bytesToBase64Url(bytes) {
let binary = '';
for (const byte of bytes) binary += String.fromCharCode(byte);
+53
View File
@@ -8,6 +8,7 @@ import {
buildPurchaseCopyEventDraft,
buildPurchaseCopyCoordinate,
buildPurchaseCopyPlaintext,
buildPremiumPublishStamp,
buildVaultV2Item,
claimStatus,
normalizeReceiptEvidence,
@@ -39,6 +40,58 @@ test('validates admin policy event and rejects untrusted policy shapes', async (
assert.equal(validatePremiumPolicyEvent(wrongD, { platformPubkey }).reason, 'wrong_d_tag');
});
test('builds automatic premium publish stamps from admin policy', async () => {
const valid = await fixture('policy-valid-active.json');
const { policy } = validatePremiumPolicyEvent(valid, { platformPubkey });
const stamp = buildPremiumPublishStamp(policy, {
now: 1783200000,
supportedSoftgateEpochs: ['2026-05', '2026-07'],
});
assert.equal(stamp.premiumMode, 'softgate-v1.5');
assert.equal(stamp.softgateEpoch, '2026-07');
assert.equal(stamp.claimEpoch, '2026-07');
assert.equal(stamp.claimUntil, 1790976000);
assert.deepEqual(stamp.envelopeSoftgate, {
claimEpoch: '2026-07',
claimUntil: 1790976000,
postPurchaseAction: 'private-snapshot-required',
});
assert.deepEqual(stamp.tags, [
['premium-mode', 'softgate-v1.5'],
['softgate-epoch', '2026-07'],
['claim-epoch', '2026-07'],
['claim-until', '1790976000'],
['claim-policy', 'private-snapshot-required'],
['post-purchase-action', 'private-snapshot-required'],
]);
});
test('rejects premium publish stamps for unsupported or closed policy epochs', async () => {
const valid = await fixture('policy-valid-active.json');
const { policy } = validatePremiumPolicyEvent(valid, { platformPubkey });
assert.throws(
() => buildPremiumPublishStamp(policy, { supportedSoftgateEpochs: ['2026-05'] }),
/not supported/,
);
const closedPolicy = {
...policy,
epochs: {
...policy.epochs,
[policy.activeSoftgateEpoch]: {
...policy.epochs[policy.activeSoftgateEpoch],
status: 'closed',
},
},
};
assert.throws(
() => buildPremiumPublishStamp(closedPolicy, { supportedSoftgateEpochs: ['2026-07'] }),
/closed/,
);
});
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), {
@@ -26,6 +26,28 @@ test('index page wires Premium 2.0 private-copy unlock flow', async () => {
assert.match(html, /Private copy saved\. Syncing purchase vault/);
});
test('index page wires admin policy into premium publish stamping', async () => {
const html = await indexHtml();
assert.match(html, /async function _loadPremiumPublishPolicy/);
assert.match(html, /helpers\.validatePremiumPolicyEvent/);
assert.match(html, /helpers\.buildPremiumPublishStamp/);
assert.match(html, /NWC\.verifyEvent\(event\)/);
assert.match(html, /supportedSoftgateEpochs: Premium\.SUPPORTED_EPOCHS/);
assert.match(html, /const premiumPolicy = await _loadPremiumPublishPolicy\(\)/);
assert.match(html, /Premium\.encryptPayload\(payload, coord, \{ epoch: premiumPolicy\.stamp\.softgateEpoch \}\)/);
assert.match(html, /envelope\.premiumMode = premiumPolicy\.stamp\.premiumMode/);
assert.match(html, /\.\.\.premiumPolicy\.stamp\.tags/);
assert.match(html, /\['encrypted', premiumPolicy\.stamp\.encryptedTag\]/);
});
test('soft-gate crypto supports the active Premium 2.0 policy epoch', async () => {
const html = await indexHtml();
assert.match(html, /'2026-07': \[/);
assert.match(html, /const CURRENT_EPOCH = '2026-07'/);
});
test('vault item stubs preserve Premium 2.0 private-copy fields', async () => {
const html = await indexHtml();
const stubStart = html.indexOf('function _vaultItemStub(item)');