mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-06 12:52:26 +00:00
Merge pull request #1989 from PR0M3TH3AN/fix/nip07-adapter-race-condition-531357967915833285
Fix NIP-07 adapter race condition for encrypted lists
This commit is contained in:
5 files changed
+150
-34
No files matched your search
@@ -90,12 +90,6 @@ export async function createNip07Adapter(initialExtension) {
|
||||
? normalizeActorKey(await getPublicKey())
|
||||
: "";
|
||||
|
||||
const hasNip04 =
|
||||
bootstrapExtension.nip04 && typeof bootstrapExtension.nip04 === "object";
|
||||
|
||||
const bootstrapNip44 = resolveNip44Module(bootstrapExtension);
|
||||
const hasNip44 = !!bootstrapNip44;
|
||||
|
||||
// Wrapper builders to ensure dynamic lookup at call time
|
||||
const createNip04Encrypt = () => (pubkey, plaintext, options = {}) => {
|
||||
const ext = getExtension();
|
||||
@@ -206,30 +200,29 @@ export async function createNip07Adapter(initialExtension) {
|
||||
requestPermissions,
|
||||
destroy: async () => {},
|
||||
canSign: () => typeof getExtension()?.signEvent === "function",
|
||||
capabilities: {
|
||||
sign: typeof bootstrapExtension.signEvent === "function",
|
||||
nip44: hasNip44,
|
||||
nip04: hasNip04,
|
||||
},
|
||||
// Always expose these methods; they perform their own existence checks at runtime.
|
||||
// This handles race conditions where NIP-04/44 APIs are injected lazily.
|
||||
nip04Encrypt: createNip04Encrypt(),
|
||||
nip04Decrypt: createNip04Decrypt(),
|
||||
nip44Encrypt: createNip44Encrypt(),
|
||||
nip44Decrypt: createNip44Decrypt(),
|
||||
};
|
||||
|
||||
if (hasNip04) {
|
||||
if (typeof bootstrapExtension.nip04.encrypt === "function") {
|
||||
signer.nip04Encrypt = createNip04Encrypt();
|
||||
}
|
||||
if (typeof bootstrapExtension.nip04.decrypt === "function") {
|
||||
signer.nip04Decrypt = createNip04Decrypt();
|
||||
}
|
||||
}
|
||||
|
||||
if (hasNip44) {
|
||||
if (typeof bootstrapNip44.encrypt === "function") {
|
||||
signer.nip44Encrypt = createNip44Encrypt();
|
||||
}
|
||||
if (typeof bootstrapNip44.decrypt === "function") {
|
||||
signer.nip44Decrypt = createNip44Decrypt();
|
||||
}
|
||||
}
|
||||
// Define capabilities as dynamic properties to reflect current extension state
|
||||
Object.defineProperty(signer, "capabilities", {
|
||||
get: () => {
|
||||
const ext = getExtension();
|
||||
const hasNip04 = !!(ext?.nip04 && typeof ext.nip04 === "object");
|
||||
const hasNip44 = !!resolveNip44Module(ext);
|
||||
return {
|
||||
sign: typeof ext?.signEvent === "function",
|
||||
nip44: hasNip44,
|
||||
nip04: hasNip04,
|
||||
};
|
||||
},
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
return signer;
|
||||
}
|
||||
+25
-3
@@ -358,7 +358,13 @@ function setActiveSigner(signer) {
|
||||
|
||||
hydrateExtensionSignerCapabilities(signer);
|
||||
attachNipMethodAliases(signer);
|
||||
signer.capabilities = resolveSignerCapabilities(signer);
|
||||
|
||||
const capsDescriptor = Object.getOwnPropertyDescriptor(signer, "capabilities");
|
||||
const isGetter = capsDescriptor && typeof capsDescriptor.get === "function";
|
||||
|
||||
if (!isGetter) {
|
||||
signer.capabilities = resolveSignerCapabilities(signer);
|
||||
}
|
||||
|
||||
const pubkey =
|
||||
typeof signer.pubkey === "string" && signer.pubkey.trim()
|
||||
@@ -376,7 +382,15 @@ function getActiveSigner() {
|
||||
hydrateExtensionSignerCapabilities(signer);
|
||||
attachNipMethodAliases(signer);
|
||||
if (signer && typeof signer === "object") {
|
||||
signer.capabilities = resolveSignerCapabilities(signer);
|
||||
const capsDescriptor = Object.getOwnPropertyDescriptor(
|
||||
signer,
|
||||
"capabilities",
|
||||
);
|
||||
const isGetter = capsDescriptor && typeof capsDescriptor.get === "function";
|
||||
|
||||
if (!isGetter) {
|
||||
signer.capabilities = resolveSignerCapabilities(signer);
|
||||
}
|
||||
}
|
||||
return signer;
|
||||
}
|
||||
@@ -394,7 +408,15 @@ function resolveActiveSigner(pubkey) {
|
||||
hydrateExtensionSignerCapabilities(signer);
|
||||
attachNipMethodAliases(signer);
|
||||
if (signer && typeof signer === "object") {
|
||||
signer.capabilities = resolveSignerCapabilities(signer);
|
||||
const capsDescriptor = Object.getOwnPropertyDescriptor(
|
||||
signer,
|
||||
"capabilities",
|
||||
);
|
||||
const isGetter = capsDescriptor && typeof capsDescriptor.get === "function";
|
||||
|
||||
if (!isGetter) {
|
||||
signer.capabilities = resolveSignerCapabilities(signer);
|
||||
}
|
||||
}
|
||||
return signer;
|
||||
}
|
||||
|
||||
@@ -76,7 +76,20 @@ function setupLoginEnvironment({ enableImpl, getPublicKey = HEX_PUBKEY } = {}) {
|
||||
|
||||
windowRef.NostrTools = toolkitStub;
|
||||
global.NostrTools = toolkitStub;
|
||||
global.__BITVID_CANONICAL_NOSTR_TOOLS__ = toolkitStub;
|
||||
// Use Object.defineProperty to override potential read-only global from bootstrap
|
||||
try {
|
||||
Object.defineProperty(global, "__BITVID_CANONICAL_NOSTR_TOOLS__", {
|
||||
value: toolkitStub,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
} catch (e) {
|
||||
try {
|
||||
global.__BITVID_CANONICAL_NOSTR_TOOLS__ = toolkitStub;
|
||||
} catch (ignore) {
|
||||
// Ignore assignment errors if property is read-only
|
||||
}
|
||||
}
|
||||
global.nostrToolsReady = Promise.resolve({ ok: true, value: toolkitStub });
|
||||
|
||||
const originalAccessControl = {
|
||||
|
||||
@@ -25,7 +25,17 @@ test("createNsecAdapter wires signing and cipher capabilities", async () => {
|
||||
const canonicalTools = { ...nostrTools };
|
||||
|
||||
try {
|
||||
globalThis.__BITVID_CANONICAL_NOSTR_TOOLS__ = canonicalTools;
|
||||
try {
|
||||
Object.defineProperty(globalThis, "__BITVID_CANONICAL_NOSTR_TOOLS__", {
|
||||
value: canonicalTools,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
} catch (e) {
|
||||
try {
|
||||
globalThis.__BITVID_CANONICAL_NOSTR_TOOLS__ = canonicalTools;
|
||||
} catch (ignore) {}
|
||||
}
|
||||
globalThis.NostrTools = canonicalTools;
|
||||
globalThis.nostrToolsReady = Promise.resolve({
|
||||
ok: true,
|
||||
@@ -55,7 +65,17 @@ test("createNsecAdapter wires signing and cipher capabilities", async () => {
|
||||
if (previousCanonical === undefined) {
|
||||
delete globalThis.__BITVID_CANONICAL_NOSTR_TOOLS__;
|
||||
} else {
|
||||
globalThis.__BITVID_CANONICAL_NOSTR_TOOLS__ = previousCanonical;
|
||||
try {
|
||||
Object.defineProperty(globalThis, "__BITVID_CANONICAL_NOSTR_TOOLS__", {
|
||||
value: previousCanonical,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
} catch (e) {
|
||||
try {
|
||||
globalThis.__BITVID_CANONICAL_NOSTR_TOOLS__ = previousCanonical;
|
||||
} catch (ignore) {}
|
||||
}
|
||||
}
|
||||
|
||||
if (previousNostrTools === undefined) {
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import '../test-helpers/setup-localstorage.mjs';
|
||||
import assert from 'node:assert';
|
||||
import { test, describe, beforeEach, afterEach } from 'node:test';
|
||||
import { createNip07Adapter } from '../../js/nostr/adapters/nip07Adapter.js';
|
||||
|
||||
describe('Nip07Adapter Race Condition', () => {
|
||||
let originalWindow;
|
||||
|
||||
beforeEach(() => {
|
||||
originalWindow = global.window;
|
||||
global.window = {};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
global.window = originalWindow;
|
||||
});
|
||||
|
||||
test('should detect capabilities dynamically even if injected late', async () => {
|
||||
// 1. Setup initial window.nostr with NO encryption capabilities
|
||||
global.window.nostr = {
|
||||
getPublicKey: async () => 'pubkey',
|
||||
signEvent: async (e) => e,
|
||||
// nip04 and nip44 are missing initially
|
||||
};
|
||||
|
||||
// 2. Initialize adapter
|
||||
const adapter = await createNip07Adapter();
|
||||
|
||||
// 3. Verify initial state: capabilities should be false
|
||||
assert.strictEqual(adapter.capabilities.nip04, false, 'Should not have nip04 capability initially');
|
||||
assert.strictEqual(adapter.capabilities.nip44, false, 'Should not have nip44 capability initially');
|
||||
|
||||
// 4. Simulate lazy injection of encryption APIs
|
||||
global.window.nostr.nip04 = { encrypt: () => {}, decrypt: () => {} };
|
||||
global.window.nostr.nip44 = { encrypt: () => {}, decrypt: () => {} };
|
||||
|
||||
// 5. Verify dynamic state: capabilities should now be true
|
||||
assert.strictEqual(adapter.capabilities.nip04, true, 'Should detect nip04 capability after injection');
|
||||
assert.strictEqual(adapter.capabilities.nip44, true, 'Should detect nip44 capability after injection');
|
||||
});
|
||||
|
||||
test('should call the injected method even if added late', async () => {
|
||||
// 1. Setup initial window.nostr with NO encryption capabilities
|
||||
global.window.nostr = {
|
||||
getPublicKey: async () => 'pubkey',
|
||||
signEvent: async (e) => e,
|
||||
};
|
||||
|
||||
const adapter = await createNip07Adapter();
|
||||
|
||||
// 2. Call nip04Encrypt - should fail or throw because it's missing
|
||||
await assert.rejects(async () => {
|
||||
await adapter.nip04Encrypt('pubkey', 'message');
|
||||
}, /NIP-04/); // Expect some error related to missing NIP-04
|
||||
|
||||
// 3. Inject
|
||||
let called = false;
|
||||
global.window.nostr.nip04 = {
|
||||
encrypt: async () => { called = true; return 'encrypted'; },
|
||||
decrypt: async () => {}
|
||||
};
|
||||
|
||||
// 4. Call again - should succeed
|
||||
const result = await adapter.nip04Encrypt('pubkey', 'message');
|
||||
assert.strictEqual(result, 'encrypted');
|
||||
assert.strictEqual(called, true);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user