mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
[admin] fix whitelist/blacklist edits with nsec & NIP-46; loosen file-size limits
Admin list edits worked with a NIP-07 extension but silently failed with nsec and NIP-46 (Amber). persistNostrState signed with window.nostr.signEvent directly (and required requestDefaultExtensionPermissions), so non-extension logins threw "nostr-extension-missing". Fix: sign with the ACTIVE signer (getActiveSigner() from the registry — extension/nsec/ NIP-46 all expose signEvent), and only run the extension-permission prompt when the signer is an extension (type extension/nip07). Regression test in tests/admin-list-store.test.mjs (nsec signer used while window.nostr.signEvent throws). Also (per maintainer request): raise the file-size lint margins to stop churning on an arbitrary cap — THRESHOLD 1000→1200 (new files), GROWTH_MARGIN 50→200 (grandfathered). TODOs added: #42 follow-up audit of every signing path (must use the active signer, not window.nostr); #43 logged-in profile card at the top of the profile modal (half-on/half- off); #44 in-modal storage selector + unlock for the upload modal. Build + lint clean; admin-list suite green. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
ad83c9f8db
commit
c48083dfcc
4 files changed
+83
-20
No files matched your search
+18
-18
@@ -12,6 +12,7 @@ import {
|
||||
import { SHORT_TIMEOUT_MS } from "./constants.js";
|
||||
import {
|
||||
getRegisteredNostrClient,
|
||||
getActiveSigner,
|
||||
requestDefaultExtensionPermissions,
|
||||
} from "./nostrClientRegistry.js";
|
||||
import { devLogger, userLogger } from "./utils/logger.js";
|
||||
@@ -574,8 +575,7 @@ async function fetchLatestListEvent(filter, contextLabel = "admin-list") {
|
||||
pubkey,
|
||||
dTag,
|
||||
relayUrls: relays,
|
||||
// Force a full fetch: admin lists are replaceable (see
|
||||
// tests/nostr/relayBatchFetcherReplaceable.test.mjs).
|
||||
// Full fetch: admin lists are replaceable (relayBatchFetcherReplaceable.test).
|
||||
since: 0,
|
||||
}),
|
||||
timeoutPromise,
|
||||
@@ -682,8 +682,7 @@ async function loadCommunityBlacklistEntries() {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Batch all curator blacklists into ONE REQ (was the cold-start kind-30000 storm).
|
||||
// Best-effort: on failure the cached admin state carries over.
|
||||
// Batch all curator blacklists into ONE REQ (best-effort; cache carries over on fail).
|
||||
try {
|
||||
const matched = await fetchBatchedReferenceEvents({
|
||||
references,
|
||||
@@ -874,20 +873,21 @@ async function persistNostrState(actorNpub, updates = {}) {
|
||||
}
|
||||
|
||||
ensureNostrReady();
|
||||
const permissionResult = await requestDefaultExtensionPermissions();
|
||||
if (!permissionResult.ok) {
|
||||
throw createError(
|
||||
"extension-permission-denied",
|
||||
"The NIP-07 extension must allow signing before updating admin lists.",
|
||||
permissionResult.error,
|
||||
);
|
||||
// Sign with the ACTIVE signer (extension / nsec / NIP-46), not window.nostr.
|
||||
const signer = getActiveSigner();
|
||||
if (!signer || typeof signer.signEvent !== "function") {
|
||||
throw createError("nostr-signer-missing", "No active signer to update admin lists. Please log in.");
|
||||
}
|
||||
const extension = window?.nostr;
|
||||
if (!extension || typeof extension.signEvent !== "function") {
|
||||
throw createError(
|
||||
"nostr-extension-missing",
|
||||
"A Nostr extension with signEvent support is required."
|
||||
);
|
||||
// Only a NIP-07 extension needs an explicit permission prompt before signing.
|
||||
if (signer.type === "extension" || signer.type === "nip07") {
|
||||
const permissionResult = await requestDefaultExtensionPermissions();
|
||||
if (!permissionResult.ok) {
|
||||
throw createError(
|
||||
"extension-permission-denied",
|
||||
"The NIP-07 extension must allow signing before updating admin lists.",
|
||||
permissionResult.error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const actorHex = decodeNpubToHex(actorNpub);
|
||||
@@ -950,7 +950,7 @@ async function persistNostrState(actorNpub, updates = {}) {
|
||||
|
||||
let signedEvent;
|
||||
try {
|
||||
signedEvent = await extension.signEvent(event);
|
||||
signedEvent = await signer.signEvent(event);
|
||||
} catch (error) {
|
||||
throw createError("signature-failed", "Failed to sign admin list event.", error);
|
||||
}
|
||||
|
||||
@@ -19,8 +19,8 @@ import { join, relative } from "node:path";
|
||||
|
||||
const ROOT = new URL("..", import.meta.url).pathname.replace(/\/$/, "");
|
||||
const SOURCE_DIR = join(ROOT, "js");
|
||||
const THRESHOLD = 1000; // lines — new files must stay under this
|
||||
const GROWTH_MARGIN = 50; // lines — allowlisted files may grow by this much for minor fixes
|
||||
const THRESHOLD = 1200; // lines — new files must stay under this (soft ceiling)
|
||||
const GROWTH_MARGIN = 200; // lines — allowlisted files may grow by this much for fixes
|
||||
|
||||
// Grandfathered files: path relative to repo root → line count when recorded.
|
||||
// These files are known to be oversized and are decomposition targets.
|
||||
|
||||
@@ -667,4 +667,32 @@ assert.ok(
|
||||
"should warn when blacklist updates fail on a relay",
|
||||
);
|
||||
|
||||
// Regression: admin list edits must sign with the ACTIVE signer (nsec / NIP-46), not
|
||||
// window.nostr — persistNostrState previously hardcoded the NIP-07 extension, so nsec
|
||||
// and NIP-46 logins couldn't edit the whitelist.
|
||||
let nsecSignerCalls = 0;
|
||||
globalThis.window.nostr.signEvent = async () => {
|
||||
throw new Error("window.nostr must NOT be used to sign admin lists");
|
||||
};
|
||||
setActiveSigner({
|
||||
pubkey: superHex,
|
||||
type: "nsec",
|
||||
signEvent: async (event) => {
|
||||
nsecSignerCalls += 1;
|
||||
return { ...event, id: "nsec-signed" };
|
||||
},
|
||||
});
|
||||
nostrClient.writeRelays = [relays[1]];
|
||||
setPublishBehaviors([{ url: relays[1], success: true }]);
|
||||
|
||||
await assert.doesNotReject(
|
||||
() =>
|
||||
persistAdminState(ADMIN_SUPER_NPUB, { whitelist: ["npub1nsecwhitelistmember"] }),
|
||||
"admin list edit must succeed with an nsec signer and no usable window.nostr",
|
||||
);
|
||||
assert.ok(
|
||||
nsecSignerCalls > 0,
|
||||
"should sign admin list updates with the active (nsec) signer, not window.nostr",
|
||||
);
|
||||
|
||||
console.log("admin-list-store tests passed");
|
||||
@@ -523,6 +523,41 @@ toward freshness and looked identical. Gave each a structural identity:
|
||||
Build + lint clean; affected suites green. NOTE: `npm run build`/`lint` do NOT
|
||||
parse JS — verify migrations by importing the module / running its test.
|
||||
|
||||
### 42. Admin whitelist edit fails with nsec / NIP-46 (only NIP-07 worked) — FIXED
|
||||
- [x] **DONE 2026-06-30.** Editing the admin whitelist/blacklist worked with a NIP-07
|
||||
extension but silently failed with nsec and NIP-46 (Amber). Root cause:
|
||||
`adminListStore.persistNostrState` signed with `window.nostr.signEvent` directly
|
||||
(and required `requestDefaultExtensionPermissions`), so non-extension logins threw
|
||||
"nostr-extension-missing". Fix: sign with the **active signer**
|
||||
(`getActiveSigner()` from the registry — works for extension/nsec/NIP-46, all expose
|
||||
`signEvent`), and only run the extension-permission prompt when the signer is an
|
||||
extension (`type === "extension"/"nip07"`). Regression test added to
|
||||
`tests/admin-list-store.test.mjs` (nsec signer used while `window.nostr.signEvent`
|
||||
throws). Build + lint clean.
|
||||
- [ ] **AUDIT (follow-up):** sweep EVERY place that signs an event and confirm it uses the
|
||||
active-signer path (`getActiveSigner().signEvent` / the client's sign helper), NOT
|
||||
`window.nostr` directly. `window.nostr`-only signing breaks nsec + NIP-46. Grep for
|
||||
`window.nostr`, `extension.signEvent`, and direct `signEvent` calls across publish
|
||||
paths (lists, reactions, reposts, comments, profile metadata, NWC, watch history,
|
||||
deletes, NIP-71 mirror, view events) and fix any that bypass the active signer.
|
||||
|
||||
### 43. Logged-in profile card at the top of the profile modal (UX)
|
||||
- [ ] Show the **logged-in user's profile photo + name** at the **top-center** of the
|
||||
profile modal, styled as a "profile card" that sits **half-on / half-off** the
|
||||
modal edge (overlapping the top border) for a polished look. Goal: users can see
|
||||
WHICH profile they're acting as (e.g. uploading as) at a glance. Pull avatar/name
|
||||
from the profile cache for the active pubkey; update on profile switch.
|
||||
|
||||
### 44. In-modal storage selector for uploads (UX)
|
||||
- [ ] Make storage unlock + selection easier from the **video upload modal**: let the
|
||||
user **pick any configured storage connection** (R2 / B2 / Custom S3) and unlock it
|
||||
**without leaving the upload modal** — today they must close it, open Profile →
|
||||
Storage, change the default/selected connection, then reopen upload. Add a provider
|
||||
picker (driven by `storageService.listConnections`) + inline unlock in the upload
|
||||
modal's storage section; selecting one sets it as the active upload target for this
|
||||
upload (and optionally the default). Reuse `loadFromStorage` / the per-provider
|
||||
connection model.
|
||||
|
||||
### 8. Orphan storage garbage-collection tool
|
||||
- [x] **Largely delivered by the My Videos tab** (`9d3a0df0`): lists bucket objects no
|
||||
live note references and offers per-file delete (under the user's prefix only).
|
||||
|
||||
Reference in new issue
Block a user