chore(ai): event schema validation tool + fixes

- Added `scripts/agent/validate-events.mjs` to validate event builders against schemas.
- Fixed `NOTE_TYPES.REPOST` schema content format (json instead of empty).
- Added `NOTE_TYPES.MUTE_LIST` and `buildMuteListEvent`.
- Added `buildProfileMetadataEvent` to standardise Kind 0 creation.
- Updated `js/ui/profileModalController.js` to use `buildProfileMetadataEvent`.
- Updated `js/userBlocks.js` to use `buildMuteListEvent` for publishing mute list snapshots.
- Updated `docs/nostr-event-schemas.md` with new schemas and builders.
This commit is contained in:
google-labs-jules[bot] committed 2026-01-19 05:47:37 +00:00
1 parent 474071e84a
commit 4d2bb8bf3d
5 files changed
+406 -26

No files matched your search

+91 -2
View File
@@ -28,6 +28,7 @@ export const NOTE_TYPES = Object.freeze({
ADMIN_BLACKLIST: "adminBlacklist",
ADMIN_WHITELIST: "adminWhitelist",
PROFILE_METADATA: "profileMetadata",
MUTE_LIST: "muteList",
});
export const SUBSCRIPTION_LIST_IDENTIFIER = "subscriptions";
@@ -270,8 +271,8 @@ const BASE_SCHEMAS = {
kind: 6,
appendTags: DEFAULT_APPEND_TAGS,
content: {
format: "empty",
description: "Content field intentionally empty for pure repost events.",
format: "json",
description: "Content field contains the JSON-serialized event being reposted.",
},
},
[NOTE_TYPES.NIP71_VIDEO]: {
@@ -458,6 +459,17 @@ const BASE_SCHEMAS = {
description: "Standard NIP-01 profile metadata (name, about, picture, etc.).",
},
},
[NOTE_TYPES.MUTE_LIST]: {
type: NOTE_TYPES.MUTE_LIST,
label: "Mute list",
kind: 10000,
participantTagName: "p",
appendTags: DEFAULT_APPEND_TAGS,
content: {
format: "text",
description: "Optional content (often encrypted) with public p tags.",
},
},
};
let schemaOverrides = {};
@@ -929,6 +941,83 @@ export function buildRelayListEvent(params) {
};
}
export function buildProfileMetadataEvent(params) {
const {
pubkey,
created_at,
metadata = {},
content,
additionalTags = [],
} = params || {};
const schema = getNostrEventSchema(NOTE_TYPES.PROFILE_METADATA);
const tags = [];
appendSchemaTags(tags, schema);
const sanitizedAdditionalTags = sanitizeAdditionalTags(additionalTags);
if (sanitizedAdditionalTags.length) {
tags.push(...sanitizedAdditionalTags.map((tag) => tag.slice()));
}
let serializedContent = "";
if (typeof content === "string") {
serializedContent = content;
} else {
try {
serializedContent = JSON.stringify(metadata || {});
} catch (error) {
serializedContent = "{}";
}
}
return {
kind: schema?.kind ?? 0,
pubkey,
created_at,
tags,
content: serializedContent,
};
}
export function buildMuteListEvent(params) {
const {
pubkey,
created_at,
pTags = [],
content = "",
encrypted = false,
encryptionTag = "",
additionalTags = [],
} = params || {};
const schema = getNostrEventSchema(NOTE_TYPES.MUTE_LIST);
const tags = [];
const participantTagName = schema?.participantTagName || "p";
if (Array.isArray(pTags)) {
pTags.forEach((hex) => {
if (typeof hex === "string" && hex.trim()) {
tags.push([participantTagName, hex.trim()]);
}
});
}
if (encrypted && encryptionTag) {
tags.push(["encrypted", encryptionTag]);
}
appendSchemaTags(tags, schema);
const sanitizedAdditionalTags = sanitizeAdditionalTags(additionalTags);
if (sanitizedAdditionalTags.length) {
tags.push(...sanitizedAdditionalTags.map((tag) => tag.slice()));
}
return {
kind: schema?.kind ?? 10000,
pubkey,
created_at,
tags,
content: typeof content === "string" ? content : "",
};
}
export function buildViewEvent(params) {
const {
pubkey,
+5 -7
View File
@@ -22,6 +22,7 @@ import {
} from "../utils/hashtagNormalization.js";
import { getActiveSigner } from "../nostr/client.js";
import { buildPublicUrl, buildR2Key } from "../r2.js";
import { buildProfileMetadataEvent } from "../nostrEventSchemas.js";
const noop = () => {};
@@ -6376,14 +6377,11 @@ export class ProfileModalController {
}
try {
const content = JSON.stringify(profile);
const event = {
kind: 0,
created_at: Math.floor(Date.now() / 1000),
const event = buildProfileMetadataEvent({
pubkey,
tags: [],
content,
};
created_at: Math.floor(Date.now() / 1000),
metadata: profile,
});
const result =
await this.services.nostrClient.signAndPublishEvent(event);
+11 -16
View File
@@ -6,7 +6,7 @@ import {
import { getActiveSigner } from "./nostr/index.js";
import { isSessionActor } from "./nostr/sessionActor.js";
import { normalizeNostrPubkey } from "./nostr/nip46Client.js";
import { buildBlockListEvent, BLOCK_LIST_IDENTIFIER, NOTE_TYPES } from "./nostrEventSchemas.js";
import { buildBlockListEvent, buildMuteListEvent, BLOCK_LIST_IDENTIFIER, NOTE_TYPES } from "./nostrEventSchemas.js";
import { CACHE_POLICIES, STORAGE_TIERS } from "./nostr/cachePolicies.js";
import { devLogger, userLogger } from "./utils/logger.js";
import {
@@ -1536,20 +1536,13 @@ class UserBlockListManager {
return null;
}
const tags = sanitizeMuteTags(blockedPubkeys, owner);
const sanitizedTags = sanitizeMuteTags(blockedPubkeys, owner);
const pTags = sanitizedTags.map((tag) => tag[1]);
const timestamp = Number.isFinite(createdAt)
? Math.max(0, Math.floor(createdAt))
: Math.floor(Date.now() / 1000);
const event = {
kind: 10000,
pubkey: owner,
created_at: timestamp,
tags,
content: "",
};
const payloadText = typeof plaintext === "string" ? plaintext : "";
let encryptedContent = "";
let encryptionTagValue = "";
@@ -1579,12 +1572,14 @@ class UserBlockListManager {
}
}
if (encryptedContent) {
event.content = encryptedContent;
if (encryptionTagValue) {
event.tags.push(["encrypted", encryptionTagValue]);
}
}
const event = buildMuteListEvent({
pubkey: owner,
created_at: timestamp,
pTags,
content: encryptedContent,
encrypted: !!encryptionTagValue,
encryptionTag: encryptionTagValue,
});
onStatus?.({ status: "mute-publishing" });