mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Merge pull request #2246 from PR0M3TH3AN/ai/schema-validate-20260208-8755993239566575953
chore(ai): event schema validation tool + fixes
This commit is contained in:
4 files changed
+252
-44
No files matched your search
@@ -167,6 +167,9 @@ import { updateWatchHistoryListWithDefaultClient } from "./nostrWatchHistoryFaca
|
||||
| DM attachment (`NOTE_TYPES.DM_ATTACHMENT`) | `15` | `['p', <recipient pubkey>]`, `['x', <sha256 of uploaded bytes>]`, `['url', <download url>]`, optional `['name', <filename>]`, `['type', <mime>]`, `['size', <bytes>]`, optional `['k', <base64 key>]`; inherits schema append tags | Empty content; attachment metadata is represented as tags |
|
||||
| DM read receipt (`NOTE_TYPES.DM_READ_RECEIPT`) | `20001` | `['p', <recipient pubkey>]`, `['e', <message event id>]`, optional `['k', <message kind>]`; inherits schema append tags | Empty content; ephemeral receipt for message activity |
|
||||
| DM typing indicator (`NOTE_TYPES.DM_TYPING`) | `20002` | `['p', <recipient pubkey>]`, optional `['e', <conversation event id>]`, `['t','typing']`, `['expiration', <unix seconds>]`; inherits schema append tags | Empty content; ephemeral typing indicator that expires quickly |
|
||||
| Gift Wrap (`NOTE_TYPES.GIFT_WRAP`) | `1059` | `['p', <recipient pubkey>]` | NIP-44 encrypted seal |
|
||||
| Seal (`NOTE_TYPES.SEAL`) | `13` | Empty tags usually | NIP-44 encrypted rumor |
|
||||
| Chat Message (`NOTE_TYPES.CHAT_MESSAGE`) | `14` | `['p', <recipient pubkey>]` | Plain text chat message |
|
||||
|
||||
> **Publishing note:** Session actors only emit passive telemetry (for example, view counters) and must **not** sign video comments. Require a logged-in Nostr signer for comment publishing via [`commentEvents`](../js/nostr/commentEvents.js).
|
||||
|
||||
|
||||
+18
-21
@@ -68,6 +68,9 @@ import {
|
||||
buildDmAttachmentEvent,
|
||||
buildDeletionEvent,
|
||||
buildLegacyDirectMessageEvent,
|
||||
buildGiftWrapEvent,
|
||||
buildSealEvent,
|
||||
buildChatMessageEvent,
|
||||
getNostrEventSchema,
|
||||
NOTE_TYPES,
|
||||
} from "../nostrEventSchemas.js";
|
||||
@@ -4851,13 +4854,12 @@ export class NostrClient {
|
||||
const createdAt = Math.floor(Date.now() / 1000);
|
||||
|
||||
if (trimmedMessage) {
|
||||
rumorEvents.push({
|
||||
kind: 14,
|
||||
created_at: createdAt,
|
||||
tags: [["p", targetHex]],
|
||||
content: trimmedMessage,
|
||||
rumorEvents.push(buildChatMessageEvent({
|
||||
pubkey: actorHex,
|
||||
});
|
||||
created_at: createdAt,
|
||||
recipientPubkey: targetHex,
|
||||
content: trimmedMessage,
|
||||
}));
|
||||
}
|
||||
|
||||
if (hasAttachments) {
|
||||
@@ -4896,16 +4898,14 @@ export class NostrClient {
|
||||
relayHint,
|
||||
rumorEvent,
|
||||
) => {
|
||||
const sealPayload = {
|
||||
kind: 13,
|
||||
const sealPayload = buildSealEvent({
|
||||
pubkey: actorHex,
|
||||
created_at: randomPastTimestamp(),
|
||||
tags: [],
|
||||
content: await signer.nip44Encrypt(
|
||||
ciphertext: await signer.nip44Encrypt(
|
||||
recipientPubkey,
|
||||
JSON.stringify(rumorEvent),
|
||||
),
|
||||
pubkey: actorHex,
|
||||
};
|
||||
});
|
||||
|
||||
const signedSeal = await signingAdapter.signEvent(sealPayload);
|
||||
if (!signedSeal || typeof signedSeal.id !== "string") {
|
||||
@@ -4928,17 +4928,14 @@ export class NostrClient {
|
||||
recipientPubkey,
|
||||
JSON.stringify(signedSeal),
|
||||
);
|
||||
const wrapTags = relayHint
|
||||
? [["p", recipientPubkey, relayHint]]
|
||||
: [["p", recipientPubkey]];
|
||||
|
||||
const wrapEvent = {
|
||||
kind: 1059,
|
||||
created_at: randomPastTimestamp(),
|
||||
tags: wrapTags,
|
||||
content: wrapCiphertext,
|
||||
const wrapEvent = buildGiftWrapEvent({
|
||||
pubkey: wrapperKeys.pubkey,
|
||||
};
|
||||
created_at: randomPastTimestamp(),
|
||||
recipientPubkey,
|
||||
relayHint,
|
||||
ciphertext: wrapCiphertext,
|
||||
});
|
||||
|
||||
return {
|
||||
wrap: signEventWithPrivateKey(wrapEvent, wrapperKeys.privateKey),
|
||||
|
||||
@@ -45,6 +45,9 @@ export const NOTE_TYPES = Object.freeze({
|
||||
LEGACY_DM: "legacyDm",
|
||||
HTTP_AUTH: "httpAuth",
|
||||
REPORT: "report",
|
||||
GIFT_WRAP: "giftWrap",
|
||||
SEAL: "seal",
|
||||
CHAT_MESSAGE: "chatMessage",
|
||||
});
|
||||
|
||||
export const SUBSCRIPTION_LIST_IDENTIFIER = "subscriptions";
|
||||
@@ -665,6 +668,38 @@ const BASE_SCHEMAS = {
|
||||
description: "Report reason.",
|
||||
},
|
||||
},
|
||||
[NOTE_TYPES.GIFT_WRAP]: {
|
||||
type: NOTE_TYPES.GIFT_WRAP,
|
||||
label: "Gift Wrap",
|
||||
kind: 1059,
|
||||
recipientTagName: "p",
|
||||
appendTags: DEFAULT_APPEND_TAGS,
|
||||
content: {
|
||||
format: "text",
|
||||
description: "NIP-44 encrypted seal.",
|
||||
},
|
||||
},
|
||||
[NOTE_TYPES.SEAL]: {
|
||||
type: NOTE_TYPES.SEAL,
|
||||
label: "Seal",
|
||||
kind: 13,
|
||||
appendTags: DEFAULT_APPEND_TAGS,
|
||||
content: {
|
||||
format: "text",
|
||||
description: "NIP-44 encrypted rumor.",
|
||||
},
|
||||
},
|
||||
[NOTE_TYPES.CHAT_MESSAGE]: {
|
||||
type: NOTE_TYPES.CHAT_MESSAGE,
|
||||
label: "Chat Message (Rumor)",
|
||||
kind: 14,
|
||||
participantTagName: "p",
|
||||
appendTags: DEFAULT_APPEND_TAGS,
|
||||
content: {
|
||||
format: "text",
|
||||
description: "Plain text chat message.",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let schemaOverrides = {};
|
||||
@@ -1097,6 +1132,116 @@ export function buildReportEvent(params) {
|
||||
return event;
|
||||
}
|
||||
|
||||
export function buildGiftWrapEvent(params) {
|
||||
const {
|
||||
pubkey,
|
||||
created_at,
|
||||
recipientPubkey,
|
||||
ciphertext = "",
|
||||
relayHint,
|
||||
additionalTags = [],
|
||||
} = params || {};
|
||||
const schema = getNostrEventSchema(NOTE_TYPES.GIFT_WRAP);
|
||||
const tags = [];
|
||||
|
||||
const recipient = normalizePointerIdentifier(recipientPubkey);
|
||||
if (recipient) {
|
||||
const pTag = [schema?.recipientTagName || "p", recipient];
|
||||
if (typeof relayHint === "string" && relayHint.trim()) {
|
||||
pTag.push(relayHint.trim());
|
||||
}
|
||||
tags.push(pTag);
|
||||
}
|
||||
|
||||
appendSchemaTags(tags, schema);
|
||||
const sanitizedAdditionalTags = sanitizeAdditionalTags(additionalTags);
|
||||
if (sanitizedAdditionalTags.length) {
|
||||
tags.push(...sanitizedAdditionalTags.map((tag) => tag.slice()));
|
||||
}
|
||||
|
||||
const event = {
|
||||
kind: schema?.kind ?? 1059,
|
||||
pubkey,
|
||||
created_at,
|
||||
tags,
|
||||
content: typeof ciphertext === "string" ? ciphertext : "",
|
||||
};
|
||||
|
||||
if (isDevMode) {
|
||||
validateEventAgainstSchema(NOTE_TYPES.GIFT_WRAP, event);
|
||||
}
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
export function buildSealEvent(params) {
|
||||
const {
|
||||
pubkey,
|
||||
created_at,
|
||||
ciphertext = "",
|
||||
additionalTags = [],
|
||||
} = params || {};
|
||||
const schema = getNostrEventSchema(NOTE_TYPES.SEAL);
|
||||
const tags = [];
|
||||
|
||||
appendSchemaTags(tags, schema);
|
||||
const sanitizedAdditionalTags = sanitizeAdditionalTags(additionalTags);
|
||||
if (sanitizedAdditionalTags.length) {
|
||||
tags.push(...sanitizedAdditionalTags.map((tag) => tag.slice()));
|
||||
}
|
||||
|
||||
const event = {
|
||||
kind: schema?.kind ?? 13,
|
||||
pubkey,
|
||||
created_at,
|
||||
tags,
|
||||
content: typeof ciphertext === "string" ? ciphertext : "",
|
||||
};
|
||||
|
||||
if (isDevMode) {
|
||||
validateEventAgainstSchema(NOTE_TYPES.SEAL, event);
|
||||
}
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
export function buildChatMessageEvent(params) {
|
||||
const {
|
||||
pubkey,
|
||||
created_at,
|
||||
recipientPubkey,
|
||||
content = "",
|
||||
additionalTags = [],
|
||||
} = params || {};
|
||||
const schema = getNostrEventSchema(NOTE_TYPES.CHAT_MESSAGE);
|
||||
const tags = [];
|
||||
|
||||
const participant = normalizePointerIdentifier(recipientPubkey);
|
||||
if (participant) {
|
||||
tags.push([schema?.participantTagName || "p", participant]);
|
||||
}
|
||||
|
||||
appendSchemaTags(tags, schema);
|
||||
const sanitizedAdditionalTags = sanitizeAdditionalTags(additionalTags);
|
||||
if (sanitizedAdditionalTags.length) {
|
||||
tags.push(...sanitizedAdditionalTags.map((tag) => tag.slice()));
|
||||
}
|
||||
|
||||
const event = {
|
||||
kind: schema?.kind ?? 14,
|
||||
pubkey,
|
||||
created_at,
|
||||
tags,
|
||||
content: typeof content === "string" ? content : "",
|
||||
};
|
||||
|
||||
if (isDevMode) {
|
||||
validateEventAgainstSchema(NOTE_TYPES.CHAT_MESSAGE, event);
|
||||
}
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
export function buildVideoMirrorEvent(params) {
|
||||
const {
|
||||
pubkey,
|
||||
|
||||
@@ -65,7 +65,12 @@ import {
|
||||
buildSubscriptionListEvent,
|
||||
buildBlockListEvent,
|
||||
buildHashtagPreferenceEvent,
|
||||
buildAdminListEvent
|
||||
buildAdminListEvent,
|
||||
buildGiftWrapEvent,
|
||||
buildSealEvent,
|
||||
buildChatMessageEvent,
|
||||
getNostrEventSchema,
|
||||
sanitizeAdditionalTags
|
||||
} from '../../js/nostrEventSchemas.js';
|
||||
|
||||
import { buildNip71VideoEvent } from '../../js/nostr/nip71.js';
|
||||
@@ -73,7 +78,7 @@ import { buildNip71VideoEvent } from '../../js/nostr/nip71.js';
|
||||
const TEST_PUBKEY = '0000000000000000000000000000000000000000000000000000000000000001';
|
||||
const TEST_TIMESTAMP = 1672531200; // 2023-01-01
|
||||
|
||||
function runTest(name, builderFn, input, expectedType) {
|
||||
function runTest(name, builderFn, input, expectedType, additionalCheck) {
|
||||
console.log(`\nTesting: ${name}`);
|
||||
try {
|
||||
const event = builderFn(input);
|
||||
@@ -84,6 +89,14 @@ function runTest(name, builderFn, input, expectedType) {
|
||||
|
||||
const { valid, errors } = validateEventStructure(expectedType, event);
|
||||
if (valid) {
|
||||
if (additionalCheck) {
|
||||
try {
|
||||
additionalCheck(event);
|
||||
} catch (checkError) {
|
||||
console.error(`❌ Additional check failed: ${checkError.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
console.log(`✅ Valid ${expectedType} (Kind: ${event.kind})`);
|
||||
return true;
|
||||
} else {
|
||||
@@ -101,47 +114,70 @@ async function main() {
|
||||
console.log('Starting Event Schema Validation...');
|
||||
let failures = 0;
|
||||
|
||||
// 1. Video Post
|
||||
const videoInput = {
|
||||
// --- Real-world usage patterns ---
|
||||
|
||||
// 1. Video Post (mimicking js/nostr/client.js publishVideo)
|
||||
// Testing auto-generation of 's' tag from URL
|
||||
const videoInputReal = {
|
||||
pubkey: TEST_PUBKEY,
|
||||
created_at: TEST_TIMESTAMP,
|
||||
dTagValue: 'test-video-id',
|
||||
dTagValue: 'test-video-id-123',
|
||||
content: {
|
||||
version: 3,
|
||||
title: 'Test Video',
|
||||
videoRootId: 'test-video-id',
|
||||
title: 'Real World Video',
|
||||
videoRootId: 'test-video-id-123',
|
||||
url: 'https://example.com/video.mp4',
|
||||
magnet: 'magnet:?xt=urn:btih:1234567890abcdef1234567890abcdef12345678',
|
||||
thumbnail: 'https://example.com/thumb.jpg',
|
||||
description: 'A test video',
|
||||
mode: 'live',
|
||||
deleted: false,
|
||||
isPrivate: false,
|
||||
isNsfw: false,
|
||||
isForKids: true,
|
||||
isForKids: false,
|
||||
enableComments: true
|
||||
}
|
||||
},
|
||||
// No explicit 's' tag passed, should be generated
|
||||
additionalTags: [['t', 'art']]
|
||||
};
|
||||
if (!runTest('buildVideoPostEvent', buildVideoPostEvent, videoInput, NOTE_TYPES.VIDEO_POST)) failures++;
|
||||
|
||||
// 2. Relay List
|
||||
const relayListInput = {
|
||||
if (!runTest('buildVideoPostEvent (Real Usage - Auto Storage Pointer)', buildVideoPostEvent, videoInputReal, NOTE_TYPES.VIDEO_POST, (event) => {
|
||||
const sTag = event.tags.find(t => t[0] === 's');
|
||||
if (!sTag) throw new Error("Missing 's' tag (Storage Pointer) which should have been auto-generated from URL");
|
||||
if (!sTag[1].startsWith('url:')) throw new Error(`'s' tag should start with 'url:', got ${sTag[1]}`);
|
||||
})) failures++;
|
||||
|
||||
// 2. Relay List (mimicking js/relayManager.js)
|
||||
const relayListInputReal = {
|
||||
pubkey: TEST_PUBKEY,
|
||||
created_at: TEST_TIMESTAMP,
|
||||
relays: [
|
||||
{ url: 'wss://relay.example.com', mode: 'read' },
|
||||
{ url: 'wss://relay.other.com', mode: 'write' },
|
||||
'wss://relay.both.com'
|
||||
{ url: 'wss://relay.damus.io', mode: 'read' },
|
||||
{ url: 'wss://nos.lol', mode: 'write' },
|
||||
{ url: 'wss://relay.primal.net', mode: 'both' }
|
||||
]
|
||||
};
|
||||
if (!runTest('buildRelayListEvent', buildRelayListEvent, relayListInput, NOTE_TYPES.RELAY_LIST)) failures++;
|
||||
if (!runTest('buildRelayListEvent (Real Usage)', buildRelayListEvent, relayListInputReal, NOTE_TYPES.RELAY_LIST, (event) => {
|
||||
const rTags = event.tags.filter(t => t[0] === 'r');
|
||||
if (rTags.length !== 3) throw new Error(`Expected 3 'r' tags, got ${rTags.length}`);
|
||||
const damus = rTags.find(t => t[1] === 'wss://relay.damus.io');
|
||||
if (!damus || damus[2] !== 'read') throw new Error("Damus relay missing or incorrect marker");
|
||||
})) failures++;
|
||||
|
||||
// 3. Hashtag Preferences
|
||||
const hashtagInput = {
|
||||
// 3. Hashtag Preferences (mimicking js/services/hashtagPreferencesService.js)
|
||||
// Content is encrypted string (ciphertext)
|
||||
const hashtagInputReal = {
|
||||
pubkey: TEST_PUBKEY,
|
||||
created_at: TEST_TIMESTAMP,
|
||||
content: JSON.stringify({ interests: ['art', 'code'], disinterests: ['spam'] })
|
||||
content: "nip44:ciphertext-blob-simulated",
|
||||
// Service adds encrypted tag
|
||||
additionalTags: [['encrypted', 'nip44']]
|
||||
};
|
||||
if (!runTest('buildHashtagPreferenceEvent', buildHashtagPreferenceEvent, hashtagInput, NOTE_TYPES.HASHTAG_PREFERENCES)) failures++;
|
||||
if (!runTest('buildHashtagPreferenceEvent (Real Usage - Encrypted)', buildHashtagPreferenceEvent, hashtagInputReal, NOTE_TYPES.HASHTAG_PREFERENCES, (event) => {
|
||||
if (event.content !== "nip44:ciphertext-blob-simulated") throw new Error("Content mismtach");
|
||||
const dTag = event.tags.find(t => t[0] === 'd');
|
||||
if (!dTag || dTag[1] !== 'bitvid:tag-preferences') throw new Error("Incorrect 'd' tag identifier");
|
||||
})) failures++;
|
||||
|
||||
|
||||
// --- Standard Checks ---
|
||||
|
||||
// 4. HTTP Auth
|
||||
const httpAuthInput = {
|
||||
@@ -339,6 +375,33 @@ async function main() {
|
||||
};
|
||||
if (!runTest('buildNip71VideoEvent', buildNip71VideoEvent, nip71Input, NOTE_TYPES.NIP71_VIDEO)) failures++;
|
||||
|
||||
// 26. NIP-17 Gift Wrap
|
||||
const giftWrapInput = {
|
||||
pubkey: TEST_PUBKEY,
|
||||
created_at: TEST_TIMESTAMP,
|
||||
recipientPubkey: TEST_PUBKEY,
|
||||
ciphertext: 'encrypted-seal',
|
||||
relayHint: 'wss://relay.example.com'
|
||||
};
|
||||
if (!runTest('buildGiftWrapEvent', buildGiftWrapEvent, giftWrapInput, NOTE_TYPES.GIFT_WRAP)) failures++;
|
||||
|
||||
// 27. NIP-17 Seal
|
||||
const sealInput = {
|
||||
pubkey: TEST_PUBKEY,
|
||||
created_at: TEST_TIMESTAMP,
|
||||
ciphertext: 'encrypted-rumor'
|
||||
};
|
||||
if (!runTest('buildSealEvent', buildSealEvent, sealInput, NOTE_TYPES.SEAL)) failures++;
|
||||
|
||||
// 28. NIP-17 Chat Message
|
||||
const chatMessageInput = {
|
||||
pubkey: TEST_PUBKEY,
|
||||
created_at: TEST_TIMESTAMP,
|
||||
recipientPubkey: TEST_PUBKEY,
|
||||
content: 'Hello World'
|
||||
};
|
||||
if (!runTest('buildChatMessageEvent', buildChatMessageEvent, chatMessageInput, NOTE_TYPES.CHAT_MESSAGE)) failures++;
|
||||
|
||||
|
||||
console.log('\n-----------------------------------');
|
||||
if (failures === 0) {
|
||||
|
||||
Reference in new issue
Block a user