mirror of
https://github.com/PR0M3TH3AN/Artstr-Studio.git
synced 2026-11-04 12:40:40 +00:00
Add Premium 2.0 admin policy publish stamping
This commit is contained in:
1 parent
170756f722
commit
9c079dc402
4 files changed
+176
-7
No files matched your search
+49
-7
@@ -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'],
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)');
|
||||
|
||||
Reference in new issue
Block a user