mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 16:02:26 +00:00
- Resolve scheduler context and preflight exclusions - Select `ci-health-agent` from roster using round-robin logic - Acquire distributed lock for execution window - Perform necessary repository check fixes (linting and tests) - Publish successful completion lock event - Write final `task-logs` event record - Output execution summary Co-authored-by: PR0M3TH3AN <[email protected]>
440 lines
14 KiB
JavaScript
440 lines
14 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { Buffer } from "node:buffer";
|
|
import test from "node:test";
|
|
|
|
const loadNostrTools = async () => {
|
|
const nostrTools = await import("nostr-tools");
|
|
globalThis.nostrToolsReady = Promise.resolve(nostrTools);
|
|
return nostrTools;
|
|
};
|
|
|
|
const loadNip46Module = () => import("../../js/nostr/nip46Client.js");
|
|
|
|
const buildSignEventStub = () => (event) => ({
|
|
...event,
|
|
id: "stub-id",
|
|
sig: "stub-signature",
|
|
});
|
|
|
|
test("Nip46RpcClient encrypts payloads with nip44 conversation keys", async () => {
|
|
const nostrTools = await loadNostrTools();
|
|
const { generateSecretKey, getPublicKey, nip44, utils } = nostrTools;
|
|
const {
|
|
Nip46RpcClient,
|
|
} = await loadNip46Module();
|
|
|
|
const clientSecret = utils.bytesToHex(generateSecretKey());
|
|
const remoteSecret = utils.bytesToHex(generateSecretKey());
|
|
const clientPublicKey = getPublicKey(utils.hexToBytes(clientSecret));
|
|
const remotePubkey = getPublicKey(utils.hexToBytes(remoteSecret));
|
|
|
|
const rpcClient = new Nip46RpcClient({
|
|
nostrClient: { relays: [] },
|
|
clientPrivateKey: clientSecret,
|
|
clientPublicKey,
|
|
remotePubkey,
|
|
signEvent: buildSignEventStub(),
|
|
});
|
|
|
|
const serialized = JSON.stringify({ message: "hello", count: 42 });
|
|
const ciphertext = await rpcClient.encryptPayload({ message: "hello", count: 42 });
|
|
|
|
assert.equal(typeof ciphertext, "string", "ciphertext should be encoded as a string");
|
|
|
|
const conversationKey =
|
|
typeof nip44.v2.getConversationKey === "function"
|
|
? nip44.v2.getConversationKey(utils.hexToBytes(clientSecret), remotePubkey)
|
|
: nip44.v2.utils.getConversationKey(utils.hexToBytes(clientSecret), remotePubkey);
|
|
const decryptedWithTools = nip44.v2.decrypt(ciphertext, conversationKey);
|
|
assert.equal(
|
|
decryptedWithTools,
|
|
serialized,
|
|
"nip44 decrypt should recover the serialized payload",
|
|
);
|
|
|
|
const roundTrip = await rpcClient.decryptPayload(ciphertext);
|
|
assert.equal(roundTrip, serialized, "client should decrypt its own ciphertext");
|
|
|
|
const externalCiphertext = nip44.v2.encrypt(serialized, conversationKey);
|
|
const externalRoundTrip = await rpcClient.decryptPayload(externalCiphertext);
|
|
assert.equal(
|
|
externalRoundTrip,
|
|
serialized,
|
|
"client should decrypt ciphertext produced by nostr-tools",
|
|
);
|
|
});
|
|
|
|
test("decryptNip46PayloadWithKeys handles nip44.v2 ciphertext", async () => {
|
|
const nostrTools = await loadNostrTools();
|
|
const { generateSecretKey, getPublicKey, nip44, utils } = nostrTools;
|
|
const { decryptNip46PayloadWithKeys } = await loadNip46Module();
|
|
|
|
const clientSecret = utils.bytesToHex(generateSecretKey());
|
|
const remoteSecret = utils.bytesToHex(generateSecretKey());
|
|
const remotePubkey = getPublicKey(utils.hexToBytes(remoteSecret));
|
|
|
|
const serialized = JSON.stringify({ message: "hello", count: 42 });
|
|
const conversationKey =
|
|
typeof nip44.v2.getConversationKey === "function"
|
|
? nip44.v2.getConversationKey(utils.hexToBytes(clientSecret), remotePubkey)
|
|
: nip44.v2.utils.getConversationKey(utils.hexToBytes(clientSecret), remotePubkey);
|
|
const ciphertext = nip44.v2.encrypt(serialized, conversationKey);
|
|
|
|
const { plaintext, algorithm } = await decryptNip46PayloadWithKeys(
|
|
clientSecret,
|
|
remotePubkey,
|
|
ciphertext,
|
|
);
|
|
|
|
assert.equal(plaintext, serialized, "helper should decrypt nip44.v2 ciphertext");
|
|
assert.equal(
|
|
algorithm,
|
|
"nip44.v2",
|
|
"helper should report nip44.v2 as the active algorithm",
|
|
);
|
|
});
|
|
|
|
test("decryptNip46PayloadWithKeys coerces structured handshake payloads", async () => {
|
|
const nostrTools = await loadNostrTools();
|
|
const { generateSecretKey, getPublicKey, nip44, utils } = nostrTools;
|
|
const {
|
|
decryptNip46PayloadWithKeys,
|
|
normalizeNip46CiphertextPayload,
|
|
} = await loadNip46Module();
|
|
|
|
const clientSecret = utils.bytesToHex(generateSecretKey());
|
|
const remoteSecret = utils.bytesToHex(generateSecretKey());
|
|
const remotePubkey = getPublicKey(utils.hexToBytes(remoteSecret));
|
|
|
|
const serialized = JSON.stringify({ message: "hello", count: 42 });
|
|
const conversationKey =
|
|
typeof nip44.v2.getConversationKey === "function"
|
|
? nip44.v2.getConversationKey(utils.hexToBytes(clientSecret), remotePubkey)
|
|
: nip44.v2.utils.getConversationKey(utils.hexToBytes(clientSecret), remotePubkey);
|
|
const ciphertext = nip44.v2.encrypt(serialized, conversationKey);
|
|
const [ciphertextPart, nonce] = ciphertext.split("\n");
|
|
|
|
const structuredPayload = {
|
|
ciphertext: ciphertextPart,
|
|
nonce,
|
|
};
|
|
|
|
const normalized = normalizeNip46CiphertextPayload(structuredPayload);
|
|
assert.ok(
|
|
normalized.includes(ciphertext),
|
|
"normalizer should include the raw ciphertext from structured payloads",
|
|
);
|
|
|
|
const { plaintext: structuredPlaintext, algorithm } =
|
|
await decryptNip46PayloadWithKeys(
|
|
clientSecret,
|
|
remotePubkey,
|
|
structuredPayload,
|
|
);
|
|
|
|
assert.equal(
|
|
structuredPlaintext,
|
|
serialized,
|
|
"helper should decrypt ciphertext encoded with newline nonce",
|
|
);
|
|
assert.equal(
|
|
algorithm,
|
|
"nip44.v2",
|
|
"structured payload should report nip44.v2 algorithm",
|
|
);
|
|
});
|
|
|
|
test("decryptNip46PayloadWithKeys decodes buffer-based handshake payloads", async () => {
|
|
const nostrTools = await loadNostrTools();
|
|
const { generateSecretKey, getPublicKey, nip44, utils } = nostrTools;
|
|
const {
|
|
decryptNip46PayloadWithKeys,
|
|
normalizeNip46CiphertextPayload,
|
|
} = await loadNip46Module();
|
|
|
|
const clientSecret = utils.bytesToHex(generateSecretKey());
|
|
const remoteSecret = utils.bytesToHex(generateSecretKey());
|
|
const remotePubkey = getPublicKey(utils.hexToBytes(remoteSecret));
|
|
|
|
const serialized = JSON.stringify({ message: "hello", count: 42 });
|
|
const conversationKey =
|
|
typeof nip44.v2.getConversationKey === "function"
|
|
? nip44.v2.getConversationKey(utils.hexToBytes(clientSecret), remotePubkey)
|
|
: nip44.v2.utils.getConversationKey(utils.hexToBytes(clientSecret), remotePubkey);
|
|
const ciphertext = nip44.v2.encrypt(serialized, conversationKey);
|
|
const [ciphertextPart, nonce] = ciphertext.split("\n");
|
|
const ciphertextBuffer = Buffer.from(ciphertext, "utf8");
|
|
|
|
const normalized = normalizeNip46CiphertextPayload(ciphertextBuffer);
|
|
assert.ok(
|
|
normalized.includes(ciphertext),
|
|
"normalizer should extract the raw ciphertext from buffers",
|
|
);
|
|
|
|
const { plaintext: bufferPlaintext, algorithm } =
|
|
await decryptNip46PayloadWithKeys(
|
|
clientSecret,
|
|
remotePubkey,
|
|
ciphertextBuffer,
|
|
);
|
|
|
|
assert.equal(
|
|
bufferPlaintext,
|
|
serialized,
|
|
"helper should decrypt ciphertext derived from buffers",
|
|
);
|
|
assert.equal(
|
|
algorithm,
|
|
"nip44.v2",
|
|
"buffer payload should report nip44.v2 algorithm",
|
|
);
|
|
});
|
|
|
|
test("decryptNip46PayloadWithKeys supports nip04-style ciphertext wrappers", async () => {
|
|
const nostrTools = await loadNostrTools();
|
|
const { generateSecretKey, getPublicKey, nip04, utils } = nostrTools;
|
|
const { createNip46Cipher, normalizeNip46CiphertextPayload } = await loadNip46Module();
|
|
|
|
const clientSecret = utils.bytesToHex(generateSecretKey());
|
|
const remoteSecret = utils.bytesToHex(generateSecretKey());
|
|
const remotePubkey = getPublicKey(utils.hexToBytes(remoteSecret));
|
|
|
|
const serialized = JSON.stringify({ message: "hello", count: 42 });
|
|
const ciphertext = await nip04.encrypt(clientSecret, remotePubkey, serialized);
|
|
const [ciphertextPart, iv] = ciphertext.split("?iv=");
|
|
|
|
const structuredPayload = {
|
|
ciphertext: ciphertextPart,
|
|
iv,
|
|
};
|
|
|
|
const patchedTools = {
|
|
...(await import("nostr-tools")),
|
|
nip44: undefined,
|
|
};
|
|
|
|
const candidates = normalizeNip46CiphertextPayload(structuredPayload);
|
|
|
|
assert.ok(
|
|
candidates.includes(`${ciphertextPart}?iv=${iv}`),
|
|
"normalizer should include nip04-style ciphertext",
|
|
);
|
|
|
|
const cipher = createNip46Cipher(patchedTools, clientSecret, remotePubkey);
|
|
assert.equal(
|
|
cipher.algorithm,
|
|
"nip04",
|
|
"nip04-only toolchain should resolve the nip04 algorithm",
|
|
);
|
|
const { decrypt } = cipher;
|
|
let decrypted = "";
|
|
for (const candidate of candidates) {
|
|
try {
|
|
decrypted = decrypt(candidate);
|
|
if (decrypted) {
|
|
break;
|
|
}
|
|
} catch (error) {
|
|
// continue trying other candidates
|
|
}
|
|
}
|
|
|
|
assert.equal(
|
|
decrypted,
|
|
serialized,
|
|
"nip04 decrypt should succeed for structured payload candidates",
|
|
);
|
|
});
|
|
|
|
test("parseNip46ConnectionString handles remote signer key hints", async () => {
|
|
const nostrTools = await loadNostrTools();
|
|
const { generateSecretKey, getPublicKey, nip19, utils } = nostrTools;
|
|
const { parseNip46ConnectionString } = await loadNip46Module();
|
|
|
|
const signerSecretBytes = generateSecretKey();
|
|
const userSecretBytes = generateSecretKey();
|
|
|
|
const signerPubkey = getPublicKey(signerSecretBytes).toLowerCase();
|
|
const userPubkey = getPublicKey(userSecretBytes).toLowerCase();
|
|
|
|
const signerNpub = nip19.npubEncode(signerPubkey);
|
|
const userNpub = nip19.npubEncode(userPubkey);
|
|
|
|
const uri =
|
|
`bunker://${userNpub}?remote-signer-key=${encodeURIComponent(signerNpub)}` +
|
|
`&relay=${encodeURIComponent("wss://relay.example.com")}`;
|
|
|
|
const parsed = parseNip46ConnectionString(uri);
|
|
|
|
assert.ok(parsed, "parser should return a payload");
|
|
assert.equal(
|
|
parsed.remotePubkey,
|
|
signerPubkey,
|
|
"remote signer pubkey should come from the remote-signer-key parameter",
|
|
);
|
|
assert.equal(
|
|
parsed.userPubkeyHint,
|
|
userPubkey,
|
|
"user pubkey hint should reflect the bunker URI hostname",
|
|
);
|
|
assert.deepEqual(
|
|
parsed.relays,
|
|
["wss://relay.example.com"],
|
|
"relay parameters should be decoded",
|
|
);
|
|
});
|
|
|
|
test(
|
|
"attemptDecryptNip46HandshakePayload falls back to expected remote signer key",
|
|
async () => {
|
|
const nostrTools = await loadNostrTools();
|
|
const { generateSecretKey, getPublicKey, nip44, utils } = nostrTools;
|
|
const { attemptDecryptNip46HandshakePayload } = await loadNip46Module();
|
|
|
|
const clientSecretBytes = generateSecretKey();
|
|
const clientSecret = utils.bytesToHex(clientSecretBytes);
|
|
const remoteSignerSecretBytes = generateSecretKey();
|
|
const userSecretBytes = generateSecretKey();
|
|
|
|
const remoteSignerPubkey = getPublicKey(remoteSignerSecretBytes).toLowerCase();
|
|
const userPubkey = getPublicKey(userSecretBytes).toLowerCase();
|
|
|
|
const conversationKey =
|
|
typeof nip44.v2.getConversationKey === "function"
|
|
? nip44.v2.getConversationKey(utils.hexToBytes(clientSecret), remoteSignerPubkey)
|
|
: nip44.v2.utils.getConversationKey(utils.hexToBytes(clientSecret), remoteSignerPubkey);
|
|
|
|
const payload = JSON.stringify({ id: "ack", result: "ok" });
|
|
const ciphertext = nip44.v2.encrypt(payload, conversationKey);
|
|
|
|
const result = await attemptDecryptNip46HandshakePayload({
|
|
clientPrivateKey: clientSecret,
|
|
candidateRemotePubkeys: [userPubkey, remoteSignerPubkey],
|
|
ciphertext,
|
|
});
|
|
|
|
assert.equal(
|
|
result.remotePubkey,
|
|
remoteSignerPubkey,
|
|
"helper should return the remote signer key that successfully decrypted the payload",
|
|
);
|
|
assert.equal(
|
|
result.plaintext,
|
|
payload,
|
|
"helper should yield the handshake plaintext",
|
|
);
|
|
},
|
|
);
|
|
|
|
test(
|
|
"attemptDecryptNip46HandshakePayload handles array-encoded nip04 payloads",
|
|
async () => {
|
|
const nostrTools = await loadNostrTools();
|
|
const { generateSecretKey, getPublicKey, nip04, utils } = nostrTools;
|
|
const { attemptDecryptNip46HandshakePayload } = await loadNip46Module();
|
|
|
|
const clientSecret = utils.bytesToHex(generateSecretKey());
|
|
const clientPubkey = getPublicKey(utils.hexToBytes(clientSecret)).toLowerCase();
|
|
const remoteSecret = utils.bytesToHex(generateSecretKey());
|
|
const remotePubkey = getPublicKey(utils.hexToBytes(remoteSecret)).toLowerCase();
|
|
|
|
const payload = JSON.stringify({ id: "ack", result: "ack" });
|
|
const ciphertext = await nip04.encrypt(remoteSecret, clientPubkey, payload);
|
|
const [ciphertextPart, ivPart] = ciphertext.split("?iv=");
|
|
|
|
assert.ok(ivPart, "nip04 encryption should emit an iv segment");
|
|
|
|
const encodedPayload = `${ciphertextPart}?iv=${ivPart}`;
|
|
|
|
const result = await attemptDecryptNip46HandshakePayload({
|
|
clientPrivateKey: clientSecret,
|
|
candidateRemotePubkeys: [remotePubkey],
|
|
ciphertext: encodedPayload,
|
|
});
|
|
|
|
assert.equal(
|
|
result.remotePubkey,
|
|
remotePubkey,
|
|
"helper should resolve the remote signer key for array payloads",
|
|
);
|
|
assert.equal(
|
|
result.plaintext,
|
|
payload,
|
|
"helper should decrypt nip04 payloads serialized as arrays",
|
|
);
|
|
assert.equal(
|
|
result.algorithm,
|
|
"nip04",
|
|
"helper should report the nip04 algorithm for nip04 ciphertext",
|
|
);
|
|
},
|
|
);
|
|
|
|
test("Nip46RpcClient sendRpc publishes events and resolves responses", async () => {
|
|
const { Nip46RpcClient, NIP46_RPC_KIND } = await loadNip46Module();
|
|
|
|
const remotePubkey = "b".repeat(64);
|
|
const clientPrivateKey = "a".repeat(64);
|
|
const clientPublicKey = "c".repeat(64);
|
|
|
|
let eventListener = null;
|
|
const pool = {
|
|
sub(relays, filters) {
|
|
return {
|
|
on(type, handler) {
|
|
if (type === "event") {
|
|
eventListener = handler;
|
|
}
|
|
},
|
|
unsub() {},
|
|
};
|
|
},
|
|
};
|
|
|
|
const publishCalls = [];
|
|
const client = new Nip46RpcClient({
|
|
nostrClient: {
|
|
relays: ["wss://relay.example.com"],
|
|
pool,
|
|
async ensurePool() {
|
|
return pool;
|
|
},
|
|
},
|
|
clientPrivateKey,
|
|
clientPublicKey,
|
|
remotePubkey,
|
|
relays: ["wss://relay.example.com"],
|
|
signEvent: (event) => ({ ...event, id: "evt", sig: "sig" }),
|
|
publishEventToRelays: async (unusedPool, relays, event) => {
|
|
publishCalls.push({ relays, event });
|
|
return relays.map((relay) => ({ relay, ok: true }));
|
|
},
|
|
assertAnyRelayAccepted: () => {},
|
|
});
|
|
|
|
client.ensureCipher = async () => ({
|
|
encrypt: (payload) => (typeof payload === "string" ? payload : JSON.stringify(payload)),
|
|
decrypt: (ciphertext) => ciphertext,
|
|
});
|
|
|
|
const rpcPromise = client.sendRpc("ping", []);
|
|
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
|
|
assert.ok(publishCalls.length > 0, "sendRpc should publish an event");
|
|
assert.ok(eventListener, "sendRpc should register an event listener");
|
|
|
|
const [{ event }] = publishCalls;
|
|
const requestPayload = JSON.parse(event.content);
|
|
|
|
eventListener({
|
|
kind: NIP46_RPC_KIND,
|
|
pubkey: remotePubkey,
|
|
tags: [["p", clientPublicKey]],
|
|
content: JSON.stringify({ id: requestPayload.id, result: "pong" }),
|
|
});
|
|
|
|
const result = await rpcPromise;
|
|
assert.equal(result, "pong", "RPC result should resolve from response event");
|
|
});
|