Back to Black Site Map Editor
SOURCE / PINNED RELEASE

Made of little things.

Black Site Map Editor

Release
32a57733b7e9…
Author-recorded commit
3f54890f542f…
License
LICENSE
Author’s source reference
nostr://npub1ye5ptcxfyyxl5vjvdjar2ua3f0hynkjzpx552mu5snj3qmx5pzjscpknpr/wss%3A%2F%2Fgit.shakespeare.diy%2F/black-site-map-editor

Archive hash verified: 7a4b1abbac7db5b0…. The source-to-build association is the author’s claim; it has not been independently rebuilt.

src/vendor/lineage.mjs
// Remix lineage (../remix-lineage.md) for kind 35858 maps and kind 35859 campaigns, in NIP-22
// style: uppercase tags point at the root of the remix tree, lowercase tags at the direct parent,
// and every author in the lineage is tagged with `p`. Dependency-free, so an editor can copy it.
//
//   remixTags(parentEvent, { relay })  -> lineage tags for a new remix of a signed event
//   parseLineage(event)                -> { ok: true, lineage: {...} | null } | { ok: false, errors }

/** Kinds that can be remixed. A remix always has the same kind as its parent. */
export const REMIX_KINDS = [35858, 35859];
/** Most `p` tags a remix carries. The root author is always kept. */
export const MAX_LINEAGE_AUTHORS = 50;

const ID = /^[a-z0-9][a-z0-9-]{0,31}$/;
const HEX64 = /^[0-9a-f]{64}$/;

/**
 * Tags that mark a new event as a remix of `parent`, a signed kind 35858 or 35859 event.
 * `relay` is a relay hint where the parent was found. The root, its kind and its author are
 * copied from the parent's own lineage, or are the parent itself when it isn't a remix.
 */
export function remixTags(parent, { relay } = {}) {
  if (!parent || !REMIX_KINDS.includes(parent.kind)) throw new Error(`only kinds ${REMIX_KINDS.join(' and ')} can be remixed`);
  if (!HEX64.test(parent.pubkey || '')) throw new Error('parent pubkey must be 64 lowercase hex chars');
  if (!HEX64.test(parent.id || '')) throw new Error('parent must be a signed event with an id');
  const parsed = parseLineage(parent);
  if (!parsed.ok) throw new Error(`parent lineage is invalid:\n${parsed.errors.join('\n')}`);
  const d = parent.tags.find((t) => t[0] === 'd')?.[1];
  if (!ID.test(d || '')) throw new Error('parent d tag must be a valid id');

  const hint = relayHint(relay);
  const kind = String(parent.kind);
  const address = `${kind}:${parent.pubkey}:${d}`;
  const inherited = parsed.lineage;
  const root = inherited
    ? inherited.root
    : { address, eventId: parent.id, pubkey: parent.pubkey, relay: hint };

  const authors = dedupe([parent.pubkey, ...(inherited?.authors || []), root.pubkey]);
  if (authors.length > MAX_LINEAGE_AUTHORS) authors.splice(MAX_LINEAGE_AUTHORS - 1, Infinity, root.pubkey);

  return [
    withHint(['A', root.address], root.relay),
    ...(root.eventId ? [['E', root.eventId, root.relay || '', root.pubkey]] : []),
    ['K', kind],
    ['P', root.pubkey],
    withHint(['a', address], hint),
    ['e', parent.id, hint || '', parent.pubkey],
    ['k', kind],
    ...authors.map((pubkey) => ['p', pubkey]),
  ];
}

/**
 * Reads the lineage of a kind 35858 or 35859 event. Returns `lineage: null` for an original.
 * Only `A`/`a` tags whose address has the event's own kind are lineage, so a campaign's `a`
 * tags for its stage maps (kind 35858) are not mistaken for a parent.
 */
export function parseLineage(event) {
  const kind = String(event.kind);
  const tags = Array.isArray(event.tags) ? event.tags.filter((t) => Array.isArray(t) && typeof t[0] === 'string') : [];
  const ofKind = (name) => tags.filter((t) => t[0] === name && typeof t[1] === 'string' && t[1].startsWith(`${kind}:`));
  const roots = ofKind('A');
  const parents = ofKind('a');
  if (!roots.length && !parents.length) return { ok: true, lineage: null };

  const errors = [];
  if (roots.length !== 1 || parents.length !== 1) {
    return { ok: false, errors: [`a remix needs exactly one A (root) and one a (parent) tag of kind ${kind}`] };
  }
  const root = readAddress(roots[0][1]);
  const parent = readAddress(parents[0][1]);
  if (!root) errors.push(`A tag must be ${kind}:<pubkey hex>:<id>`);
  if (!parent) errors.push(`a tag must be ${kind}:<pubkey hex>:<id>`);
  if (errors.length) return { ok: false, errors };

  const own = `${kind}:${event.pubkey}:${tags.find((t) => t[0] === 'd')?.[1]}`;
  if (parent.address === own || root.address === own) errors.push('an event cannot be a remix of itself');
  for (const [name, value] of [['K', tags.find((t) => t[0] === 'K')?.[1]], ['k', tags.find((t) => t[0] === 'k')?.[1]]]) {
    if (value !== undefined && value !== kind) errors.push(`${name} tag must be "${kind}"`);
  }
  const rootAuthor = tags.find((t) => t[0] === 'P')?.[1];
  if (rootAuthor !== root.pubkey) errors.push('P tag must be the root author');
  const authors = dedupe(tags.filter((t) => t[0] === 'p' && HEX64.test(t[1] || '')).map((t) => t[1]));
  if (!authors.includes(parent.pubkey)) errors.push('p tags must include the parent author');
  if (errors.length) return { ok: false, errors };

  const eventId = (name, pubkey) => {
    const t = tags.find((t) => t[0] === name && HEX64.test(t[1] || ''));
    return t && (t[3] === undefined || t[3] === pubkey) ? t[1] : null;
  };
  return {
    ok: true,
    lineage: {
      root: { ...root, eventId: eventId('E', root.pubkey), relay: relayHint(roots[0][2]) },
      parent: { ...parent, eventId: eventId('e', parent.pubkey), relay: relayHint(parents[0][2]) },
      authors,
    },
  };
}

function readAddress(address) {
  const [kind, pubkey, id, ...rest] = address.split(':');
  if (rest.length || !REMIX_KINDS.includes(Number(kind)) || !HEX64.test(pubkey || '') || !ID.test(id || '')) return null;
  return { address, pubkey, id };
}

function relayHint(url) {
  try {
    return typeof url === 'string' && new URL(url).protocol === 'wss:' ? url : null;
  } catch {
    return null;
  }
}

const withHint = (tag, hint) => (hint ? [...tag, hint] : tag);
const dedupe = (list) => [...new Set(list)];