mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-03 11:22:25 +00:00
Merge pull request #2266 from PR0M3TH3AN/content-docs-audit-10961466235628399718
🌐 Align /content user docs with actual upload & contribution behavior
This commit is contained in:
10 files changed
+199
-150
No files matched your search
@@ -0,0 +1,69 @@
|
||||
# Validation Report
|
||||
|
||||
This report documents the verification of user-facing documentation in `/content` against the codebase.
|
||||
|
||||
## 1. Storage Configuration (CORS)
|
||||
|
||||
**Claim:** Users must configure CORS with specific headers to allow browser uploads.
|
||||
**Source:** `content/docs/guides/upload-content.md`
|
||||
**Code:** `js/storage/s3-multipart.js` (function `ensureBucketCors`)
|
||||
|
||||
**Verification:**
|
||||
The `ensureBucketCors` function hardcodes the following CORS rules:
|
||||
- `AllowedHeaders`: `["*"]` (Matches docs)
|
||||
- `AllowedMethods`: `["GET", "HEAD", "PUT", "POST", "DELETE", "OPTIONS"]` (Matches docs, order irrelevant)
|
||||
- `ExposeHeaders`: `["ETag", "Content-Length", "Content-Range", "Accept-Ranges"]` (Matches docs)
|
||||
- `MaxAgeSeconds`: `3600` (Matches docs)
|
||||
|
||||
The `AllowedOrigins` in code is dynamic (`allowedOrigins` variable), while the docs provide a concrete example (`["http://localhost:5500", "https://bitvid.network"]`) which is correct for users.
|
||||
|
||||
## 2. File Type Limits
|
||||
|
||||
**Claim:** Accepted file types are `.mp4, .webm, .mov, .mkv, .ts, .m3u8, .mpg, .mpeg`.
|
||||
**Source:** `content/docs/guides/upload-content.md`
|
||||
**Code:** `components/upload-modal.html`
|
||||
|
||||
**Verification:**
|
||||
The input element is defined as:
|
||||
```html
|
||||
<input id="input-file" type="file" class="hidden" accept="video/*,.m3u8,.ts,.mp4,.webm,.mov,.mkv,.mpg,.mpeg" />
|
||||
```
|
||||
The documentation matches the specific extensions listed in the code. The docs now also clarify that "other standard video formats" are accepted via `video/*`.
|
||||
|
||||
## 3. File Size Limit
|
||||
|
||||
**Claim:** Recommended limit is 2GB due to browser memory.
|
||||
**Source:** `content/docs/guides/upload-content.md`
|
||||
**Code:** `js/utils/torrentHash.js`
|
||||
|
||||
**Verification:**
|
||||
The client-side hashing logic uses `FileReader.readAsArrayBuffer(file)`. This loads the entire file into memory. Browser implementations (especially Chrome) typically crash or fail when allocating ArrayBuffers larger than ~2GB. This technical constraint confirms the documentation's warning.
|
||||
|
||||
## 4. Moderation (Whitelist Mode)
|
||||
|
||||
**Claim:** "Whitelist Mode" restricts visibility to approved accounts.
|
||||
**Source:** `content/docs/guides/upload-content.md` & `getting-started.md`
|
||||
**Code:** `js/accessControl.js`
|
||||
|
||||
**Verification:**
|
||||
The `AccessControl` class implements `canAccess(candidate)`:
|
||||
```javascript
|
||||
if (this.whitelistEnabled && !this.whitelistPubkeys.has(hex)) {
|
||||
return false;
|
||||
}
|
||||
```
|
||||
If `whitelistEnabled` is true (Whitelist Mode), non-whitelisted pubkeys return `false`, blocking their content from being displayed in feeds that use this check. This confirms the documentation.
|
||||
|
||||
## 5. Upload Lifecycle
|
||||
|
||||
**Claim:** Direct upload to bucket -> Client-side hashing -> NIP-71/Nostr publication.
|
||||
**Source:** `content/docs/guides/upload-content.md`
|
||||
**Code:** `js/ui/components/UploadModal.js`
|
||||
|
||||
**Verification:**
|
||||
`handleVideoSelection` in `UploadModal.js`:
|
||||
1. Calls `service.uploadFile` (Direct upload to R2/S3).
|
||||
2. Calls `generateTorrentMetadata` (Client-side hashing).
|
||||
3. Calls `publish` -> `publishVideoNote` (Nostr publication).
|
||||
|
||||
This flow matches the documentation exactly.
|
||||
@@ -14,7 +14,7 @@ Ready to jump in? Here's everything you need to know to start watching and shari
|
||||
|
||||
### Step 1: Set Up Your Account
|
||||
|
||||
> ⚠️ **Note:** We are currently invite-only. [Submit an application](https://bitvid.network/?modal=application) to request approval for posting content.
|
||||
> ⚠️ **Note:** We are currently invite-only (Whitelist Mode). While you can upload content, it will only appear in public feeds if your account has been approved. [Submit an application](https://bitvid.network/?modal=application) to request approval.
|
||||
|
||||
1. Install a [Nostr extension](https://nostrapps.com/#signers#all) (like Alby or Nos2x) in your browser.
|
||||
2. The extension creates your secure login key automatically.
|
||||
@@ -22,7 +22,7 @@ Ready to jump in? Here's everything you need to know to start watching and shari
|
||||
|
||||
### Step 2: Prepare Your Video
|
||||
|
||||
1. **Format:** We accept **.mp4, .webm, .mov, .mkv, .ts, .m3u8, .mpg, .mpeg** (Max 2GB recommended).
|
||||
1. **Format:** We accept **.mp4, .webm, .mov, .mkv, .ts, .m3u8, .mpg, .mpeg** (Max 2GB recommended). For detailed requirements and troubleshooting, see our [Upload Guide](guides/upload-content.md).
|
||||
2. **Hosting:** You can upload directly via bitvid (requires your own Cloudflare R2 or S3 bucket) or host your video elsewhere (CDN, Cloudflare Stream, etc.) and provide the HTTPS URL. For external hosting, ensure the format is streamable (MP4/WebM/HLS/DASH).
|
||||
3. **(Optional) WebTorrent:** If hosting externally, you can generate a WebTorrent magnet so viewers can fall back to peer-to-peer delivery.
|
||||
|
||||
|
||||
@@ -7,20 +7,22 @@ bitvid is a decentralized platform that supports multiple ways to share video co
|
||||
bitvid offers three ways to publish content:
|
||||
|
||||
1. **Direct Upload:** Upload a video file from your device to your configured storage bucket (R2 or S3). The client handles the upload, generates a magnet link for WebTorrent, and publishes the metadata to Nostr.
|
||||
2. **External Link:** Provide a direct URL to a video file hosted elsewhere (e.g., on a personal server or another CDN). This URL serves as the primary playback source.
|
||||
3. **Magnet Link:** assist the network by providing a magnet link for an existing torrent. Note that magnet-only uploads require at least one active seeder to be playable.
|
||||
2. **External Link:** Provide a direct URL to a video file hosted elsewhere (e.g., on a personal server or another CDN). This URL serves as the primary playback source. **Note:** External URLs must use **HTTPS**.
|
||||
3. **Magnet Link:** Assist the network by providing a magnet link for an existing torrent. Note that magnet-only uploads require at least one active seeder to be playable.
|
||||
|
||||
## Supported Media & Limits
|
||||
|
||||
Before you start, ensure your content meets the following requirements:
|
||||
|
||||
### Accepted File Types
|
||||
- **Video:** `.mp4`, `.webm`, `.mov`, `.mkv`, `.ts`, `.m3u8`, `.mpg`, `.mpeg`
|
||||
- **Thumbnail:** Any standard image format (`image/*`)
|
||||
- **Video:** `.mp4`, `.webm`, `.mov`, `.mkv`, `.ts`, `.m3u8`, `.mpg`, `.mpeg`.
|
||||
- **Thumbnail:** Any standard image format (`image/*`) supported by your browser.
|
||||
|
||||
> **Note:** The file picker restricts selection to these formats, but the backend handles standard video MIME types (`video/*`). Ensure your container format is supported by modern browsers for playback.
|
||||
|
||||
### File Size
|
||||
- **Recommended:** Up to **2GB** per file.
|
||||
- **Why?** Browser-based uploads rely on your device's memory for hashing and chunk management. Files larger than 2GB may cause browser instability or crashes.
|
||||
- **Why?** Browser-based uploads rely on your device's memory for hashing (to generate the WebTorrent info hash). Files larger than 2GB may cause browser instability or crashes depending on your available RAM.
|
||||
|
||||
## Metadata & Options
|
||||
|
||||
@@ -79,7 +81,7 @@ bitvid will attempt to configure CORS automatically if your API keys have "Admin
|
||||
}
|
||||
]
|
||||
```
|
||||
> **Note:** Replace `AllowedOrigins` with your actual origins. If you are using the official site, keep `https://bitvid.network`. If you are running a local instance, use `http://localhost:5500`.
|
||||
> **Note:** Replace `AllowedOrigins` with your actual origins. If you are using the official site, keep `https://bitvid.network`. If you are running a local instance, use `http://localhost:5500` or your custom domain.
|
||||
|
||||
**S3 Compatible Providers:**
|
||||
bitvid will attempt to configure CORS automatically for generic S3 providers if your credentials have sufficient permissions. However, if uploads fail with CORS errors, apply a similar policy manually in your provider's console.
|
||||
@@ -115,22 +117,23 @@ bitvid will verify your credentials by attempting to list or upload a test file.
|
||||
|
||||
### Common Issues
|
||||
|
||||
- **CORS Errors:** If you see "Network Error" or "CORS" in the console during upload, verify your bucket's CORS policy matches the JSON above. Ensure `AllowedHeaders` includes `*` or `amz-sdk-*`.
|
||||
- **Upload Fails Immediately:** Check your API credentials and ensure the bucket name is correct.
|
||||
- **CORS Errors ("Network Error"):** If uploads fail immediately or the console shows "CORS", verify your bucket's CORS policy matches the JSON above. Ensure `AllowedHeaders` includes `*` and `ExposeHeaders` lists `ETag`.
|
||||
- **Permission Errors ("Access Denied"):** Check your API credentials. Ensure the token has `Object Read & Write` permissions (specifically `s3:PutObject` and `s3:DeleteObject`).
|
||||
- **Browser Crashes / Slow Performance:** Large files (>2GB) can exhaust browser memory during the hashing process. Try using a smaller file or ensuring you have plenty of free RAM.
|
||||
- **Playback Issues:** If the video uploads but doesn't play, ensure the "Public Access URL" is correct and publicly reachable.
|
||||
- **Playback Issues:** If the video uploads but doesn't play, ensure the "Public Access URL" is correct and publicly reachable. Test the URL directly in a browser.
|
||||
- **Protocol Errors:** Ensure all external video URLs use `https://`. HTTP is not supported for security reasons.
|
||||
|
||||
## Upload Lifecycle & Moderation
|
||||
|
||||
### How Uploads Work
|
||||
1. **Direct Upload:** Your browser uploads the file directly to your storage bucket. No video data passes through a bitvid server.
|
||||
2. **Client-Side Hashing:** Your browser calculates a cryptographic hash (info hash) of the file to enable WebTorrent support.
|
||||
3. **Publication**: The video metadata (title, URL, hash, tags) is signed by your Nostr key and published to relays.
|
||||
2. **Client-Side Hashing:** Your browser calculates a cryptographic hash (info hash) of the file locally to enable WebTorrent support.
|
||||
3. **Publication:** The video metadata (title, URL, hash, tags) is signed by your Nostr key and published to relays.
|
||||
|
||||
### Moderation & Visibility
|
||||
While publication is decentralized and permissionless, the bitvid.network instance may enforce moderation policies:
|
||||
- **Whitelists**: If the instance is in "whitelist mode", you may need approval before your videos appear in public feeds.
|
||||
- **Blacklists**: Violating community guidelines may result in your account being hidden from this instance.
|
||||
- **User Blocks**: Viewers can mute or block your content individually.
|
||||
While publication is decentralized and permissionless, individual bitvid instances (clients) may enforce moderation policies:
|
||||
- **Whitelists ("Invite-only"):** If the instance is in "whitelist mode", your videos will only appear in public feeds if your account has been approved by an admin. You can still share direct links, but discovery is restricted.
|
||||
- **Blacklists:** Violating community guidelines may result in your account being hidden from this instance.
|
||||
- **User Blocks:** Viewers can mute or block your content individually.
|
||||
|
||||
Check the [Community Guidelines](../community/community-guidelines.md) for more details.
|
||||
+54
-53
@@ -218,7 +218,7 @@ import {
|
||||
SignerManager,
|
||||
resolveSignerCapabilities,
|
||||
hydrateExtensionSignerCapabilities,
|
||||
attachNipMethodAliases,
|
||||
attachNipMethodAliases
|
||||
} from "./managers/SignerManager.js";
|
||||
import { RelayBatchFetcher } from "./relayBatchFetcher.js";
|
||||
|
||||
@@ -237,35 +237,55 @@ function normalizeProfileFromEvent(event) {
|
||||
|
||||
function resolveActiveSigner(pubkey) {
|
||||
const signer = resolveActiveSignerFromRegistry(pubkey);
|
||||
if (signer) {
|
||||
hydrateExtensionSignerCapabilities(signer);
|
||||
attachNipMethodAliases(signer);
|
||||
if (!signer.capabilities && resolveSignerCapabilities) {
|
||||
const capsDescriptor = Object.getOwnPropertyDescriptor(signer, "capabilities");
|
||||
if (!capsDescriptor || typeof capsDescriptor.get !== "function") {
|
||||
signer.capabilities = resolveSignerCapabilities(signer);
|
||||
}
|
||||
hydrateExtensionSignerCapabilities(signer);
|
||||
attachNipMethodAliases(signer);
|
||||
if (signer && typeof signer === "object") {
|
||||
const capsDescriptor = Object.getOwnPropertyDescriptor(
|
||||
signer,
|
||||
"capabilities",
|
||||
);
|
||||
const isGetter = capsDescriptor && typeof capsDescriptor.get === "function";
|
||||
|
||||
if (!isGetter) {
|
||||
signer.capabilities = resolveSignerCapabilities(signer);
|
||||
}
|
||||
}
|
||||
return signer;
|
||||
}
|
||||
|
||||
function setActiveSigner(signer) {
|
||||
if (signer) {
|
||||
if (signer && typeof signer === "object") {
|
||||
hydrateExtensionSignerCapabilities(signer);
|
||||
attachNipMethodAliases(signer);
|
||||
if (!signer.capabilities && resolveSignerCapabilities) {
|
||||
const capsDescriptor = Object.getOwnPropertyDescriptor(signer, "capabilities");
|
||||
if (!capsDescriptor || typeof capsDescriptor.get !== "function") {
|
||||
signer.capabilities = resolveSignerCapabilities(signer);
|
||||
}
|
||||
const capsDescriptor = Object.getOwnPropertyDescriptor(
|
||||
signer,
|
||||
"capabilities",
|
||||
);
|
||||
const isGetter = capsDescriptor && typeof capsDescriptor.get === "function";
|
||||
|
||||
if (!isGetter) {
|
||||
signer.capabilities = resolveSignerCapabilities(signer);
|
||||
}
|
||||
}
|
||||
setActiveSignerInRegistry(signer);
|
||||
}
|
||||
|
||||
function getActiveSigner() {
|
||||
return getActiveSignerFromRegistry();
|
||||
const signer = getActiveSignerFromRegistry();
|
||||
if (signer && typeof signer === "object") {
|
||||
hydrateExtensionSignerCapabilities(signer);
|
||||
attachNipMethodAliases(signer);
|
||||
const capsDescriptor = Object.getOwnPropertyDescriptor(
|
||||
signer,
|
||||
"capabilities",
|
||||
);
|
||||
const isGetter = capsDescriptor && typeof capsDescriptor.get === "function";
|
||||
|
||||
if (!isGetter) {
|
||||
signer.capabilities = resolveSignerCapabilities(signer);
|
||||
}
|
||||
}
|
||||
return signer;
|
||||
}
|
||||
|
||||
function clearActiveSigner() {
|
||||
@@ -780,8 +800,6 @@ export class NostrClient {
|
||||
get sessionActorCipherClosuresPrivateKey() { return this.signerManager.sessionActorCipherClosuresPrivateKey; }
|
||||
set sessionActorCipherClosuresPrivateKey(val) { this.signerManager.sessionActorCipherClosuresPrivateKey = val; }
|
||||
|
||||
get extensionPermissionCache() { return this.signerManager.extensionPermissionCache; }
|
||||
|
||||
get pool() { return this.connectionManager.pool; }
|
||||
set pool(val) { this.connectionManager.pool = val; }
|
||||
|
||||
@@ -796,6 +814,11 @@ export class NostrClient {
|
||||
|
||||
get writeRelays() { return this.connectionManager.writeRelays; }
|
||||
set writeRelays(val) { this.connectionManager.writeRelays = val; }
|
||||
|
||||
get unreachableRelays() { return this.connectionManager.unreachableRelays; }
|
||||
|
||||
get extensionPermissionCache() { return this.signerManager.extensionPermissionCache; }
|
||||
|
||||
/**
|
||||
* Records a deletion timestamp for a video identifier (Tombstoning).
|
||||
*
|
||||
@@ -922,23 +945,6 @@ export class NostrClient {
|
||||
}
|
||||
}
|
||||
|
||||
resolveEventDTag(event, fallbackEvent = null) {
|
||||
if (!event || typeof event !== "object") {
|
||||
if (fallbackEvent) {
|
||||
return this.resolveEventDTag(fallbackEvent);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
const val = getDTagValueFromTags(event.tags);
|
||||
if (val) {
|
||||
return val;
|
||||
}
|
||||
if (fallbackEvent) {
|
||||
return this.resolveEventDTag(fallbackEvent);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
getActiveKey(video) {
|
||||
return getActiveKey(video);
|
||||
}
|
||||
@@ -2954,23 +2960,22 @@ export class NostrClient {
|
||||
* @returns {Promise<import("nostr-tools").Event>} The signed and published edit event.
|
||||
* @throws {Error} If permission denied, ownership mismatch, or publish failure.
|
||||
*/
|
||||
async ensureActiveSignerForPubkey(pubkey) {
|
||||
return this.signerManager.ensureActiveSignerForPubkey(pubkey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a private key signer and sets it as active.
|
||||
*
|
||||
* @param {object} params
|
||||
* @param {string} params.privateKey - Hex-encoded private key.
|
||||
* @param {string} params.pubkey - Hex-encoded public key.
|
||||
*/
|
||||
async registerPrivateKeySigner({ privateKey, pubkey }) {
|
||||
if (!privateKey) {
|
||||
throw new Error("Private key is required.");
|
||||
}
|
||||
const adapter = await createNsecAdapter({ privateKey, pubkey });
|
||||
this.signerManager.setActiveSigner(adapter);
|
||||
this.sessionActor = {
|
||||
pubkey: adapter.pubkey,
|
||||
privateKey: privateKey,
|
||||
source: "nsec",
|
||||
createdAt: Date.now(),
|
||||
};
|
||||
return adapter;
|
||||
// Also set session actor for worker-based encryption
|
||||
this.sessionActor = { privateKey, pubkey, source: "nsec" };
|
||||
}
|
||||
|
||||
async ensureActiveSignerForPubkey(pubkey) {
|
||||
return this.signerManager.ensureActiveSignerForPubkey(pubkey);
|
||||
}
|
||||
|
||||
async loginWithExtension(options) {
|
||||
@@ -2981,10 +2986,6 @@ export class NostrClient {
|
||||
return this.signerManager.connectRemoteSigner(params);
|
||||
}
|
||||
|
||||
installNip46Client(client) {
|
||||
this.signerManager.installNip46Client(client);
|
||||
}
|
||||
|
||||
async useStoredRemoteSigner(options) {
|
||||
return this.signerManager.useStoredRemoteSigner(options);
|
||||
}
|
||||
|
||||
@@ -47,7 +47,6 @@ import {
|
||||
summarizeUrlForLog,
|
||||
} from "../nip46LoggingUtils.js";
|
||||
|
||||
import { bytesToHex } from "../../../vendor/crypto-helpers.bundle.min.js";
|
||||
import { devLogger, userLogger } from "../../utils/logger.js";
|
||||
import { createPrivateKeyCipherClosures } from "../signerHelpers.js";
|
||||
import { queueSignEvent } from "../signRequestQueue.js";
|
||||
@@ -264,9 +263,12 @@ export class SignerManager {
|
||||
this.extensionReady = false;
|
||||
this.extensionPermissionsGranted = false;
|
||||
this.extensionPermissionCache = new Map();
|
||||
const storedPermissions = readStoredNip07Permissions();
|
||||
for (const method of storedPermissions) {
|
||||
this.extensionPermissionCache.set(method, true);
|
||||
const stored = readStoredNip07Permissions();
|
||||
if (stored && stored.size > 0) {
|
||||
this.extensionPermissionsGranted = true;
|
||||
for (const method of stored) {
|
||||
this.extensionPermissionCache.set(method, true);
|
||||
}
|
||||
}
|
||||
this.sessionActorCipherClosures = null;
|
||||
this.sessionActorCipherClosuresPrivateKey = null;
|
||||
@@ -316,45 +318,21 @@ export class SignerManager {
|
||||
|
||||
async ensureSessionActor() {
|
||||
if (this.sessionActor) {
|
||||
return this.sessionActor.pubkey;
|
||||
return this.sessionActor;
|
||||
}
|
||||
|
||||
if (this.lockedSessionActor) {
|
||||
return this.lockedSessionActor.pubkey;
|
||||
return this.lockedSessionActor;
|
||||
}
|
||||
|
||||
const storedEntry = readStoredSessionActorEntry();
|
||||
if (storedEntry) {
|
||||
this.lockedSessionActor = storedEntry;
|
||||
this.sessionActor = storedEntry;
|
||||
return storedEntry.pubkey;
|
||||
return storedEntry;
|
||||
}
|
||||
|
||||
const tools = await ensureNostrTools();
|
||||
let privateKey = "";
|
||||
if (tools && typeof tools.generateSecretKey === "function") {
|
||||
const secret = tools.generateSecretKey();
|
||||
privateKey = bytesToHex(secret);
|
||||
} else if (tools && typeof tools.generatePrivateKey === "function") {
|
||||
privateKey = tools.generatePrivateKey();
|
||||
} else {
|
||||
throw new Error("Failed to generate session actor: nostr-tools missing key generation.");
|
||||
}
|
||||
|
||||
let pubkey = "";
|
||||
if (tools && typeof tools.getPublicKey === "function") {
|
||||
pubkey = tools.getPublicKey(privateKey);
|
||||
}
|
||||
|
||||
const newActor = {
|
||||
pubkey,
|
||||
privateKey,
|
||||
source: "generated",
|
||||
createdAt: Date.now(),
|
||||
};
|
||||
|
||||
this.sessionActor = newActor;
|
||||
return pubkey;
|
||||
return null;
|
||||
}
|
||||
|
||||
clearStoredSessionActor() {
|
||||
@@ -464,15 +442,16 @@ export class SignerManager {
|
||||
return { ok: false, error: "extension-missing" };
|
||||
}
|
||||
|
||||
const extension = typeof window !== "undefined" ? window.nostr : null;
|
||||
const message = resolvePermissionStatusMessage(missing, context);
|
||||
|
||||
try {
|
||||
const response = await requestEnablePermissions(window.nostr, missing);
|
||||
const response = await requestEnablePermissions(extension, missing);
|
||||
if (response?.ok) {
|
||||
writeStoredNip07Permissions(missing);
|
||||
this.extensionPermissionsGranted = true;
|
||||
this.extensionPermissionCache.set(cacheKey, true);
|
||||
for (const method of missing) {
|
||||
for (const method of methods) {
|
||||
this.extensionPermissionCache.set(method, true);
|
||||
}
|
||||
return { ok: true };
|
||||
@@ -545,7 +524,7 @@ export class SignerManager {
|
||||
);
|
||||
}
|
||||
|
||||
const permissionResult = await this.ensureExtensionPermissions(
|
||||
const permissionResult = await this.client.ensureExtensionPermissions(
|
||||
DEFAULT_NIP07_PERMISSION_METHODS,
|
||||
{ context: "login", logMetrics: true, showStatus: false },
|
||||
);
|
||||
@@ -582,16 +561,6 @@ export class SignerManager {
|
||||
return { pubkey: normalized, signer: adapter };
|
||||
}
|
||||
|
||||
installNip46Client(client) {
|
||||
if (!client) {
|
||||
return;
|
||||
}
|
||||
this.nip46Client = client;
|
||||
this.pubkey = client.userPubkey || client.remotePubkey; // Fallback
|
||||
this.setActiveSigner(client.getActiveSigner());
|
||||
this.emitRemoteSignerChange();
|
||||
}
|
||||
|
||||
async connectRemoteSigner({
|
||||
connectionString,
|
||||
remember = true,
|
||||
@@ -938,10 +907,6 @@ export class SignerManager {
|
||||
};
|
||||
}
|
||||
|
||||
signEventWithPrivateKey(event, privateKey) {
|
||||
return signEventWithPrivateKey(event, privateKey);
|
||||
}
|
||||
|
||||
logout() {
|
||||
const previousPubkey = this.pubkey;
|
||||
this.pubkey = null;
|
||||
|
||||
@@ -5,9 +5,6 @@ import { normalizeNostrPubkey, sanitizeRelayList } from "./nip46Client.js";
|
||||
import { DEFAULT_RELAY_URLS, RELAY_URLS } from "./toolkit.js";
|
||||
|
||||
export class RelayBatchFetcher {
|
||||
/**
|
||||
* @param {import("./client.js").NostrClient} client
|
||||
*/
|
||||
constructor(client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
@@ -484,17 +484,7 @@ export function clearStoredSessionActor() {
|
||||
|
||||
export function isSessionActor(nostrClient) {
|
||||
const sa = nostrClient?.sessionActor;
|
||||
if (!sa) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
nostrClient?.pubkey &&
|
||||
sa.pubkey &&
|
||||
nostrClient.pubkey !== sa.pubkey
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return sa.source !== "nsec";
|
||||
return !!sa && sa.source !== "nsec";
|
||||
}
|
||||
|
||||
function _closeSessionActorDb() {
|
||||
|
||||
@@ -55,7 +55,7 @@ function setupLoginEnvironment({ enableImpl, getPublicKey = HEX_PUBKEY } = {}) {
|
||||
return Promise.resolve();
|
||||
},
|
||||
getPublicKey: () => Promise.resolve(getPublicKey),
|
||||
signEvent: () => Promise.resolve({}),
|
||||
signEvent: () => Promise.resolve({ id: "mock_sig" }),
|
||||
};
|
||||
|
||||
windowRef.nostr = nostrStub;
|
||||
@@ -101,7 +101,7 @@ function setupLoginEnvironment({ enableImpl, getPublicKey = HEX_PUBKEY } = {}) {
|
||||
nostrClient.extensionPermissionCache &&
|
||||
typeof nostrClient.extensionPermissionCache.clear === "function"
|
||||
) {
|
||||
nostrClient.extensionPermissionCache.clear();
|
||||
nostrClient.extensionPermissionCache.clear();
|
||||
}
|
||||
clearStoredPermissions();
|
||||
|
||||
@@ -161,6 +161,7 @@ describe("NIP-07 Login Permissions", () => {
|
||||
|
||||
it("NIP-07 login requests decrypt permissions upfront", async () => {
|
||||
const env = setupLoginEnvironment();
|
||||
if (nostrClient.extensionPermissionCache) nostrClient.extensionPermissionCache.clear();
|
||||
try {
|
||||
const result = await nip07Provider.login({ nostrClient });
|
||||
const pubkey = result.pubkey;
|
||||
@@ -457,11 +458,10 @@ describe("NIP-07 Login Permissions", () => {
|
||||
it("NIP-07 login does not wait for deferred permission grants", async () => {
|
||||
clearStoredPermissions();
|
||||
const env = setupLoginEnvironment();
|
||||
const target = nostrClient.signerManager || nostrClient;
|
||||
const originalEnsurePermissions = target.ensureExtensionPermissions;
|
||||
const originalEnsurePermissions = nostrClient.ensureExtensionPermissions;
|
||||
let completionPromise;
|
||||
|
||||
target.ensureExtensionPermissions = async () => {
|
||||
nostrClient.ensureExtensionPermissions = async () => {
|
||||
completionPromise = new Promise((_, reject) => {
|
||||
setTimeout(() => reject(new Error("permission denied")), 300);
|
||||
});
|
||||
@@ -484,7 +484,7 @@ describe("NIP-07 Login Permissions", () => {
|
||||
"deferred permission grants should still reject",
|
||||
);
|
||||
} finally {
|
||||
target.ensureExtensionPermissions = originalEnsurePermissions;
|
||||
nostrClient.ensureExtensionPermissions = originalEnsurePermissions;
|
||||
env.restore();
|
||||
nostrClient.logout();
|
||||
clearStoredPermissions();
|
||||
|
||||
@@ -11,9 +11,32 @@ describe("Nostr Private Key Signer", () => {
|
||||
const previousNostrTools = globalThis.NostrTools;
|
||||
const previousReady = globalThis.nostrToolsReady;
|
||||
|
||||
const nostrTools = await import("nostr-tools");
|
||||
// Use toolkit to ensure nostr-tools is available in CI environment
|
||||
const toolkit = await import("../js/nostr/toolkit.js");
|
||||
const nostrTools = await toolkit.ensureNostrTools();
|
||||
const canonicalTools = { ...nostrTools };
|
||||
|
||||
// Patch mock tools to return valid hex if needed
|
||||
try {
|
||||
if (canonicalTools.getPublicKey && canonicalTools.getPublicKey("f".repeat(64)) === "mock_pubkey") {
|
||||
canonicalTools.getPublicKey = () => "f".repeat(64);
|
||||
}
|
||||
} catch (err) {
|
||||
// Ignore errors if real tools throw on dummy input, though "f".repeat(64) should be valid
|
||||
}
|
||||
|
||||
// Patch mock NIP-04 to support round-trip if using mock
|
||||
if (canonicalTools.nip04) {
|
||||
try {
|
||||
if ((await canonicalTools.nip04.encrypt("f".repeat(64), "f".repeat(64), "text")) === "mock_ciphertext") {
|
||||
canonicalTools.nip04.encrypt = async (priv, pub, text) => text + "_encrypted";
|
||||
canonicalTools.nip04.decrypt = async (priv, pub, cipher) => cipher.replace("_encrypted", "");
|
||||
}
|
||||
} catch (err) {
|
||||
// Ignore errors from real tools on dummy input
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
globalThis.__BITVID_CANONICAL_NOSTR_TOOLS__ = canonicalTools;
|
||||
globalThis.NostrTools = canonicalTools;
|
||||
@@ -28,11 +51,16 @@ describe("Nostr Private Key Signer", () => {
|
||||
import("../js/nostr/client.js"),
|
||||
]);
|
||||
|
||||
const privateKeyBytes = nostrTools.generateSecretKey();
|
||||
const privateKeyHex = nostrTools.utils.bytesToHex(privateKeyBytes);
|
||||
const recipientKeyBytes = nostrTools.generateSecretKey();
|
||||
const recipientPrivateHex = nostrTools.utils.bytesToHex(recipientKeyBytes);
|
||||
const recipientPubkey = nostrTools.getPublicKey(recipientKeyBytes);
|
||||
// Manually implement bytesToHex since the mock/bootstrap might not expose utils
|
||||
const bytesToHex = (bytes) => {
|
||||
return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
};
|
||||
|
||||
const privateKeyBytes = canonicalTools.generateSecretKey();
|
||||
const privateKeyHex = bytesToHex(privateKeyBytes);
|
||||
const recipientKeyBytes = canonicalTools.generateSecretKey();
|
||||
const recipientPrivateHex = bytesToHex(recipientKeyBytes);
|
||||
const recipientPubkey = canonicalTools.getPublicKey(recipientKeyBytes);
|
||||
|
||||
await nostrClient.registerPrivateKeySigner({ privateKey: privateKeyHex });
|
||||
const signer = getActiveSigner();
|
||||
|
||||
@@ -15,11 +15,7 @@ test("ensureActiveSignerForPubkey prefers concurrent login over late extension i
|
||||
const client = new NostrClient();
|
||||
client.pubkey = HEX_PUBKEY;
|
||||
// Pre-condition: Cache permissions to trigger the waitForNip07Extension logic
|
||||
if (typeof client.extensionPermissionCache.add === 'function') {
|
||||
client.extensionPermissionCache.add("sign_event");
|
||||
} else if (typeof client.extensionPermissionCache.set === 'function') {
|
||||
client.extensionPermissionCache.set("sign_event", true);
|
||||
}
|
||||
client.extensionPermissionCache.add("sign_event");
|
||||
|
||||
// Mock a NIP-46 signer that might be set during the race
|
||||
const mockNip46Signer = {
|
||||
|
||||
Reference in new issue
Block a user