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.
This commit is contained in:
google-labs-jules[bot] committed 2026-01-20 05:21:18 +00:00
1 parent 59f09e5149
commit 3b80692eb0
3 files changed
+455 -242

No files matched your search

+1 -2
View File
@@ -159,14 +159,13 @@ import { updateWatchHistoryListWithDefaultClient } from "./nostrWatchHistoryFaca
| Relay list (`NOTE_TYPES.RELAY_LIST`) | `10002` | Repeating `['r', <relay url>]` tags, optionally with a marker of `'read'` or `'write'` to scope the relay; marker omitted for read/write relays | Empty content |
| DM relay hints (`NOTE_TYPES.DM_RELAY_LIST`) | `10050` | Repeating `['relay', <relay url>]` tags to advertise delivery relays for NIP-17 gift-wrapped DMs | Empty content |
| View counter (`NOTE_TYPES.VIEW_EVENT`) | `WATCH_HISTORY_KIND` (default `30079`) | Canonical tag set: `['t','view']`, a pointer tag (`['e', <eventId>]` or `['a', <address>]`), and a stable dedupe tag `['d', <view identifier>]`, with optional `['session','true']` when a session actor signs; schema overrides may append extra tags. `['video', ...]` is supported for legacy overrides only. | Optional plaintext message |
| Watch history month (`NOTE_TYPES.WATCH_HISTORY_INDEX`) | `WATCH_HISTORY_KIND` (default `30079`) | Replaceable list tag `['d', `${WATCH_HISTORY_LIST_IDENTIFIER}:<YYYY-MM>`]` with optional `['month', <YYYY-MM>]` marker plus schema append tags; no chunk pointers required. | JSON payload `{ version, month: 'YYYY-MM', items: [{ id: <eventId\|address>, watched_at?: <unix seconds> }] }` |
| Watch history month (`NOTE_TYPES.WATCH_HISTORY`) | `WATCH_HISTORY_KIND` (default `30079`) | Replaceable list tag `['d', `${WATCH_HISTORY_LIST_IDENTIFIER}:<YYYY-MM>`]` with optional `['month', <YYYY-MM>]` marker plus schema append tags; no chunk pointers required. | JSON payload `{ version, month: 'YYYY-MM', items: [{ id: <eventId\|address>, watched_at?: <unix seconds> }] }` |
| Subscription list (`NOTE_TYPES.SUBSCRIPTION_LIST`) | `30000` | `['d', 'subscriptions']` | NIP-04/NIP-44 encrypted JSON array of NIP-51 follow-set tuples (e.g., `[['p', <hex>], …]`) |
| User block list (`NOTE_TYPES.USER_BLOCK_LIST`) | `10000` | `['d', 'user-blocks']` | NIP-04/NIP-44 encrypted JSON `{ blockedPubkeys: string[] }` |
| Hashtag preferences (`NOTE_TYPES.HASHTAG_PREFERENCES`) | `30015` | `['d', 'bitvid:tag-preferences']` plus schema-appended `['encrypted','nip44_v2']` | NIP-44 encrypted JSON `{ version, interests: string[], disinterests: string[] }` |
| Admin moderation list (`NOTE_TYPES.ADMIN_MODERATION_LIST`) | `30000` | `['d', 'bitvid:admin:editors']`, repeated `['p', <pubkey>]` entries | Empty content |
| Admin blacklist (`NOTE_TYPES.ADMIN_BLACKLIST`) | `30000` | `['d', 'bitvid:admin:blacklist']`, repeated `['p', <pubkey>]` entries | Empty content |
| Admin whitelist (`NOTE_TYPES.ADMIN_WHITELIST`) | `30000` | `['d', 'bitvid:admin:whitelist']`, repeated `['p', <pubkey>]` entries | Empty content |
| Storage challenge (`NOTE_TYPES.STORAGE_CHALLENGE`) | `22242` | `['challenge', <hex>]`, `['purpose', 'bitvid-storage-key']` | Empty content. This event is **ephemeral** and never published to relays. It serves as a challenge for the signer; the deterministic signature produced is used to derive the local encryption Master Key for the Storage Service. |
| Profile metadata (`NOTE_TYPES.PROFILE_METADATA`) | `0` | `['d', ...]` is not used | JSON payload with NIP-01 fields (`name`, `about`, `picture`, `nip05`, etc.) |
| Mute list (`NOTE_TYPES.MUTE_LIST`) | `10000` | Repeated `['p', <pubkey>]` tags for blocked/muted users | Optional content (often encrypted) |
| Zap request (`NOTE_TYPES.ZAP_REQUEST`) | `9734` | `['p', <recipient pubkey>]` plus optional `['e', <event id>]`, `['a', <coordinate>]`, `['amount', <msats>]`, `['lnurl', <bech32>]`, and `['relays', ...]` for receipt publishing | Optional plaintext zap note |
+78 -1
View File
@@ -801,13 +801,19 @@ export function buildVideoPostEvent(params) {
}
}
return {
const event = {
kind: schema?.kind ?? 30078,
pubkey,
created_at,
tags,
content: serializedContent,
};
if (isDevMode) {
validateEventAgainstSchema(NOTE_TYPES.VIDEO_POST, event);
}
return event;
}
export function buildVideoMirrorEvent(params) {
@@ -1958,11 +1964,82 @@ export function buildAdminListEvent(listKey, params) {
};
}
function hasTag(tags, tagName, tagValue = null) {
if (!Array.isArray(tags)) return false;
return tags.some(
(tag) =>
Array.isArray(tag) &&
tag[0] === tagName &&
(tagValue === null || tag[1] === tagValue)
);
}
export function validateEventAgainstSchema(type, event) {
if (!isDevMode || !event) return;
const schema = getNostrEventSchema(type);
if (!schema) return;
if (event.kind !== schema.kind) {
devLogger.warn(
`[schema] Kind mismatch for ${type}: expected ${schema.kind}, got ${event.kind}`,
);
}
if (schema.topicTag) {
if (!hasTag(event.tags, schema.topicTag.name, schema.topicTag.value)) {
devLogger.warn(
`[schema] Missing topic tag for ${type}: ${schema.topicTag.name}=${schema.topicTag.value}`,
);
}
}
if (schema.identifierTag) {
const expectedValue = schema.identifierTag.value;
if (!hasTag(event.tags, schema.identifierTag.name, expectedValue)) {
devLogger.warn(
`[schema] Missing identifier tag for ${type}: ${schema.identifierTag.name}${
expectedValue ? "=" + expectedValue : ""
}`,
);
}
}
if (schema.appendTags) {
schema.appendTags.forEach((appendTag) => {
if (Array.isArray(appendTag) && appendTag.length >= 2) {
const tagName = appendTag[0];
const tagValue = appendTag[1];
if (!hasTag(event.tags, tagName, tagValue)) {
devLogger.warn(
`[schema] Missing append tag for ${type}: ${tagName}=${tagValue}`,
);
}
}
});
}
if (schema.content) {
if (schema.content.format === "json") {
try {
JSON.parse(event.content);
} catch (e) {
devLogger.warn(`[schema] Content is not valid JSON for ${type}`);
}
} else if (schema.content.format === "empty") {
if (event.content !== "") {
devLogger.warn(`[schema] Content should be empty for ${type}`);
}
}
}
}
if (typeof window !== "undefined") {
window.bitvidNostrEvents = {
NOTE_TYPES,
getSchema: getNostrEventSchema,
getAllSchemas: getAllNostrEventSchemas,
setOverrides: setNostrEventSchemaOverrides,
validateEvent: validateEventAgainstSchema,
};
}
+376 -239
View File
@@ -1,10 +1,18 @@
import {
NOTE_TYPES,
getNostrEventSchema,
buildVideoPostEvent,
buildVideoMirrorEvent,
buildRepostEvent,
buildRelayListEvent,
buildDmRelayListEvent,
buildProfileMetadataEvent,
buildMuteListEvent,
buildDmAttachmentEvent,
buildDmReadReceiptEvent,
buildDmTypingIndicatorEvent,
buildViewEvent,
buildZapRequestEvent,
buildReactionEvent,
buildCommentEvent,
buildWatchHistoryEvent,
@@ -12,284 +20,413 @@ import {
buildBlockListEvent,
buildHashtagPreferenceEvent,
buildAdminListEvent,
buildProfileMetadataEvent,
buildMuteListEvent,
NOTE_TYPES,
ADMIN_LIST_IDENTIFIERS
} from "../../js/nostrEventSchemas.js";
const TEST_PUBKEY = "0000000000000000000000000000000000000000000000000000000000000001";
const TEST_TIMESTAMP = 1700000000;
import { buildNip71VideoEvent } from "../../js/nostr/nip71.js";
function validateEvent(event, schema, label) {
// 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 = [];
// Check Kind
if (schema.kind !== undefined && event.kind !== schema.kind) {
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}`);
}
// Check Content Format
// 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 {
const parsed = JSON.parse(event.content);
JSON.parse(event.content);
// TODO: Validate JSON schema fields if defined (schema.content.fields)
if (schema.content.fields) {
for (const field of schema.content.fields) {
if (field.required && parsed[field.key] === undefined) {
errors.push(`Missing required content field: ${field.key}`);
}
if (parsed[field.key] !== undefined) {
// Basic type checking
if (field.type === 'number' && typeof parsed[field.key] !== 'number') {
errors.push(`Field ${field.key} expected number, got ${typeof parsed[field.key]}`);
}
if (field.type === 'string' && typeof parsed[field.key] !== 'string') {
errors.push(`Field ${field.key} expected string, got ${typeof parsed[field.key]}`);
}
if (field.type === 'boolean' && typeof parsed[field.key] !== 'boolean') {
errors.push(`Field ${field.key} expected boolean, got ${typeof parsed[field.key]}`);
}
}
}
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: ${e.message}`);
errors.push("Content is not valid JSON");
}
} else if (schema.content.format === "empty") {
if (event.content !== "") {
errors.push(`Content expected to be empty, got length ${event.content.length}`);
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");
}
}
}
// Check Tags
if (schema.topicTag) {
const hasTag = event.tags.some(t => t[0] === schema.topicTag.name && t[1] === schema.topicTag.value);
if (!hasTag) {
errors.push(`Missing topic tag: ${schema.topicTag.name}=${schema.topicTag.value}`);
}
}
if (schema.identifierTag) {
const hasTag = event.tags.some(t => t[0] === schema.identifierTag.name);
if (!hasTag) {
errors.push(`Missing identifier tag: ${schema.identifierTag.name}`);
}
if (schema.identifierTag.value) {
const hasValue = event.tags.some(t => t[0] === schema.identifierTag.name && t[1] === schema.identifierTag.value);
if (!hasValue) {
errors.push(`Missing identifier tag value: ${schema.identifierTag.name}=${schema.identifierTag.value}`);
}
}
}
// Append Tags Check (Simple existence check for fixed tags)
if (schema.appendTags) {
schema.appendTags.forEach(expectedTag => {
if (Array.isArray(expectedTag)) {
const found = event.tags.some(t =>
t.length >= expectedTag.length &&
expectedTag.every((val, i) => t[i] === val)
);
if (!found) {
errors.push(`Missing appended tag: ${JSON.stringify(expectedTag)}`);
}
}
});
}
if (errors.length > 0) {
console.error(`❌ ${label} validation failed:`);
errors.forEach(e => console.error(` - ${e}`));
return false;
} else {
console.log(`✅ ${label} validated successfully.`);
return true;
}
return errors;
}
async function runValidation() {
console.log("Starting Event Schema Validation...\n");
let allPassed = true;
// 1. Video Post
// Test cases
const tests = [
{
const schema = getNostrEventSchema(NOTE_TYPES.VIDEO_POST);
const event = buildVideoPostEvent({
pubkey: TEST_PUBKEY,
created_at: TEST_TIMESTAMP,
dTagValue: "test-video",
name: "Video Post",
type: NOTE_TYPES.VIDEO_POST,
builder: buildVideoPostEvent,
params: {
pubkey: MOCK_PUBKEY,
created_at: MOCK_TIMESTAMP,
dTagValue: MOCK_D_TAG,
content: {
version: 1,
version: 3,
title: "Test Video",
videoRootId: "root-id"
}
});
if (!validateEvent(event, schema, "Video Post")) allPassed = false;
}
// 2. Video Mirror
videoRootId: MOCK_EVENT_ID,
},
},
},
{
const schema = getNostrEventSchema(NOTE_TYPES.VIDEO_MIRROR);
const event = buildVideoMirrorEvent({
pubkey: TEST_PUBKEY,
created_at: TEST_TIMESTAMP,
content: "mirror description"
});
if (!validateEvent(event, schema, "Video Mirror")) allPassed = false;
}
// 3. Repost
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]],
},
},
{
const schema = getNostrEventSchema(NOTE_TYPES.REPOST);
const event = buildRepostEvent({
pubkey: TEST_PUBKEY,
created_at: TEST_TIMESTAMP,
eventId: "event-id",
eventRelay: "wss://relay.example.com",
serializedEvent: JSON.stringify({ id: "event-id", kind: 1, tags: [], content: "test" })
});
// Schema updated to expect JSON content, so this should pass now.
if (!validateEvent(event, schema, "Repost")) allPassed = false;
}
// 4. Relay List
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 },
},
},
{
const schema = getNostrEventSchema(NOTE_TYPES.RELAY_LIST);
const event = buildRelayListEvent({
pubkey: TEST_PUBKEY,
created_at: TEST_TIMESTAMP,
relays: ["wss://relay.one", { url: "wss://relay.two", mode: "read" }]
});
if (!validateEvent(event, schema, "Relay List")) allPassed = false;
}
// 5. View Event
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 },
],
},
},
{
const schema = getNostrEventSchema(NOTE_TYPES.VIEW_EVENT);
const event = buildViewEvent({
pubkey: TEST_PUBKEY,
created_at: TEST_TIMESTAMP,
dedupeTag: "dedupe-val",
includeSessionTag: true,
content: "view log"
});
if (!validateEvent(event, schema, "View Event")) allPassed = false;
}
// 6. Reaction Event
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"],
},
},
{
const schema = getNostrEventSchema(NOTE_TYPES.VIDEO_REACTION);
const event = buildReactionEvent({
pubkey: TEST_PUBKEY,
created_at: TEST_TIMESTAMP,
targetPointer: { type: "e", value: "target-id", relay: "wss://r.com" },
content: "+"
});
if (!validateEvent(event, schema, "Reaction Event")) allPassed = false;
}
// 7. Comment Event
name: "Profile Metadata",
type: NOTE_TYPES.PROFILE_METADATA,
builder: buildProfileMetadataEvent,
params: {
pubkey: MOCK_PUBKEY,
created_at: MOCK_TIMESTAMP,
metadata: { name: "TestUser", about: "Testing" },
},
},
{
const schema = getNostrEventSchema(NOTE_TYPES.VIDEO_COMMENT);
const event = buildCommentEvent({
pubkey: TEST_PUBKEY,
created_at: TEST_TIMESTAMP,
videoEventId: "vid-id",
content: "Nice video!"
});
if (!validateEvent(event, schema, "Comment Event")) allPassed = false;
}
// 8. Watch History
name: "Mute List",
type: NOTE_TYPES.MUTE_LIST,
builder: buildMuteListEvent,
params: {
pubkey: MOCK_PUBKEY,
created_at: MOCK_TIMESTAMP,
pTags: [MOCK_PUBKEY],
},
},
{
const schema = getNostrEventSchema(NOTE_TYPES.WATCH_HISTORY);
const event = buildWatchHistoryEvent({
pubkey: TEST_PUBKEY,
created_at: TEST_TIMESTAMP,
monthIdentifier: "2023-10",
content: JSON.stringify(["watched-id-1"])
});
if (!validateEvent(event, schema, "Watch History")) allPassed = false;
}
// 9. Subscription List
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" },
},
},
{
const schema = getNostrEventSchema(NOTE_TYPES.SUBSCRIPTION_LIST);
const event = buildSubscriptionListEvent({
pubkey: TEST_PUBKEY,
created_at: TEST_TIMESTAMP,
content: "encrypted-stuff"
});
if (!validateEvent(event, schema, "Subscription List")) allPassed = false;
}
// 10. Block List
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,
},
},
{
const schema = getNostrEventSchema(NOTE_TYPES.USER_BLOCK_LIST);
const event = buildBlockListEvent({
pubkey: TEST_PUBKEY,
created_at: TEST_TIMESTAMP,
content: "encrypted-stuff"
});
if (!validateEvent(event, schema, "Block List")) allPassed = false;
}
// 11. Hashtag Preference
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,
},
},
{
const schema = getNostrEventSchema(NOTE_TYPES.HASHTAG_PREFERENCES);
const event = buildHashtagPreferenceEvent({
pubkey: TEST_PUBKEY,
created_at: TEST_TIMESTAMP,
content: JSON.stringify({ version: 1, interests: [], disinterests: [] })
});
if (!validateEvent(event, schema, "Hashtag Preferences")) allPassed = false;
}
// 12. Admin List (Moderation)
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",
},
},
{
const schema = getNostrEventSchema(NOTE_TYPES.ADMIN_MODERATION_LIST);
const event = buildAdminListEvent("moderation", {
pubkey: TEST_PUBKEY,
created_at: TEST_TIMESTAMP,
hexPubkeys: ["0000000000000000000000000000000000000000000000000000000000000002"]
});
if (!validateEvent(event, schema, "Admin Moderation List")) allPassed = false;
}
// 13. Profile Metadata
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,
},
},
{
const schema = getNostrEventSchema(NOTE_TYPES.PROFILE_METADATA);
const event = buildProfileMetadataEvent({
pubkey: TEST_PUBKEY,
created_at: TEST_TIMESTAMP,
metadata: { name: "Test User", about: "Testing" }
});
if (!validateEvent(event, schema, "Profile Metadata")) allPassed = false;
}
// 14. Mute List
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: "+",
},
},
{
const schema = getNostrEventSchema(NOTE_TYPES.MUTE_LIST);
const event = buildMuteListEvent({
pubkey: TEST_PUBKEY,
created_at: TEST_TIMESTAMP,
pTags: ["0000000000000000000000000000000000000000000000000000000000000002"]
});
if (!validateEvent(event, schema, "Mute List")) allPassed = false;
}
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",
},
},
];
if (!allPassed) {
console.error("\n❌ Some validations failed.");
process.exit(1);
} else {
console.log("\n✅ All validations passed.");
}
}
// Execution
console.log("Starting Event Validation...");
let failCount = 0;
runValidation().catch(e => {
console.error(e);
process.exit(1);
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);
}