mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-06 04:42:57 +00:00
Merge pull request #699 from PR0M3TH3AN/codex/update-nostr-login-permissions-tests
Persist cached NIP-07 permissions and extend login tests
This commit is contained in:
2 files changed
+226
-19
No files matched your search
+84
-5
@@ -83,6 +83,76 @@ function getEnableVariantTimeoutMs() {
|
||||
|
||||
return DEFAULT_ENABLE_VARIANT_TIMEOUT_MS;
|
||||
}
|
||||
|
||||
function normalizePermissionMethod(method) {
|
||||
return typeof method === "string" && method.trim() ? method.trim() : "";
|
||||
}
|
||||
|
||||
function readStoredNip07Permissions() {
|
||||
if (typeof localStorage === "undefined" || !localStorage) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let rawValue;
|
||||
try {
|
||||
rawValue = localStorage.getItem(NIP07_PERMISSIONS_STORAGE_KEY);
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!rawValue) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(rawValue);
|
||||
const storedMethods = Array.isArray(parsed)
|
||||
? parsed
|
||||
: Array.isArray(parsed?.grantedMethods)
|
||||
? parsed.grantedMethods
|
||||
: Array.isArray(parsed?.methods)
|
||||
? parsed.methods
|
||||
: [];
|
||||
return storedMethods
|
||||
.map((method) => normalizePermissionMethod(method))
|
||||
.filter(Boolean);
|
||||
} catch (error) {
|
||||
try {
|
||||
localStorage.removeItem(NIP07_PERMISSIONS_STORAGE_KEY);
|
||||
} catch (cleanupError) {
|
||||
// ignore cleanup errors
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function writeStoredNip07Permissions(methods) {
|
||||
if (typeof localStorage === "undefined" || !localStorage) {
|
||||
return;
|
||||
}
|
||||
|
||||
const normalized = Array.from(
|
||||
new Set(
|
||||
Array.from(methods || [])
|
||||
.map((method) => normalizePermissionMethod(method))
|
||||
.filter(Boolean),
|
||||
),
|
||||
);
|
||||
|
||||
try {
|
||||
if (!normalized.length) {
|
||||
localStorage.removeItem(NIP07_PERMISSIONS_STORAGE_KEY);
|
||||
return;
|
||||
}
|
||||
|
||||
localStorage.setItem(
|
||||
NIP07_PERMISSIONS_STORAGE_KEY,
|
||||
JSON.stringify({ grantedMethods: normalized }),
|
||||
);
|
||||
} catch (error) {
|
||||
// ignore persistence failures
|
||||
}
|
||||
}
|
||||
const SESSION_ACTOR_STORAGE_KEY = "bitvid:sessionActor:v1";
|
||||
const VIEW_EVENT_GUARD_PREFIX = "bitvid:viewed";
|
||||
const REBROADCAST_GUARD_PREFIX = "bitvid:rebroadcast:v1";
|
||||
@@ -93,12 +163,15 @@ const WATCH_HISTORY_REPUBLISH_BASE_DELAY_MS = 2000;
|
||||
const WATCH_HISTORY_REPUBLISH_MAX_DELAY_MS = 5 * 60 * 1000;
|
||||
const WATCH_HISTORY_REPUBLISH_MAX_ATTEMPTS = 8;
|
||||
const WATCH_HISTORY_REPUBLISH_JITTER = 0.25;
|
||||
const NIP07_PERMISSIONS_STORAGE_KEY = "bitvid:nip07:permissions";
|
||||
|
||||
export const DEFAULT_NIP07_PERMISSION_METHODS = Object.freeze([
|
||||
"get_public_key",
|
||||
"sign_event",
|
||||
"nip04.encrypt",
|
||||
"nip04.decrypt",
|
||||
"nip44.encrypt",
|
||||
"nip44.decrypt",
|
||||
"read_relays",
|
||||
"write_relays",
|
||||
]);
|
||||
@@ -3066,7 +3139,7 @@ export class NostrClient {
|
||||
this.watchHistoryLastCreatedAt = 0;
|
||||
this.countRequestCounter = 0;
|
||||
this.countUnsupportedRelays = new Set();
|
||||
this.extensionPermissionCache = new Set();
|
||||
this.extensionPermissionCache = new Set(readStoredNip07Permissions());
|
||||
}
|
||||
|
||||
markExtensionPermissions(methods = []) {
|
||||
@@ -3078,14 +3151,20 @@ export class NostrClient {
|
||||
return;
|
||||
}
|
||||
|
||||
let didChange = false;
|
||||
for (const method of methods) {
|
||||
if (typeof method !== "string") {
|
||||
const normalized = normalizePermissionMethod(method);
|
||||
if (!normalized) {
|
||||
continue;
|
||||
}
|
||||
const normalized = method.trim();
|
||||
if (normalized) {
|
||||
this.extensionPermissionCache.add(normalized);
|
||||
if (!this.extensionPermissionCache.has(normalized)) {
|
||||
didChange = true;
|
||||
}
|
||||
this.extensionPermissionCache.add(normalized);
|
||||
}
|
||||
|
||||
if (didChange) {
|
||||
writeStoredNip07Permissions(this.extensionPermissionCache);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,24 @@ import "./test-helpers/setup-localstorage.mjs";
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { nostrClient, __testExports } from "../js/nostr.js";
|
||||
import { NostrClient, nostrClient, __testExports } from "../js/nostr.js";
|
||||
import { accessControl } from "../js/accessControl.js";
|
||||
|
||||
const { runNip07WithRetry } = __testExports ?? {};
|
||||
|
||||
const HEX_PUBKEY = "f".repeat(64);
|
||||
const PERMISSIONS_STORAGE_KEY = "bitvid:nip07:permissions";
|
||||
|
||||
function clearStoredPermissions() {
|
||||
if (typeof localStorage === "undefined" || !localStorage) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
localStorage.removeItem(PERMISSIONS_STORAGE_KEY);
|
||||
} catch (error) {
|
||||
// Ignore storage cleanup issues in tests
|
||||
}
|
||||
}
|
||||
|
||||
function setupLoginEnvironment({ enableImpl, getPublicKey = HEX_PUBKEY } = {}) {
|
||||
const previousWindow = typeof global.window === "undefined" ? undefined : global.window;
|
||||
@@ -116,6 +128,7 @@ function setupLoginEnvironment({ enableImpl, getPublicKey = HEX_PUBKEY } = {}) {
|
||||
}
|
||||
|
||||
test("NIP-07 login requests decrypt permissions upfront", async () => {
|
||||
clearStoredPermissions();
|
||||
const env = setupLoginEnvironment();
|
||||
try {
|
||||
const pubkey = await nostrClient.login();
|
||||
@@ -133,13 +146,100 @@ test("NIP-07 login requests decrypt permissions upfront", async () => {
|
||||
nostrClient.extensionPermissionCache.has("sign_event"),
|
||||
"sign_event permission should be tracked as granted",
|
||||
);
|
||||
assert.ok(
|
||||
nostrClient.extensionPermissionCache.has("nip44.encrypt"),
|
||||
"nip44.encrypt permission should be tracked as granted",
|
||||
);
|
||||
assert.ok(
|
||||
nostrClient.extensionPermissionCache.has("nip44.decrypt"),
|
||||
"nip44.decrypt permission should be tracked as granted",
|
||||
);
|
||||
} finally {
|
||||
env.restore();
|
||||
nostrClient.logout();
|
||||
clearStoredPermissions();
|
||||
}
|
||||
});
|
||||
|
||||
test("NIP-07 decrypt reuses cached extension permissions", async () => {
|
||||
clearStoredPermissions();
|
||||
const env = setupLoginEnvironment();
|
||||
try {
|
||||
let decryptCalls = 0;
|
||||
window.nostr.nip04 = {
|
||||
decrypt: async (actorKey, ciphertext) => {
|
||||
decryptCalls += 1;
|
||||
assert.equal(actorKey, HEX_PUBKEY);
|
||||
return `plaintext:${ciphertext}`;
|
||||
},
|
||||
};
|
||||
|
||||
const pubkey = await nostrClient.login();
|
||||
assert.equal(pubkey, HEX_PUBKEY);
|
||||
|
||||
const storedRaw = localStorage.getItem(PERMISSIONS_STORAGE_KEY);
|
||||
assert.ok(storedRaw, "extension permissions should persist to localStorage");
|
||||
let storedMethods = [];
|
||||
try {
|
||||
const parsed = JSON.parse(storedRaw);
|
||||
storedMethods = Array.isArray(parsed)
|
||||
? parsed
|
||||
: Array.isArray(parsed?.grantedMethods)
|
||||
? parsed.grantedMethods
|
||||
: [];
|
||||
} catch (error) {
|
||||
storedMethods = [];
|
||||
}
|
||||
const storedSet = new Set(
|
||||
storedMethods
|
||||
.filter((method) => typeof method === "string")
|
||||
.map((method) => method.trim())
|
||||
.filter(Boolean),
|
||||
);
|
||||
assert.ok(
|
||||
storedSet.has("nip04.decrypt"),
|
||||
"stored permissions should include nip04.decrypt",
|
||||
);
|
||||
assert.ok(
|
||||
storedSet.has("nip44.decrypt"),
|
||||
"stored permissions should include nip44.decrypt",
|
||||
);
|
||||
|
||||
const baselineEnableCalls = env.enableCalls.length;
|
||||
const freshClient = new NostrClient();
|
||||
freshClient.pubkey = HEX_PUBKEY;
|
||||
|
||||
assert.ok(
|
||||
freshClient.extensionPermissionCache.has("nip04.decrypt"),
|
||||
"fresh client should hydrate nip04.decrypt from storage",
|
||||
);
|
||||
assert.ok(
|
||||
freshClient.extensionPermissionCache.has("nip44.decrypt"),
|
||||
"fresh client should hydrate nip44.decrypt from storage",
|
||||
);
|
||||
|
||||
const permissionResult = await freshClient.ensureExtensionPermissions([
|
||||
"nip04.decrypt",
|
||||
]);
|
||||
assert.equal(permissionResult.ok, true);
|
||||
assert.equal(
|
||||
env.enableCalls.length,
|
||||
baselineEnableCalls,
|
||||
"cached permissions should prevent extra enable() calls",
|
||||
);
|
||||
|
||||
const plaintext = await window.nostr.nip04.decrypt(HEX_PUBKEY, "ciphertext");
|
||||
assert.equal(plaintext, "plaintext:ciphertext");
|
||||
assert.equal(decryptCalls, 1);
|
||||
} finally {
|
||||
env.restore();
|
||||
nostrClient.logout();
|
||||
clearStoredPermissions();
|
||||
}
|
||||
});
|
||||
|
||||
test("NIP-07 login falls back when structured permissions fail", async () => {
|
||||
clearStoredPermissions();
|
||||
const env = setupLoginEnvironment({
|
||||
enableImpl(options) {
|
||||
if (
|
||||
@@ -160,28 +260,34 @@ test("NIP-07 login falls back when structured permissions fail", async () => {
|
||||
try {
|
||||
const pubkey = await nostrClient.login();
|
||||
assert.equal(pubkey, HEX_PUBKEY);
|
||||
assert.equal(env.enableCalls.length >= 3, true, "should retry with alternate payloads");
|
||||
const [firstCall, secondCall, thirdCall] = env.enableCalls;
|
||||
assert.equal(firstCall, undefined, "first attempt should try plain enable() call");
|
||||
assert.ok(Array.isArray(secondCall?.permissions));
|
||||
assert.ok(env.enableCalls.length >= 2, "should retry with alternate payloads");
|
||||
const [objectCall, stringCall, plainCall] = env.enableCalls;
|
||||
assert.ok(Array.isArray(objectCall?.permissions));
|
||||
assert.equal(
|
||||
typeof secondCall.permissions[0],
|
||||
typeof objectCall.permissions[0],
|
||||
"object",
|
||||
"second attempt should use structured permission objects",
|
||||
"structured permission request should be attempted",
|
||||
);
|
||||
assert.ok(Array.isArray(thirdCall?.permissions));
|
||||
assert.ok(Array.isArray(stringCall?.permissions));
|
||||
assert.equal(
|
||||
typeof thirdCall.permissions[0],
|
||||
typeof stringCall.permissions[0],
|
||||
"string",
|
||||
"third attempt should fall back to string permissions",
|
||||
"string-based permissions should be attempted after structured payloads",
|
||||
);
|
||||
assert.equal(
|
||||
plainCall,
|
||||
undefined,
|
||||
"plain enable() should only be attempted if prior payloads fail",
|
||||
);
|
||||
} finally {
|
||||
env.restore();
|
||||
nostrClient.logout();
|
||||
clearStoredPermissions();
|
||||
}
|
||||
});
|
||||
|
||||
test("NIP-07 login supports extensions that only allow enable() without payload", async () => {
|
||||
clearStoredPermissions();
|
||||
const env = setupLoginEnvironment({
|
||||
enableImpl(options) {
|
||||
if (options !== undefined) {
|
||||
@@ -194,11 +300,29 @@ test("NIP-07 login supports extensions that only allow enable() without payload"
|
||||
try {
|
||||
const pubkey = await nostrClient.login();
|
||||
assert.equal(pubkey, HEX_PUBKEY);
|
||||
assert.equal(env.enableCalls.length, 1, "should not need alternate payloads when plain enable works");
|
||||
assert.equal(env.enableCalls[0], undefined, "plain enable should be attempted first");
|
||||
assert.equal(
|
||||
env.enableCalls.length,
|
||||
3,
|
||||
"should fall back to plain enable() after payload rejections",
|
||||
);
|
||||
const [objectCall, stringCall, plainCall] = env.enableCalls;
|
||||
assert.ok(Array.isArray(objectCall?.permissions));
|
||||
assert.equal(
|
||||
typeof objectCall.permissions[0],
|
||||
"object",
|
||||
"structured permissions should be attempted before falling back",
|
||||
);
|
||||
assert.ok(Array.isArray(stringCall?.permissions));
|
||||
assert.equal(
|
||||
typeof stringCall.permissions[0],
|
||||
"string",
|
||||
"string permissions should be attempted before plain enable()",
|
||||
);
|
||||
assert.equal(plainCall, undefined, "final attempt should use plain enable()");
|
||||
} finally {
|
||||
env.restore();
|
||||
nostrClient.logout();
|
||||
clearStoredPermissions();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -206,6 +330,7 @@ test("NIP-07 login quickly retries when a permission payload stalls", async () =
|
||||
const originalOverride = global.__BITVID_NIP07_ENABLE_VARIANT_TIMEOUT_MS__;
|
||||
global.__BITVID_NIP07_ENABLE_VARIANT_TIMEOUT_MS__ = 80;
|
||||
|
||||
clearStoredPermissions();
|
||||
const env = setupLoginEnvironment({
|
||||
enableImpl(options) {
|
||||
if (options === undefined) {
|
||||
@@ -232,8 +357,8 @@ test("NIP-07 login quickly retries when a permission payload stalls", async () =
|
||||
duration < 500,
|
||||
`login should fall back quickly from stalled enable payloads (duration: ${duration}ms)`,
|
||||
);
|
||||
assert.equal(env.enableCalls.length, 3, "should attempt all payload variants");
|
||||
const [, objectCall, stringCall] = env.enableCalls;
|
||||
assert.equal(env.enableCalls.length, 2, "should attempt structured and string payload variants");
|
||||
const [objectCall, stringCall] = env.enableCalls;
|
||||
assert.ok(Array.isArray(objectCall?.permissions));
|
||||
assert.equal(typeof objectCall.permissions[0], "object");
|
||||
assert.ok(Array.isArray(stringCall?.permissions));
|
||||
@@ -246,10 +371,12 @@ test("NIP-07 login quickly retries when a permission payload stalls", async () =
|
||||
} else {
|
||||
global.__BITVID_NIP07_ENABLE_VARIANT_TIMEOUT_MS__ = originalOverride;
|
||||
}
|
||||
clearStoredPermissions();
|
||||
}
|
||||
});
|
||||
|
||||
test("NIP-07 login surfaces enable permission errors", async () => {
|
||||
clearStoredPermissions();
|
||||
const env = setupLoginEnvironment({
|
||||
enableImpl() {
|
||||
return Promise.reject(new Error("permission denied"));
|
||||
@@ -261,6 +388,7 @@ test("NIP-07 login surfaces enable permission errors", async () => {
|
||||
} finally {
|
||||
env.restore();
|
||||
nostrClient.logout();
|
||||
clearStoredPermissions();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user