Files
bitvid/scripts/agent/validate-events.mjs
T
google-labs-jules[bot] 3b80692eb0 chore(ai): event schema validation tool + fixes
- Created `scripts/agent/validate-events.mjs` to validate all event builders against schemas.
- Updated `docs/nostr-event-schemas.md` to match codebase (fixed `WATCH_HISTORY` key, removed unused `STORAGE_CHALLENGE`).
- Added `validateEventAgainstSchema` to `js/nostrEventSchemas.js` for runtime validation in dev mode.
- Instrumented `buildVideoPostEvent` to use runtime validation.
- Verified that all 22 builders produce conformant events.
2026-01-20 05:21:18 +00:00

433 lines
12 KiB
JavaScript

import {
NOTE_TYPES,
getNostrEventSchema,
buildVideoPostEvent,
buildVideoMirrorEvent,
buildRepostEvent,
buildRelayListEvent,
buildDmRelayListEvent,
buildProfileMetadataEvent,
buildMuteListEvent,
buildDmAttachmentEvent,
buildDmReadReceiptEvent,
buildDmTypingIndicatorEvent,
buildViewEvent,
buildZapRequestEvent,
buildReactionEvent,
buildCommentEvent,
buildWatchHistoryEvent,
buildSubscriptionListEvent,
buildBlockListEvent,
buildHashtagPreferenceEvent,
buildAdminListEvent,
} from "../../js/nostrEventSchemas.js";
import { buildNip71VideoEvent } from "../../js/nostr/nip71.js";
// Mock data
const MOCK_PUBKEY = "0000000000000000000000000000000000000000000000000000000000000001";
const MOCK_EVENT_ID = "0000000000000000000000000000000000000000000000000000000000000002";
const MOCK_TIMESTAMP = 1700000000;
const MOCK_URL = "https://example.com/video.mp4";
const MOCK_THUMBNAIL = "https://example.com/thumb.jpg";
const MOCK_MAGNET = "magnet:?xt=urn:btih:example";
const MOCK_RELAY = "wss://relay.example.com";
const MOCK_D_TAG = "mock-d-tag";
// Helper to check if a tag exists
function hasTag(tags, tagName, tagValue = null) {
return tags.some(
(tag) =>
Array.isArray(tag) &&
tag[0] === tagName &&
(tagValue === null || tag[1] === tagValue)
);
}
// Validation logic
function validateEvent(type, event) {
const schema = getNostrEventSchema(type);
const errors = [];
if (!schema) {
return [`Schema not found for type: ${type}`];
}
// 1. Validate Kind
if (event.kind !== schema.kind) {
errors.push(`Kind mismatch: expected ${schema.kind}, got ${event.kind}`);
}
// 2. Validate Required Tags
// Topic tag
if (schema.topicTag) {
if (!hasTag(event.tags, schema.topicTag.name, schema.topicTag.value)) {
errors.push(
`Missing topic tag: ${schema.topicTag.name}=${schema.topicTag.value}`
);
}
}
// Identifier tag (d tag)
if (schema.identifierTag) {
// For lists with fixed identifier values (like subscription list), check the value
const expectedValue = schema.identifierTag.value;
if (!hasTag(event.tags, schema.identifierTag.name, expectedValue)) {
errors.push(
`Missing identifier tag: ${schema.identifierTag.name}${
expectedValue ? "=" + expectedValue : ""
}`
);
}
}
// Append tags
if (schema.appendTags) {
schema.appendTags.forEach((appendTag) => {
const tagName = appendTag[0];
const tagValue = appendTag[1];
if (!hasTag(event.tags, tagName, tagValue)) {
errors.push(`Missing append tag: ${tagName}=${tagValue}`);
}
});
}
// Specific schema tags
if (schema.relayTagName) {
// Check if at least one relay tag exists if we added relays in mock data
// This is slightly context dependent, but we can check if the builder respects the tag name
// Logic: if we expect the builder to use 'r' or 'relay', we can't easily verify generally unless we check specific output
// But we can check if any tag with that name exists IF we passed relays.
}
// 3. Validate Content Format
if (schema.content) {
if (schema.content.format === "json") {
try {
JSON.parse(event.content);
// TODO: Validate JSON schema fields if defined (schema.content.fields)
if (schema.content.fields) {
const parsed = JSON.parse(event.content);
schema.content.fields.forEach(field => {
if (field.required && parsed[field.key] === undefined) {
errors.push(`Missing required content field: ${field.key}`);
}
if (parsed[field.key] !== undefined) {
if (field.type === 'number' && typeof parsed[field.key] !== 'number') {
errors.push(`Invalid type for content field ${field.key}: expected number, got ${typeof parsed[field.key]}`);
}
if (field.type === 'string' && typeof parsed[field.key] !== 'string') {
errors.push(`Invalid type for content field ${field.key}: expected string, got ${typeof parsed[field.key]}`);
}
if (field.type === 'boolean' && typeof parsed[field.key] !== 'boolean') {
errors.push(`Invalid type for content field ${field.key}: expected boolean, got ${typeof parsed[field.key]}`);
}
}
});
}
} catch (e) {
errors.push("Content is not valid JSON");
}
} else if (schema.content.format === "empty") {
if (event.content !== "") {
errors.push("Content should be empty");
}
} else if (
schema.content.format === "text" ||
schema.content.format === "encrypted-tag-list" ||
schema.content.format === "nip44-json"
) {
if (typeof event.content !== "string") {
errors.push("Content must be a string");
}
}
}
return errors;
}
// Test cases
const tests = [
{
name: "Video Post",
type: NOTE_TYPES.VIDEO_POST,
builder: buildVideoPostEvent,
params: {
pubkey: MOCK_PUBKEY,
created_at: MOCK_TIMESTAMP,
dTagValue: MOCK_D_TAG,
content: {
version: 3,
title: "Test Video",
videoRootId: MOCK_EVENT_ID,
},
},
},
{
name: "Video Mirror",
type: NOTE_TYPES.VIDEO_MIRROR,
builder: buildVideoMirrorEvent,
params: {
pubkey: MOCK_PUBKEY,
created_at: MOCK_TIMESTAMP,
content: "Alt text",
tags: [["magnet", MOCK_MAGNET]],
},
},
{
name: "Repost",
type: NOTE_TYPES.REPOST,
builder: buildRepostEvent,
params: {
pubkey: MOCK_PUBKEY,
created_at: MOCK_TIMESTAMP,
eventId: MOCK_EVENT_ID,
eventRelay: MOCK_RELAY,
targetEvent: { id: MOCK_EVENT_ID, pubkey: MOCK_PUBKEY, kind: 1, content: "test", tags: [], created_at: MOCK_TIMESTAMP },
},
},
{
name: "Relay List",
type: NOTE_TYPES.RELAY_LIST,
builder: buildRelayListEvent,
params: {
pubkey: MOCK_PUBKEY,
created_at: MOCK_TIMESTAMP,
relays: [
{ url: "wss://r1.com", read: true, write: true },
{ url: "wss://r2.com", read: true, write: false },
],
},
},
{
name: "DM Relay List",
type: NOTE_TYPES.DM_RELAY_LIST,
builder: buildDmRelayListEvent,
params: {
pubkey: MOCK_PUBKEY,
created_at: MOCK_TIMESTAMP,
relays: ["wss://dm.relay.com"],
},
},
{
name: "Profile Metadata",
type: NOTE_TYPES.PROFILE_METADATA,
builder: buildProfileMetadataEvent,
params: {
pubkey: MOCK_PUBKEY,
created_at: MOCK_TIMESTAMP,
metadata: { name: "TestUser", about: "Testing" },
},
},
{
name: "Mute List",
type: NOTE_TYPES.MUTE_LIST,
builder: buildMuteListEvent,
params: {
pubkey: MOCK_PUBKEY,
created_at: MOCK_TIMESTAMP,
pTags: [MOCK_PUBKEY],
},
},
{
name: "DM Attachment",
type: NOTE_TYPES.DM_ATTACHMENT,
builder: buildDmAttachmentEvent,
params: {
pubkey: MOCK_PUBKEY,
created_at: MOCK_TIMESTAMP,
recipientPubkey: MOCK_PUBKEY,
attachment: { x: "hash", url: MOCK_URL, type: "image/jpeg" },
},
},
{
name: "DM Read Receipt",
type: NOTE_TYPES.DM_READ_RECEIPT,
builder: buildDmReadReceiptEvent,
params: {
pubkey: MOCK_PUBKEY,
created_at: MOCK_TIMESTAMP,
recipientPubkey: MOCK_PUBKEY,
eventId: MOCK_EVENT_ID,
messageKind: 4,
},
},
{
name: "DM Typing Indicator",
type: NOTE_TYPES.DM_TYPING,
builder: buildDmTypingIndicatorEvent,
params: {
pubkey: MOCK_PUBKEY,
created_at: MOCK_TIMESTAMP,
recipientPubkey: MOCK_PUBKEY,
eventId: MOCK_EVENT_ID,
expiresAt: MOCK_TIMESTAMP + 60,
},
},
{
name: "View Event",
type: NOTE_TYPES.VIEW_EVENT,
builder: buildViewEvent,
params: {
pubkey: MOCK_PUBKEY,
created_at: MOCK_TIMESTAMP,
pointerTag: ["e", MOCK_EVENT_ID],
dedupeTag: "unique-view-id",
},
},
{
name: "Zap Request",
type: NOTE_TYPES.ZAP_REQUEST,
builder: buildZapRequestEvent,
params: {
pubkey: MOCK_PUBKEY,
created_at: MOCK_TIMESTAMP,
recipientPubkey: MOCK_PUBKEY,
amountSats: 100,
relays: [MOCK_RELAY],
eventId: MOCK_EVENT_ID,
},
},
{
name: "Reaction",
type: NOTE_TYPES.VIDEO_REACTION,
builder: buildReactionEvent,
params: {
pubkey: MOCK_PUBKEY,
created_at: MOCK_TIMESTAMP,
targetPointer: { type: "e", value: MOCK_EVENT_ID, relay: MOCK_RELAY },
content: "+",
},
},
{
name: "Video Comment",
type: NOTE_TYPES.VIDEO_COMMENT,
builder: buildCommentEvent,
params: {
pubkey: MOCK_PUBKEY,
created_at: MOCK_TIMESTAMP,
videoEventId: MOCK_EVENT_ID,
videoEventRelay: MOCK_RELAY,
content: "Great video!",
},
},
{
name: "Watch History",
type: NOTE_TYPES.WATCH_HISTORY,
builder: buildWatchHistoryEvent,
params: {
pubkey: MOCK_PUBKEY,
created_at: MOCK_TIMESTAMP,
monthIdentifier: "2023-11",
pointerTags: [["e", MOCK_EVENT_ID]],
content: JSON.stringify({ version: 2, month: "2023-11", items: [] }),
},
},
{
name: "Subscription List",
type: NOTE_TYPES.SUBSCRIPTION_LIST,
builder: buildSubscriptionListEvent,
params: {
pubkey: MOCK_PUBKEY,
created_at: MOCK_TIMESTAMP,
content: "encrypted-content",
encryption: "nip44",
},
},
{
name: "Block List",
type: NOTE_TYPES.USER_BLOCK_LIST,
builder: buildBlockListEvent,
params: {
pubkey: MOCK_PUBKEY,
created_at: MOCK_TIMESTAMP,
content: "encrypted-content",
encryption: "nip44",
},
},
{
name: "Hashtag Preferences",
type: NOTE_TYPES.HASHTAG_PREFERENCES,
builder: buildHashtagPreferenceEvent,
params: {
pubkey: MOCK_PUBKEY,
created_at: MOCK_TIMESTAMP,
content: "encrypted-content",
},
},
{
name: "Admin Moderation List",
type: NOTE_TYPES.ADMIN_MODERATION_LIST,
builder: (params) => buildAdminListEvent("moderation", params),
params: {
pubkey: MOCK_PUBKEY,
created_at: MOCK_TIMESTAMP,
hexPubkeys: [MOCK_PUBKEY],
},
},
{
name: "Admin Blacklist",
type: NOTE_TYPES.ADMIN_BLACKLIST,
builder: (params) => buildAdminListEvent("blacklist", params),
params: {
pubkey: MOCK_PUBKEY,
created_at: MOCK_TIMESTAMP,
hexPubkeys: [MOCK_PUBKEY],
},
},
{
name: "Admin Whitelist",
type: NOTE_TYPES.ADMIN_WHITELIST,
builder: (params) => buildAdminListEvent("whitelist", params),
params: {
pubkey: MOCK_PUBKEY,
created_at: MOCK_TIMESTAMP,
hexPubkeys: [MOCK_PUBKEY],
},
},
{
name: "NIP-71 Video",
type: NOTE_TYPES.NIP71_VIDEO,
builder: buildNip71VideoEvent,
params: {
metadata: {
kind: 21,
title: "Test NIP-71 Video",
summary: "This is a test summary",
},
pubkey: MOCK_PUBKEY,
title: "Test NIP-71 Video",
},
},
];
// Execution
console.log("Starting Event Validation...");
let failCount = 0;
tests.forEach((test) => {
try {
const event = test.builder(test.params);
const errors = validateEvent(test.type, event);
if (errors.length > 0) {
console.error(`❌ ${test.name} Failed:`);
errors.forEach((err) => console.error(` - ${err}`));
failCount++;
} else {
console.log(`✅ ${test.name} Passed`);
}
} catch (e) {
console.error(`❌ ${test.name} threw exception:`, e);
failCount++;
}
});
if (failCount > 0) {
console.log(`\nValidation failed with ${failCount} errors.`);
process.exit(1);
} else {
console.log("\nAll events validated successfully!");
process.exit(0);
}