test(desktop): cover section workspace authority boundaries

Signed-off-by: Other Brother Darryl <cee32d92756729ee0c097c5661b879c6199931cd25315c8cf398dcbf0f155cf1@buzz.block.builderlab.xyz>
This commit is contained in:
Other Brother Darryl
2026-08-13 18:37:08 -04:00
parent ec2d28cd10
commit d3a2abaf06
3 changed files with 579 additions and 22 deletions

View File

@@ -8,6 +8,8 @@ import { finalizeEvent } from "nostr-tools/pure";
import { relayClient } from "@/shared/api/relayClient";
import {
canonicalJson,
canonicalMetadataAad,
canonicalRelayAuthority,
parseSectionWorkspaceProjection,
parseStrictJson,
projectionRevisionAction,
@@ -113,6 +115,38 @@ test("section workspace consumes every shared projection fixture revision case",
}
});
test("canonical relay authority and metadata AAD match the frozen fixture", () => {
const aadCanonical = fixture.metadata_crypto.aad_canonical;
const owner = OWNER;
const sectionId = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa";
assert.equal(
canonicalRelayAuthority("wss://Relay.Example/"),
"relay.example",
);
assert.equal(
canonicalRelayAuthority("https://RELAY.EXAMPLE"),
"relay.example",
);
assert.equal(
canonicalRelayAuthority("wss://Relay.Example:8443/"),
"relay.example:8443",
);
assert.equal(
canonicalMetadataAad("wss://Relay.Example/", owner, sectionId, 1, "label"),
aadCanonical,
);
assert.notEqual(
canonicalMetadataAad(
"wss://Relay.Example:8443/",
owner,
sectionId,
1,
"label",
),
aadCanonical,
);
});
test("section workspace rejects unknown fields, unsupported versions, duplicate IDs, and malformed JSON", () => {
const valid = structuredClone(fixture.projection_cases[0].projection);
assert.throws(() =>
@@ -137,6 +171,46 @@ test("section workspace canonicalization matches the frozen legacy hash vector",
);
});
test("valid projection is rejected for wrong, null, or failed relay self without cache or canonical cutover", async () => {
const storage = memoryStorage();
const event = makeProjectionEvent();
const relaySelfValues = [
"2222222222222222222222222222222222222222222222222222222222222222",
null,
new Error("NIP-11 unavailable"),
];
for (const relaySelf of relaySelfValues) {
const restoreRelay = installRelayStubs({
fetchEvents: async () => [event],
publishEvent: async () => {},
subscribeLive: async () => async () => {},
});
const restoreWindow = installWindow(storage, async (command) => {
if (command === "get_relay_self") {
if (relaySelf instanceof Error) throw relaySelf;
return relaySelf;
}
throw new Error(`unexpected command ${command}`);
});
try {
const manager = new SectionWorkspaceSyncManager(OWNER, RELAY);
const result = await manager.bootstrap(async () => null);
assert.equal(result, null);
assert.equal(manager.isCanonical(), false);
assert.equal(manager.getCachedStore(), null);
assert.equal(
storage.getItem(
`buzz-section-workspace.v1:${OWNER}:wss%3A%2F%2Frelay.example`,
),
null,
);
} finally {
restoreWindow();
restoreRelay();
}
}
});
test("projection is decrypted, cached by normalized relay, and rendered during outage", async () => {
const storage = memoryStorage();
let fetchCount = 0;
@@ -277,3 +351,93 @@ test("migration stores one canonical command and replays exact bytes after a fai
restoreRelay();
}
});
test("failed import persistence never publishes and restart replays only the durable command", async () => {
const values = new Map();
let failActionWrite = true;
const storage = {
getItem: (key) => values.get(key) ?? null,
setItem: (key, value) => {
if (failActionWrite && key.includes(":import-action:")) {
throw new Error("quota");
}
values.set(key, String(value));
},
removeItem: (key) => values.delete(key),
clear: () => values.clear(),
};
const legacyStore = {
version: 1,
sections: [
{
id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
name: "Alpha",
order: 0,
},
],
assignments: {},
};
const legacyPlaintext = JSON.stringify(legacyStore);
const legacyEvent = {
id: "2222222222222222222222222222222222222222222222222222222222222222",
pubkey: OWNER,
created_at: 1,
kind: 30078,
content: "legacy-ciphertext",
tags: [["d", "channel-sections"]],
sig: "legacy-signature",
};
const published = [];
const restoreRelay = installRelayStubs({
fetchEvents: async () => [],
publishEvent: async (event) => {
published.push(event);
return event;
},
subscribeLive: async () => async () => {},
});
let signCount = 0;
const restoreWindow = installWindow(storage, async (command, args) => {
if (command === "generate_workspace_key")
return "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f";
if (command === "encrypt_workspace_metadata")
return `ciphertext:${args.aad}`;
if (command === "nip44_encrypt_to_self") return "owner-envelope";
if (command === "sign_event") {
signCount += 1;
return JSON.stringify({
...legacyEvent,
id: `event-${signCount}`,
kind: args.kind,
content: args.content,
tags: args.tags,
});
}
throw new Error(`unexpected command ${command}`);
});
try {
const first = new SectionWorkspaceSyncManager(OWNER, RELAY);
await first.bootstrap(async () => ({
event: legacyEvent,
plaintext: legacyPlaintext,
store: legacyStore,
}));
assert.equal(published.length, 0);
assert.equal(values.size, 1);
const durableCommand = [...values.values()][0];
assert.match(durableCommand, /"action_id"/);
failActionWrite = false;
const restarted = new SectionWorkspaceSyncManager(OWNER, RELAY);
await restarted.bootstrap(async () => {
throw new Error(
"restart must replay durable command without legacy fetch",
);
});
assert.equal(published.length, 1);
assert.equal(published[0].content, durableCommand);
} finally {
restoreWindow();
restoreRelay();
}
});

View File

@@ -44,6 +44,12 @@ const MAX_ASSIGNMENTS = 1_000;
const MAX_ENCRYPTED_METADATA_BYTES = 65_535;
const MAX_KEY_ENVELOPE_BYTES = 4_096;
const CACHE_PREFIX = "buzz-section-workspace.v1";
class UntrustedProjectionError extends Error {
constructor(message: string) {
super(message);
this.name = "UntrustedProjectionError";
}
}
type ProjectionSection = {
id: string;
rank: number;
@@ -533,7 +539,7 @@ function writeCache(
);
} catch {}
}
function aad(
export function canonicalMetadataAad(
community: string,
owner: string,
sectionId: string,
@@ -542,7 +548,7 @@ function aad(
): string {
return canonicalJson({
version: 1,
community,
community: canonicalRelayAuthority(community),
owner_pubkey: owner,
section_id: sectionId,
key_epoch: epoch,
@@ -571,13 +577,7 @@ async function encryptMetadata(
await encryptWorkspaceMetadata({
keyHex: key,
plaintext: value,
aad: aad(
canonicalRelayAuthority(community),
owner,
section.id,
1,
purpose,
),
aad: canonicalMetadataAad(community, owner, section.id, 1, purpose),
}),
"encrypted_metadata",
MAX_ENCRYPTED_METADATA_BYTES,
@@ -595,13 +595,7 @@ async function decryptMetadata(
return decryptWorkspaceMetadata({
keyHex: key,
envelope: value,
aad: aad(
canonicalRelayAuthority(community),
owner,
sectionId,
epoch,
purpose,
),
aad: canonicalMetadataAad(community, owner, sectionId, epoch, purpose),
});
}
async function projectionStore(
@@ -690,10 +684,10 @@ async function projectionFromEvent(
try {
relaySelf = await getRelaySelf();
} catch {
throw new Error("relay signer is untrusted");
throw new UntrustedProjectionError("relay signer is untrusted");
}
if (!relaySelf || relaySelf !== event.pubkey)
throw new Error("invalid projection relay signer");
throw new UntrustedProjectionError("invalid projection relay signer");
const projection = parseSectionWorkspaceProjection(
parseStrictJson(event.content),
);
@@ -937,8 +931,10 @@ export class SectionWorkspaceSyncManager {
);
}
}
} catch {
this.workspaceProbeBlocked = true;
} catch (error) {
if (!(error instanceof UntrustedProjectionError)) {
this.workspaceProbeBlocked = true;
}
}
return this.cached ? this.getCachedStore() : null;
}
@@ -974,8 +970,10 @@ export class SectionWorkspaceSyncManager {
if (action !== "accept") return;
const store = await this.acceptProjection(remote);
onStore(store);
} catch {
this.workspaceProbeBlocked = true;
} catch (error) {
if (!(error instanceof UntrustedProjectionError)) {
this.workspaceProbeBlocked = true;
}
}
})();
},

View File

@@ -218,3 +218,398 @@ test("legacy subscription resolving after workspace cutover is disposed and cann
relayClient.subscribeToReconnects = originalSubscribeToReconnects;
}
});
test("canonical cutover ignores storage/reconnect/live ingress and all legacy mutations", async () => {
const { act, cleanup, renderHook } = await import("@testing-library/react");
const { finalizeEvent } = await import("nostr-tools/pure");
const { relayClient } = await import("@/shared/api/relayClient");
const { storageKey } = await import("./channelSectionsStorage.ts");
const { useChannelSections } = await import("./useChannelSections.ts");
const originalFetchEvents = relayClient.fetchEvents;
const originalSubscribeLive = relayClient.subscribeLive;
const originalSubscribeToReconnects = relayClient.subscribeToReconnects;
const owner =
"1111111111111111111111111111111111111111111111111111111111111111";
const relaySelf =
"1b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f";
const ownerBytes = new Uint8Array(32).fill(1);
const projection = {
version: 1,
owner_pubkey: owner,
revision: 4,
layout_revision: 1,
key_epoch: 2,
migration: {
source_event_id:
"2222222222222222222222222222222222222222222222222222222222222222",
source_hash:
"3333333333333333333333333333333333333333333333333333333333333333",
},
reader_key_envelope: "workspace-key-envelope",
sections: [
{
id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
rank: 0,
encrypted_label: "ciphertext-alpha",
encrypted_icon: null,
},
{
id: "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
rank: 1,
encrypted_label: "ciphertext-beta",
encrypted_icon: "ciphertext-icon",
},
],
assignments: [
{
channel_id: "cccccccc-cccc-4ccc-8ccc-cccccccccccc",
section_id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
revision: 1,
},
],
};
const projectionEvent = finalizeEvent(
{
pubkey: owner,
created_at: 1,
kind: 30623,
tags: [
["d", owner],
["p", owner],
],
content: JSON.stringify(projection),
},
ownerBytes,
);
const legacyEvent = {
id: "4444444444444444444444444444444444444444444444444444444444444444",
pubkey: owner,
created_at: 2,
kind: 30078,
tags: [["d", "channel-sections"]],
content: "legacy-ciphertext",
sig: "legacy-signature",
};
const legacyStore = {
version: 1,
sections: [{ id: "legacy", name: "Legacy", order: 0 }],
assignments: {},
};
let legacyLiveCallback;
let reconnectCallback;
let legacyFetchCount = 0;
let publishCount = 0;
relayClient.fetchEvents = async (filter) => {
if (filter.kinds.includes(30623)) return [projectionEvent];
legacyFetchCount += 1;
return [];
};
relayClient.subscribeLive = async (filter, onEvent) => {
if (filter.kinds.includes(30078)) legacyLiveCallback = onEvent;
return async () => {};
};
relayClient.publishEvent = async () => {
publishCount += 1;
};
relayClient.subscribeToReconnects = (listener) => {
reconnectCallback = listener;
return () => {};
};
const previousInternals = window.__TAURI_INTERNALS__;
window.__TAURI_INTERNALS__ = {
invoke: async (command, args) => {
if (command === "get_relay_self") return relaySelf;
if (command === "nip44_decrypt_from_self") {
return args.ciphertext === "legacy-ciphertext"
? JSON.stringify(legacyStore)
: "workspace-key";
}
if (command === "decrypt_workspace_metadata") {
if (args.envelope === "ciphertext-alpha") return "Alpha";
if (args.envelope === "ciphertext-beta") return "Beta";
if (args.envelope === "ciphertext-icon") return "folder";
}
throw new Error(`unexpected command ${command}`);
},
};
try {
const { result, unmount } = renderHook(() =>
useChannelSections(owner, "wss://relay.example"),
);
const flush = async () => {
for (let index = 0; index < 5; index += 1) {
await new Promise((resolve) => setImmediate(resolve));
}
};
await act(flush);
const canonical = result.current.sections;
const legacyKey = storageKey(owner, "wss://relay.example");
const beforeStorage = window.localStorage.getItem(legacyKey);
const beforeFetches = legacyFetchCount;
window.dispatchEvent(
new window.StorageEvent("storage", {
key: legacyKey,
newValue: JSON.stringify(legacyStore),
}),
);
reconnectCallback();
legacyLiveCallback(legacyEvent);
await act(flush);
assert.deepEqual(result.current.sections, canonical);
assert.equal(window.localStorage.getItem(legacyKey), beforeStorage);
assert.equal(legacyFetchCount, beforeFetches);
assert.equal(publishCount, 0);
const mutationResults = [
result.current.createSection("blocked"),
result.current.renameSection(
"aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
"blocked",
),
result.current.deleteSection("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"),
result.current.moveSectionUp("bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"),
result.current.moveSectionDown("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"),
result.current.reorderSections([
"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
"aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
]),
result.current.assignChannel(
"cccccccc-cccc-4ccc-8ccc-cccccccccccc",
"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
),
result.current.unassignChannel("cccccccc-cccc-4ccc-8ccc-cccccccccccc"),
];
assert.equal(mutationResults[0], null);
await act(flush);
assert.deepEqual(result.current.sections, canonical);
assert.deepEqual(result.current.assignments, {
"cccccccc-cccc-4ccc-8ccc-cccccccccccc":
"aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
});
assert.equal(window.localStorage.getItem(legacyKey), beforeStorage);
assert.equal(publishCount, 0);
unmount();
} finally {
cleanup();
window.__TAURI_INTERNALS__ = previousInternals;
relayClient.fetchEvents = originalFetchEvents;
relayClient.subscribeLive = originalSubscribeLive;
relayClient.subscribeToReconnects = originalSubscribeToReconnects;
}
});
test("legacy live, reconnect, and edit remain compatible before migration", async () => {
const { act, cleanup, renderHook } = await import("@testing-library/react");
const { relayClient } = await import("@/shared/api/relayClient");
const { useChannelSections } = await import("./useChannelSections.ts");
const originalFetchEvents = relayClient.fetchEvents;
const originalSubscribeLive = relayClient.subscribeLive;
const originalSubscribeToReconnects = relayClient.subscribeToReconnects;
const owner = "legacy-compatible-owner";
const relayUrl = "wss://relay.compatible";
const liveEvent = {
id: "1111111111111111111111111111111111111111111111111111111111111111",
pubkey: owner,
created_at: 1,
kind: 30078,
tags: [["d", "channel-sections"]],
content: "legacy-live",
sig: "signature",
};
const reconnectEvent = {
...liveEvent,
id: "2222222222222222222222222222222222222222222222222222222222222222",
created_at: 2,
content: "legacy-reconnect",
};
const liveStore = {
version: 1,
sections: [{ id: "live", name: "Live", order: 0 }],
assignments: {},
};
const reconnectStore = {
version: 1,
sections: [{ id: "reconnect", name: "Reconnect", order: 0 }],
assignments: {},
};
let liveCallback;
let reconnectCallback;
let reconnectFetches = 0;
relayClient.fetchEvents = async (filter) => {
if (filter.kinds.includes(30078)) {
reconnectFetches += 1;
return reconnectFetches > 2 ? [reconnectEvent] : [];
}
return [];
};
relayClient.subscribeLive = async (filter, onEvent) => {
if (filter.kinds.includes(30078)) liveCallback = onEvent;
return async () => {};
};
relayClient.subscribeToReconnects = (listener) => {
reconnectCallback = listener;
return () => {};
};
const previousInternals = window.__TAURI_INTERNALS__;
window.__TAURI_INTERNALS__ = {
invoke: async (command, args) => {
if (command === "nip44_decrypt_from_self") {
if (args.ciphertext === "legacy-live") return JSON.stringify(liveStore);
if (args.ciphertext === "legacy-reconnect") {
return JSON.stringify(reconnectStore);
}
}
throw new Error(`unexpected command ${command}`);
},
};
try {
const { result, unmount } = renderHook(() =>
useChannelSections(owner, relayUrl),
);
const flush = async () => {
for (let index = 0; index < 5; index += 1) {
await new Promise((resolve) => setImmediate(resolve));
}
};
await act(flush);
assert.equal(result.current.sections.length, 0);
liveCallback(liveEvent);
await act(flush);
assert.deepEqual(result.current.sections, [
{ id: "live", name: "Live", order: 0 },
]);
reconnectCallback();
await act(flush);
assert.deepEqual(result.current.sections, [
{ id: "reconnect", name: "Reconnect", order: 0 },
]);
act(() => result.current.renameSection("reconnect", "Edited"));
assert.deepEqual(result.current.sections, [
{ id: "reconnect", name: "Edited", order: 0 },
]);
unmount();
} finally {
cleanup();
window.__TAURI_INTERNALS__ = previousInternals;
relayClient.fetchEvents = originalFetchEvents;
relayClient.subscribeLive = originalSubscribeLive;
relayClient.subscribeToReconnects = originalSubscribeToReconnects;
}
});
test("canonical cutover cancels a pending legacy publish", async () => {
const { act, cleanup, renderHook } = await import("@testing-library/react");
const { finalizeEvent } = await import("nostr-tools/pure");
const { relayClient } = await import("@/shared/api/relayClient");
const { useChannelSections } = await import("./useChannelSections.ts");
const originalFetchEvents = relayClient.fetchEvents;
const originalSubscribeLive = relayClient.subscribeLive;
const originalSubscribeToReconnects = relayClient.subscribeToReconnects;
const originalSetTimeout = window.setTimeout;
const originalClearTimeout = window.clearTimeout;
const owner =
"3333333333333333333333333333333333333333333333333333333333333333";
const relaySelf =
"1b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f";
const ownerBytes = new Uint8Array(32).fill(1);
const projection = {
version: 1,
owner_pubkey: owner,
revision: 1,
layout_revision: 1,
key_epoch: 1,
migration: {
source_event_id:
"4444444444444444444444444444444444444444444444444444444444444444",
source_hash:
"5555555555555555555555555555555555555555555555555555555555555555",
},
reader_key_envelope: "workspace-key-envelope",
sections: [
{
id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
rank: 0,
encrypted_label: "canonical-label",
encrypted_icon: null,
},
],
assignments: [],
};
const projectionEvent = finalizeEvent(
{
pubkey: owner,
created_at: 1,
kind: 30623,
tags: [
["d", owner],
["p", owner],
],
content: JSON.stringify(projection),
},
ownerBytes,
);
let workspaceCallback;
let pendingTimer;
let pendingTimerCleared = false;
let publishCount = 0;
relayClient.fetchEvents = async () => [];
relayClient.subscribeLive = async (filter, onEvent) => {
if (filter.kinds.includes(30623)) workspaceCallback = onEvent;
return async () => {};
};
relayClient.subscribeToReconnects = () => () => {};
relayClient.publishEvent = async () => {
publishCount += 1;
};
window.setTimeout = (callback, delay) => {
if (delay === 2000) {
pendingTimer = callback;
return 42;
}
return originalSetTimeout(callback, delay);
};
window.clearTimeout = (timer) => {
if (timer === 42) pendingTimerCleared = true;
else originalClearTimeout(timer);
};
const previousInternals = window.__TAURI_INTERNALS__;
window.__TAURI_INTERNALS__ = {
invoke: async (command) => {
if (command === "get_relay_self") return relaySelf;
if (command === "nip44_decrypt_from_self") return "workspace-key";
if (command === "decrypt_workspace_metadata") return "canonical-name";
throw new Error(`unexpected command ${command}`);
},
};
try {
const { result, unmount } = renderHook(() =>
useChannelSections(owner, "wss://relay.pending"),
);
const flush = async () => {
for (let index = 0; index < 5; index += 1) {
await new Promise((resolve) => setImmediate(resolve));
}
};
await act(flush);
act(() => result.current.createSection("pending"));
assert.equal(typeof pendingTimer, "function");
workspaceCallback(projectionEvent);
await act(flush);
assert.equal(pendingTimerCleared, true);
assert.equal(publishCount, 0);
unmount();
} finally {
cleanup();
window.setTimeout = originalSetTimeout;
window.clearTimeout = originalClearTimeout;
window.__TAURI_INTERNALS__ = previousInternals;
relayClient.fetchEvents = originalFetchEvents;
relayClient.subscribeLive = originalSubscribeLive;
relayClient.subscribeToReconnects = originalSubscribeToReconnects;
}
});