From c026a8cc450b87c52080e19675b4fbca0445aa19 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 25 Jan 2026 15:43:39 +0000 Subject: [PATCH] chore(ai): doc js/nostr/client.js (agent) --- docs/nostr-client-overview.md | 40 ++++++++++++++ js/nostr/client.js | 99 ++++++++++++++++++++++++++++++++++- 2 files changed, 138 insertions(+), 1 deletion(-) diff --git a/docs/nostr-client-overview.md b/docs/nostr-client-overview.md index c90549b3..f20e2893 100644 --- a/docs/nostr-client-overview.md +++ b/docs/nostr-client-overview.md @@ -97,6 +97,46 @@ Used for things like Mute Lists (Kind 10000) or Pin Lists. - **Fallback**: If the delta sync fails, performs a full fetch to ensure consistency. - **Concurrency**: Batches requests to 4 relays at a time to prevent browser network saturation. +### Video Deletion Flow (`deleteAllVersions`) + +Nostr's immutability makes "deleting" a video series complex, as multiple edit versions might exist across relays. +`deleteAllVersions` handles this via a multi-stage process: + +1. **Hydration**: Calls `hydrateVideoHistory` to find *every* event related to the video (scanning by `videoRootId` and `d` tag). +2. **Soft Delete (Revert)**: For each unique version identifier (Root + D-Tag combination), it publishes a new event with `deleted: true` and cleared content. This ensures clients resolving the "latest" version see the deletion immediately. +3. **Hard Delete (NIP-09)**: Publishes a Kind 5 Deletion Request referencing the Event IDs of *all* versions found. Compliant relays will physically remove the data. +4. **Tombstoning**: Updates the local `tombstones` map with the deletion timestamp to preventing caching mechanisms from resurrecting old versions. + +```mermaid +sequenceDiagram + participant UI as VideoEditModal + participant Client as NostrClient + participant Relay + + UI->>Client: deleteAllVersions(rootId) + activate Client + + Note right of Client: 1. Find all versions + Client->>Client: hydrateVideoHistory(video) + Client->>Relay: Query (Kind 30078 + #d) + Relay-->>Client: Events [v1, v2, v3] + + Note right of Client: 2. Soft Delete (Revert) + loop For each unique branch + Client->>Relay: Publish Kind 30078 (deleted=true) + end + + Note right of Client: 3. Hard Delete (NIP-09) + Client->>Relay: Publish Kind 5 (referencing v1, v2, v3 ids) + + Note right of Client: 4. Local Cleanup + Client->>Client: recordTombstone(rootId) + Client->>Client: saveLocalData() + + Client-->>UI: Success + deactivate Client +``` + ## When to change - **Update `EVENTS_CACHE_VERSION`**: If you change the internal `Video` object structure (e.g., rename fields in `convertEventToVideo`), bump the DB version in `EventsCacheStore` to force a wipe/rebuild on client devices. diff --git a/js/nostr/client.js b/js/nostr/client.js index 50e2b35d..b741d83a 100644 --- a/js/nostr/client.js +++ b/js/nostr/client.js @@ -1554,6 +1554,25 @@ export class NostrClient { return signer; } + /** + * Waits for a remote signer (Nostr Connect) to acknowledge a connection request. + * + * **Protocol (NIP-46):** + * - Subscribes to Kind 24133 (NIP-46 RPC) events addressed to the client. + * - Decrypts incoming messages using the ephemeral `clientPrivateKey`. + * - Looks for "connect" acknowledgement or "auth_url" challenges. + * + * @param {object} params + * @param {string} params.clientPrivateKey - Ephemeral private key for the handshake. + * @param {string} params.clientPublicKey - Ephemeral public key. + * @param {string[]} params.relays - Relay list for the handshake. + * @param {string} [params.secret] - Optional secret to validate the "ack" response. + * @param {function} [params.onAuthUrl] - Callback for out-of-band auth challenges. + * @param {function} [params.onStatus] - Callback for status updates. + * @param {number} [params.timeoutMs] - Max wait time. + * @param {string} [params.expectedRemotePubkey] - If known, filters events to this sender. + * @returns {Promise<{remotePubkey: string, eventPubkey: string, response: object, algorithm: string}>} + */ async waitForRemoteSignerHandshake({ clientPrivateKey, clientPublicKey, @@ -2203,6 +2222,19 @@ export class NostrClient { } } + /** + * Reconnects to a previously persisted NIP-46 remote signer session. + * + * **Usage:** + * Called during `init()` to restore the session without user interaction. + * It reads credentials from localStorage (`bitvid:nip46:session`), validates them, + * and re-establishes the RPC subscription. + * + * @param {object} [options] + * @param {boolean} [options.silent=false] - Suppress error logs if restore fails. + * @param {boolean} [options.forgetOnError=false] - Auto-delete session if invalid. + * @returns {Promise<{pubkey: string, signer: object}>} + */ async useStoredRemoteSigner(options = {}) { const normalizedOptions = options && typeof options === "object" ? options : {}; @@ -2355,6 +2387,12 @@ export class NostrClient { return attempt; } + /** + * Terminates the NIP-46 remote signer connection. + * + * @param {object} [options] + * @param {boolean} [options.keepStored=true] - If false, wipes the session from localStorage. + */ async disconnectRemoteSigner({ keepStored = true } = {}) { this.pendingRemoteSignerRestore = null; if (this.nip46Client) { @@ -2385,6 +2423,16 @@ export class NostrClient { }); } + /** + * Records a deletion timestamp for a video identifier (Tombstoning). + * + * **Purpose:** + * Enforces "Eventual Consistency". If we receive an old version of a video + * *after* we've seen it was deleted, this tombstone ensures we ignore the zombie event. + * + * @param {string} activeKey - The unique key (root ID or pubkey:dTag). + * @param {number} createdAt - The timestamp of the deletion event. + */ recordTombstone(activeKey, createdAt) { const key = typeof activeKey === "string" ? activeKey.trim() : ""; if (!key) { @@ -2460,6 +2508,13 @@ export class NostrClient { return createdAtValue > 0 && createdAtValue <= normalizedTombstone; } + /** + * Checks if a video event is superseded by a known tombstone. + * If so, marks the video object as `deleted = true`. + * + * @param {object} video - The video object to check. + * @returns {boolean} True if the video was marked deleted by a tombstone. + */ applyTombstoneGuard(video) { if (!video || typeof video !== "object") { return false; @@ -2714,6 +2769,13 @@ export class NostrClient { return earliest; } + /** + * Attempts to load a "Session Actor" (Ephemeral Key) from storage. + * Session actors are used for non-critical signing (e.g. view telemetry) + * to avoid nagging the user for NIP-07 signatures constantly. + * + * @returns {object|null} The session actor object if found. + */ restoreSessionActorFromStorage() { const entry = readStoredSessionActorEntry(); if (!entry) { @@ -3152,6 +3214,18 @@ export class NostrClient { return { pubkey: normalizedPubkey }; } + /** + * Returns a valid public key for ephemeral signing ("Session Actor"). + * + * **Priority:** + * 1. **Active Signer**: If the user is logged in and the signer is non-promiscuous (local nsec), use it. + * 2. **Existing Session**: If we already have a generated ephemeral key, use it. + * 3. **Restored Session**: Try to load one from storage. + * 4. **Mint New**: Generate a new random keypair and save it. + * + * @param {boolean} [forceRenew=false] - Force generation of a new keypair. + * @returns {Promise} The pubkey of the session actor. + */ async ensureSessionActor(forceRenew = false) { const normalizedLogged = typeof this.pubkey === "string" && this.pubkey @@ -3867,7 +3941,20 @@ export class NostrClient { } /** - * Attempt login with a Nostr extension + * Authenticates the user via a NIP-07 browser extension (e.g., Alby, nos2x). + * + * **Process:** + * 1. Checks for `window.nostr` presence. + * 2. Requests permissions (sign_event, nip04, etc.) via `ensureExtensionPermissions`. + * 3. Retrieves the public key. + * 4. Validates access control (whitelist/blacklist) via `accessControl`. + * 5. Sets the active signer to a `Nip07Adapter`. + * + * @param {object} [options] + * @param {boolean} [options.allowAccountSelection=false] - Whether to prompt the extension to select an account (NIP-07 extension). + * @param {string} [options.expectPubkey] - Enforce that the login matches a specific pubkey. + * @returns {Promise} The authenticated public key (hex). + * @throws {Error} If extension is missing, permissions denied, or account blocked. */ async login(options = {}) { try { @@ -3988,6 +4075,16 @@ export class NostrClient { } } + /** + * Logs out the current user and clears all session state. + * + * **Cleanup:** + * - Clears `this.pubkey` and notifies the global signer registry. + * - Wipes session actor (ephemeral keys). + * - Disconnects NIP-46 remote signer (if active). + * - Clears Watch History cache. + * - Resets permissions cache. + */ logout() { const previousPubkey = this.pubkey; this.pubkey = null;