This commit is contained in:
thePR0M3TH3AN committed 2026-02-21 15:53:09 -05:00
1 parent 00f96bbe0e
commit 135dbe4e37
4 files changed
+153 -15

No files matched your search

+14 -2
View File
@@ -549,7 +549,12 @@ export class SignerManager {
throw new Error("Please install a Nostr extension (Alby, nos2x, etc.).");
}
const { allowAccountSelection = false, expectPubkey } =
const {
allowAccountSelection = false,
expectPubkey,
getPublicKeyTimeoutMs,
getPublicKeyRetryMultiplier,
} =
typeof options === "object" && options !== null ? options : {};
const normalizedExpectedPubkey =
typeof expectPubkey === "string" && expectPubkey.trim()
@@ -576,7 +581,14 @@ export class SignerManager {
throw denialError;
}
const pubkey = await extension.getPublicKey();
const pubkey = await runNip07WithRetry(
() => extension.getPublicKey(),
{
label: "extension.getPublicKey",
timeoutMs: getPublicKeyTimeoutMs,
retryMultiplier: getPublicKeyRetryMultiplier,
},
);
if (!pubkey) {
throw new Error("Extension did not return a public key.");
}
+63 -10
View File
@@ -318,9 +318,8 @@ export async function requestEnablePermissions(
return { ok: false, error: new Error("extension-unavailable") };
}
if (typeof extension.enable !== "function") {
return { ok: true, code: "enable-unavailable" };
}
const hasRequestPermissions = typeof extension.requestPermissions === "function";
const hasEnable = typeof extension.enable === "function";
const normalized = Array.isArray(outstandingMethods)
? outstandingMethods
@@ -328,16 +327,70 @@ export async function requestEnablePermissions(
.filter(Boolean)
: [];
const permissionVariants = [];
if (normalized.length) {
permissionVariants.push({
permissions: normalized.map((method) => ({ method })),
});
permissionVariants.push({ permissions: normalized });
const permissionVariants = (() => {
const variants = [];
if (normalized.length) {
variants.push({
permissions: normalized.map((method) => ({ method })),
});
variants.push({ permissions: normalized });
variants.push(normalized);
}
variants.push(null);
return variants;
})();
if (!hasRequestPermissions && !hasEnable) {
return { ok: true, code: "enable-unavailable" };
}
permissionVariants.push(null);
let lastError = null;
if (hasRequestPermissions) {
for (const options of permissionVariants) {
const variantTimeoutOverrides = options
? {
timeoutMs: Math.min(3000, getEnableVariantTimeoutMs()),
retryMultiplier: 1,
}
: {
timeoutMs: Math.min(
NIP07_LOGIN_TIMEOUT_MS,
getEnableVariantTimeoutMs(),
),
retryMultiplier: 1,
};
try {
await runNip07WithRetry(
() =>
options === null
? extension.requestPermissions()
: extension.requestPermissions(options),
{
label: "extension.requestPermissions",
...variantTimeoutOverrides,
},
);
return { ok: true };
} catch (error) {
lastError = error;
if (options && isDevMode) {
userLogger.warn(
"[nostr] extension.requestPermissions call failed:",
error,
);
}
}
}
}
if (!hasEnable) {
return {
ok: false,
error: lastError || new Error("permission-denied"),
};
}
for (const options of permissionVariants) {
// If specific permissions are requested (variants 1 & 2), fail fast (3s)
// to avoid hanging if the extension doesn't support the structured format.
+39 -2
View File
@@ -392,8 +392,14 @@ describe("NIP-07 Login Permissions", () => {
const [objectCall, stringCall, plainCall] = env.enableCalls;
assert.ok(objectCall !== undefined);
assert.ok(stringCall !== undefined);
// plainCall is the one that succeeded (called with undefined/no args)
assert.equal(plainCall, undefined, "successful attempt should use plain enable()");
// Success may come from requestPermissions() (array payload) or fallback enable().
assert.ok(
plainCall === undefined ||
(Array.isArray(plainCall) &&
plainCall.includes("get_public_key") &&
plainCall.includes("sign_event")),
"successful attempt should use a plain permission payload or plain enable()",
);
} finally {
env.restore();
nostrClient.logout();
@@ -455,6 +461,37 @@ describe("NIP-07 Login Permissions", () => {
}
});
it("NIP-07 login times out instead of hanging when getPublicKey stalls", async () => {
clearStoredPermissions();
const env = setupLoginEnvironment({
getPublicKey: new Promise(() => {}),
});
try {
const start = Date.now();
await assert.rejects(
() =>
nip07Provider.login({
nostrClient,
options: {
getPublicKeyTimeoutMs: 80,
getPublicKeyRetryMultiplier: 1,
},
}),
/Timed out waiting for the NIP-07 extension/,
);
const duration = Date.now() - start;
assert.ok(
duration < 1000,
`getPublicKey timeout should fail quickly (duration: ${duration}ms)`,
);
} finally {
env.restore();
nostrClient.logout();
clearStoredPermissions();
}
});
it("NIP-07 login does not wait for deferred permission grants", async () => {
clearStoredPermissions();
const env = setupLoginEnvironment();
+37 -1
View File
@@ -64,11 +64,47 @@ test("requestEnablePermissions retries explicit and fallback variants", async ()
assert.deepStrictEqual(calls, [
{ permissions: [{ method: "sign_event" }] },
{ permissions: ["sign_event"] },
undefined,
["sign_event"],
]);
assert.deepStrictEqual(result, { ok: true });
});
test("requestEnablePermissions supports requestPermissions-only extensions", async () => {
clearStoredNip07Permissions();
const calls = [];
const extension = {
requestPermissions: (options) => {
calls.push(options);
return Promise.resolve({ granted: true });
},
};
const result = await requestEnablePermissions(
extension,
["sign_event"],
{ isDevMode: false },
);
assert.deepStrictEqual(result, { ok: true });
assert.deepStrictEqual(calls, [{ permissions: [{ method: "sign_event" }] }]);
});
test("requestEnablePermissions fails when requestPermissions exists but denies", async () => {
clearStoredNip07Permissions();
const extension = {
requestPermissions: () => Promise.reject(new Error("permission denied")),
};
const result = await requestEnablePermissions(
extension,
["sign_event"],
{ isDevMode: false },
);
assert.strictEqual(result.ok, false);
assert.ok(result.error instanceof Error);
});
test("requestEnablePermissions reports unavailable extension", async () => {
clearStoredNip07Permissions();
const result = await requestEnablePermissions(null, ["sign_event"]);