mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
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:
1 parent
59f09e5149
commit
3b80692eb0
3 files changed
+455
-242
No files matched your search
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
Reference in new issue
Block a user