diff --git a/src/index.html b/src/index.html
index 31126df..10575c9 100644
--- a/src/index.html
+++ b/src/index.html
@@ -6646,6 +6646,41 @@
return { resolveLinked, walkLinkedTree, walkContributors, clearCache, peek, keyFor, _parseAddressCoord, _cacheKey };
})();
+ // Walk the host's linked tree to collect (a) distinct contributor
+ // pubkeys (excluding the host author — they're already the event
+ // signer) and (b) distinct linked refs. Returns the two tag
+ // arrays ready to splice into a kind-30078 tag set per
+ // LINKED_DESIGNS_FEATURE.md §10.2:
+ // ['p', , , 'design-contributor']
+ // ['link', , ]
+ // Resolves each linked layer through linkedDesigns.walkLinkedTree
+ // (already cycle- + depth- + byte-capped). Encrypted sources
+ // return null payloads, so their authors silently don't get
+ // p-tagged — which is what we want for Phase 1.
+ async function _buildLinkedAttributionTags(payload, myPubkey) {
+ const contributors = new Set();
+ const refs = new Set();
+ try {
+ await linkedDesigns.walkLinkedTree(payload, {
+ onLink: ({ spec, entry }) => {
+ const pk = entry?.sourceEvent?.pubkey;
+ if (pk && /^[0-9a-f]{64}$/i.test(pk) && pk !== myPubkey) {
+ contributors.add(pk);
+ }
+ if (spec?.ref) refs.add(spec.ref);
+ },
+ });
+ } catch (err) {
+ console.warn('[linked-designs] attribution build failed', err);
+ }
+ const relayHint = (splitRelayList(state.community?.relays || '')[0]) || '';
+ const contributorTags = Array.from(contributors).map(
+ (pk) => ['p', pk, relayHint, 'design-contributor']
+ );
+ const linkTags = Array.from(refs).map((ref) => ['link', ref, relayHint]);
+ return { contributorTags, linkTags };
+ }
+
// Pre-publish hook. Linked layers carry render-only fields
// (lastResolvedAt, lastResolvedSize, transient fallbackPreview)
// that are session-local and should not ship in the published
@@ -15459,6 +15494,16 @@
// in the published event. Strip them once here, before the path
// branches into private/premium/public publish.
stripLinkedDesignEphemera(payload);
+ // Attribution chain (LINKED_DESIGNS_FEATURE.md §10.2). Resolve the
+ // linked tree once and gather contributor `p` tags + `link` tags
+ // for the published event. Skipped for private publishes to
+ // avoid leaking the link graph through unencrypted tags.
+ const myPubkey = state.community?.myPubkey || '';
+ let attributionTags = [];
+ if (state.visibility !== 'private') {
+ const built = await _buildLinkedAttributionTags(payload, myPubkey);
+ attributionTags = [...built.contributorTags, ...built.linkTags];
+ }
// Private: encrypt and publish a feed-invisible event instead of the
// normal public, tagged one.
if (state.visibility === 'private') {
@@ -15475,7 +15520,7 @@
// preview JPEG, with the §5.1 tag set. Soft-gate; honest about the
// limitation in the publish-modal copy.
if (state.publish?.zapGate) {
- const signed = await _publishPremiumDesign(payload, mode, typeTag);
+ const signed = await _publishPremiumDesign(payload, mode, typeTag, attributionTags);
const label = isEditing() ? `Updated "${state.meta.title}" (premium)` : `Premium "${state.meta.title}"`;
await finalizePublishedTemplateEvent(signed, { label });
if (isEditing()) exitEditMode();
@@ -15489,7 +15534,8 @@
const dTag = isEditing() ? state.editingDTag : `${mode}:${dBase}`;
const tags = [
['d', dTag],
- ...getTemplateTags(payload, typeTag)
+ ...getTemplateTags(payload, typeTag),
+ ...attributionTags,
];
const evt = { kind: 30078, created_at: Math.floor(Date.now() / 1000), tags, content: JSON.stringify(payload) };
const signed = await publishCommunityEvent(evt);
@@ -15503,7 +15549,7 @@
// 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 _publishPremiumDesign(payload, mode, typeTag) {
+ 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.
@@ -15590,6 +15636,9 @@
// consumers don't depend on the client constant.
['zap', myPubkey, relayHint, String(10000 - PLATFORM_FEE_BPS)],
['zap', platformPubkey, relayHint, String(PLATFORM_FEE_BPS)],
+ // Linked-design attribution (LINKED_DESIGNS_FEATURE.md §10.2).
+ // Empty when the host has no linked layers.
+ ...attributionTags,
];
const evt = {
kind: 30078,