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 { SHORT_TIMEOUT_MS } from "./constants.js";
|
||||||
import {
|
import {
|
||||||
getRegisteredNostrClient,
|
getRegisteredNostrClient,
|
||||||
|
getActiveSigner,
|
||||||
requestDefaultExtensionPermissions,
|
requestDefaultExtensionPermissions,
|
||||||
} from "./nostrClientRegistry.js";
|
} from "./nostrClientRegistry.js";
|
||||||
import { devLogger, userLogger } from "./utils/logger.js";
|
import { devLogger, userLogger } from "./utils/logger.js";
|
||||||
@@ -574,8 +575,7 @@ async function fetchLatestListEvent(filter, contextLabel = "admin-list") {
|
|||||||
pubkey,
|
pubkey,
|
||||||
dTag,
|
dTag,
|
||||||
relayUrls: relays,
|
relayUrls: relays,
|
||||||
// Force a full fetch: admin lists are replaceable (see
|
// Full fetch: admin lists are replaceable (relayBatchFetcherReplaceable.test).
|
||||||
// tests/nostr/relayBatchFetcherReplaceable.test.mjs).
|
|
||||||
since: 0,
|
since: 0,
|
||||||
}),
|
}),
|
||||||
timeoutPromise,
|
timeoutPromise,
|
||||||
@@ -682,8 +682,7 @@ async function loadCommunityBlacklistEntries() {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Batch all curator blacklists into ONE REQ (was the cold-start kind-30000 storm).
|
// Batch all curator blacklists into ONE REQ (best-effort; cache carries over on fail).
|
||||||
// Best-effort: on failure the cached admin state carries over.
|
|
||||||
try {
|
try {
|
||||||
const matched = await fetchBatchedReferenceEvents({
|
const matched = await fetchBatchedReferenceEvents({
|
||||||
references,
|
references,
|
||||||
@@ -874,20 +873,21 @@ async function persistNostrState(actorNpub, updates = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ensureNostrReady();
|
ensureNostrReady();
|
||||||
const permissionResult = await requestDefaultExtensionPermissions();
|
// Sign with the ACTIVE signer (extension / nsec / NIP-46), not window.nostr.
|
||||||
if (!permissionResult.ok) {
|
const signer = getActiveSigner();
|
||||||
throw createError(
|
if (!signer || typeof signer.signEvent !== "function") {
|
||||||
"extension-permission-denied",
|
throw createError("nostr-signer-missing", "No active signer to update admin lists. Please log in.");
|
||||||
"The NIP-07 extension must allow signing before updating admin lists.",
|
|
||||||
permissionResult.error,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
const extension = window?.nostr;
|
// Only a NIP-07 extension needs an explicit permission prompt before signing.
|
||||||
if (!extension || typeof extension.signEvent !== "function") {
|
if (signer.type === "extension" || signer.type === "nip07") {
|
||||||
throw createError(
|
const permissionResult = await requestDefaultExtensionPermissions();
|
||||||
"nostr-extension-missing",
|
if (!permissionResult.ok) {
|
||||||
"A Nostr extension with signEvent support is required."
|
throw createError(
|
||||||
);
|
"extension-permission-denied",
|
||||||
|
"The NIP-07 extension must allow signing before updating admin lists.",
|
||||||
|
permissionResult.error,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const actorHex = decodeNpubToHex(actorNpub);
|
const actorHex = decodeNpubToHex(actorNpub);
|
||||||
@@ -950,7 +950,7 @@ async function persistNostrState(actorNpub, updates = {}) {
|
|||||||
|
|
||||||
let signedEvent;
|
let signedEvent;
|
||||||
try {
|
try {
|
||||||
signedEvent = await extension.signEvent(event);
|
signedEvent = await signer.signEvent(event);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw createError("signature-failed", "Failed to sign admin list event.", 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 ROOT = new URL("..", import.meta.url).pathname.replace(/\/$/, "");
|
||||||
const SOURCE_DIR = join(ROOT, "js");
|
const SOURCE_DIR = join(ROOT, "js");
|
||||||
const THRESHOLD = 1000; // lines — new files must stay under this
|
const THRESHOLD = 1200; // lines — new files must stay under this (soft ceiling)
|
||||||
const GROWTH_MARGIN = 50; // lines — allowlisted files may grow by this much for minor fixes
|
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.
|
// Grandfathered files: path relative to repo root → line count when recorded.
|
||||||
// These files are known to be oversized and are decomposition targets.
|
// 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",
|
"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");
|
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
|
Build + lint clean; affected suites green. NOTE: `npm run build`/`lint` do NOT
|
||||||
parse JS — verify migrations by importing the module / running its test.
|
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
|
### 8. Orphan storage garbage-collection tool
|
||||||
- [x] **Largely delivered by the My Videos tab** (`9d3a0df0`): lists bucket objects no
|
- [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).
|
live note references and offers per-file delete (under the user's prefix only).
|
||||||
|
|||||||
Reference in new issue
Block a user