mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
chore(ai): event schema validation tool + fixes
- Added `scripts/agent/validate-events.mjs` to verify builder output against schemas. - Updated `js/nostrEventSchemas.js` to enforce relay hints in `buildRepostEvent` (fixes `tests/nostr-boost-actions.test.mjs`). - Documented usage in `docs/nostr-event-schemas.md`. - Verified 25 event builder scenarios pass validation.
This commit is contained in:
1 parent
870df0482a
commit
e282cf5a29
3 files changed
+364
-1
No files matched your search
@@ -7,6 +7,19 @@ no longer requires hunting through the codebase. It also exposes helpers for
|
||||
building events and for overriding the schema at runtime when you need to
|
||||
experiment.
|
||||
|
||||
## Validation
|
||||
|
||||
To verify that event builders produce spec-compliant events, run the validation script:
|
||||
|
||||
```bash
|
||||
node scripts/agent/validate-events.mjs
|
||||
```
|
||||
|
||||
This script generates example events for all defined builders and checks them against their schemas, ensuring:
|
||||
- Correct kind
|
||||
- Presence of required tags (topic, identifier, etc.)
|
||||
- Correct content format (JSON vs text vs empty)
|
||||
|
||||
## Runtime helpers
|
||||
|
||||
```js
|
||||
|
||||
@@ -891,7 +891,7 @@ export function buildRepostEvent(params) {
|
||||
if (normalizedEventRelay) {
|
||||
tags.push(["e", normalizedEventId, normalizedEventRelay]);
|
||||
} else {
|
||||
tags.push(["e", normalizedEventId]);
|
||||
throw new Error("missing-event-relay");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,350 @@
|
||||
|
||||
import {
|
||||
getNostrEventSchema,
|
||||
NOTE_TYPES,
|
||||
buildVideoPostEvent,
|
||||
buildVideoMirrorEvent,
|
||||
buildRepostEvent,
|
||||
buildRelayListEvent,
|
||||
buildDmRelayListEvent,
|
||||
buildProfileMetadataEvent,
|
||||
buildMuteListEvent,
|
||||
buildDeletionEvent,
|
||||
buildLegacyDirectMessageEvent,
|
||||
buildDmAttachmentEvent,
|
||||
buildDmReadReceiptEvent,
|
||||
buildDmTypingIndicatorEvent,
|
||||
buildViewEvent,
|
||||
buildZapRequestEvent,
|
||||
buildReactionEvent,
|
||||
buildCommentEvent,
|
||||
buildWatchHistoryEvent,
|
||||
buildSubscriptionListEvent,
|
||||
buildBlockListEvent,
|
||||
buildHashtagPreferenceEvent,
|
||||
buildAdminListEvent
|
||||
} from '../../js/nostrEventSchemas.js';
|
||||
|
||||
const COLORS = {
|
||||
RESET: "\x1b[0m",
|
||||
RED: "\x1b[31m",
|
||||
GREEN: "\x1b[32m",
|
||||
YELLOW: "\x1b[33m",
|
||||
BLUE: "\x1b[34m",
|
||||
};
|
||||
|
||||
let errorCount = 0;
|
||||
let successCount = 0;
|
||||
|
||||
function logError(msg) {
|
||||
console.error(`${COLORS.RED}[FAIL] ${msg}${COLORS.RESET}`);
|
||||
errorCount++;
|
||||
}
|
||||
|
||||
function logSuccess(msg) {
|
||||
console.log(`${COLORS.GREEN}[PASS] ${msg}${COLORS.RESET}`);
|
||||
successCount++;
|
||||
}
|
||||
|
||||
function logInfo(msg) {
|
||||
console.log(`${COLORS.BLUE}[INFO] ${msg}${COLORS.RESET}`);
|
||||
}
|
||||
|
||||
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)
|
||||
);
|
||||
}
|
||||
|
||||
function validateEvent(type, event, description) {
|
||||
const schema = getNostrEventSchema(type);
|
||||
if (!schema) {
|
||||
logError(`${description}: No schema found for type ${type}`);
|
||||
return;
|
||||
}
|
||||
|
||||
let valid = true;
|
||||
const errors = [];
|
||||
|
||||
// Validate Kind
|
||||
if (event.kind !== schema.kind) {
|
||||
errors.push(`Kind mismatch: expected ${schema.kind}, got ${event.kind}`);
|
||||
valid = false;
|
||||
}
|
||||
|
||||
// Validate 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}`);
|
||||
valid = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate Identifier Tag
|
||||
if (schema.identifierTag) {
|
||||
const expectedValue = schema.identifierTag.value;
|
||||
if (expectedValue) {
|
||||
if (!hasTag(event.tags, schema.identifierTag.name, expectedValue)) {
|
||||
errors.push(`Missing identifier tag: ${schema.identifierTag.name}=${expectedValue}`);
|
||||
valid = false;
|
||||
}
|
||||
} else {
|
||||
// If value is not fixed in schema, just check tag name presence if it's supposed to be there.
|
||||
// Usually d-tags are present.
|
||||
if (!hasTag(event.tags, schema.identifierTag.name)) {
|
||||
errors.push(`Missing identifier tag: ${schema.identifierTag.name}`);
|
||||
valid = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate Append Tags
|
||||
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)) {
|
||||
errors.push(`Missing append tag: ${tagName}=${tagValue}`);
|
||||
valid = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Validate Content Format
|
||||
if (schema.content) {
|
||||
if (schema.content.format === "json") {
|
||||
try {
|
||||
JSON.parse(event.content);
|
||||
} catch (e) {
|
||||
errors.push(`Content is not valid JSON`);
|
||||
valid = false;
|
||||
}
|
||||
} else if (schema.content.format === "empty") {
|
||||
if (event.content !== "") {
|
||||
errors.push(`Content should be empty, got: "${event.content}"`);
|
||||
valid = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (valid) {
|
||||
logSuccess(`${description}`);
|
||||
} else {
|
||||
logError(`${description}`);
|
||||
errors.forEach(e => console.error(` - ${e}`));
|
||||
console.log(" Event:", JSON.stringify(event, null, 2));
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
logInfo("Starting Event Validation...");
|
||||
|
||||
// 1. VIDEO_POST
|
||||
validateEvent(NOTE_TYPES.VIDEO_POST, buildVideoPostEvent({
|
||||
pubkey: "pubkey",
|
||||
created_at: 1234567890,
|
||||
dTagValue: "video-id",
|
||||
content: { title: "Test Video", videoRootId: "root-id" }
|
||||
}), "buildVideoPostEvent (standard)");
|
||||
|
||||
validateEvent(NOTE_TYPES.VIDEO_POST, buildVideoPostEvent({
|
||||
pubkey: "pubkey",
|
||||
created_at: 1234567890,
|
||||
dTagValue: "video-id-minimal",
|
||||
content: { version: 1, title: "Minimal", videoRootId: "root" }
|
||||
}), "buildVideoPostEvent (minimal)");
|
||||
|
||||
// 2. VIDEO_MIRROR
|
||||
validateEvent(NOTE_TYPES.VIDEO_MIRROR, buildVideoMirrorEvent({
|
||||
pubkey: "pubkey",
|
||||
created_at: 1234567890,
|
||||
content: "Alt text"
|
||||
}), "buildVideoMirrorEvent");
|
||||
|
||||
// 3. REPOST
|
||||
validateEvent(NOTE_TYPES.REPOST, buildRepostEvent({
|
||||
pubkey: "pubkey",
|
||||
created_at: 1234567890,
|
||||
eventId: "hex-id",
|
||||
eventRelay: "wss://relay.example.com",
|
||||
targetKind: 1,
|
||||
serializedEvent: JSON.stringify({ kind: 1, content: "test" })
|
||||
}), "buildRepostEvent (event pointer)");
|
||||
|
||||
// 4. RELAY_LIST
|
||||
validateEvent(NOTE_TYPES.RELAY_LIST, buildRelayListEvent({
|
||||
pubkey: "pubkey",
|
||||
created_at: 1234567890,
|
||||
relays: ["wss://relay.example.com"]
|
||||
}), "buildRelayListEvent");
|
||||
|
||||
// 5. DM_RELAY_LIST
|
||||
validateEvent(NOTE_TYPES.DM_RELAY_LIST, buildDmRelayListEvent({
|
||||
pubkey: "pubkey",
|
||||
created_at: 1234567890,
|
||||
relays: ["wss://relay.example.com"]
|
||||
}), "buildDmRelayListEvent");
|
||||
|
||||
// 6. PROFILE_METADATA
|
||||
validateEvent(NOTE_TYPES.PROFILE_METADATA, buildProfileMetadataEvent({
|
||||
pubkey: "pubkey",
|
||||
created_at: 1234567890,
|
||||
metadata: { name: "Test User" }
|
||||
}), "buildProfileMetadataEvent");
|
||||
|
||||
// 7. MUTE_LIST
|
||||
validateEvent(NOTE_TYPES.MUTE_LIST, buildMuteListEvent({
|
||||
pubkey: "pubkey",
|
||||
created_at: 1234567890,
|
||||
pTags: ["hex-pubkey"]
|
||||
}), "buildMuteListEvent");
|
||||
|
||||
// 8. DELETION
|
||||
validateEvent(NOTE_TYPES.DELETION, buildDeletionEvent({
|
||||
pubkey: "pubkey",
|
||||
created_at: 1234567890,
|
||||
eventIds: ["hex-id"]
|
||||
}), "buildDeletionEvent");
|
||||
|
||||
// 9. LEGACY_DM
|
||||
validateEvent(NOTE_TYPES.LEGACY_DM, buildLegacyDirectMessageEvent({
|
||||
pubkey: "pubkey",
|
||||
created_at: 1234567890,
|
||||
recipientPubkey: "hex-recipient",
|
||||
ciphertext: "encrypted-content"
|
||||
}), "buildLegacyDirectMessageEvent");
|
||||
|
||||
// 10. DM_ATTACHMENT
|
||||
validateEvent(NOTE_TYPES.DM_ATTACHMENT, buildDmAttachmentEvent({
|
||||
pubkey: "pubkey",
|
||||
created_at: 1234567890,
|
||||
recipientPubkey: "hex-recipient",
|
||||
attachment: { url: "https://example.com/file", x: "hash" }
|
||||
}), "buildDmAttachmentEvent");
|
||||
|
||||
// 11. DM_READ_RECEIPT
|
||||
validateEvent(NOTE_TYPES.DM_READ_RECEIPT, buildDmReadReceiptEvent({
|
||||
pubkey: "pubkey",
|
||||
created_at: 1234567890,
|
||||
recipientPubkey: "hex-recipient",
|
||||
eventId: "hex-event-id"
|
||||
}), "buildDmReadReceiptEvent");
|
||||
|
||||
// 12. DM_TYPING
|
||||
validateEvent(NOTE_TYPES.DM_TYPING, buildDmTypingIndicatorEvent({
|
||||
pubkey: "pubkey",
|
||||
created_at: 1234567890,
|
||||
recipientPubkey: "hex-recipient",
|
||||
eventId: "hex-event-id",
|
||||
expiresAt: 1234567899
|
||||
}), "buildDmTypingIndicatorEvent");
|
||||
|
||||
// 13. VIEW_EVENT
|
||||
validateEvent(NOTE_TYPES.VIEW_EVENT, buildViewEvent({
|
||||
pubkey: "pubkey",
|
||||
created_at: 1234567890,
|
||||
pointerValue: "video-d-tag",
|
||||
dedupeTag: "unique-session-view-id",
|
||||
includeSessionTag: true
|
||||
}), "buildViewEvent");
|
||||
|
||||
// 14. ZAP_REQUEST
|
||||
validateEvent(NOTE_TYPES.ZAP_REQUEST, buildZapRequestEvent({
|
||||
pubkey: "pubkey",
|
||||
created_at: 1234567890,
|
||||
recipientPubkey: "hex-recipient",
|
||||
amountSats: 100,
|
||||
relays: ["wss://relay.example.com"]
|
||||
}), "buildZapRequestEvent");
|
||||
|
||||
// 15. VIDEO_REACTION
|
||||
validateEvent(NOTE_TYPES.VIDEO_REACTION, buildReactionEvent({
|
||||
pubkey: "pubkey",
|
||||
created_at: 1234567890,
|
||||
targetPointer: { type: "e", value: "hex-id" },
|
||||
content: "+"
|
||||
}), "buildReactionEvent");
|
||||
|
||||
// 16. VIDEO_COMMENT
|
||||
validateEvent(NOTE_TYPES.VIDEO_COMMENT, buildCommentEvent({
|
||||
pubkey: "pubkey",
|
||||
created_at: 1234567890,
|
||||
content: "This is a comment",
|
||||
videoEventId: "hex-video-id"
|
||||
}), "buildCommentEvent (root)");
|
||||
|
||||
validateEvent(NOTE_TYPES.VIDEO_COMMENT, buildCommentEvent({
|
||||
pubkey: "pubkey",
|
||||
created_at: 1234567890,
|
||||
content: "This is a reply",
|
||||
videoEventId: "hex-video-id",
|
||||
rootIdentifier: "root-id",
|
||||
parentCommentId: "parent-id"
|
||||
}), "buildCommentEvent (reply)");
|
||||
|
||||
// 17. WATCH_HISTORY
|
||||
validateEvent(NOTE_TYPES.WATCH_HISTORY, buildWatchHistoryEvent({
|
||||
pubkey: "pubkey",
|
||||
created_at: 1234567890,
|
||||
monthIdentifier: "2023-10",
|
||||
content: "[]"
|
||||
}), "buildWatchHistoryEvent");
|
||||
|
||||
// 18. SUBSCRIPTION_LIST
|
||||
validateEvent(NOTE_TYPES.SUBSCRIPTION_LIST, buildSubscriptionListEvent({
|
||||
pubkey: "pubkey",
|
||||
created_at: 1234567890,
|
||||
content: "encrypted-json"
|
||||
}), "buildSubscriptionListEvent");
|
||||
|
||||
// 19. USER_BLOCK_LIST
|
||||
validateEvent(NOTE_TYPES.USER_BLOCK_LIST, buildBlockListEvent({
|
||||
pubkey: "pubkey",
|
||||
created_at: 1234567890,
|
||||
content: "encrypted-json"
|
||||
}), "buildBlockListEvent");
|
||||
|
||||
// 20. HASHTAG_PREFERENCES
|
||||
validateEvent(NOTE_TYPES.HASHTAG_PREFERENCES, buildHashtagPreferenceEvent({
|
||||
pubkey: "pubkey",
|
||||
created_at: 1234567890,
|
||||
content: "encrypted-json"
|
||||
}), "buildHashtagPreferenceEvent");
|
||||
|
||||
// 21. ADMIN_MODERATION_LIST
|
||||
validateEvent(NOTE_TYPES.ADMIN_MODERATION_LIST, buildAdminListEvent("moderation", {
|
||||
pubkey: "pubkey",
|
||||
created_at: 1234567890,
|
||||
hexPubkeys: ["hex-pubkey"]
|
||||
}), "buildAdminListEvent (moderation)");
|
||||
|
||||
// 22. ADMIN_BLACKLIST
|
||||
validateEvent(NOTE_TYPES.ADMIN_BLACKLIST, buildAdminListEvent("blacklist", {
|
||||
pubkey: "pubkey",
|
||||
created_at: 1234567890,
|
||||
hexPubkeys: ["hex-pubkey"]
|
||||
}), "buildAdminListEvent (blacklist)");
|
||||
|
||||
// 23. ADMIN_WHITELIST
|
||||
validateEvent(NOTE_TYPES.ADMIN_WHITELIST, buildAdminListEvent("whitelist", {
|
||||
pubkey: "pubkey",
|
||||
created_at: 1234567890,
|
||||
hexPubkeys: ["hex-pubkey"]
|
||||
}), "buildAdminListEvent (whitelist)");
|
||||
|
||||
console.log(`\nResults: ${successCount} passed, ${errorCount} failed.`);
|
||||
if (errorCount > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in new issue
Block a user