fix(nip71): keep upload mirrors in sync

This commit is contained in:
thePR0M3TH3AN committed 2026-07-26 12:51:00 -04:00
1 parent f0a3ede3e5
commit e8e2b04de7
12 files changed
+325 -82

No files matched your search

+4 -4
View File
@@ -85,14 +85,14 @@ test("resolveDeleteSync: attempts teardown whenever the feature is on, regardles
assert.deepEqual(resolveDeleteSync({ featureOn: false, enabled: false }), { action: "none" });
});
test("auto-share preference persists per pubkey, off by default", () => {
test("auto-share preference is on by default and supports a durable opt-out", () => {
localStorage.clear();
assert.equal(isAutoShareEnabled(PK), false);
setAutoShareEnabled(PK, true);
assert.equal(isAutoShareEnabled(PK), true);
assert.equal(isAutoShareEnabled("c".repeat(64)), false, "scoped per pubkey");
setAutoShareEnabled(PK, false);
assert.equal(isAutoShareEnabled(PK), false);
assert.equal(isAutoShareEnabled("c".repeat(64)), true, "default is scoped per pubkey");
setAutoShareEnabled(PK, true);
assert.equal(isAutoShareEnabled(PK), true);
});
test("resolvePublishSync: auto-mirror a new video only when auto-share on AND eligible", () => {
+54 -13
View File
@@ -106,11 +106,11 @@ test("publish is unavailable without a signer / pubkey", async () => {
assert.equal(result.error, "unavailable");
});
test("remove() publishes a NIP-09 delete (both addressable kinds) AND a tombstone", async () => {
test("remove() publishes a NIP-09 delete and tombstones both addressable kinds", async () => {
const { service, relay } = makeService();
const result = await service.remove(baseVideo());
assert.equal(result.ok, true);
assert.equal(relay.events.length, 2, "delete + empty-replace tombstone");
assert.equal(relay.events.length, 3, "delete + empty replacements for both kinds");
const del = relay.events.find((e) => e.kind === 5);
assert.ok(del, "must publish a NIP-09 kind-5 delete");
@@ -122,21 +122,19 @@ test("remove() publishes a NIP-09 delete (both addressable kinds) AND a tombston
const kTags = del.tags.filter((t) => t[0] === "k").map((t) => t[1]).sort();
assert.deepEqual(kTags, ["34235", "34236"]);
const tomb = relay.events.find((e) => e.kind === 34235 || e.kind === 34236);
assert.ok(tomb, "must publish an empty-replace tombstone");
assert.equal(tomb.tags.find((t) => t[0] === "d")[1], "root-1", "same d-tag");
assert.equal(
tomb.tags.some((t) => t[0] === "imeta"),
false,
"tombstone has no playable imeta",
);
const tombs = relay.events.filter((e) => e.kind === 34235 || e.kind === 34236);
assert.deepEqual(tombs.map((event) => event.kind).sort(), [34235, 34236]);
for (const tomb of tombs) {
assert.equal(tomb.tags.find((t) => t[0] === "d")[1], "root-1", "same d-tag");
assert.equal(tomb.tags.some((t) => t[0] === "imeta"), false, "not playable");
}
});
test("remove() tombstone uses the short kind for portrait videos", async () => {
const { service, relay } = makeService();
await service.remove(baseVideo({ width: 1080, height: 1920 }));
const tomb = relay.events.find((e) => e.kind === 34235 || e.kind === 34236);
assert.equal(tomb.kind, 34236, "portrait => 34236 tombstone");
const tombs = relay.events.filter((e) => e.kind === 34235 || e.kind === 34236);
assert.deepEqual(tombs.map((event) => event.kind).sort(), [34235, 34236]);
});
test("remove() is unavailable without a signer", async () => {
@@ -150,7 +148,7 @@ test("remove() is unavailable without a signer", async () => {
// (34236 short for portrait, else 34235), but (kind,pubkey,d) is the addressable
// identity — so a kind flip between mirror attempts produced a DUPLICATE in NIP-71
// clients. publish() must reuse the kind of any existing mirror instead.
function makeServiceWithExisting(existing) {
function makeServiceWithExisting(existing, { now = () => Date.now() } = {}) {
const relay = makeRelay();
const signer = { signEvent: async (tpl) => ({ ...tpl, id: "id", sig: "sig" }) };
const service = createNip71MirrorService({
@@ -163,6 +161,7 @@ function makeServiceWithExisting(existing) {
signEvent: signer.signEvent,
allowNsfw: () => false,
fetchExistingMirrors: async () => existing,
now,
});
return { service, relay };
}
@@ -208,6 +207,48 @@ test("an explicit options.short override is respected over any existing mirror",
assert.equal(relay.events[0].kind, 34235, "explicit override wins");
});
test("re-mirror advances past an existing addressable timestamp", async () => {
const { service, relay } = makeServiceWithExisting(
[{ kind: 34235, created_at: 500 }],
{ now: () => 500_000 },
);
const result = await service.publish(baseVideo());
assert.equal(result.ok, true);
assert.equal(relay.events[0].created_at, 501, "mirror replacement must win its coordinate");
});
test("remove advances its delete and tombstones past an existing mirror", async () => {
const { service, relay } = makeServiceWithExisting(
[{ kind: 34235, created_at: 500 }],
{ now: () => 500_000 },
);
const result = await service.remove(baseVideo());
assert.equal(result.ok, true);
assert.deepEqual(relay.events.map((event) => event.created_at), [501, 501, 501]);
});
test("findMirrors resolves a library in one batched relay lookup", async () => {
let batchCalls = 0;
const service = createNip71MirrorService({
getActivePubkey: () => PUBKEY,
fetchMirrorsByRoots: async ({ roots }) => {
batchCalls += 1;
assert.deepEqual(roots.sort(), ["root-a", "root-b"]);
return [
{ root: "root-a", kind: 34235, created_at: 10 },
{ root: "root-b", kind: 34236, created_at: 11 },
];
},
});
const states = await service.findMirrors([
{ videoRootId: "root-a" },
{ videoRootId: "root-b" },
]);
assert.equal(batchCalls, 1);
assert.deepEqual(states.get("root-a"), { mirrored: true, kinds: [34235], duplicate: false });
assert.deepEqual(states.get("root-b"), { mirrored: true, kinds: [34236], duplicate: false });
});
test("findMirror derives mirror state from relays (truth), flagging duplicates", async () => {
const dup = makeServiceWithExisting([{ kind: 34235 }, { kind: 34236 }]).service;
assert.deepEqual(await dup.findMirror(baseVideo()), {
+103 -1
View File
@@ -6,10 +6,16 @@ import "./test-helpers/setup-localstorage.mjs";
import assert from "node:assert/strict";
import test, { mock } from "node:test";
import {
syncNip71MirrorAfterEdit,
syncNip71MirrorAfterPublish,
syncNip71MirrorAfterDelete,
initNip71MirrorSync,
} from "../js/services/nip71MirrorSync.js";
import { isMirrorEnabled, setMirrorEnabled } from "../js/services/nip71MirrorFlags.js";
import {
isMirrorEnabled,
setMirrorEnabled,
setAutoShareEnabled,
} from "../js/services/nip71MirrorFlags.js";
import { nip71MirrorService } from "../js/services/nip71MirrorService.js";
const PK = "a".repeat(64);
@@ -63,6 +69,102 @@ test("delete sync never throws when teardown fails (best-effort)", async () => {
}
});
test("editing a mirrored video uses the original root when the form omits it", async () => {
localStorage.clear();
setMirrorEnabled(PK, "root-edit", true);
const canMirror = mock.method(nip71MirrorService, "canMirror", () => ({ ok: true }));
const publishSpy = mock.method(nip71MirrorService, "publish", async () => ({ ok: true }));
try {
await syncNip71MirrorAfterEdit({
updatedData: { title: "Edited", url: "https://cdn.example/video.mp4" },
originalEvent: { videoRootId: "root-edit" },
pubkey: PK,
});
assert.equal(publishSpy.mock.callCount(), 1, "mirror is republished after a normal edit");
assert.equal(publishSpy.mock.calls[0].arguments[0].videoRootId, "root-edit");
} finally {
canMirror.mock.restore();
publishSpy.mock.restore();
}
});
test("failed edit-time unshare keeps the flag so a later edit retries cleanup", async () => {
localStorage.clear();
setMirrorEnabled(PK, "root-retry", true);
const canMirror = mock.method(nip71MirrorService, "canMirror", () => ({ ok: false, reason: "private" }));
const removeSpy = mock.method(nip71MirrorService, "remove", async () => ({ ok: false }));
try {
await syncNip71MirrorAfterEdit({
updatedData: { isPrivate: true },
originalEvent: { videoRootId: "root-retry" },
pubkey: PK,
});
assert.equal(removeSpy.mock.callCount(), 1);
assert.equal(isMirrorEnabled(PK, "root-retry"), true, "failed cleanup stays retryable");
} finally {
canMirror.mock.restore();
removeSpy.mock.restore();
}
});
test("a per-upload opt-out prevents the default NIP-71 mirror", async () => {
localStorage.clear();
setAutoShareEnabled(PK, true);
const publishSpy = mock.method(nip71MirrorService, "publish", async () => ({ ok: true }));
try {
await syncNip71MirrorAfterPublish({
pubkey: PK,
payload: { mirrorNip71: false },
result: {
legacy: {
id: "event-id",
pubkey: PK,
created_at: 1,
tags: [["d", "root-opt-out"]],
content: JSON.stringify({
videoRootId: "root-opt-out",
title: "No mirror",
url: "https://cdn.example/video.mp4",
}),
},
},
});
assert.equal(publishSpy.mock.callCount(), 0);
} finally {
publishSpy.mock.restore();
}
});
test("a new eligible upload mirrors by default", async () => {
localStorage.clear();
const canMirror = mock.method(nip71MirrorService, "canMirror", () => ({ ok: true }));
const publishSpy = mock.method(nip71MirrorService, "publish", async () => ({ ok: true }));
try {
await syncNip71MirrorAfterPublish({
pubkey: PK,
payload: { mirrorNip71: true },
result: {
legacy: {
id: "event-id-default",
pubkey: PK,
created_at: 1,
tags: [["d", "root-default"]],
content: JSON.stringify({
videoRootId: "root-default",
title: "Default mirror",
url: "https://cdn.example/video.mp4",
}),
},
},
});
assert.equal(publishSpy.mock.callCount(), 1);
assert.equal(isMirrorEnabled(PK, "root-default"), true);
} finally {
canMirror.mock.restore();
publishSpy.mock.restore();
}
});
test("initNip71MirrorSync subscribes to edit+delete events, idempotently", () => {
const events = {};
const svc = {
+16
View File
@@ -28,6 +28,22 @@ test("normalizes minimal payload with hosted URL", () => {
assert.equal(payload.legacyFormData.enableComments, true);
assert.equal(payload.legacyFormData.isNsfw, false);
assert.equal(payload.legacyFormData.isForKids, false);
assert.equal(payload.mirrorNip71, true, "public uploads mirror by default");
});
test("preserves an explicit per-upload NIP-71 mirror opt-out outside canonical content", () => {
const { payload, errors } = normalizeVideoNotePayload({
title: "Private distribution choice",
url: "https://cdn.example.com/video.mp4",
mirrorNip71: false,
});
assert.deepEqual(errors, []);
assert.equal(payload.mirrorNip71, false);
assert.equal(
Object.hasOwn(payload.legacyFormData, "mirrorNip71"),
false,
"opt-out does not leak into the canonical 30078 payload",
);
});
test("normalizes mode and boolean flags", () => {