mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-31 18:02:27 +00:00
Merge pull request #1035 from PR0M3TH3AN/codex/add-tag-sanitization-helper-and-updates
Sanitize additional Nostr tags
This commit is contained in:
7 files changed
+252
-75
No files matched your search
@@ -2,6 +2,7 @@ import {
|
||||
buildCommentEvent,
|
||||
getNostrEventSchema,
|
||||
NOTE_TYPES,
|
||||
sanitizeAdditionalTags,
|
||||
} from "../nostrEventSchemas.js";
|
||||
import { publishEventToRelay } from "../nostrPublish.js";
|
||||
import { RELAY_URLS } from "./toolkit.js";
|
||||
@@ -1158,11 +1159,7 @@ export async function publishComment(
|
||||
? Math.floor(options.created_at)
|
||||
: Math.floor(Date.now() / 1000);
|
||||
|
||||
const additionalTags = Array.isArray(options.additionalTags)
|
||||
? options.additionalTags.filter(
|
||||
(tag) => Array.isArray(tag) && typeof tag[0] === "string",
|
||||
)
|
||||
: [];
|
||||
const additionalTags = sanitizeAdditionalTags(options.additionalTags);
|
||||
|
||||
let content = "";
|
||||
if (typeof options.content === "string") {
|
||||
|
||||
@@ -4,7 +4,11 @@ import {
|
||||
assertAnyRelayAccepted,
|
||||
summarizePublishResults,
|
||||
} from "../nostrPublish.js";
|
||||
import { buildRepostEvent, buildVideoMirrorEvent } from "../nostrEventSchemas.js";
|
||||
import {
|
||||
buildRepostEvent,
|
||||
buildVideoMirrorEvent,
|
||||
sanitizeAdditionalTags,
|
||||
} from "../nostrEventSchemas.js";
|
||||
import { normalizePointerInput } from "./watchHistory.js";
|
||||
import { sanitizeRelayList } from "./nip46Client.js";
|
||||
import {
|
||||
@@ -893,9 +897,7 @@ export async function repostEvent({
|
||||
? Math.max(0, Math.floor(options.created_at))
|
||||
: Math.floor(Date.now() / 1000);
|
||||
|
||||
const additionalTags = Array.isArray(options.additionalTags)
|
||||
? options.additionalTags.filter((tag) => Array.isArray(tag) && tag.length >= 2)
|
||||
: [];
|
||||
const additionalTags = sanitizeAdditionalTags(options.additionalTags);
|
||||
|
||||
const repostKind =
|
||||
Number.isFinite(targetKind) && targetKind !== 1
|
||||
@@ -1082,10 +1084,10 @@ export async function mirrorVideoEvent({
|
||||
tags.push(["ox", originalFileSha256]);
|
||||
}
|
||||
|
||||
const additionalTags = Array.isArray(options.additionalTags)
|
||||
? options.additionalTags.filter((tag) => Array.isArray(tag) && tag.length >= 2)
|
||||
: [];
|
||||
tags.push(...additionalTags);
|
||||
const additionalTags = sanitizeAdditionalTags(options.additionalTags);
|
||||
if (additionalTags.length) {
|
||||
tags.push(...additionalTags.map((tag) => tag.slice()));
|
||||
}
|
||||
|
||||
let actorPubkey =
|
||||
typeof options.actorPubkey === "string" && options.actorPubkey.trim()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { buildReactionEvent } from "../nostrEventSchemas.js";
|
||||
import { buildReactionEvent, sanitizeAdditionalTags } from "../nostrEventSchemas.js";
|
||||
import { publishEventToRelay } from "../nostrPublish.js";
|
||||
import { RELAY_URLS } from "./toolkit.js";
|
||||
import { normalizePointerInput } from "./watchHistory.js";
|
||||
@@ -157,11 +157,7 @@ export async function publishVideoReaction(
|
||||
? Math.floor(options.created_at)
|
||||
: Math.floor(Date.now() / 1000);
|
||||
|
||||
const additionalTags = Array.isArray(options.additionalTags)
|
||||
? options.additionalTags.filter(
|
||||
(tag) => Array.isArray(tag) && typeof tag[0] === "string"
|
||||
)
|
||||
: [];
|
||||
const additionalTags = sanitizeAdditionalTags(options.additionalTags);
|
||||
|
||||
const optionRelay =
|
||||
typeof options.pointerRelay === "string" && options.pointerRelay.trim()
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
buildViewEvent,
|
||||
getNostrEventSchema,
|
||||
NOTE_TYPES,
|
||||
sanitizeAdditionalTags,
|
||||
} from "../nostrEventSchemas.js";
|
||||
import { publishEventToRelay } from "../nostrPublish.js";
|
||||
import { RELAY_URLS } from "./toolkit.js";
|
||||
@@ -710,11 +711,7 @@ export async function publishViewEvent(
|
||||
const usingSessionActor =
|
||||
normalizedActor && normalizedActor !== normalizedLogged;
|
||||
|
||||
const additionalTags = Array.isArray(options.additionalTags)
|
||||
? options.additionalTags.filter(
|
||||
(tag) => Array.isArray(tag) && typeof tag[0] === "string"
|
||||
)
|
||||
: [];
|
||||
const additionalTags = sanitizeAdditionalTags(options.additionalTags);
|
||||
|
||||
const pointerTag =
|
||||
pointer.type === "a"
|
||||
|
||||
+117
-44
@@ -1,4 +1,5 @@
|
||||
import { devLogger } from "./utils/logger.js";
|
||||
import { decodeNpubToHex } from "./nostr/nip46Client.js";
|
||||
import {
|
||||
ADMIN_LIST_NAMESPACE,
|
||||
ADMIN_COMMUNITY_BLACKLIST_SOURCES,
|
||||
@@ -45,6 +46,101 @@ const DEFAULT_APPEND_TAGS = [];
|
||||
|
||||
let cachedUtf8Encoder = null;
|
||||
|
||||
const ADDITIONAL_POINTER_TAGS = new Set(["p", "e"]);
|
||||
const HEX_32_BYTE_REGEX = /^[0-9a-f]{64}$/i;
|
||||
|
||||
function normalizePointerIdentifier(value) {
|
||||
if (typeof value !== "string") {
|
||||
return "";
|
||||
}
|
||||
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (HEX_32_BYTE_REGEX.test(trimmed)) {
|
||||
return trimmed.toLowerCase();
|
||||
}
|
||||
|
||||
const decoded = decodeNpubToHex(trimmed);
|
||||
if (decoded && HEX_32_BYTE_REGEX.test(decoded)) {
|
||||
return decoded.toLowerCase();
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
function normalizeTagSlotValue(value) {
|
||||
if (typeof value === "string") {
|
||||
const trimmed = value.trim();
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
if (typeof value === "number") {
|
||||
if (!Number.isFinite(value)) {
|
||||
return "";
|
||||
}
|
||||
return String(value);
|
||||
}
|
||||
|
||||
if (value === null || value === undefined) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
export function sanitizeAdditionalTags(additionalTags) {
|
||||
if (!Array.isArray(additionalTags) || !additionalTags.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const sanitized = [];
|
||||
|
||||
additionalTags.forEach((tag) => {
|
||||
if (!Array.isArray(tag) || tag.length < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rawName = tag[0];
|
||||
if (typeof rawName !== "string") {
|
||||
return;
|
||||
}
|
||||
|
||||
const tagName = rawName.trim();
|
||||
if (!tagName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lowerName = tagName.toLowerCase();
|
||||
let primaryValue = "";
|
||||
|
||||
if (ADDITIONAL_POINTER_TAGS.has(lowerName)) {
|
||||
primaryValue = normalizePointerIdentifier(tag[1]);
|
||||
} else {
|
||||
primaryValue = normalizeTagSlotValue(tag[1]);
|
||||
}
|
||||
|
||||
if (!primaryValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sanitizedTag = [tagName, primaryValue];
|
||||
|
||||
for (let index = 2; index < tag.length; index += 1) {
|
||||
const extraValue = normalizeTagSlotValue(tag[index]);
|
||||
if (extraValue) {
|
||||
sanitizedTag.push(extraValue);
|
||||
}
|
||||
}
|
||||
|
||||
sanitized.push(sanitizedTag);
|
||||
});
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
function ensureValidUtf8Content(value) {
|
||||
let normalized = "";
|
||||
if (typeof value === "string") {
|
||||
@@ -606,12 +702,9 @@ export function buildVideoPostEvent({
|
||||
tags.push([schema.identifierTag.name, dTagValue]);
|
||||
}
|
||||
appendSchemaTags(tags, schema);
|
||||
if (Array.isArray(additionalTags)) {
|
||||
additionalTags.forEach((tag) => {
|
||||
if (Array.isArray(tag) && tag.length >= 2) {
|
||||
tags.push(tag.map((value) => (typeof value === "string" ? value : String(value))));
|
||||
}
|
||||
});
|
||||
const sanitizedAdditionalTags = sanitizeAdditionalTags(additionalTags);
|
||||
if (sanitizedAdditionalTags.length) {
|
||||
tags.push(...sanitizedAdditionalTags.map((tag) => tag.slice()));
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -693,13 +786,9 @@ export function buildRepostEvent({
|
||||
}
|
||||
|
||||
appendSchemaTags(tags, schema);
|
||||
|
||||
if (Array.isArray(additionalTags)) {
|
||||
additionalTags.forEach((tag) => {
|
||||
if (Array.isArray(tag) && tag.length >= 2) {
|
||||
tags.push(tag.map((value) => (typeof value === "string" ? value : String(value))));
|
||||
}
|
||||
});
|
||||
const sanitizedAdditionalTags = sanitizeAdditionalTags(additionalTags);
|
||||
if (sanitizedAdditionalTags.length) {
|
||||
tags.push(...sanitizedAdditionalTags.map((tag) => tag.slice()));
|
||||
}
|
||||
|
||||
const normalizedRepostKind =
|
||||
@@ -822,12 +911,9 @@ export function buildRelayListEvent({
|
||||
|
||||
appendSchemaTags(tags, schema);
|
||||
|
||||
if (Array.isArray(additionalTags)) {
|
||||
additionalTags.forEach((tag) => {
|
||||
if (Array.isArray(tag) && tag.length >= 2) {
|
||||
tags.push(tag.map((value) => (typeof value === "string" ? value : String(value))));
|
||||
}
|
||||
});
|
||||
const sanitizedAdditionalTags = sanitizeAdditionalTags(additionalTags);
|
||||
if (sanitizedAdditionalTags.length) {
|
||||
tags.push(...sanitizedAdditionalTags.map((tag) => tag.slice()));
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -864,12 +950,9 @@ export function buildViewEvent({
|
||||
normalizedPointerTags.forEach((tag) => {
|
||||
tags.push(tag);
|
||||
});
|
||||
if (Array.isArray(additionalTags)) {
|
||||
additionalTags.forEach((tag) => {
|
||||
if (Array.isArray(tag) && tag.length >= 2) {
|
||||
tags.push(tag.map((value) => (typeof value === "string" ? value : String(value))));
|
||||
}
|
||||
});
|
||||
const sanitizedAdditionalTags = sanitizeAdditionalTags(additionalTags);
|
||||
if (sanitizedAdditionalTags.length) {
|
||||
tags.push(...sanitizedAdditionalTags.map((tag) => tag.slice()));
|
||||
}
|
||||
|
||||
if (dedupeTag && schema?.identifierTag?.name) {
|
||||
@@ -1015,12 +1098,9 @@ export function buildReactionEvent({
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(additionalTags)) {
|
||||
additionalTags.forEach((tag) => {
|
||||
if (Array.isArray(tag) && tag.length >= 2) {
|
||||
tags.push(tag.map((value) => (typeof value === "string" ? value : String(value))));
|
||||
}
|
||||
});
|
||||
const sanitizedAdditionalTags = sanitizeAdditionalTags(additionalTags);
|
||||
if (sanitizedAdditionalTags.length) {
|
||||
tags.push(...sanitizedAdditionalTags.map((tag) => tag.slice()));
|
||||
}
|
||||
|
||||
appendSchemaTags(tags, schema);
|
||||
@@ -1250,12 +1330,9 @@ export function buildCommentEvent({
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(additionalTags)) {
|
||||
additionalTags.forEach((tag) => {
|
||||
if (Array.isArray(tag) && tag.length >= 2) {
|
||||
tags.push(tag.map((value) => (typeof value === "string" ? value : String(value))));
|
||||
}
|
||||
});
|
||||
const sanitizedAdditionalTags = sanitizeAdditionalTags(additionalTags);
|
||||
if (sanitizedAdditionalTags.length) {
|
||||
tags.push(...sanitizedAdditionalTags.map((tag) => tag.slice()));
|
||||
}
|
||||
|
||||
appendSchemaTags(tags, schema);
|
||||
@@ -1303,13 +1380,9 @@ export function buildWatchHistoryIndexEvent({
|
||||
});
|
||||
|
||||
appendSchemaTags(tags, schema);
|
||||
|
||||
if (Array.isArray(additionalTags)) {
|
||||
additionalTags.forEach((tag) => {
|
||||
if (Array.isArray(tag) && tag.length >= 2) {
|
||||
tags.push(tag.map((value) => (typeof value === "string" ? value : String(value))));
|
||||
}
|
||||
});
|
||||
const sanitizedAdditionalTags = sanitizeAdditionalTags(additionalTags);
|
||||
if (sanitizedAdditionalTags.length) {
|
||||
tags.push(...sanitizedAdditionalTags.map((tag) => tag.slice()));
|
||||
}
|
||||
|
||||
let resolvedContent = content;
|
||||
|
||||
@@ -1,11 +1,34 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
const SANITIZED_SAMPLE_HEX = "deadbeef".repeat(8);
|
||||
const SANITIZED_SAMPLE_NPUB = "npub1commentadditionaltagfixture000000000000000000000";
|
||||
|
||||
if (typeof globalThis.window === "undefined") {
|
||||
globalThis.window = {};
|
||||
}
|
||||
|
||||
if (!globalThis.window.NostrTools) {
|
||||
globalThis.window.NostrTools = {};
|
||||
}
|
||||
|
||||
const existingNip19 =
|
||||
typeof globalThis.window.NostrTools.nip19 === "object"
|
||||
? globalThis.window.NostrTools.nip19
|
||||
: {};
|
||||
|
||||
if (typeof existingNip19.decode !== "function") {
|
||||
globalThis.window.NostrTools.nip19 = {
|
||||
...existingNip19,
|
||||
decode: (value) => ({ type: "npub", data: SANITIZED_SAMPLE_HEX }),
|
||||
};
|
||||
}
|
||||
|
||||
const {
|
||||
buildCommentEvent,
|
||||
getNostrEventSchema,
|
||||
NOTE_TYPES,
|
||||
} from "../js/nostrEventSchemas.js";
|
||||
} = await import("../js/nostrEventSchemas.js");
|
||||
|
||||
const VIDEO_COMMENT_KIND =
|
||||
getNostrEventSchema(NOTE_TYPES.VIDEO_COMMENT)?.kind ?? 1111;
|
||||
@@ -84,7 +107,6 @@ test("buildCommentEvent normalizes relays and preserves explicit overrides", ()
|
||||
["k", String(VIDEO_COMMENT_KIND)],
|
||||
["p", "cafecafe", "wss://profile"],
|
||||
["client", "bitvid"],
|
||||
["p", "cafecafe", "wss://override"],
|
||||
]);
|
||||
assert.equal(event.content, " Appreciated! ");
|
||||
});
|
||||
@@ -136,3 +158,44 @@ test("buildCommentEvent accepts partial metadata for parent overrides", () => {
|
||||
["p", "parent-only", "wss://parent-author"],
|
||||
]);
|
||||
});
|
||||
|
||||
test("buildCommentEvent sanitizes additional tags before signing", () => {
|
||||
const event = buildCommentEvent({
|
||||
pubkey: "sanitizer",
|
||||
created_at: 1700000600,
|
||||
videoEventId: "sanitized-event",
|
||||
threadParticipantPubkey: "cafecafe",
|
||||
additionalTags: [
|
||||
["p", null],
|
||||
["p", SANITIZED_SAMPLE_NPUB],
|
||||
["client", { foo: "bar" }],
|
||||
["client", "bitvid"],
|
||||
],
|
||||
content: "sanitize",
|
||||
});
|
||||
|
||||
const clientTags = event.tags.filter(
|
||||
(tag) => Array.isArray(tag) && tag[0] === "client",
|
||||
);
|
||||
assert.deepStrictEqual(clientTags, [["client", "bitvid"]]);
|
||||
|
||||
const normalizedAdditionalPubkeys = event.tags.filter(
|
||||
(tag) => Array.isArray(tag) && tag[0] === "p" && tag[1] === SANITIZED_SAMPLE_HEX,
|
||||
);
|
||||
assert.equal(
|
||||
normalizedAdditionalPubkeys.length,
|
||||
1,
|
||||
"sanitized npub additional tag should normalize to hex",
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
!event.tags.some(
|
||||
(tag) =>
|
||||
Array.isArray(tag) &&
|
||||
tag[0] === "client" &&
|
||||
typeof tag[1] === "string" &&
|
||||
tag[1].includes("[object Object]"),
|
||||
),
|
||||
"invalid object values should be removed instead of stringified",
|
||||
);
|
||||
});
|
||||
@@ -10,10 +10,6 @@ const {
|
||||
VIEW_COUNT_CACHE_TTL_MS,
|
||||
} = await import("../js/config.js");
|
||||
|
||||
const { buildViewEvent, setNostrEventSchemaOverrides } = await import(
|
||||
"../js/nostrEventSchemas.js"
|
||||
);
|
||||
|
||||
const VIEW_COUNTER_STORAGE_KEY = "bitvid:view-counter:v1";
|
||||
const CACHE_TTL_TEST_POINTER = { type: "e", value: "view-counter-cache-ttl" };
|
||||
|
||||
@@ -47,6 +43,19 @@ if (!globalThis.window.NostrTools) {
|
||||
globalThis.window.NostrTools = {};
|
||||
}
|
||||
|
||||
const VIEW_SANITIZED_SAMPLE_HEX = "deadbeef".repeat(8);
|
||||
const existingNip19Decoder =
|
||||
typeof globalThis.window.NostrTools.nip19 === "object"
|
||||
? globalThis.window.NostrTools.nip19
|
||||
: {};
|
||||
|
||||
if (typeof existingNip19Decoder.decode !== "function") {
|
||||
globalThis.window.NostrTools.nip19 = {
|
||||
...existingNip19Decoder,
|
||||
decode: () => ({ type: "npub", data: VIEW_SANITIZED_SAMPLE_HEX }),
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof globalThis.window.NostrTools.getEventHash !== "function") {
|
||||
globalThis.window.NostrTools.getEventHash = () => "test-event-hash";
|
||||
}
|
||||
@@ -55,6 +64,10 @@ if (typeof globalThis.window.NostrTools.signEvent !== "function") {
|
||||
globalThis.window.NostrTools.signEvent = () => "test-event-sig";
|
||||
}
|
||||
|
||||
const { buildViewEvent, setNostrEventSchemaOverrides } = await import(
|
||||
"../js/nostrEventSchemas.js"
|
||||
);
|
||||
|
||||
setNostrEventSchemaOverrides({});
|
||||
|
||||
const canonicalViewEvent = buildViewEvent({
|
||||
@@ -93,6 +106,42 @@ assert.deepEqual(
|
||||
"view event should append the session tag when requested"
|
||||
);
|
||||
|
||||
const sanitizedViewEvent = buildViewEvent({
|
||||
pubkey: "actor-sanitize",
|
||||
created_at: 3000,
|
||||
pointerValue: "event-sanitize",
|
||||
pointerTag: ["e", "event-sanitize"],
|
||||
additionalTags: [
|
||||
["p", null],
|
||||
["p", "npub1viewadditionaltagfixture000000000000000000000"],
|
||||
["client", { foo: "bar" }],
|
||||
["client", "bitvid"],
|
||||
],
|
||||
});
|
||||
|
||||
assert.ok(
|
||||
sanitizedViewEvent.tags.some(
|
||||
(tag) => Array.isArray(tag) && tag[0] === "p" && tag[1] === VIEW_SANITIZED_SAMPLE_HEX
|
||||
),
|
||||
"view event should normalize npub additional tags to hex"
|
||||
);
|
||||
|
||||
const sanitizedClientTags = sanitizedViewEvent.tags.filter(
|
||||
(tag) => Array.isArray(tag) && tag[0] === "client"
|
||||
);
|
||||
assert.deepEqual(sanitizedClientTags, [["client", "bitvid"]]);
|
||||
|
||||
assert.ok(
|
||||
!sanitizedViewEvent.tags.some(
|
||||
(tag) =>
|
||||
Array.isArray(tag) &&
|
||||
tag[0] === "client" &&
|
||||
typeof tag[1] === "string" &&
|
||||
tag[1].includes("[object Object]")
|
||||
),
|
||||
"invalid additional tag values should not be coerced to object strings"
|
||||
);
|
||||
|
||||
const { nostrClient } = await import("../js/nostr.js");
|
||||
|
||||
function createMockNostrHarness() {
|
||||
|
||||
Reference in new issue
Block a user