mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Merge pull request #378 from PR0M3TH3AN/codex/refactor-more-menu-into-controller-ax8cjy
Fix watch history decryption permissions
This commit is contained in:
2 files changed
+193
-10
No files matched your search
+112
-8
@@ -72,6 +72,15 @@ 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 DEFAULT_NIP07_PERMISSION_METHODS = Object.freeze([
|
||||
"get_public_key",
|
||||
"sign_event",
|
||||
"nip04.encrypt",
|
||||
"nip04.decrypt",
|
||||
"read_relays",
|
||||
"write_relays",
|
||||
]);
|
||||
|
||||
const viewEventPublishMemory = new Map();
|
||||
const rebroadcastAttemptMemory = new Map();
|
||||
|
||||
@@ -2886,6 +2895,99 @@ export class NostrClient {
|
||||
this.watchHistoryLastCreatedAt = 0;
|
||||
this.countRequestCounter = 0;
|
||||
this.countUnsupportedRelays = new Set();
|
||||
this.extensionPermissionCache = new Set();
|
||||
}
|
||||
|
||||
markExtensionPermissions(methods = []) {
|
||||
if (!this.extensionPermissionCache) {
|
||||
this.extensionPermissionCache = new Set();
|
||||
}
|
||||
|
||||
if (!Array.isArray(methods)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const method of methods) {
|
||||
if (typeof method !== "string") {
|
||||
continue;
|
||||
}
|
||||
const normalized = method.trim();
|
||||
if (normalized) {
|
||||
this.extensionPermissionCache.add(normalized);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async ensureExtensionPermissions(methods = DEFAULT_NIP07_PERMISSION_METHODS) {
|
||||
if (!Array.isArray(methods)) {
|
||||
methods = [];
|
||||
}
|
||||
|
||||
const normalized = Array.from(
|
||||
new Set(
|
||||
methods
|
||||
.map((method) =>
|
||||
typeof method === "string" && method.trim() ? method.trim() : "",
|
||||
)
|
||||
.filter(Boolean),
|
||||
),
|
||||
);
|
||||
|
||||
if (!normalized.length) {
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
const outstanding = normalized.filter((method) =>
|
||||
this.extensionPermissionCache ? !this.extensionPermissionCache.has(method) : true,
|
||||
);
|
||||
|
||||
if (!outstanding.length) {
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
const extension = typeof window !== "undefined" ? window.nostr : null;
|
||||
if (!extension) {
|
||||
return { ok: false, error: new Error("extension-unavailable") };
|
||||
}
|
||||
|
||||
if (typeof extension.enable !== "function") {
|
||||
this.markExtensionPermissions(outstanding);
|
||||
return { ok: true, code: "enable-unavailable" };
|
||||
}
|
||||
|
||||
const permissionVariants = [];
|
||||
if (outstanding.length) {
|
||||
permissionVariants.push({
|
||||
permissions: outstanding.map((method) => ({ method })),
|
||||
});
|
||||
permissionVariants.push({ permissions: outstanding });
|
||||
}
|
||||
permissionVariants.push(null);
|
||||
|
||||
let lastError = null;
|
||||
for (const options of permissionVariants) {
|
||||
try {
|
||||
await runNip07WithRetry(
|
||||
() => (options ? extension.enable(options) : extension.enable()),
|
||||
{ label: "extension.enable" },
|
||||
);
|
||||
this.markExtensionPermissions(outstanding);
|
||||
return { ok: true };
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
if (options && isDevMode) {
|
||||
console.warn(
|
||||
"[nostr] extension.enable request with explicit permissions failed:",
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ok: false,
|
||||
error: lastError || new Error("permission-denied"),
|
||||
};
|
||||
}
|
||||
|
||||
applyRootCreatedAt(video) {
|
||||
@@ -3813,6 +3915,10 @@ export class NostrClient {
|
||||
}
|
||||
}
|
||||
|
||||
if (useExtensionEncrypt) {
|
||||
await this.ensureExtensionPermissions(DEFAULT_NIP07_PERMISSION_METHODS);
|
||||
}
|
||||
|
||||
const canonicalItems = canonicalizeWatchHistoryItems(
|
||||
Array.isArray(rawItems) ? rawItems : [],
|
||||
WATCH_HISTORY_MAX_ITEMS,
|
||||
@@ -4590,6 +4696,7 @@ export class NostrClient {
|
||||
typeof extension?.nip04?.decrypt === "function" &&
|
||||
normalizeActorKey(this.pubkey) === actorKey;
|
||||
if (extensionDecrypt) {
|
||||
await this.ensureExtensionPermissions(DEFAULT_NIP07_PERMISSION_METHODS);
|
||||
return extension.nip04.decrypt(resolvedActor, ciphertext);
|
||||
}
|
||||
if (!this.sessionActor || this.sessionActor.pubkey !== actorKey) {
|
||||
@@ -5471,14 +5578,9 @@ export class NostrClient {
|
||||
if (isDevMode) {
|
||||
console.log("Requesting permissions from NIP-07 extension...");
|
||||
}
|
||||
const requestedPermissionMethods = [
|
||||
"get_public_key",
|
||||
"sign_event",
|
||||
"nip04.encrypt",
|
||||
"nip04.decrypt",
|
||||
"read_relays",
|
||||
"write_relays",
|
||||
];
|
||||
const requestedPermissionMethods = Array.from(
|
||||
DEFAULT_NIP07_PERMISSION_METHODS,
|
||||
);
|
||||
|
||||
const permissionVariants = [];
|
||||
const objectPermissions = requestedPermissionMethods
|
||||
@@ -5529,6 +5631,8 @@ export class NostrClient {
|
||||
: "The NIP-07 extension denied the permission request.",
|
||||
);
|
||||
}
|
||||
|
||||
this.markExtensionPermissions(requestedPermissionMethods);
|
||||
}
|
||||
|
||||
if (allowAccountSelection && typeof extension.selectAccounts === "function") {
|
||||
|
||||
@@ -7,6 +7,8 @@ const {
|
||||
WATCH_HISTORY_PAYLOAD_MAX_BYTES,
|
||||
WATCH_HISTORY_MAX_ITEMS,
|
||||
WATCH_HISTORY_CACHE_TTL_MS,
|
||||
WATCH_HISTORY_BATCH_RESOLVE,
|
||||
WATCH_HISTORY_BATCH_PAGE_SIZE,
|
||||
} =
|
||||
await import("../js/config.js");
|
||||
const {
|
||||
@@ -58,6 +60,14 @@ const originalScheduleWatchHistoryRepublish =
|
||||
const originalResolveWatchHistory = nostrClient.resolveWatchHistory;
|
||||
const originalGetWatchHistoryFingerprint =
|
||||
nostrClient.getWatchHistoryFingerprint;
|
||||
const originalExtensionPermissionCache = nostrClient.extensionPermissionCache;
|
||||
const originalExtensionPermissionSnapshot = Array.isArray(
|
||||
originalExtensionPermissionCache,
|
||||
)
|
||||
? Array.from(originalExtensionPermissionCache)
|
||||
: [];
|
||||
|
||||
nostrClient.extensionPermissionCache = new Set();
|
||||
|
||||
nostrClient.watchHistoryCache = new Map();
|
||||
nostrClient.watchHistoryStorage = {
|
||||
@@ -255,7 +265,12 @@ function installExtensionCrypto({ actor }) {
|
||||
let extensionEncrypts = 0;
|
||||
let extensionDecrypts = 0;
|
||||
let fallbackEncrypts = 0;
|
||||
const enableCalls = [];
|
||||
window.nostr = {
|
||||
enable: async (options) => {
|
||||
enableCalls.push(options || null);
|
||||
return { ok: true };
|
||||
},
|
||||
signEvent: async (event) => ({
|
||||
...event,
|
||||
id: `ext-${event.kind}-${event.created_at}`,
|
||||
@@ -304,6 +319,7 @@ function installExtensionCrypto({ actor }) {
|
||||
getExtensionEncrypts: () => extensionEncrypts,
|
||||
getExtensionDecrypts: () => extensionDecrypts,
|
||||
getFallbackEncrypts: () => fallbackEncrypts,
|
||||
getEnableCalls: () => [...enableCalls],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -552,6 +568,11 @@ async function testPublishSnapshotUsesExtensionCrypto() {
|
||||
result.items.length,
|
||||
"decrypted payload should match canonical item count",
|
||||
);
|
||||
assert.equal(
|
||||
extension.getEnableCalls().length,
|
||||
1,
|
||||
"extension permissions should be requested once before encrypting",
|
||||
);
|
||||
} finally {
|
||||
extension.restore();
|
||||
sessionRestore.restore();
|
||||
@@ -561,6 +582,43 @@ async function testPublishSnapshotUsesExtensionCrypto() {
|
||||
}
|
||||
}
|
||||
|
||||
async function testEnsureExtensionPermissionCaching() {
|
||||
const actor = "permission-actor";
|
||||
const extension = installExtensionCrypto({ actor });
|
||||
const previousCache = nostrClient.extensionPermissionCache;
|
||||
nostrClient.extensionPermissionCache = new Set();
|
||||
|
||||
try {
|
||||
const first = await nostrClient.ensureExtensionPermissions([
|
||||
"nip04.decrypt",
|
||||
]);
|
||||
assert.equal(
|
||||
first.ok,
|
||||
true,
|
||||
"ensureExtensionPermissions should resolve when extension is available",
|
||||
);
|
||||
assert.equal(
|
||||
extension.getEnableCalls().length > 0,
|
||||
true,
|
||||
"ensureExtensionPermissions should invoke extension.enable to request access",
|
||||
);
|
||||
|
||||
const callsAfterFirst = extension.getEnableCalls().length;
|
||||
const second = await nostrClient.ensureExtensionPermissions([
|
||||
"nip04.decrypt",
|
||||
]);
|
||||
assert.equal(second.ok, true, "cached permission requests should still resolve");
|
||||
assert.equal(
|
||||
extension.getEnableCalls().length,
|
||||
callsAfterFirst,
|
||||
"cached permission requests should not trigger duplicate enable calls",
|
||||
);
|
||||
} finally {
|
||||
extension.restore();
|
||||
nostrClient.extensionPermissionCache = previousCache;
|
||||
}
|
||||
}
|
||||
|
||||
async function testPublishSnapshotFailureRetry() {
|
||||
poolHarness.reset();
|
||||
nostrClient.watchHistoryLastCreatedAt = 0;
|
||||
@@ -852,10 +910,22 @@ async function testResolveWatchHistoryBatchingWindow() {
|
||||
forceRefresh: true,
|
||||
});
|
||||
|
||||
const batchPageSizeRaw = Number(WATCH_HISTORY_BATCH_PAGE_SIZE);
|
||||
const hasCustomBatchSize =
|
||||
Boolean(WATCH_HISTORY_BATCH_RESOLVE) &&
|
||||
Number.isFinite(batchPageSizeRaw) &&
|
||||
batchPageSizeRaw > 0;
|
||||
const expectedCount = hasCustomBatchSize
|
||||
? Math.min(
|
||||
Math.floor(batchPageSizeRaw),
|
||||
WATCH_HISTORY_MAX_ITEMS,
|
||||
syntheticItems.length,
|
||||
)
|
||||
: Math.min(WATCH_HISTORY_MAX_ITEMS, syntheticItems.length);
|
||||
assert.equal(
|
||||
resolved.length,
|
||||
Math.min(WATCH_HISTORY_MAX_ITEMS, syntheticItems.length),
|
||||
"batched resolve should return the full window when no page size override is configured",
|
||||
expectedCount,
|
||||
"batched resolve should honor the configured page size when batching is enabled",
|
||||
);
|
||||
assert(resolved.length > 1, "resolved history should include multiple entries");
|
||||
assert.equal(
|
||||
@@ -1321,6 +1391,7 @@ async function testWatchHistoryAppLoginFallback() {
|
||||
|
||||
await testPublishSnapshotCanonicalizationAndChunking();
|
||||
await testPublishSnapshotUsesExtensionCrypto();
|
||||
await testEnsureExtensionPermissionCaching();
|
||||
await testPublishSnapshotFailureRetry();
|
||||
await testWatchHistoryPartialRelayRetry();
|
||||
await testResolveWatchHistoryBatchingWindow();
|
||||
@@ -1351,6 +1422,14 @@ nostrClient.resolveWatchHistory = originalResolveWatchHistory;
|
||||
nostrClient.getWatchHistoryFingerprint =
|
||||
originalGetWatchHistoryFingerprint;
|
||||
|
||||
if (originalExtensionPermissionCache instanceof Set) {
|
||||
originalExtensionPermissionCache.clear();
|
||||
for (const method of originalExtensionPermissionSnapshot) {
|
||||
originalExtensionPermissionCache.add(method);
|
||||
}
|
||||
}
|
||||
nostrClient.extensionPermissionCache = originalExtensionPermissionCache;
|
||||
|
||||
if (!originalFlag) {
|
||||
setWatchHistoryV2Enabled(false);
|
||||
}
|
||||
Reference in new issue
Block a user