diff --git a/scripts/agent/fuzz-dmDecryptor.mjs b/scripts/agent/fuzz-dmDecryptor.mjs
index e3a7aa3b..a161b5be 100644
--- a/scripts/agent/fuzz-dmDecryptor.mjs
+++ b/scripts/agent/fuzz-dmDecryptor.mjs
@@ -1,179 +1,75 @@
-import { decryptDM } from '../../js/dmDecryptor.js';
-import {
- fuzzBoolean,
- fuzzInt,
- fuzzString,
- fuzzHexString,
- fuzzJSON,
- pickOne,
- fuzzSurrogatePairString,
- saveFuzzReport,
- saveReproducer
-} from './fuzz-utils.mjs';
-import fs from 'fs';
-import path from 'path';
-const ITERATIONS = 1000;
-const FINDINGS = [];
+import fs from "fs";
+import path from "path";
+import { runFuzzer, rng } from "./fuzz-shared.mjs";
-function saveReproducerDM(id, input, error) {
- const reproDir = path.join('examples', 'reproducers', `fuzz-dmDecryptor-${id}`);
- if (!fs.existsSync(reproDir)) {
- fs.mkdirSync(reproDir, { recursive: true });
- }
+const originalPath = "../../js/dmDecryptor.js";
+const tempPath = "./temp_dmDecryptor.mjs";
- const readmeContent = `# Fuzz Reproducer: dmDecryptor - ${id}
+// Read original file
+const originalContent = fs.readFileSync(new URL(originalPath, import.meta.url), "utf-8");
-## Error
-\`\`\`
-${error.stack || error.message}
-\`\`\`
-`;
+// Replace import
+// The original file has: import { normalizeActorKey } from "./nostr/watchHistory.js";
+// We want to point to ./mocks/watchHistory.js
+const patchedContent = originalContent.replace(
+ /import \{ normalizeActorKey \} from "\.\/nostr\/watchHistory\.js";/,
+ 'import { normalizeActorKey } from "./mocks/watchHistory.js";'
+);
- fs.writeFileSync(path.join(reproDir, 'README.md'), readmeContent);
+// Write temp file
+fs.writeFileSync(new URL(tempPath, import.meta.url), patchedContent);
- // Serialize input parts
- const event = input.event;
- const context = input.context;
+// Import the patched module
+const DmDecryptor = await import(tempPath);
- // We can't easily capture the 'shouldFail' state unless we stored it on the object,
- // but we can infer or just log it. For now, let's just save the structure.
-
- const serializedContext = {
- actorPubkey: context?.actorPubkey,
- decryptorConfigs: context?.decryptors && Array.isArray(context.decryptors) ? context.decryptors.map(d => ({
- scheme: d.scheme,
- supportsGiftWrap: d.supportsGiftWrap,
- })) : context?.decryptors
+async function fuzzTest(iteration) {
+ const genEvent = () => {
+ return {
+ kind: rng.oneOf([4, 1059, rng.int(0, 20000)]),
+ pubkey: rng.mixedString(64),
+ created_at: rng.int(0, 2000000000),
+ content: rng.mixedString(100),
+ tags: rng.array(() => rng.array(() => rng.mixedString(20), 5), 5)
};
+ };
- fs.writeFileSync(path.join(reproDir, 'input-event.json'), JSON.stringify(event, null, 2));
- fs.writeFileSync(path.join(reproDir, 'input-context-config.json'), JSON.stringify(serializedContext, null, 2));
+ const genDecryptor = () => {
+ return {
+ scheme: rng.oneOf(["nip04", "nip44", "nip44_v2", rng.mixedString(10)]),
+ decrypt: async (pubkey, ciphertext) => {
+ if (rng.bool()) {
+ throw new Error("Decryption failed " + rng.mixedString(10));
+ }
+ // Return either a string (valid) or garbage
+ if (rng.bool()) {
+ return JSON.stringify({ content: "decrypted message" });
+ }
+ return rng.nastyString();
+ }
+ };
+ };
- const scriptContent = `
-import { decryptDM } from '../../../../js/dmDecryptor.js';
-import fs from 'fs';
-import path from 'path';
+ const event = rng.bool() ? genEvent() : rng.nastyString();
+ const context = {
+ actorPubkey: rng.mixedString(64),
+ decryptors: rng.array(genDecryptor, 3)
+ };
-const event = JSON.parse(fs.readFileSync('input-event.json', 'utf8'));
-const contextConfig = JSON.parse(fs.readFileSync('input-context-config.json', 'utf8'));
+ // Run the target
+ await DmDecryptor.decryptDM(event, context);
-// Reconstruct decryptors
-const decryptors = Array.isArray(contextConfig.decryptorConfigs)
- ? contextConfig.decryptorConfigs.map(config => ({
- scheme: config.scheme,
- supportsGiftWrap: config.supportsGiftWrap,
- decrypt: async (pubkey, ciphertext) => {
- console.log('Mock decrypt called for:', pubkey);
- return "mock-decrypted-content";
- }
- }))
- : contextConfig.decryptorConfigs;
-
-const context = {
- actorPubkey: contextConfig.actorPubkey,
- decryptors
-};
-
-console.log('Running reproduction...');
-try {
- await decryptDM(event, context);
- console.log('Execution finished without crash.');
-} catch (error) {
- console.error('Crash reproduced:', error);
-}
-`;
- fs.writeFileSync(path.join(reproDir, 'repro.mjs'), scriptContent);
- console.log(`Reproducer saved to ${reproDir}`);
+ return { event, context };
}
-function logError(target, input, error) {
- const id = Date.now().toString() + Math.floor(Math.random() * 1000);
- console.error(`[FAIL] ${target} crashed! ID: ${id}`);
- FINDINGS.push({
- id,
- target,
- error: error.message,
- stack: error.stack,
- input
+runFuzzer("dmDecryptor", 5000, fuzzTest)
+ .catch(err => {
+ console.error("Fatal fuzzer error:", err);
+ process.exit(1);
+ })
+ .finally(() => {
+ // Cleanup
+ if (fs.existsSync(new URL(tempPath, import.meta.url))) {
+ fs.unlinkSync(new URL(tempPath, import.meta.url));
+ }
});
- saveReproducerDM(id, input, error);
-}
-
-function mockDecryptor(shouldFail = false) {
- return {
- scheme: pickOne(['nip04', 'nip44', 'nip44_v2', fuzzString()]),
- decrypt: async (pubkey, ciphertext) => {
- if (shouldFail) {
- throw new Error('Mock Decryption Failed');
- }
- // Return a valid stringified JSON event sometimes, or random garbage
- if (fuzzBoolean()) {
- return JSON.stringify({
- content: fuzzString(20),
- pubkey: fuzzHexString(),
- created_at: Math.floor(Date.now() / 1000),
- tags: []
- });
- }
- return fuzzString(50);
- },
- supportsGiftWrap: fuzzBoolean()
- };
-}
-
-function generateRandomEvent() {
- const kind = pickOne([4, 1059, fuzzInt(0, 20000)]);
- const content = fuzzBoolean() ? fuzzString(100) : fuzzJSON(2);
-
- return {
- kind,
- pubkey: fuzzHexString(),
- created_at: Math.floor(Date.now() / 1000),
- tags: fuzzBoolean() ? [] : [['p', fuzzHexString()], ['encrypted', 'nip44']],
- content: typeof content === 'string' ? content : JSON.stringify(content)
- };
-}
-
-function generateRandomContext() {
- const decryptors = [];
- const count = fuzzInt(0, 3);
- for (let i = 0; i < count; i++) {
- decryptors.push(mockDecryptor(fuzzBoolean())); // some will fail
- }
-
- return {
- actorPubkey: fuzzHexString(),
- decryptors
- };
-}
-
-async function fuzzDecryptor() {
- console.log('Starting fuzzing for dmDecryptor.js...');
-
- for (let i = 0; i < ITERATIONS; i++) {
- const event = generateRandomEvent();
- const context = generateRandomContext();
-
- // Also test null/undefined/garbage inputs
- const inputEvent = pickOne([event, null, undefined, "garbage", {}]);
- const inputContext = pickOne([context, null, undefined, {}, { decryptors: "garbage" }]);
-
- try {
- await decryptDM(inputEvent, inputContext);
- } catch (error) {
- logError('decryptDM', { event: inputEvent, context: inputContext }, error);
- }
- }
-
- saveFuzzReport('dmDecryptor', FINDINGS);
- console.log(`Fuzzing complete. Found ${FINDINGS.length} crashes.`);
- if (FINDINGS.length > 0) {
- process.exit(1);
- }
-}
-
-fuzzDecryptor().catch(err => {
- console.error('Fuzzing script failed:', err);
- process.exit(1);
-});
diff --git a/scripts/agent/fuzz-magnetUtils.mjs b/scripts/agent/fuzz-magnetUtils.mjs
index 09b89736..8a99abb5 100644
--- a/scripts/agent/fuzz-magnetUtils.mjs
+++ b/scripts/agent/fuzz-magnetUtils.mjs
@@ -1,90 +1,43 @@
-import { normalizeAndAugmentMagnet } from '../../js/magnetUtils.js';
-import {
- fuzzBoolean,
- fuzzInt,
- fuzzString,
- fuzzHexString,
- fuzzJSON,
- pickOne,
- fuzzSurrogatePairString,
- saveFuzzReport,
- saveReproducer
-} from './fuzz-utils.mjs';
-const ITERATIONS = 1000;
-const FINDINGS = [];
+import { runFuzzer, rng } from "./fuzz-shared.mjs";
-function logError(target, input, error) {
- const id = Date.now().toString() + Math.floor(Math.random() * 1000);
- console.error(`[FAIL] ${target} crashed! ID: ${id}`);
- FINDINGS.push({
- id,
- target,
- error: error.message,
- stack: error.stack,
- input
- });
- saveReproducer(target, id, input, error);
-}
-
-function generateRandomMagnet() {
- const scheme = 'magnet:?';
- const xt = `xt=urn:btih:${fuzzHexString(40)}`;
- const dn = `dn=${fuzzString(10)}`;
- const tr = `tr=${encodeURIComponent('wss://' + fuzzString(10) + '.com')}`;
-
- // Mix valid parts with garbage
- const parts = [xt, dn, tr];
- if (fuzzBoolean()) parts.push(`ws=${encodeURIComponent('https://' + fuzzString(10) + '.com')}`);
- if (fuzzBoolean()) parts.push(fuzzString(5) + '=' + fuzzString(5));
-
- // Shuffle
- parts.sort(() => Math.random() - 0.5);
-
- return scheme + parts.join('&');
-}
-
-function generateRandomOptions() {
- return {
- webSeed: fuzzBoolean() ? [fuzzString()] : fuzzString(),
- torrentUrl: fuzzBoolean() ? fuzzString() : undefined,
- xs: fuzzBoolean() ? fuzzString() : undefined,
- extraTrackers: fuzzBoolean() ? [fuzzString()] : [],
- appProtocol: pickOne(['http:', 'https:', fuzzString(), undefined]),
- logger: fuzzBoolean() ? () => {} : undefined
- };
-}
-
-function fuzzMagnetUtils() {
- console.log('Starting fuzzing for magnetUtils.js...');
-
- for (let i = 0; i < ITERATIONS; i++) {
- // Inputs: valid magnet, partial, garbage, huge string, percent encoded mess
- const rawValue = pickOne([
- generateRandomMagnet(),
- fuzzString(100),
- `magnet:?xt=${fuzzString(10)}`, // invalid xt
- fuzzSurrogatePairString(50),
- null,
- undefined,
- {},
- 123
- ]);
-
- const options = fuzzBoolean() ? generateRandomOptions() : undefined;
-
- try {
- normalizeAndAugmentMagnet(rawValue, options);
- } catch (error) {
- logError('normalizeAndAugmentMagnet', { rawValue, options }, error);
- }
+// Polyfills
+global.window = {
+ location: {
+ protocol: "https:"
}
+};
- saveFuzzReport('magnetUtils', FINDINGS);
- console.log(`Fuzzing complete. Found ${FINDINGS.length} crashes.`);
- if (FINDINGS.length > 0) {
- process.exit(1);
- }
+// Import the target module
+// Note: We are in scripts/agent/
+import * as MagnetUtils from "../../js/magnetUtils.js";
+
+async function fuzzTest(iteration) {
+ // Randomize window protocol sometimes
+ global.window.location.protocol = rng.bool() ? "https:" : "http:";
+
+ const rawValue = rng.mixedString(100);
+
+ const genOptions = () => {
+ if (rng.bool()) return null;
+ return {
+ webSeed: rng.bool() ? rng.mixedString(50) : rng.array(() => rng.mixedString(50), 3),
+ torrentUrl: rng.mixedString(50),
+ xs: rng.mixedString(50),
+ extraTrackers: rng.array(() => rng.mixedString(30), 5),
+ logger: rng.bool() ? (() => {}) : null,
+ appProtocol: rng.bool() ? rng.oneOf(["http:", "https:", rng.nastyString()]) : undefined
+ };
+ };
+
+ const options = genOptions();
+
+ MagnetUtils.normalizeAndAugmentMagnet(rawValue, options);
+
+ return { rawValue, options };
}
-fuzzMagnetUtils();
+runFuzzer("magnetUtils", 5000, fuzzTest).catch(err => {
+ console.error("Fatal fuzzer error:", err);
+ process.exit(1);
+});
diff --git a/scripts/agent/fuzz-nostrEventSchemas.mjs b/scripts/agent/fuzz-nostrEventSchemas.mjs
index 7f77bf12..c92720de 100644
--- a/scripts/agent/fuzz-nostrEventSchemas.mjs
+++ b/scripts/agent/fuzz-nostrEventSchemas.mjs
@@ -1,173 +1,126 @@
-import {
- buildVideoPostEvent,
- buildHttpAuthEvent,
- buildReportEvent,
- buildVideoMirrorEvent,
- buildRepostEvent,
- buildShareEvent,
- buildRelayListEvent,
- buildDmRelayListEvent,
- buildProfileMetadataEvent,
- buildMuteListEvent,
- buildDeletionEvent,
- buildLegacyDirectMessageEvent,
- buildDmAttachmentEvent,
- buildDmReadReceiptEvent,
- buildDmTypingIndicatorEvent,
- buildViewEvent,
- buildZapRequestEvent,
- buildReactionEvent,
- buildCommentEvent,
- buildWatchHistoryEvent,
- buildSubscriptionListEvent,
- buildBlockListEvent,
- buildHashtagPreferenceEvent,
- buildAdminListEvent,
- validateEventStructure,
- sanitizeAdditionalTags,
- NOTE_TYPES
-} from '../../js/nostrEventSchemas.js';
-import {
- fuzzBoolean,
- fuzzInt,
- fuzzString,
- fuzzHexString,
- fuzzJSON,
- pickOne,
- fuzzSurrogatePairString,
- saveFuzzReport,
- saveReproducer
-} from './fuzz-utils.mjs';
+import { runFuzzer, rng } from "./fuzz-shared.mjs";
+import { TextEncoder, TextDecoder } from "util";
-const ITERATIONS = 1000;
-const FINDINGS = [];
+// Polyfills
+global.TextEncoder = TextEncoder;
+global.TextDecoder = TextDecoder;
+global.window = {
+ bitvidNostrEventOverrides: {}
+};
-function logError(target, input, error) {
- const id = Date.now().toString() + Math.floor(Math.random() * 1000);
- console.error(`[FAIL] ${target} crashed! ID: ${id}`);
- FINDINGS.push({
- id,
- target,
- error: error.message,
- stack: error.stack,
- input
- });
- saveReproducer(target, id, input, error);
-}
+// Import the target module
+// Note: We are in scripts/agent/
+import * as Schemas from "../../js/nostrEventSchemas.js";
-function generateRandomParams() {
- const params = {};
- const keys = ['pubkey', 'created_at', 'content', 'tags', 'additionalTags', 'dTagValue', 'url', 'method', 'payload', 'eventId', 'userId', 'reportType', 'relayHint', 'recipientPubkey', 'ciphertext', 'attachment', 'messageKind', 'expiresAt', 'pointerValue', 'pointerTag', 'dedupeTag', 'amountSats', 'lnurl', 'coordinate', 'videoEventId', 'rootKind'];
+async function fuzzTest(iteration) {
+ // We choose a random function to fuzz in each iteration or fuzz all of them?
+ // Let's pick one random target per iteration to keep it simple and isolate crashes.
- // Randomly select some keys to populate
- keys.forEach(key => {
- if (fuzzBoolean()) {
- if (key === 'created_at' || key === 'amountSats' || key === 'messageKind' || key === 'expiresAt') {
- params[key] = fuzzBoolean() ? fuzzInt() : fuzzString(); // Mix types
- } else if (key === 'pubkey' || key === 'recipientPubkey') {
- params[key] = fuzzBoolean() ? fuzzHexString() : fuzzString();
- } else if (key === 'tags' || key === 'additionalTags') {
- params[key] = fuzzBoolean() ? [] : fuzzJSON(2); // Can be valid array or random JSON
- } else if (key === 'content') {
- params[key] = fuzzBoolean() ? fuzzSurrogatePairString(100) : fuzzJSON(2);
- } else {
- params[key] = fuzzString();
- }
- }
- });
+ const targets = [
+ "buildVideoPostEvent",
+ "buildViewEvent",
+ "buildCommentEvent",
+ "buildReactionEvent",
+ "buildZapRequestEvent",
+ "sanitizeAdditionalTags",
+ "validateEventStructure"
+ ];
- return params;
-}
+ const targetName = rng.oneOf(targets);
+ let inputs = {};
-function fuzzBuilder(name, builderFn, argsGenerator) {
- for (let i = 0; i < ITERATIONS; i++) {
- const args = argsGenerator();
- try {
- if (Array.isArray(args)) {
- builderFn(...args);
- } else {
- builderFn(args);
- }
- } catch (error) {
- logError(name, args, error);
- }
- }
-}
+ // Helper to generate a comprehensive random params object
+ const genParams = () => {
+ // 50% chance of being undefined/null/non-object to test robustness
+ if (Math.random() < 0.1) return rng.nastyString();
+ if (Math.random() < 0.1) return null;
-console.log('Starting fuzzing for nostrEventSchemas.js...');
+ // Otherwise generate a rich object
+ const params = {
+ pubkey: rng.mixedString(64),
+ created_at: rng.bool() ? rng.int(0, 2000000000) : rng.nastyString(),
+ content: rng.bool() ? rng.mixedString(1000) : rng.recursiveObject(2),
+ additionalTags: rng.bool() ? rng.array(() => rng.array(() => rng.mixedString(20), 5), 10) : rng.nastyString(),
+ };
-// Fuzz all builders
-const builders = [
- { name: 'buildVideoPostEvent', fn: buildVideoPostEvent },
- { name: 'buildHttpAuthEvent', fn: buildHttpAuthEvent },
- { name: 'buildReportEvent', fn: buildReportEvent },
- { name: 'buildVideoMirrorEvent', fn: buildVideoMirrorEvent },
- { name: 'buildRepostEvent', fn: buildRepostEvent },
- { name: 'buildShareEvent', fn: buildShareEvent },
- { name: 'buildRelayListEvent', fn: buildRelayListEvent },
- { name: 'buildDmRelayListEvent', fn: buildDmRelayListEvent },
- { name: 'buildProfileMetadataEvent', fn: buildProfileMetadataEvent },
- { name: 'buildMuteListEvent', fn: buildMuteListEvent },
- { name: 'buildDeletionEvent', fn: buildDeletionEvent },
- { name: 'buildLegacyDirectMessageEvent', fn: buildLegacyDirectMessageEvent },
- { name: 'buildDmAttachmentEvent', fn: buildDmAttachmentEvent },
- { name: 'buildDmReadReceiptEvent', fn: buildDmReadReceiptEvent },
- { name: 'buildDmTypingIndicatorEvent', fn: buildDmTypingIndicatorEvent },
- { name: 'buildViewEvent', fn: buildViewEvent },
- { name: 'buildZapRequestEvent', fn: buildZapRequestEvent },
- { name: 'buildReactionEvent', fn: buildReactionEvent },
- { name: 'buildCommentEvent', fn: buildCommentEvent },
- { name: 'buildWatchHistoryEvent', fn: buildWatchHistoryEvent },
- { name: 'buildSubscriptionListEvent', fn: buildSubscriptionListEvent },
- { name: 'buildBlockListEvent', fn: buildBlockListEvent },
- { name: 'buildHashtagPreferenceEvent', fn: buildHashtagPreferenceEvent },
-];
+ // Add specific fields that might be used by specific builders
+ const extraFields = [
+ "dTagValue", "url", "magnet", "thumbnail", "description", "mode", "videoRootId",
+ "pointerValue", "pointerTag", "pointerTags", "dedupeTag", "includeSessionTag",
+ "videoEventId", "videoEventRelay", "videoDefinitionAddress", "rootIdentifier",
+ "parentCommentId", "rootKind", "rootAuthorPubkey", "parentKind",
+ "recipientPubkey", "relays", "amountSats", "lnurl", "eventId", "coordinate",
+ "targetPointer", "targetAuthorPubkey"
+ ];
-builders.forEach(({ name, fn }) => {
- fuzzBuilder(name, fn, () => [generateRandomParams()]);
-});
-
-// Special case for buildAdminListEvent
-fuzzBuilder('buildAdminListEvent', buildAdminListEvent, () => {
- const listKey = pickOne(['moderation', 'editors', 'whitelist', 'blacklist', fuzzString()]);
- return [listKey, generateRandomParams()];
-});
-
-// Fuzz validateEventStructure
-fuzzBuilder('validateEventStructure', validateEventStructure, () => {
- const type = pickOne([...Object.values(NOTE_TYPES), fuzzString()]);
- const event = fuzzJSON(2); // Random object/JSON
- return [type, event];
-});
-
-// Fuzz sanitizeAdditionalTags
-fuzzBuilder('sanitizeAdditionalTags', sanitizeAdditionalTags, () => {
- // Random input: array of arrays, array of strings, random object, etc.
- if (fuzzBoolean()) {
- // structured but potentially malformed tags
- const tags = [];
- const count = fuzzInt(0, 10);
- for (let i = 0; i < count; i++) {
- if (fuzzBoolean()) {
- const tag = [fuzzString()];
- const args = fuzzInt(0, 5);
- for (let j = 0; j < args; j++) {
- tag.push(fuzzBoolean() ? fuzzString() : fuzzJSON(1));
+ extraFields.forEach(field => {
+ if (rng.bool()) {
+ params[field] = rng.mixedString(50);
+ } else if (rng.bool()) {
+ params[field] = rng.recursiveObject(1); // Potentially invalid type
}
- tags.push(tag);
- } else {
- tags.push(pickOne([null, undefined, 123, {}, "string"]));
- }
- }
- return [tags];
- }
- // completely random input
- return [fuzzJSON(2)];
-});
+ });
-saveFuzzReport('nostrEventSchemas', FINDINGS);
-console.log(`Fuzzing complete. Found ${FINDINGS.length} crashes.`);
-if (FINDINGS.length > 0) {
- process.exit(1);
+ return params;
+ };
+
+ switch (targetName) {
+ case "buildVideoPostEvent":
+ inputs = genParams();
+ Schemas.buildVideoPostEvent(inputs);
+ break;
+
+ case "buildViewEvent":
+ inputs = genParams();
+ Schemas.buildViewEvent(inputs);
+ break;
+
+ case "buildCommentEvent":
+ inputs = genParams();
+ Schemas.buildCommentEvent(inputs);
+ break;
+
+ case "buildReactionEvent":
+ inputs = genParams();
+ Schemas.buildReactionEvent(inputs);
+ break;
+
+ case "buildZapRequestEvent":
+ inputs = genParams();
+ Schemas.buildZapRequestEvent(inputs);
+ break;
+
+ case "sanitizeAdditionalTags":
+ // sanitizeAdditionalTags expects an array of tags (arrays)
+ if (rng.bool()) {
+ inputs = rng.array(() => rng.array(() => rng.mixedString(20), 5), 20);
+ } else {
+ inputs = rng.nastyString(); // or object, etc.
+ }
+ Schemas.sanitizeAdditionalTags(inputs);
+ break;
+
+ case "validateEventStructure":
+ const type = rng.oneOf(Object.values(Schemas.NOTE_TYPES));
+ let event = genParams(); // repurpose genParams to make a fake event
+ // Ensure event is an object for this specific test setup, unless we want to fuzz that too.
+ // validateEventStructure checks: if (!event || typeof event !== "object") return { valid: false ... }
+ // So passing a string is valid input for the function (should not crash),
+ // but my fuzzer crashes trying to assign .kind to it.
+ if (event && typeof event === 'object') {
+ event.kind = rng.int(0, 50000);
+ event.tags = rng.array(() => rng.array(() => rng.mixedString(10), 3), 5);
+ }
+ inputs = { type, event };
+ Schemas.validateEventStructure(type, event);
+ break;
+ }
+
+ return { target: targetName, inputs };
}
+
+runFuzzer("nostrEventSchemas", 5000, fuzzTest).catch(err => {
+ console.error("Fatal fuzzer error:", err);
+ process.exit(1);
+});
diff --git a/scripts/agent/fuzz-shared.mjs b/scripts/agent/fuzz-shared.mjs
index f968bb05..28f6f165 100644
--- a/scripts/agent/fuzz-shared.mjs
+++ b/scripts/agent/fuzz-shared.mjs
@@ -5,133 +5,187 @@ import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
-const REPRODUCERS_DIR = path.resolve(__dirname, "../../examples/reproducers");
-const ARTIFACTS_DIR = path.resolve(__dirname, "../../artifacts");
+
+const REPRODUCER_DIR = path.join(process.cwd(), "examples/reproducers");
+const ARTIFACTS_DIR = path.join(process.cwd(), "artifacts");
// Ensure directories exist
-if (!fs.existsSync(REPRODUCERS_DIR)) {
- fs.mkdirSync(REPRODUCERS_DIR, { recursive: true });
+if (!fs.existsSync(REPRODUCER_DIR)) {
+ fs.mkdirSync(REPRODUCER_DIR, { recursive: true });
}
if (!fs.existsSync(ARTIFACTS_DIR)) {
fs.mkdirSync(ARTIFACTS_DIR, { recursive: true });
}
-export function randomInt(min, max) {
- return Math.floor(Math.random() * (max - min + 1)) + min;
-}
+export const rng = {
+ bool: () => Math.random() > 0.5,
+ int: (min = 0, max = 100) => Math.floor(Math.random() * (max - min + 1)) + min,
+ float: (min = 0, max = 100) => Math.random() * (max - min) + min,
+ oneOf: (arr) => arr[Math.floor(Math.random() * arr.length)],
-export function randomString(length) {
- const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+{}|:<>?~`-=[]\\;',./";
- let result = "";
- for (let i = 0; i < length; i++) {
- result += chars.charAt(Math.floor(Math.random() * chars.length));
- }
- return result;
-}
-
-export function randomHex(length) {
- const chars = "0123456789abcdef";
- let result = "";
- for (let i = 0; i < length; i++) {
- result += chars.charAt(Math.floor(Math.random() * chars.length));
- }
- return result;
-}
-
-export function randomBoolean() {
- return Math.random() < 0.5;
-}
-
-export function randomItem(array) {
- return array[Math.floor(Math.random() * array.length)];
-}
-
-export function randomJSON(depth = 3, maxFields = 5) {
- if (depth <= 0) {
- const type = randomInt(0, 3);
- if (type === 0) return randomString(10);
- if (type === 1) return randomInt(-1000, 1000);
- if (type === 2) return randomBoolean();
- return null;
- }
-
- const type = randomInt(0, 5);
- if (type === 0) {
- // Array
- const arr = [];
- const len = randomInt(0, maxFields);
+ string: (maxLength = 100) => {
+ const len = Math.floor(Math.random() * maxLength);
+ const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+ let result = "";
for (let i = 0; i < len; i++) {
- arr.push(randomJSON(depth - 1, maxFields));
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
}
- return arr;
- } else if (type === 1) {
- // Object
- const obj = {};
- const len = randomInt(0, maxFields);
+ return result;
+ },
+
+ nastyString: () => {
+ const candidates = [
+ "",
+ "null",
+ "undefined",
+ "NaN",
+ "\x00",
+ "\n",
+ "\r",
+ "\t",
+ " ",
+ " ",
+ "https://example.com",
+ "wss://relay.example.com",
+ "",
+ "' OR 1=1 --",
+ "javascript:void(0)",
+ "data:text/plain;base64,SGVsbG8=",
+ "😀",
+ "ñ",
+ "\uD83D\uDCA9", // Pile of poo
+ "\uD800", // Lone surrogate (high)
+ "\uDFFF", // Lone surrogate (low)
+ "A".repeat(1000), // Long string
+ "A".repeat(10000), // Very long string
+ JSON.stringify({ foo: "bar" }),
+ "{}",
+ "[]"
+ ];
+ return candidates[Math.floor(Math.random() * candidates.length)];
+ },
+
+ mixedString: (maxLength = 100) => {
+ if (Math.random() < 0.3) return rng.nastyString();
+ return rng.string(maxLength);
+ },
+
+ array: (generator, maxLen = 10) => {
+ const len = rng.int(0, maxLen);
+ const result = [];
for (let i = 0; i < len; i++) {
- obj[randomString(5)] = randomJSON(depth - 1, maxFields);
+ result.push(generator());
+ }
+ return result;
+ },
+
+ object: (keyGen, valGen, maxKeys = 10) => {
+ const len = rng.int(0, maxKeys);
+ const result = {};
+ for (let i = 0; i < len; i++) {
+ result[keyGen()] = valGen();
+ }
+ return result;
+ },
+
+ recursiveObject: (depth = 3) => {
+ if (depth <= 0) return rng.mixedString();
+
+ const type = rng.int(0, 4);
+ if (type === 0) return rng.int(-1000, 1000);
+ if (type === 1) return rng.mixedString();
+ if (type === 2) return rng.bool();
+ if (type === 3) return null;
+
+ if (type === 4) {
+ // Array
+ const len = rng.int(0, 5);
+ const arr = [];
+ for(let i=0; i 0) {
+ process.stdout.write(".");
}
}
+ process.stdout.write("\n");
- console.log("\n");
- const duration = (Date.now() - start) / 1000;
- console.log(`[FUZZ] Finished ${name} in ${duration.toFixed(2)}s. Crashes: ${crashes.length}`);
+ stats.endTime = Date.now();
+ stats.duration = stats.endTime - stats.startTime;
- const reportPath = path.join(ARTIFACTS_DIR, `fuzz-report-${name}.json`);
- const report = {
- target: name,
- iterations,
- duration,
- crashes
- };
- fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
- console.log(`[FUZZ] Saved report to ${reportPath}`);
+ console.log(`[FUZZ] Finished. ${stats.passed} passed, ${stats.failed} failed.`);
+ saveReport(targetName, stats);
+
+ return stats;
}
diff --git a/scripts/agent/mocks/watchHistory.js b/scripts/agent/mocks/watchHistory.js
new file mode 100644
index 00000000..ac73c958
--- /dev/null
+++ b/scripts/agent/mocks/watchHistory.js
@@ -0,0 +1,6 @@
+
+export function normalizeActorKey(key) {
+ if (typeof key !== 'string') return '';
+ // simple hex check or return as is for mocking
+ return key.toLowerCase().trim();
+}