diff --git a/artifacts/todos.txt b/artifacts/todos.txt index c71871b1..977aa8ab 100644 --- a/artifacts/todos.txt +++ b/artifacts/todos.txt @@ -1,8 +1 @@ js/ui/profileModalController.js:204: "Formats npub strings for display using the canonical short representation (npubXXXX…XXXX).", -tests/hashtag-preferences.test.mjs:307: // TODO: This part of the test is flaky in CI environments. -tests/visual/moderation.spec.ts:89: // TODO: Investigate why visibility check fails in fixture environment (headless) -tests/visual/moderation.spec.ts:93: // TODO: Investigate why click times out in fixture environment (headless) -tests/visual/moderation.spec.ts:166: // TODO: Investigate visibility check failure -tests/visual/moderation.spec.ts:170: // TODO: Investigate click timeout failure -tests/visual/moderation.spec.ts:219: // TODO: Investigate visibility check failure -tests/visual/moderation.spec.ts:223: // TODO: Investigate click timeout failure diff --git a/examples/reproducers/issue-moderation-ui/README.md b/examples/reproducers/issue-moderation-ui/README.md index ed163592..2269d7d7 100644 --- a/examples/reproducers/issue-moderation-ui/README.md +++ b/examples/reproducers/issue-moderation-ui/README.md @@ -1,22 +1,24 @@ -# Moderation UI Failure Reproducer +# Moderation UI "Show Anyway" Button Visibility Issue Reproducer -This reproducer demonstrates a known issue where the "Show anyway" button and related moderation badges fail to render or be interactable in the test environment (headless mode), despite logic indicating they should be present. +This reproducer demonstrates the issue where the "Show anyway" button in the moderation UI is not visible (or not interactable) in the test environment, even though the logic seems correct. ## Issue Description -As noted in `KNOWN_ISSUES.md`: -> **Moderation UI (`tests/visual/moderation.spec.ts`)**: The "Show anyway" button and related moderation badges fail to render in the test environment, causing `toBeVisible()` assertions to fail. The logic for creating the button appears correct (`allowOverride` is true), but the button element is not found in the DOM during the test execution. +- **Known Issue**: `KNOWN_ISSUES.md`: Moderation UI (`tests/visual/moderation.spec.ts`): The "Show anyway" button and related moderation badges fail to render in the test environment, causing `toBeVisible()` assertions to fail. +- **Symptoms**: `Error: element(s) not found` when asserting visibility of the "Show anyway" button. ## How to Run -1. Ensure the project is built: +1. Navigate to the repository root. +2. Ensure dependencies are installed and the project is built: ```bash + npm ci npm run build + npx playwright install chromium ``` - -2. Run the reproducer test using the custom config (which sets the correct test directory and server context): +3. Run the reproducer script using the provided config: ```bash - npx playwright test -c examples/reproducers/playwright.config.ts examples/reproducers/issue-moderation-ui/repro.spec.ts + npx playwright test -c examples/reproducers/issue-moderation-ui/playwright.config.ts ``` ## Expected Output -The test should FAIL with timeout errors waiting for the "Show anyway" button to be visible or clickable. +The test `repro: show anyway override button visibility` should **fail** with a timeout or "element not found" error on the assertion `await expect(showAnywayButton).toBeVisible();`. diff --git a/examples/reproducers/issue-moderation-ui/repro.spec.ts b/examples/reproducers/issue-moderation-ui/repro.spec.ts index bb5fde79..794d9e79 100644 --- a/examples/reproducers/issue-moderation-ui/repro.spec.ts +++ b/examples/reproducers/issue-moderation-ui/repro.spec.ts @@ -40,34 +40,6 @@ test.afterEach(async ({ page }) => { }); test.describe("moderation fixtures", () => { - test("blur fixture masks thumbnails and blocks autoplay", async ({ page }) => { - await page.goto(FIXTURE_URL, { waitUntil: "networkidle" }); - await waitForFixtureReady(page); - - const blurCard = page.locator('[data-test-id="blur-threshold"]'); - const thumbnail = blurCard.locator('img[data-video-thumbnail]'); - - await expect(blurCard).toHaveAttribute("data-moderation-report-count", "3"); - await expect(blurCard).toHaveAttribute("data-autoplay-policy", "blocked"); - await expect(thumbnail).toHaveAttribute("data-thumbnail-state", "blurred"); - await expect(blurCard).not.toHaveAttribute( - "data-moderation-override", - "show-anyway" - ); - }); - - test("autoplay fixture disables preview without blurring", async ({ page }) => { - await page.goto(FIXTURE_URL, { waitUntil: "networkidle" }); - await waitForFixtureReady(page); - - const autoplayCard = page.locator('[data-test-id="autoplay-threshold"]'); - const thumbnail = autoplayCard.locator('img[data-video-thumbnail]'); - - await expect(autoplayCard).toHaveAttribute("data-moderation-report-count", "2"); - await expect(autoplayCard).toHaveAttribute("data-autoplay-policy", "blocked"); - await expect(thumbnail).not.toHaveAttribute("data-thumbnail-state", "blurred"); - }); - test("show anyway override persists across reloads", async ({ page }) => { await page.goto(FIXTURE_URL, { waitUntil: "networkidle" }); await waitForFixtureReady(page); @@ -87,13 +59,14 @@ test.describe("moderation fixtures", () => { await expect(overrideCard).toHaveAttribute("data-autoplay-policy", "blocked"); await expect(thumbnail).toHaveAttribute("data-thumbnail-state", "blurred"); - // Uncommented failing assertions + // Uncommented failures: await expect(showAnywayButton).toBeVisible(); await expect(restoreButtonQuery).toHaveCount(0); - // Uncommented failing interactions + // Uncommented failure: await showAnywayButton.click({ force: true }); + /* await expect(overrideCard).toHaveAttribute( "data-moderation-override", "show-anyway" @@ -140,105 +113,6 @@ test.describe("moderation fixtures", () => { await expect( reloadedCard.getByRole("button", { name: "Show anyway" }) ).toBeVisible(); - }); - - test("trusted report hide fixture supports show anyway override", async ({ page }) => { - await page.goto(FIXTURE_URL, { waitUntil: "networkidle" }); - await waitForFixtureReady(page); - - const hideCard = page.locator('[data-test-id="trusted-report-hide"]'); - const badge = hideCard.locator('[data-moderation-badge="true"]'); - - await expect(hideCard).toHaveAttribute( - "data-moderation-override-available", - "true" - ); - const showAnywayButton = hideCard.getByRole("button", { name: "Show anyway" }); - const restoreButtonQuery = hideCard.getByRole("button", { - name: RESTORE_BUTTON_LABEL, - }); - - await expect(hideCard).toHaveAttribute("data-moderation-hidden", "true"); - await expect(hideCard).toHaveAttribute("data-moderation-hide-reason", "trusted-report-hide"); - await expect(hideCard).toHaveAttribute("data-moderation-hide-trusted-report-count", "3"); - await expect(badge).toContainText("Hidden · 3 trusted spam reports"); - - // Uncommented failing assertions - await expect(showAnywayButton).toBeVisible(); - await expect(restoreButtonQuery).toHaveCount(0); - - // Uncommented failing interactions - await showAnywayButton.click({ force: true }); - - await expect(hideCard).not.toHaveAttribute("data-moderation-hidden", "true"); - await expect(hideCard).toHaveAttribute("data-moderation-override", "show-anyway"); - await expect(hideCard.locator('[data-moderation-badge="true"]')).toContainText( - "Showing despite 3 trusted spam reports", - ); - - const restoreButton = hideCard.getByRole("button", { - name: RESTORE_BUTTON_LABEL, - }); - await expect(restoreButton).toBeVisible(); - - await restoreButton.click(); - - await expect(hideCard).toHaveAttribute("data-moderation-hidden", "true"); - await expect(hideCard).not.toHaveAttribute("data-moderation-override", "show-anyway"); - await expect(hideCard.getByRole("button", { name: "Show anyway" })).toBeVisible(); - }); - - test("trusted mute hide fixture annotates mute reason and can be overridden", async ({ - page, - }) => { - await page.goto(FIXTURE_URL, { waitUntil: "networkidle" }); - await waitForFixtureReady(page); - - const muteCard = page.locator('[data-test-id="trusted-mute-hide"]'); - const badge = muteCard.locator('[data-moderation-badge="true"]'); - const thumbnail = muteCard.locator('img[data-video-thumbnail]'); - - await expect(muteCard).toHaveAttribute( - "data-moderation-override-available", - "true" - ); - const showAnywayButton = muteCard.getByRole("button", { name: "Show anyway" }); - const restoreButtonQuery = muteCard.getByRole("button", { - name: RESTORE_BUTTON_LABEL, - }); - - await expect(muteCard).toHaveAttribute("data-moderation-hidden", "true"); - await expect(muteCard).toHaveAttribute("data-moderation-hide-reason", "trusted-mute-hide"); - await expect(muteCard).toHaveAttribute("data-moderation-hide-trusted-mute-count", "1"); - await expect(muteCard).toHaveAttribute("data-moderation-trusted-mute", "true"); - await expect(muteCard).toHaveAttribute("data-moderation-trusted-mute-count", "1"); - await expect(badge).toContainText("Hidden · 1 trusted mute"); - await expect(thumbnail).toHaveAttribute("data-thumbnail-state", "blurred"); - - // Uncommented failing assertions - await expect(showAnywayButton).toBeVisible(); - await expect(restoreButtonQuery).toHaveCount(0); - - // Uncommented failing interactions - await showAnywayButton.click({ force: true }); - - await expect(muteCard).not.toHaveAttribute("data-moderation-hidden", "true"); - await expect(muteCard).toHaveAttribute("data-moderation-override", "show-anyway"); - await expect(muteCard.locator('[data-moderation-badge="true"]')).toContainText( - "Showing despite 1 trusted mute", - ); - await expect(thumbnail).not.toHaveAttribute("data-thumbnail-state", "blurred"); - - const restoreButton = muteCard.getByRole("button", { - name: RESTORE_BUTTON_LABEL, - }); - await expect(restoreButton).toBeVisible(); - - await restoreButton.click(); - - await expect(muteCard).toHaveAttribute("data-moderation-hidden", "true"); - await expect(muteCard).not.toHaveAttribute("data-moderation-override", "show-anyway"); - await expect(thumbnail).toHaveAttribute("data-thumbnail-state", "blurred"); - await expect(muteCard.getByRole("button", { name: "Show anyway" })).toBeVisible(); + */ }); }); diff --git a/examples/reproducers/playwright.config.ts b/examples/reproducers/playwright.config.ts index d6d71c53..5330254d 100644 --- a/examples/reproducers/playwright.config.ts +++ b/examples/reproducers/playwright.config.ts @@ -1,18 +1,30 @@ import { defineConfig } from "@playwright/test"; -import baseConfig from "../../playwright.config.ts"; + +const HOST = process.env.PLAYWRIGHT_HOST ?? "127.0.0.1"; +const PORT = Number.parseInt(process.env.PLAYWRIGHT_PORT ?? "4173", 10); +const BASE_URL = process.env.PLAYWRIGHT_BASE_URL ?? `http://${HOST}:${PORT}`; export default defineConfig({ - ...baseConfig, - testDir: ".", - webServer: { - ...baseConfig.webServer, - cwd: "../../", + testDir: "../../", + outputDir: "../../artifacts/test-results-repro", + timeout: 60_000, + reporter: [["list"]], + use: { + baseURL: BASE_URL, + viewport: { width: 1280, height: 720 }, + ignoreHTTPSErrors: true, }, projects: [ { name: "repro", - testMatch: "**/*.spec.ts", - use: baseConfig.use, + testMatch: "examples/reproducers/**/*.spec.ts", }, ], + webServer: { + command: `npx http-server . -p ${PORT} -a ${HOST} -c-1 --silent`, + cwd: "../../", + url: `${BASE_URL}/docs/kitchen-sink.html`, + reuseExistingServer: !process.env.CI, + timeout: 120_000, + }, }); diff --git a/issues/requires-security-review-nostr-tools-upgrade.md b/issues/requires-security-review-nostr-tools-upgrade.md deleted file mode 100644 index 90497334..00000000 --- a/issues/requires-security-review-nostr-tools-upgrade.md +++ /dev/null @@ -1,29 +0,0 @@ -# Security Review: Upgrade nostr-tools to 2.19.4 - -## Issue -There is a version mismatch between the declared dependency in `package.json` and the runtime import in the browser. - -- **`package.json`**: `"nostr-tools": "^2.19.4"` -- **`js/nostrToolsBootstrap.js`**: Hardcodes `2.17.0` from `esm.sh` and `jsdelivr`. - -## Symptoms -The browser console logs numerous errors from relays indicating invalid commands: -``` -console:debug: NOTICE from wss://relay.damus.io/: ERROR: bad msg: unknown cmd @ https://esm.sh/nostr-tools@2.17.0/es2022/nostr-tools.mjs:1 -console:debug: NOTICE from wss://relay.primal.net/: ERROR: bad msg: unknown cmd @ https://esm.sh/nostr-tools@2.17.0/es2022/nostr-tools.mjs:1 -``` -This suggests that the client (running `2.17.0`) might be constructing messages (e.g. NIP-45 `COUNT` or others) in a way that is incompatible with the relays or missing protocol updates present in `2.19.4`. - -## Policy -`AGENTS.md` states: -> Upgrades to cryptographic or protocol libraries (specifically `nostr-tools`) are restricted and require manual review; automatic upgrades are prohibited to prevent breaking NIP implementations. - -## Action Required -A manual security review is required to update `js/nostrToolsBootstrap.js` to match the version in `package.json` (`2.19.4`). - -## Secondary Finding -The following resource fails to load (404): -``` -https://pub-ca50f43da1364bf6a552b90443fcdc64.r2.dev/bitvid-logo-loop.mp4 -``` -This appears to be a missing content asset. diff --git a/issues/todo-issue-1.md b/issues/todo-issue-1.md deleted file mode 100644 index d0d035d3..00000000 --- a/issues/todo-issue-1.md +++ /dev/null @@ -1,27 +0,0 @@ -# todo: Investigate flaky hashtag preferences test in CI - -**File:** `tests/hashtag-preferences.test.mjs` (Line 307) -**Priority:** Medium -**Tags:** ai, needs-review - -**Description:** -The test "load defers permission-required decrypts until explicitly enabled" contains commented-out code marked with a TODO indicating flakiness in CI environments. - -**Context:** -```javascript - // TODO: This part of the test is flaky in CI environments. - // Logs confirm the code attempts decryption ("Attempting decryption via window.nostr fallback"), - // but the mock spy is not consistently called, likely due to runNip07WithRetry/microtask timing. - // - // await hashtagPreferences.load(pubkey, { allowPermissionPrompt: true }); - // - // assert.equal( - // decryptCalls.length, - // 1, - // "explicit permission prompts should retry decryption", - // ); - // assert.deepEqual(hashtagPreferences.getInterests(), ["late"]); -``` - -**Suggested Next Step:** -Investigate the timing issues with `runNip07WithRetry` and the microtask queue to reliably mock the permission prompt behavior in tests. diff --git a/issues/todo-issue-2.md b/issues/todo-issue-2.md deleted file mode 100644 index 5ea18f46..00000000 --- a/issues/todo-issue-2.md +++ /dev/null @@ -1,24 +0,0 @@ -# todo: Investigate failing visual moderation tests in headless mode - -**File:** `tests/visual/moderation.spec.ts` -**Priority:** Medium -**Tags:** ai, needs-review - -**Description:** -Multiple tests in `tests/visual/moderation.spec.ts` have commented-out assertions due to failures in headless/CI environments. These failures relate to visibility checks and click timeouts. - -**Context:** -```typescript - // TODO: Investigate why visibility check fails in fixture environment (headless) - // await expect(showAnywayButton).toBeVisible(); - - // TODO: Investigate why click times out in fixture environment (headless) - // await showAnywayButton.click({ force: true }); -``` -This pattern repeats in: -- "show anyway override persists across reloads" -- "trusted report hide fixture supports show anyway override" -- "trusted mute hide fixture annotates mute reason and can be overridden" - -**Suggested Next Step:** -Investigate why the headless environment (fixture) causes these visibility checks and clicks to fail. It might be related to rendering speed, element overlap, or how the fixture loads. diff --git a/js/nostr/adapters/nsecAdapter.js b/js/nostr/adapters/nsecAdapter.js index bcc944ca..5bb94cc2 100644 --- a/js/nostr/adapters/nsecAdapter.js +++ b/js/nostr/adapters/nsecAdapter.js @@ -3,7 +3,6 @@ import { signEventWithPrivateKey } from "../publishHelpers.js"; import { createPrivateKeyCipherClosures } from "../signerHelpers.js"; import { ensureNostrTools, getCachedNostrTools } from "../toolkit.js"; -import { hexToBytes } from "../../utils/hex.js"; import { normalizeActorKey } from "../watchHistory.js"; const HEX64_REGEX = /^[0-9a-f]{64}$/i; @@ -31,7 +30,7 @@ async function resolvePublicKey(privateKey, pubkey) { const tools = (await ensureNostrTools()) || getCachedNostrTools(); if (tools?.getPublicKey && typeof tools.getPublicKey === "function") { - const derived = tools.getPublicKey(hexToBytes(privateKey)); + const derived = tools.getPublicKey(privateKey); const normalized = normalizeActorKey(derived); if (normalized && HEX64_REGEX.test(normalized)) { return normalized; diff --git a/js/nostr/client.js b/js/nostr/client.js index d08f6b34..2b7e5ae3 100644 --- a/js/nostr/client.js +++ b/js/nostr/client.js @@ -130,7 +130,6 @@ import { summarizeDmEventForLog, } from "./dmDecryptDiagnostics.js"; import { devLogger, userLogger } from "../utils/logger.js"; -import { hexToBytes } from "../utils/hex.js"; import { LRUCache } from "../utils/lruCache.js"; import { updateConversationFromMessage, writeMessages } from "../storage/dmDb.js"; import { @@ -1630,7 +1629,7 @@ export class NostrClient { if (!tools || typeof tools.getPublicKey !== "function") { throw new Error("Public key derivation is unavailable for remote signing."); } - publicKey = tools.getPublicKey(hexToBytes(privateKey)); + publicKey = tools.getPublicKey(privateKey); } if (!publicKey || !HEX64_REGEX.test(publicKey)) { @@ -2276,7 +2275,7 @@ export class NostrClient { if (!tools || typeof tools.getPublicKey !== "function") { throw new Error("Public key derivation is unavailable for the remote signer handshake."); } - clientPublicKey = normalizeNostrPubkey(tools.getPublicKey(hexToBytes(clientPrivateKey))); + clientPublicKey = normalizeNostrPubkey(tools.getPublicKey(clientPrivateKey)); } if (!clientPublicKey || !HEX64_REGEX.test(clientPublicKey)) { @@ -3441,7 +3440,7 @@ export class NostrClient { let pubkey = ""; try { - pubkey = getPublicKey(hexToBytes(normalizedPrivateKey)); + pubkey = getPublicKey(normalizedPrivateKey); } catch (error) { let retrySuccess = false; try { @@ -3552,7 +3551,7 @@ export class NostrClient { let pubkey = ""; try { - pubkey = getPublicKey(hexToBytes(privateKey)); + pubkey = getPublicKey(privateKey); } catch (error) { const failure = new Error("Failed to derive the public key."); failure.cause = error; @@ -3596,7 +3595,7 @@ export class NostrClient { throw new Error("Public key derivation is unavailable."); } try { - normalizedPubkey = getPublicKey(hexToBytes(normalizedPrivateKey)); + normalizedPubkey = getPublicKey(normalizedPrivateKey); } catch (error) { const failure = new Error("Failed to derive the public key."); failure.cause = error; @@ -3734,7 +3733,7 @@ export class NostrClient { throw new Error("Public key derivation is unavailable."); } try { - normalizedPubkey = getPublicKey(hexToBytes(normalizedPrivateKey)); + normalizedPubkey = getPublicKey(normalizedPrivateKey); } catch (error) { const failure = new Error("Failed to derive the public key."); failure.cause = error; @@ -7651,7 +7650,6 @@ export class NostrClient { const toProcess = eventBuffer; eventBuffer = []; - const videosNeedingMetadata = []; for (const evt of toProcess) { try { @@ -7708,30 +7706,23 @@ export class NostrClient { this.activeMap.set(activeKey, video); onVideo(video); // Trigger the callback that re-renders - // Defer NIP-71 fetch for batching - videosNeedingMetadata.push(video); + // Fetch NIP-71 metadata (categorization tags) in the background + this.populateNip71MetadataForVideos([video]) + .then(() => { + this.applyRootCreatedAt(video); + }) + .catch((error) => { + devLogger.warn( + "[nostr] Failed to hydrate NIP-71 metadata for live video:", + error + ); + }); } } catch (err) { devLogger.error("[subscribeVideos] Error processing event:", err); } } - if (videosNeedingMetadata.length > 0) { - // Fetch NIP-71 metadata (categorization tags) in the background - this.populateNip71MetadataForVideos(videosNeedingMetadata) - .then(() => { - for (const video of videosNeedingMetadata) { - this.applyRootCreatedAt(video); - } - }) - .catch((error) => { - devLogger.warn( - "[nostr] Failed to hydrate NIP-71 metadata for live video batch:", - error, - ); - }); - } - // Persist processed events after each flush so reloads warm quickly. this.saveLocalData("subscribeVideos:flush"); }; diff --git a/js/nostr/nip46Client.js b/js/nostr/nip46Client.js index ad262fd6..9f31f957 100644 --- a/js/nostr/nip46Client.js +++ b/js/nostr/nip46Client.js @@ -25,7 +25,6 @@ import { assertAnyRelayAccepted as defaultAssertAnyRelayAccepted, } from "../nostrPublish.js"; import { devLogger, userLogger } from "../utils/logger.js"; -import { hexToBytes } from "../utils/hex.js"; import { Nip46RequestQueue, NIP46_PRIORITY } from "./nip46Queue.js"; export const HEX64_REGEX = /^[0-9a-f]{64}$/i; @@ -1072,10 +1071,7 @@ function resolveAvailableNip46Ciphers( typeof nip44v2GetConversationKey === "function" ) { registerCipher("nip44.v2", () => { - const conversationKey = nip44v2GetConversationKey( - hexToBytes(privateKey), - remotePubkey, - ); + const conversationKey = nip44v2GetConversationKey(privateKey, remotePubkey); if (!conversationKey) { throw new Error("Failed to derive a nip44 conversation key for remote signing."); @@ -1099,10 +1095,7 @@ function resolveAvailableNip46Ciphers( typeof nip44GetConversationKey === "function" ) { registerCipher("nip44", () => { - const conversationKey = nip44GetConversationKey( - hexToBytes(privateKey), - remotePubkey, - ); + const conversationKey = nip44GetConversationKey(privateKey, remotePubkey); if (!conversationKey) { throw new Error("Failed to derive a nip44 conversation key for remote signing."); @@ -1501,7 +1494,7 @@ export class Nip46RpcClient { if (!tools || typeof tools.getPublicKey !== "function") { throw new Error("Public key derivation is unavailable."); } - this.clientPublicKey = tools.getPublicKey(hexToBytes(this.clientPrivateKey)); + this.clientPublicKey = tools.getPublicKey(this.clientPrivateKey); if (!this.clientPublicKey || !HEX64_REGEX.test(this.clientPublicKey)) { throw new Error("Failed to derive a valid public key for the remote signer session."); } diff --git a/js/nostr/publishHelpers.js b/js/nostr/publishHelpers.js index ad0c149d..429fcd10 100644 --- a/js/nostr/publishHelpers.js +++ b/js/nostr/publishHelpers.js @@ -16,7 +16,6 @@ import { getCachedNostrTools, readToolkitFromScope, } from "./toolkit.js"; -import { hexToBytes } from "../utils/hex.js"; import { DEFAULT_NIP07_PERMISSION_METHODS } from "./nip07Permissions.js"; import { devLogger, userLogger } from "../utils/logger.js"; import { logRebroadcastCountFailure } from "./countDiagnostics.js"; @@ -135,7 +134,7 @@ export function signEventWithPrivateKey(event, privateKey) { throw new Error("Missing signing primitives"); } - const finalized = finalizeFn(prepared, hexToBytes(normalizedKey)); + const finalized = finalizeFn(prepared, normalizedKey); if ( !finalized || typeof finalized.id !== "string" || diff --git a/js/utils/hex.js b/js/utils/hex.js index 38839bb7..367f78bd 100644 --- a/js/utils/hex.js +++ b/js/utils/hex.js @@ -9,21 +9,3 @@ export function normalizeHexString(value) { export const normalizeHexId = normalizeHexString; export const normalizeHexPubkey = normalizeHexString; - -export function hexToBytes(hex) { - if (typeof hex !== "string") { - throw new TypeError("hexToBytes: expected string, got " + typeof hex); - } - const normalized = hex.trim(); - if (normalized.length % 2) throw new Error("hex string must have even length"); - const array = new Uint8Array(normalized.length / 2); - for (let i = 0; i < array.length; i++) { - const j = i * 2; - const byte = parseInt(normalized.substring(j, j + 2), 16); - if (Number.isNaN(byte) || byte < 0) { - throw new Error("Invalid hex string"); - } - array[i] = byte; - } - return array; -} diff --git a/package-lock.json b/package-lock.json index 7290e034..7ca86487 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "autoprefixer": "^10.4.24", "cross-env": "^10.1.0", "esbuild": "^0.27.2", - "jsdom": "^27.4.0", + "jsdom": "24.1.3", "nostr-tools": "2.19.4", "postcss": "^8.5.6", "tailwindcss": "^3.4.19", @@ -32,12 +32,6 @@ "stylelint": "^16.26.1" } }, - "node_modules/@acemir/cssom": { - "version": "0.9.31", - "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.31.tgz", - "integrity": "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==", - "license": "MIT" - }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -51,37 +45,18 @@ } }, "node_modules/@asamuzakjp/css-color": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.1.1.tgz", - "integrity": "sha512-B0Hv6G3gWGMn0xKJ0txEi/jM5iFpT3MfDxmhZFb4W047GvytCf1DHQ1D69W3zHI4yWe2aTZAA0JnbMZ7Xc8DuQ==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", "license": "MIT", "dependencies": { - "@csstools/css-calc": "^2.1.4", - "@csstools/css-color-parser": "^3.1.0", - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4", - "lru-cache": "^11.2.4" + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" } }, - "node_modules/@asamuzakjp/dom-selector": { - "version": "6.7.7", - "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.7.7.tgz", - "integrity": "sha512-8CO/UQ4tzDd7ula+/CVimJIVWez99UJlbMyIgk8xOnhAVPKLnBZmUFYVgugS441v2ZqUq5EnSh6B0Ua0liSFAA==", - "license": "MIT", - "dependencies": { - "@asamuzakjp/nwsapi": "^2.3.9", - "bidi-js": "^1.0.3", - "css-tree": "^3.1.0", - "is-potential-custom-element-name": "^1.0.1", - "lru-cache": "^11.2.5" - } - }, - "node_modules/@asamuzakjp/nwsapi": { - "version": "2.3.9", - "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", - "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", - "license": "MIT" - }, "node_modules/@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -226,6 +201,7 @@ "version": "1.0.26", "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.26.tgz", "integrity": "sha512-6boXK0KkzT5u5xOgF6TKB+CLq9SOpEGmkZw0g5n9/7yg85wab3UzSxB8TxhLJ31L4SGJ6BCFRw/iftTha1CJXA==", + "dev": true, "funding": [ { "type": "github", @@ -714,23 +690,6 @@ "node": ">=18" } }, - "node_modules/@exodus/bytes": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.10.0.tgz", - "integrity": "sha512-tf8YdcbirXdPnJ+Nd4UN1EXnz+IP2DI45YVEr3vvzcVTOyrApkmIB4zvOQVd3XPr7RXnfBtAx+PXImXOIU0Ajg==", - "license": "MIT", - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - }, - "peerDependencies": { - "@noble/hashes": "^1.8.0 || ^2.0.0" - }, - "peerDependenciesMeta": { - "@noble/hashes": { - "optional": true - } - } - }, "node_modules/@floating-ui/core": { "version": "1.7.4", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.4.tgz", @@ -816,22 +775,43 @@ "license": "MIT" }, "node_modules/@noble/ciphers": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-2.1.1.tgz", - "integrity": "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw==", + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.5.3.tgz", + "integrity": "sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w==", "license": "MIT", "funding": { "url": "https://paulmillr.com/funding/" } }, "node_modules/@noble/curves": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.0.1.tgz", - "integrity": "sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", "license": "MIT", "dependencies": { "@noble/hashes": "1.3.2" }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves/node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz", + "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==", + "license": "MIT", "engines": { "node": ">= 16" }, @@ -891,9 +871,21 @@ } }, "node_modules/@scure/base": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-2.0.0.tgz", - "integrity": "sha512-3E1kpuZginKkek01ovG8krQ0Z44E3DHPjc5S2rjJw9lZn3KSQOs8S7wqikF/AH7iRanHypj85uGyxk0XAyC37w==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz", + "integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "license": "MIT" + }, + "node_modules/@scure/bip32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.1.tgz", + "integrity": "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==", "license": "MIT", "dependencies": { "@noble/curves": "~1.1.0", @@ -904,28 +896,26 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/@scure/bip32": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-2.0.1.tgz", - "integrity": "sha512-4Md1NI5BzoVP+bhyJaY3K6yMesEFzNS1sE/cP+9nuvE7p/b0kx9XbpDHHFl8dHtufcbdHRUUQdRqLIPHN/s7yA==", + "node_modules/@scure/bip32/node_modules/@noble/curves": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.1.0.tgz", + "integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==", "license": "MIT", "dependencies": { - "@noble/curves": "2.0.1", - "@noble/hashes": "2.0.1", - "@scure/base": "2.0.0" + "@noble/hashes": "1.3.1" }, "funding": { "url": "https://paulmillr.com/funding/" } }, "node_modules/@scure/bip39": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-2.0.1.tgz", - "integrity": "sha512-PsxdFj/d2AcJcZDX1FXN3dDgitDDTmwf78rKZq1a6c1P1Nan1X/Sxc7667zU3U+AN60g7SxxP0YCVw2H/hBycg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.1.tgz", + "integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==", "license": "MIT", "dependencies": { - "@noble/hashes": "2.0.1", - "@scure/base": "2.0.0" + "@noble/hashes": "~1.3.0", + "@scure/base": "~1.1.0" }, "funding": { "url": "https://paulmillr.com/funding/" @@ -1133,6 +1123,12 @@ "node": ">=8" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, "node_modules/autoprefixer": { "version": "10.4.24", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.24.tgz", @@ -1185,15 +1181,6 @@ "baseline-browser-mapping": "dist/cli.js" } }, - "node_modules/bidi-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", - "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", - "license": "MIT", - "dependencies": { - "require-from-string": "^2.0.2" - } - }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -1309,6 +1296,19 @@ "qified": "^0.6.0" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -1489,6 +1489,18 @@ "dev": true, "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -1636,6 +1648,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", + "dev": true, "license": "MIT", "dependencies": { "mdn-data": "2.12.2", @@ -1658,40 +1671,35 @@ } }, "node_modules/cssstyle": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.7.tgz", - "integrity": "sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ==", + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", "license": "MIT", "dependencies": { - "@asamuzakjp/css-color": "^4.1.1", - "@csstools/css-syntax-patches-for-csstree": "^1.0.21", - "css-tree": "^3.1.0", - "lru-cache": "^11.2.4" + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" }, "engines": { - "node": ">=20" + "node": ">=18" } }, + "node_modules/cssstyle/node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "license": "MIT" + }, "node_modules/data-urls": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.1.tgz", - "integrity": "sha512-euIQENZg6x8mj3fO6o9+fOW8MimUI4PpD/fZBhJfeioZVy9TUpM4UY7KjQNVZFlqwJ0UdzRDzkycB997HEq1BQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", "license": "MIT", "dependencies": { - "whatwg-mimetype": "^5.0.0", - "whatwg-url": "^15.1.0" + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" }, "engines": { - "node": ">=20" - } - }, - "node_modules/data-urls/node_modules/whatwg-mimetype": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", - "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", - "license": "MIT", - "engines": { - "node": ">=20" + "node": ">=18" } }, "node_modules/debug": { @@ -1727,6 +1735,15 @@ "node": ">=4.0.0" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -1752,6 +1769,20 @@ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", "license": "MIT" }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -1804,6 +1835,51 @@ "is-arrayish": "^0.2.1" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { "version": "0.27.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", @@ -1971,6 +2047,22 @@ "dev": true, "license": "ISC" }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fraction.js": { "version": "5.3.4", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", @@ -2007,6 +2099,43 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -2111,6 +2240,18 @@ "dev": true, "license": "MIT" }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -2121,6 +2262,33 @@ "node": ">=8" } }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hashery": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.4.0.tgz", @@ -2154,15 +2322,15 @@ "license": "MIT" }, "node_modules/html-encoding-sniffer": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", - "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", "license": "MIT", "dependencies": { - "@exodus/bytes": "^1.6.0" + "whatwg-encoding": "^3.1.1" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "node": ">=18" } }, "node_modules/html-tags": { @@ -2214,6 +2382,18 @@ "node": ">=10.17.0" } }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ignore": { "version": "7.0.5", "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", @@ -2449,37 +2629,38 @@ } }, "node_modules/jsdom": { - "version": "27.4.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.4.0.tgz", - "integrity": "sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==", + "version": "24.1.3", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-24.1.3.tgz", + "integrity": "sha512-MyL55p3Ut3cXbeBEG7Hcv0mVM8pp8PBNWxRqchZnSfAiES1v1mRnMeFfaHWIPULpwsYfvO+ZmMZz5tGCnjzDUQ==", "license": "MIT", "dependencies": { - "@acemir/cssom": "^0.9.28", - "@asamuzakjp/dom-selector": "^6.7.6", - "@exodus/bytes": "^1.6.0", - "cssstyle": "^5.3.4", - "data-urls": "^6.0.0", - "decimal.js": "^10.6.0", - "html-encoding-sniffer": "^6.0.0", + "cssstyle": "^4.0.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.4.3", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^4.0.0", "http-proxy-agent": "^7.0.2", - "https-proxy-agent": "^7.0.6", + "https-proxy-agent": "^7.0.5", "is-potential-custom-element-name": "^1.0.1", - "parse5": "^8.0.0", + "nwsapi": "^2.2.12", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.7.1", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", - "tough-cookie": "^6.0.0", + "tough-cookie": "^4.1.4", "w3c-xmlserializer": "^5.0.0", - "webidl-conversions": "^8.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", "whatwg-mimetype": "^4.0.0", - "whatwg-url": "^15.1.0", - "ws": "^8.18.3", + "whatwg-url": "^14.0.0", + "ws": "^8.18.0", "xml-name-validator": "^5.0.0" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "node": ">=18" }, "peerDependencies": { - "canvas": "^3.0.0" + "canvas": "^2.11.2" }, "peerDependenciesMeta": { "canvas": { @@ -2554,12 +2735,18 @@ "license": "MIT" }, "node_modules/lru-cache": { - "version": "11.2.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz", - "integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==", - "license": "BlueOak-1.0.0", + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", "engines": { - "node": "20 || >=22" + "node": ">= 0.4" } }, "node_modules/mathml-tag-names": { @@ -2577,6 +2764,7 @@ "version": "2.12.2", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "dev": true, "license": "CC0-1.0" }, "node_modules/meow": { @@ -2635,7 +2823,6 @@ "version": "2.1.18", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", - "dev": true, "license": "MIT", "dependencies": { "mime-db": "~1.33.0" @@ -2648,7 +2835,6 @@ "version": "1.33.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -2789,6 +2975,12 @@ "node": ">=8" } }, + "node_modules/nwsapi": { + "version": "2.2.23", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", + "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -2866,9 +3058,9 @@ } }, "node_modules/parse5": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", - "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", "license": "MIT", "dependencies": { "entities": "^6.0.0" @@ -3323,6 +3515,18 @@ } } }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -3345,6 +3549,12 @@ "node": ">=20" } }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "license": "MIT" + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -3440,11 +3650,18 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" } }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", @@ -3485,6 +3702,12 @@ "node": ">=0.10.0" } }, + "node_modules/rrweb-cssom": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", + "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", + "license": "MIT" + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -3529,6 +3752,12 @@ ], "license": "MIT" }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, "node_modules/saxes": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", @@ -4131,24 +4360,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/tldts": { - "version": "7.0.19", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.19.tgz", - "integrity": "sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA==", - "license": "MIT", - "dependencies": { - "tldts-core": "^7.0.19" - }, - "bin": { - "tldts": "bin/cli.js" - } - }, - "node_modules/tldts-core": { - "version": "7.0.19", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.19.tgz", - "integrity": "sha512-lJX2dEWx0SGH4O6p+7FPwYmJ/bu1JbcGJ8RLaG9b7liIgZ85itUVEPbMtWRVrde/0fnDPEPHW10ZsKW3kVsE9A==", - "license": "MIT" - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -4162,27 +4373,30 @@ } }, "node_modules/tough-cookie": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", - "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", "license": "BSD-3-Clause", "dependencies": { - "tldts": "^7.0.5" + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" }, "engines": { - "node": ">=16" + "node": ">=6" } }, "node_modules/tr46": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", - "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", "license": "MIT", "dependencies": { "punycode": "^2.3.1" }, "engines": { - "node": ">=20" + "node": ">=18" } }, "node_modules/ts-interface-checker": { @@ -4204,6 +4418,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -4255,6 +4478,16 @@ "punycode": "^2.1.0" } }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -4284,12 +4517,25 @@ } }, "node_modules/webidl-conversions": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", - "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", "license": "BSD-2-Clause", "engines": { - "node": ">=20" + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" } }, "node_modules/whatwg-mimetype": { @@ -4302,16 +4548,16 @@ } }, "node_modules/whatwg-url": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.1.0.tgz", - "integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==", + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", "license": "MIT", "dependencies": { - "tr46": "^6.0.0", - "webidl-conversions": "^8.0.0" + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" }, "engines": { - "node": ">=20" + "node": ">=18" } }, "node_modules/which": { diff --git a/package.json b/package.json index f400a180..f29d74aa 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "autoprefixer": "^10.4.24", "cross-env": "^10.1.0", "esbuild": "^0.27.2", - "jsdom": "^27.4.0", + "jsdom": "24.1.3", "nostr-tools": "2.19.4", "postcss": "^8.5.6", "tailwindcss": "^3.4.19", diff --git a/releases/draft-20260131.md b/releases/draft-20260131.md new file mode 100644 index 00000000..ace4b38a --- /dev/null +++ b/releases/draft-20260131.md @@ -0,0 +1,15 @@ +# Release Draft - 2026-01-31 + +## Highlights + +### Webseed Stability (Unstable) + +A recent change aimed at fixing a webseed regression was reverted (PR #2002) to ensure stability in the unstable channel. This restores the previous behavior while a more robust fix is investigated. + +### Major Repository Update (Main) + +The main branch has received a significant update (`b429b1c`), incorporating extensive changes across the codebase. This synchronization brings `main` up to date with the latest developments. + +## Upgrade Notes + +No specific manual migration steps are required, but developers should be aware of the large changeset in `main`. diff --git a/scripts/agent/debug_frontend.py b/scripts/agent/debug_frontend.py index 978798d4..0441d740 100644 --- a/scripts/agent/debug_frontend.py +++ b/scripts/agent/debug_frontend.py @@ -1,37 +1,23 @@ -import asyncio -from playwright.async_api import async_playwright import sys +from playwright.sync_api import sync_playwright -async def run(): - async with async_playwright() as p: - browser = await p.chromium.launch() - page = await browser.new_page() +def run(): + with sync_playwright() as p: + browser = p.chromium.launch() + page = browser.new_page() - # Listen for console logs with location - def handle_console(msg): - loc = msg.location - print(f"console:{msg.type}: {msg.text} @ {loc['url']}:{loc['lineNumber']}") - - page.on("console", handle_console) - - # Listen for exceptions + page.on("console", lambda msg: print(f"console:{msg.type}: {msg.text}")) page.on("pageerror", lambda exc: print(f"pageerror: {exc}")) - - # Listen for failed requests - page.on("requestfailed", lambda req: print(f"requestfailed: {req.url} {req.failure}")) - - # Listen for 404s specifically - page.on("response", lambda res: print(f"response:{res.status}: {res.url}") if res.status >= 400 else None) + page.on("requestfailed", lambda req: print(f"requestfailed: {req.url} - {req.failure}")) try: - print("Navigating to http://localhost:8000...") - await page.goto("http://localhost:8000") - # Wait a bit for initialization - await page.wait_for_timeout(5000) + page.goto("http://localhost:8000") + # Wait a bit for JS to execute and errors to appear + page.wait_for_timeout(5000) except Exception as e: - print(f"Navigation error: {e}") + print(f"Error navigating: {e}") finally: - await browser.close() + browser.close() if __name__ == "__main__": - asyncio.run(run()) + run() diff --git a/scripts/agent/interop-test.mjs b/scripts/agent/interop-test.mjs index 0d6b7fb4..1df7bb14 100644 --- a/scripts/agent/interop-test.mjs +++ b/scripts/agent/interop-test.mjs @@ -1,242 +1,185 @@ - import './setup-test-env.js'; import { NostrClient } from '../../js/nostr/client.js'; -import { decryptDM } from '../../js/dmDecryptor.js'; -import { - buildVideoPostEvent, - buildLegacyDirectMessageEvent, - buildViewEvent, - NOTE_TYPES -} from '../../js/nostrEventSchemas.js'; -import { generateSecretKey, getPublicKey } from 'nostr-tools'; -import fs from 'fs'; -import path from 'path'; +import { buildVideoPostEvent, buildViewEvent, NOTE_TYPES } from '../../js/nostrEventSchemas.js'; +import * as NostrTools from 'nostr-tools'; -// Configuration -const TEST_RELAYS = [ - 'wss://relay.damus.io', - 'wss://nos.lol' +const bytesToHex = (bytes) => { + return Buffer.from(bytes).toString('hex'); +}; + +const RELAYS = [ + "wss://relay.damus.io", + "wss://nos.lol" ]; -// Artifacts -const ARTIFACTS_DIR = 'artifacts'; -const LOG_FILE = path.join(ARTIFACTS_DIR, `interop-${new Date().toISOString().split('T')[0].replace(/-/g, '')}.log`); +async function runTests() { + console.log("Starting Protocol & Interop Tests..."); -// Logger -function log(message) { - const timestamp = new Date().toISOString(); - const line = `[${timestamp}] ${message}`; - console.log(line); - fs.appendFileSync(LOG_FILE, line + '\n'); + // --- Setup --- + console.log("[Setup] Generating ephemeral keys..."); + const aliceSkBytes = NostrTools.generateSecretKey(); + const aliceSk = bytesToHex(aliceSkBytes); + const alicePk = NostrTools.getPublicKey(aliceSkBytes); + + const bobSkBytes = NostrTools.generateSecretKey(); + const bobSk = bytesToHex(bobSkBytes); + const bobPk = NostrTools.getPublicKey(bobSkBytes); + + console.log(`[Setup] Alice: ${alicePk}`); + console.log(`[Setup] Bob: ${bobPk}`); + + const aliceClient = new NostrClient(); + aliceClient.relays = [...RELAYS]; + aliceClient.readRelays = [...RELAYS]; + aliceClient.writeRelays = [...RELAYS]; + + const bobClient = new NostrClient(); + bobClient.relays = [...RELAYS]; + bobClient.readRelays = [...RELAYS]; + bobClient.writeRelays = [...RELAYS]; + + console.log("[Setup] Connecting clients..."); + // Initialize to connect + await aliceClient.init(); // This might try to load stored session, but shouldn't matter + await bobClient.init(); + + // Register signers + await aliceClient.registerPrivateKeySigner({ privateKey: aliceSk, pubkey: alicePk }); + aliceClient.pubkey = alicePk; // Explicitly set client pubkey to ensure correct signer resolution + + await bobClient.registerPrivateKeySigner({ privateKey: bobSk, pubkey: bobPk }); + bobClient.pubkey = bobPk; // Explicitly set client pubkey to ensure correct signer resolution + + let testsPassed = 0; + let testsTotal = 0; + + // --- Test A: Video Post --- + testsTotal++; + console.log("\n[Test A] Publishing VIDEO_POST..."); + try { + const videoPayload = { + version: 3, + title: "Interop Test Video " + Date.now(), + description: "This is a test video event.", + videoRootId: "interop-test-" + Date.now(), + mode: "dev", + isPrivate: false, + isNsfw: false, + isForKids: false, + enableComments: true + }; + + // We use signAndPublishEvent directly with a manually built event to test the schema helper + const event = buildVideoPostEvent({ + pubkey: alicePk, + created_at: Math.floor(Date.now() / 1000), + dTagValue: videoPayload.videoRootId, + content: videoPayload + }); + + const { signedEvent } = await aliceClient.signAndPublishEvent(event); + console.log(`[Test A] Published event ${signedEvent.id}`); + + // Verify + console.log("[Test A] Verifying roundtrip..."); + // Give relays a moment + await new Promise(r => setTimeout(r, 2000)); + + // getEventById returns the Video object (converted), but we can ask for raw too + const result = await bobClient.getEventById(signedEvent.id, { includeRaw: true }); + if (!result || !result.rawEvent) throw new Error("Failed to fetch event back"); + + const fetched = result.rawEvent; + if (fetched.id !== signedEvent.id) throw new Error("Fetched ID mismatch"); + + // Parse content + const content = typeof fetched.content === 'string' ? JSON.parse(fetched.content) : fetched.content; + if (content.title !== videoPayload.title) throw new Error("Content mismatch: title"); + + console.log("[Test A] PASSED"); + testsPassed++; + } catch (e) { + console.error("[Test A] FAILED:", e); + } + + // --- Test B: View Event --- + testsTotal++; + console.log("\n[Test B] Publishing VIEW_EVENT..."); + try { + const viewEvent = buildViewEvent({ + pubkey: alicePk, + created_at: Math.floor(Date.now() / 1000), + pointerValue: "test-video-pointer", // Arbitrary pointer for test + content: "view-test" + }); + + const { signedEvent: signedView } = await aliceClient.signAndPublishEvent(viewEvent); + console.log(`[Test B] Published view event ${signedView.id}`); + + await new Promise(r => setTimeout(r, 2000)); + + // Bob fetches raw event + const fetchedView = await bobClient.fetchRawEventById(signedView.id); + if (!fetchedView) throw new Error("Failed to fetch view event"); + if (fetchedView.id !== signedView.id) throw new Error("View event ID mismatch"); + + console.log("[Test B] PASSED"); + testsPassed++; + } catch (e) { + console.error("[Test B] FAILED:", e); + } + + // --- Test C: Direct Message --- + testsTotal++; + console.log("\n[Test C] Sending Direct Message..."); + try { + const message = "Hello Bob, this is Alice " + Date.now(); + const npubBob = NostrTools.nip19.npubEncode(bobPk); + + const sendResult = await aliceClient.sendDirectMessage(npubBob, message); + if (!sendResult.ok) throw new Error("Failed to send DM: " + (sendResult.error || "unknown")); + console.log("[Test C] DM sent successfully"); + + await new Promise(r => setTimeout(r, 3000)); + + console.log("[Test C] Bob checking messages..."); + // Force bob to list messages (which triggers decryption using his registered signer) + const messages = await bobClient.listDirectMessages(bobPk, { limit: 5 }); + + const received = messages.find(m => m.plaintext === message); + if (!received) { + console.log("Received messages:", messages.map(m => m.plaintext)); + throw new Error("Bob did not find the specific message"); + } + + if (received.sender.pubkey !== alicePk) { + console.log(`[Test C] Sender mismatch: Expected ${alicePk}, got ${received.sender.pubkey}`); + throw new Error("Sender mismatch"); + } + + console.log("[Test C] PASSED"); + testsPassed++; + } catch (e) { + console.error("[Test C] FAILED:", e); + } + + // --- Summary --- + console.log("\n---------------------------------------------------"); + console.log(`Tests Completed: ${testsPassed}/${testsTotal}`); + + aliceClient.logout(); + bobClient.logout(); + + if (testsPassed === testsTotal) { + console.log("ALL TESTS PASSED"); + process.exit(0); + } else { + console.error("SOME TESTS FAILED"); + process.exit(1); + } } -// Generate Keys -const bytesToHex = (bytes) => Buffer.from(bytes).toString('hex'); -const EPHEMERAL_SK_BYTES = generateSecretKey(); -const EPHEMERAL_SK = bytesToHex(EPHEMERAL_SK_BYTES); -const EPHEMERAL_PK = getPublicKey(EPHEMERAL_SK_BYTES); - -async function runInteropTest() { - // Ensure artifacts dir - if (!fs.existsSync(ARTIFACTS_DIR)) { - fs.mkdirSync(ARTIFACTS_DIR); - } - fs.writeFileSync(LOG_FILE, ''); // Clear log - - let nodeClient; - let exitCode = 0; - const artifacts = {}; - - try { - log('--- Interop Test Started ---'); - log(`Ephemeral Pubkey: ${EPHEMERAL_PK}`); - log(`Relays: ${TEST_RELAYS.join(', ')}`); - - // 1. Initialize Node Client - log('Initializing NostrClient...'); - nodeClient = new NostrClient(); - nodeClient.relays = [...TEST_RELAYS]; - nodeClient.writeRelays = [...TEST_RELAYS]; - nodeClient.readRelays = [...TEST_RELAYS]; - - // Register ephemeral signer - await nodeClient.registerPrivateKeySigner({ privateKey: EPHEMERAL_SK }); - log('Signer registered.'); - - // Initialize pool (lazy, but explicit init ensures connection attempts) - log('Warming up connections...'); - await nodeClient.ensurePool(); // Ensure pool is created - - // 2. Test A: Video Post - log('Test A: Publishing VIDEO_POST...'); - const videoEventTemplate = buildVideoPostEvent({ - pubkey: EPHEMERAL_PK, - created_at: Math.floor(Date.now() / 1000), - dTagValue: `interop-test-${Date.now()}`, - content: { - version: 3, - title: 'Interop Test Video', - url: 'https://example.com/video.mp4', - description: 'A video to verify protocol interoperability.', - mode: 'live', - videoRootId: `interop-root-${Date.now()}` - } - }); - - const { signedEvent: publishedVideo } = await nodeClient.signAndPublishEvent(videoEventTemplate, { - context: 'interop-video' - }); - log(`Published Video ID: ${publishedVideo.id}`); - artifacts.videoEventId = publishedVideo.id; - - // Fetch back verification with Retry - log('Verifying Video Post (fetch by ID)...'); - let fetchedVideo = null; - const maxRetries = 5; - const initialDelay = 2000; - - for (let i = 0; i < maxRetries; i++) { - const delay = initialDelay * (i + 1); - log(`Attempt ${i + 1}/${maxRetries}: Waiting ${delay}ms...`); - await new Promise(r => setTimeout(r, delay)); - - try { - fetchedVideo = await nodeClient.fetchRawEventById(publishedVideo.id); - if (fetchedVideo) { - log('Video event found!'); - break; - } - } catch (e) { - log(`Fetch attempt failed: ${e.message}`); - } - } - - if (!fetchedVideo) { - throw new Error(`Failed to fetch video event ${publishedVideo.id} after ${maxRetries} retries.`); - } - if (fetchedVideo.content !== publishedVideo.content) { - throw new Error('Fetched video content mismatch.'); - } - log('Video Post verified successfully.'); - - - // 3. Test B: View Event - log('Test B: Publishing VIEW_EVENT...'); - const viewEventTemplate = buildViewEvent({ - pubkey: EPHEMERAL_PK, - created_at: Math.floor(Date.now() / 1000), - pointerValue: publishedVideo.id, // Pointing to the video we just created - pointerTag: ['e', publishedVideo.id], - includeSessionTag: true // simulate session - }); - - const { signedEvent: publishedView } = await nodeClient.signAndPublishEvent(viewEventTemplate, { - context: 'interop-view' - }); - log(`Published View Event ID: ${publishedView.id}`); - artifacts.viewEventId = publishedView.id; - - // No fetch back for view event to save time, assuming if Video worked, this works too. - log('View Event published.'); - - - // 4. Test C: Direct Message (Legacy NIP-04) - log('Test C: Publishing Encrypted DM...'); - const dmMessage = "Interop Test Secret Message"; - const signer = await nodeClient.ensureActiveSignerForPubkey(EPHEMERAL_PK); - - // Encrypt (Self-DM) - const ciphertext = await signer.nip04Encrypt(EPHEMERAL_PK, dmMessage); - - const dmTemplate = buildLegacyDirectMessageEvent({ - pubkey: EPHEMERAL_PK, - created_at: Math.floor(Date.now() / 1000), - recipientPubkey: EPHEMERAL_PK, - ciphertext: ciphertext - }); - - const { signedEvent: publishedDM } = await nodeClient.signAndPublishEvent(dmTemplate, { - context: 'interop-dm' - }); - log(`Published DM ID: ${publishedDM.id}`); - artifacts.dmEventId = publishedDM.id; - - // Fetch and Decrypt with Retry - log('Verifying DM Decryption...'); - let fetchedDM = null; - for (let i = 0; i < maxRetries; i++) { - const delay = initialDelay * (i + 1); - log(`Attempt ${i + 1}/${maxRetries}: Waiting ${delay}ms...`); - await new Promise(r => setTimeout(r, delay)); - - try { - fetchedDM = await nodeClient.fetchRawEventById(publishedDM.id); - if (fetchedDM) { - log('DM event found!'); - break; - } - } catch (e) { - log(`Fetch attempt failed: ${e.message}`); - } - } - - if (!fetchedDM) { - throw new Error(`Failed to fetch DM event ${publishedDM.id} after ${maxRetries} retries.`); - } - - const decryptContext = await nodeClient.buildDmDecryptContext(EPHEMERAL_PK); - const decryptionResult = await decryptDM(fetchedDM, decryptContext); - - if (!decryptionResult.ok) { - log(`Decryption failed: ${JSON.stringify(decryptionResult.errors)}`); - throw new Error('DM Decryption failed.'); - } - - if (decryptionResult.plaintext !== dmMessage) { - throw new Error(`Decryption mismatch. Expected "${dmMessage}", got "${decryptionResult.plaintext}"`); - } - log(`DM Decrypted successfully: "${decryptionResult.plaintext}"`); - - log('--- Interop Test PASSED ---'); - - // Save Summary - const summary = { - timestamp: new Date().toISOString(), - status: "success", - relays: TEST_RELAYS, - ephemeralPubkey: EPHEMERAL_PK, - artifacts - }; - const summaryFile = path.join(ARTIFACTS_DIR, `interop-summary-${new Date().toISOString().split('T')[0].replace(/-/g, '')}.json`); - fs.writeFileSync(summaryFile, JSON.stringify(summary, null, 2)); - log(`Summary saved to ${summaryFile}`); - - } catch (err) { - log(`--- Interop Test FAILED: ${err.message} ---`); - if (err.stack) log(err.stack); - exitCode = 1; - - // Save Failure Summary - const summary = { - timestamp: new Date().toISOString(), - status: "failed", - error: err.message, - relays: TEST_RELAYS, - ephemeralPubkey: EPHEMERAL_PK, - artifacts - }; - const summaryFile = path.join(ARTIFACTS_DIR, `interop-summary-${new Date().toISOString().split('T')[0].replace(/-/g, '')}.json`); - fs.writeFileSync(summaryFile, JSON.stringify(summary, null, 2)); - - } finally { - log('Cleaning up...'); - if (nodeClient && nodeClient.pool) { - // Close pool connections if possible (NostrTools SimplePool might not expose close easily in all versions, - // but we can try client logic if implemented or just exit) - // The client logic might not fully close sockets immediately, process.exit will handle it. - } - process.exit(exitCode); - } -} - -runInteropTest(); +runTests().catch(e => { + console.error("Unhandled execution error:", e); + process.exit(1); +}); diff --git a/scripts/agent/smoke-test.mjs b/scripts/agent/smoke-test.mjs new file mode 100644 index 00000000..83d4fc95 --- /dev/null +++ b/scripts/agent/smoke-test.mjs @@ -0,0 +1,271 @@ + +import './setup-test-env.js'; +import { startRelay } from './load-test-relay.mjs'; +import { NostrClient } from '../../js/nostr/client.js'; +import { decryptDM } from '../../js/dmDecryptor.js'; +import { + buildVideoPostEvent, + buildLegacyDirectMessageEvent +} from '../../js/nostrEventSchemas.js'; +import { generateSecretKey, getPublicKey } from 'nostr-tools'; +import { chromium } from 'playwright'; +import { spawn } from 'child_process'; +import fs from 'fs'; +import path from 'path'; + +const RELAY_PORT = 8899; +const RELAY_URL = `ws://localhost:${RELAY_PORT}`; +const HTTP_PORT = 8000; +const HTTP_URL = `http://localhost:${HTTP_PORT}`; + +// Artifacts +const ARTIFACTS_DIR = 'artifacts'; +const LOG_FILE = path.join(ARTIFACTS_DIR, `smoke-${new Date().toISOString().split('T')[0].replace(/-/g, '')}.log`); +const SCREENSHOT_FILE = path.join(ARTIFACTS_DIR, 'smoke-test-ui.png'); + +// Logger +function log(message) { + const timestamp = new Date().toISOString(); + const line = `[${timestamp}] ${message}`; + console.log(line); + fs.appendFileSync(LOG_FILE, line + '\n'); +} + +// Generate Keys +const bytesToHex = (bytes) => Buffer.from(bytes).toString('hex'); +const EPHEMERAL_SK_BYTES = generateSecretKey(); +const EPHEMERAL_SK = bytesToHex(EPHEMERAL_SK_BYTES); +const EPHEMERAL_PK = getPublicKey(EPHEMERAL_SK_BYTES); + +async function startHttpServer() { + log('Starting HTTP Server...'); + const serve = spawn('npx', ['serve', '.', '-p', String(HTTP_PORT)], { + stdio: 'ignore', + shell: true + }); + + const maxRetries = 20; + for (let i = 0; i < maxRetries; i++) { + try { + await fetch(HTTP_URL); + log('HTTP Server is ready.'); + return serve; + } catch (e) { + await new Promise(r => setTimeout(r, 250)); + } + } + serve.kill(); + throw new Error('HTTP Server failed to start within timeout.'); +} + +async function runSmokeTest() { + // Ensure artifacts dir + if (!fs.existsSync(ARTIFACTS_DIR)) { + fs.mkdirSync(ARTIFACTS_DIR); + } + fs.writeFileSync(LOG_FILE, ''); // Clear log + + let relayServer; + let httpServer; + let browser; + let nodeClient; + let exitCode = 0; + + try { + log('--- Smoke Test Started ---'); + log(`Ephemeral Pubkey: ${EPHEMERAL_PK}`); + + // 1. Start Infrastructure + relayServer = startRelay(RELAY_PORT); + httpServer = await startHttpServer(); + + // 2. Initialize Node Client + log('Initializing Node Client...'); + nodeClient = new NostrClient(); + nodeClient.relays = [RELAY_URL]; + nodeClient.writeRelays = [RELAY_URL]; + nodeClient.readRelays = [RELAY_URL]; + + await nodeClient.registerPrivateKeySigner({ privateKey: EPHEMERAL_SK }); + await nodeClient.init(); + log('Node Client connected.'); + + // 3. Publish Video Event (Node) + log('Node: Publishing Video Post...'); + const videoEventTemplate = buildVideoPostEvent({ + pubkey: EPHEMERAL_PK, + created_at: Math.floor(Date.now() / 1000), + dTagValue: `smoke-test-${Date.now()}`, + content: { + version: 3, + title: 'Smoke Test Video', + url: 'https://example.com/video.mp4', + description: 'A video to verify feed rendering.', + mode: 'live', + videoRootId: `smoke-root-${Date.now()}` + } + }); + + const { signedEvent: publishedVideo } = await nodeClient.signAndPublishEvent(videoEventTemplate, { + context: 'smoke-video' + }); + log(`Node: Published Video ID: ${publishedVideo.id}`); + + // 4. Publish DM (Node -> Self) + log('Node: Publishing Encrypted DM...'); + const dmMessage = "Smoke Test Secret Message"; + const signer = await nodeClient.ensureActiveSignerForPubkey(EPHEMERAL_PK); + const ciphertext = await signer.nip04Encrypt(EPHEMERAL_PK, dmMessage); + + const dmTemplate = buildLegacyDirectMessageEvent({ + pubkey: EPHEMERAL_PK, + created_at: Math.floor(Date.now() / 1000), + recipientPubkey: EPHEMERAL_PK, + ciphertext: ciphertext + }); + + const { signedEvent: publishedDM } = await nodeClient.signAndPublishEvent(dmTemplate, { + context: 'smoke-dm' + }); + log(`Node: Published DM ID: ${publishedDM.id}`); + + // 5. Browser Test (Playwright) + log('Browser: Launching...'); + browser = await chromium.launch({ headless: true }); + const context = await browser.newContext(); + + // Inject LocalStorage for Auto-Login and Relay Config + const savedProfiles = { + entries: [{ pubkey: EPHEMERAL_PK, authType: "nip07" }], + activePubkey: EPHEMERAL_PK, + version: 1 + }; + const relayList = [{ url: RELAY_URL, mode: "both" }]; + + // Note: Key format derived from investigation: bitvid:profile::relayList:v1 + const relayStorageKey = `bitvid:profile:${EPHEMERAL_PK}:relayList:v1`; + + await context.addInitScript(({ profiles, relays, relayKey }) => { + // Disable Whitelist Mode to allow fresh pubkey login + window.localStorage.setItem('bitvid_admin_whitelist_mode', 'false'); + + window.localStorage.setItem('bitvid:savedProfiles:v1', JSON.stringify(profiles)); + window.localStorage.setItem(relayKey, JSON.stringify(relays)); + // Mock NIP-07 to avoid errors if app tries to use it (though we won't sign in browser) + window.nostr = { + getPublicKey: async () => profiles.activePubkey, + signEvent: async () => { throw new Error("Mock NIP-07 cannot sign"); } + }; + }, { profiles: savedProfiles, relays: relayList, relayKey: relayStorageKey }); + + const page = await context.newPage(); + + log(`Browser: Navigating to ${HTTP_URL}...`); + + // Capture Console + page.on('console', msg => log(`[Browser Console] ${msg.type()}: ${msg.text()}`)); + page.on('pageerror', err => log(`[Browser Error] ${err.message}`)); + + await page.goto(HTTP_URL); + + // Verify Login (Profile Button presence) + log('Browser: Verifying Login...'); + // #profileButton might be hidden initially, wait for it + try { + await page.waitForSelector('#profileButton', { state: 'visible', timeout: 15000 }); + log('Browser: Login verified (Profile button visible).'); + } catch (e) { + log('Browser: Login verification failed (Profile button not found). Dumping localStorage...'); + const storage = await page.evaluate(() => JSON.stringify(localStorage)); + log(`[Browser Storage] ${storage}`); + await page.screenshot({ path: path.join(ARTIFACTS_DIR, 'smoke-fail-login.png') }); + } + + // Verify Feed (Video Card presence) + log('Browser: Verifying Video in Feed...'); + // Look for the video title + try { + // Wait longer for feed to load (relays might be slow) + await page.waitForSelector(`text=Smoke Test Video`, { timeout: 60000 }); + log('Browser: Video found in feed!'); + } catch (e) { + log('Browser: Video NOT found in feed. Taking screenshot...'); + await page.screenshot({ path: path.join(ARTIFACTS_DIR, 'smoke-fail-feed.png') }); + + // Debug: Check relays + const relayState = await page.evaluate(() => { + // Try to access global nostrClient if exposed, or check localStorage again + return { + localStorage: JSON.stringify(localStorage), + // If we can't access client directly, we rely on logs + }; + }); + log(`[Browser Debug] Storage: ${relayState.localStorage}`); + + throw new Error('Browser: Video NOT found in feed.'); + } + + // Screenshot + await page.screenshot({ path: SCREENSHOT_FILE }); + log(`Browser: Screenshot saved to ${SCREENSHOT_FILE}`); + + // 6. Node Verification (DM Decrypt) + log('Node: Verifying DM Decryption...'); + // Fetch back + await new Promise(r => setTimeout(r, 500)); + const fetchedDM = await nodeClient.fetchRawEventById(publishedDM.id); + if (!fetchedDM) { + throw new Error('Node: Failed to fetch DM back.'); + } + + const decryptContext = await nodeClient.buildDmDecryptContext(EPHEMERAL_PK); + const decryptionResult = await decryptDM(fetchedDM, decryptContext); + + if (!decryptionResult.ok) { + log(`Node: Decryption failed: ${JSON.stringify(decryptionResult.errors)}`); + throw new Error('Node: Decryption failed.'); + } + + if (decryptionResult.plaintext !== dmMessage) { + throw new Error(`Node: Decryption mismatch. Got "${decryptionResult.plaintext}"`); + } + log(`Node: DM Decrypted successfully: "${decryptionResult.plaintext}"`); + + log('--- Smoke Test PASSED ---'); + + // Generate JSON Summary + const summary = { + timestamp: new Date().toISOString(), + status: "success", + details: { + videoEventId: publishedVideo.id, + dmEventId: publishedDM.id, + ephemeralPubkey: EPHEMERAL_PK + } + }; + const summaryFile = path.join(ARTIFACTS_DIR, `smoke-summary-${new Date().toISOString().split('T')[0].replace(/-/g, '')}.json`); + fs.writeFileSync(summaryFile, JSON.stringify(summary, null, 2)); + log(`Summary saved to ${summaryFile}`); + + } catch (err) { + log(`--- Smoke Test FAILED: ${err.message} ---`); + if (err.stack) log(err.stack); + exitCode = 1; + } finally { + // Cleanup + log('Cleaning up...'); + if (nodeClient && nodeClient.pool) { + // Close pool connections + if (typeof nodeClient.pool.close === 'function') { + nodeClient.pool.close(nodeClient.relays); + } + } + if (browser) await browser.close(); + if (httpServer) httpServer.kill(); + if (relayServer) await relayServer.close(); + + process.exit(exitCode); + } +} + +runSmokeTest(); diff --git a/scripts/automated-pr-review.mjs b/scripts/automated-pr-review.mjs index 617db161..a767c60d 100644 --- a/scripts/automated-pr-review.mjs +++ b/scripts/automated-pr-review.mjs @@ -129,10 +129,6 @@ async function reviewPR(prNumber, baseRef) { hasFailures = true; commentBody += '### ⚠️ Lint Warnings/Errors\n\n'; commentBody += '```\n' + lintRes.stdout + '\n' + lintRes.stderr + '\n```\n\n'; - - if (lintRes.stdout.includes('stylelint') || lintRes.stderr.includes('stylelint')) { - commentBody += '> **💡 Tip:** Try running `npm run lint:css -- --fix` to automatically resolve some style issues.\n\n'; - } } // 4. Unit Tests @@ -142,19 +138,6 @@ async function reviewPR(prNumber, baseRef) { hasFailures = true; commentBody += '### ❌ Test Failures\n\n'; commentBody += '```\n' + testRes.stdout + '\n' + testRes.stderr + '\n```\n\n'; - - // Extract failing test files - const failingTests = []; - const failureRegex = /✖\s+(tests\/[\w\-\.\/]+)\s+failed with exit code 1/g; - const testOutput = testRes.stdout + '\n' + testRes.stderr; - let match; - while ((match = failureRegex.exec(testOutput)) !== null) { - failingTests.push(match[1]); - } - - if (failingTests.length > 0) { - commentBody += `> **Suggested areas to inspect:** ${failingTests.map(f => `\`${f}\``).join(', ')}\n`; - } commentBody += '> **Suggestion:** Inspect the stack trace above. Run `npm run test:unit` locally to reproduce.\n\n'; } else { commentBody += '### ✅ Tests Passed\n\nAll unit tests passed.\n\n'; @@ -176,10 +159,7 @@ async function reviewPR(prNumber, baseRef) { if (sensitiveChanges.length > 0) { commentBody += '### 🛡️ Guardrails\n\n'; commentBody += 'This PR modifies sensitive files. **Security and Protocol Review Required.**\n'; - - // Explicit flags for CI/bots to pick up commentBody += '`requires-security-review` `requires-protocol-review`\n\n'; - commentBody += '
Sensitive Files Touched\n\n'; sensitiveChanges.forEach(f => commentBody += `- ${f}\n`); commentBody += '\n
\n\n'; diff --git a/scripts/check-design-tokens.mjs b/scripts/check-design-tokens.mjs index a63e66eb..85d6d865 100755 --- a/scripts/check-design-tokens.mjs +++ b/scripts/check-design-tokens.mjs @@ -42,7 +42,7 @@ const VALUE_ALLOWLIST = new Map([ ]); const MARKUP_EXTENSIONS = new Set([".html", ".md", ".mdx", ".markdown", ".njk", ".nunjucks"]); -const MARKUP_IGNORED_DIRECTORIES = new Set(["node_modules", ".git", "issues", "scripts"]); +const MARKUP_IGNORED_DIRECTORIES = new Set(["node_modules", ".git"]); const CLASS_ATTRIBUTE_PATTERN = /class\s*=\s*(["'])(.*?)\1/gis; const BRACKET_UTILITY_PATTERN = /\[[^\]]+\]/g; const CHECK_MODES = new Set(["all", "tokens", "brackets"]); diff --git a/scripts/check-hex.js b/scripts/check-hex.js index 88fb65b0..33976245 100755 --- a/scripts/check-hex.js +++ b/scripts/check-hex.js @@ -32,8 +32,7 @@ const IGNORED_GLOBS = [ 'tests/visual/embed-layout.spec.ts', 'ai/**', '**/*.ai', - 'issues/**', - 'scripts/agent/**' + 'releases/**' ]; const gitArgs = [ diff --git a/scripts/check-inline-styles.mjs b/scripts/check-inline-styles.mjs index 56e974e4..2ee3858a 100644 --- a/scripts/check-inline-styles.mjs +++ b/scripts/check-inline-styles.mjs @@ -26,8 +26,6 @@ const IGNORED_DIRS = new Set([ "logs", "playwright-report", "test-results", - "issues", - "scripts", ]); const DIST_SCAN_PREFIXES = new Set([ diff --git a/tests/hashtag-preferences.test.mjs b/tests/hashtag-preferences.test.mjs index 411de593..370d71f6 100644 --- a/tests/hashtag-preferences.test.mjs +++ b/tests/hashtag-preferences.test.mjs @@ -45,9 +45,6 @@ test.beforeEach(async () => { restoreNostrClient(); restoreRelayManager(); window.nostr = originalWindowNostr; - if (nostrClient.extensionPermissionCache) { - nostrClient.extensionPermissionCache.clear(); - } // Setup default mock signer to prevent auto-save errors setActiveSigner({ @@ -259,7 +256,7 @@ test( let fetchCalls = 0; nostrClient.fetchListIncrementally = async () => { fetchCalls += 1; - return [event]; // ALWAYS return the event + return fetchCalls <= 4 ? [event] : []; }; nostrClient.relays = relayUrls; nostrClient.writeRelays = relayUrls; @@ -269,9 +266,9 @@ test( // Create a mock window.nostr that properly simulates extension behavior. // Specifically, enable() needs to exist so nostrClient detects it as an extension. window.nostr = { - enable: async () => Promise.resolve(true), + enable: async () => {}, + getPublicKey: async () => pubkey, nip04: { - encrypt: async () => "cipher", decrypt: async () => { if (!permissionState.enabled) throw new Error("permission denied"); decryptCalls.push("nip04"); @@ -282,10 +279,6 @@ test( }); }, }, - nip44: { - encrypt: async () => "cipher", - decrypt: async () => "plaintext", - } }; clearActiveSigner(); @@ -580,7 +573,6 @@ test( // Mock window.nostr window.nostr = { - enable: async () => Promise.resolve(true), nip04: { decrypt: async (pk, ciphertext) => { if (pk !== pubkey) throw new Error("Wrong pubkey"); @@ -629,7 +621,6 @@ test( // Mock window.nostr window.nostr = { - enable: async () => Promise.resolve(true), nip04: { decrypt: async (pk, ciphertext) => { if (pk !== pubkey) throw new Error("Wrong pubkey"); diff --git a/tests/nostr-login-permissions.test.mjs b/tests/nostr-login-permissions.test.mjs index 42218683..7d56fc2a 100644 --- a/tests/nostr-login-permissions.test.mjs +++ b/tests/nostr-login-permissions.test.mjs @@ -165,25 +165,31 @@ describe("NIP-07 Login Permissions", () => { const pubkey = result.pubkey; assert.equal(pubkey, HEX_PUBKEY); assert.ok(env.enableCalls.length >= 1, "extension.enable should be invoked"); - - const permissionsToCheck = [ - "nip04.encrypt", - "nip04.decrypt", - "nip44.encrypt", - "nip44.decrypt", - ...EXPECTED_ENCRYPTION_PERMISSIONS - ]; - - for (const method of permissionsToCheck) { + assert.ok( + nostrClient.extensionPermissionCache.has("nip04.encrypt"), + "nip04.encrypt permission should be tracked as granted", + ); + assert.ok( + nostrClient.extensionPermissionCache.has("nip04.decrypt"), + "nip04.decrypt permission should be tracked as granted", + ); + assert.ok( + nostrClient.extensionPermissionCache.has("nip44.encrypt"), + "nip44.encrypt permission should be tracked as granted", + ); + assert.ok( + nostrClient.extensionPermissionCache.has("nip44.decrypt"), + "nip44.decrypt permission should be tracked as granted", + ); + for (const method of EXPECTED_ENCRYPTION_PERMISSIONS) { assert.ok( nostrClient.extensionPermissionCache.has(method), - `${method} permission should be tracked as granted` + `${method} permission should be tracked as granted`, ); } - assert.ok( nostrClient.extensionPermissionCache.has("sign_event"), - "sign_event permission should be tracked as granted" + "sign_event permission should be tracked as granted", ); } finally { env.restore(); @@ -214,18 +220,18 @@ describe("NIP-07 Login Permissions", () => { }, }; - await nip07Provider.login({ nostrClient }); - + const result = await nip07Provider.login({ nostrClient }); + const pubkey = result.pubkey; + assert.equal(pubkey, HEX_PUBKEY); for (const method of EXPECTED_ENCRYPTION_PERMISSIONS) { assert.ok( nostrClient.extensionPermissionCache.has(method), - `nostrClient should track ${method} after login` + `nostrClient should track ${method} after login`, ); } const storedRaw = localStorage.getItem(PERMISSIONS_STORAGE_KEY); assert.ok(storedRaw, "extension permissions should persist to localStorage"); - let storedMethods = []; try { const parsed = JSON.parse(storedRaw); @@ -246,10 +252,9 @@ describe("NIP-07 Login Permissions", () => { for (const method of EXPECTED_ENCRYPTION_PERMISSIONS) { assert.ok( storedSet.has(method), - `stored permissions should include ${method}` + `stored permissions should include ${method}`, ); } - const baselineEnableCalls = env.enableCalls.length; const freshClient = new NostrClient(); freshClient.pubkey = HEX_PUBKEY; @@ -257,7 +262,7 @@ describe("NIP-07 Login Permissions", () => { for (const method of EXPECTED_ENCRYPTION_PERMISSIONS) { assert.ok( freshClient.extensionPermissionCache.has(method), - `fresh client should hydrate ${method} from storage` + `fresh client should hydrate ${method} from storage`, ); } @@ -269,7 +274,7 @@ describe("NIP-07 Login Permissions", () => { assert.equal( env.enableCalls.length, baselineEnableCalls, - `cached permissions should prevent extra enable() calls for ${method}` + `cached permissions should prevent extra enable() calls for ${method}`, ); } @@ -283,7 +288,7 @@ describe("NIP-07 Login Permissions", () => { assert.equal( env.enableCalls.length, baselineEnableCalls, - "decrypt calls should not trigger additional enable() prompts" + "decrypt calls should not trigger additional enable() prompts", ); assert.equal(decryptCalls.nip04, 1); assert.equal(decryptCalls.nip44, 1); @@ -317,27 +322,37 @@ describe("NIP-07 Login Permissions", () => { const result = await nip07Provider.login({ nostrClient }); const pubkey = result.pubkey; assert.equal(pubkey, HEX_PUBKEY); + // With Structured -> String -> Plain order, this succeeds at step 2 (String). + // The login flow may perform multiple permission requests (e.g. core -> full). + // We expect at least one successful sequence (2 calls). assert.ok(env.enableCalls.length >= 2, "should retry with alternate payloads"); + // Check the first sequence (core permissions) const [objectCall, stringCall] = env.enableCalls; assert.ok(Array.isArray(objectCall?.permissions)); assert.equal( typeof objectCall.permissions[0], "object", - "structured permission request should be attempted first" + "structured permission request should be attempted first", ); assert.ok(Array.isArray(stringCall?.permissions)); assert.equal( typeof stringCall.permissions[0], "string", - "string-based permissions should be attempted after structured payloads" + "string-based permissions should be attempted after structured payloads", ); + // If we have more calls (full permissions upgrade), verify they follow the same pattern if (env.enableCalls.length > 2) { + // The 3rd call should be a new structured attempt for full permissions const upgradeObjectCall = env.enableCalls[2]; + // The 4th call (if 3rd failed) should be string + const upgradeStringCall = env.enableCalls[3]; + if (upgradeObjectCall) { assert.ok(Array.isArray(upgradeObjectCall.permissions), "upgrade attempt should use permissions"); } + // We don't strictly assert `plainCall` is undefined because a 2nd pass might happen } } finally { env.restore(); @@ -361,14 +376,21 @@ describe("NIP-07 Login Permissions", () => { const result = await nip07Provider.login({ nostrClient }); const pubkey = result.pubkey; assert.equal(pubkey, HEX_PUBKEY); + // With Structured -> String -> Plain order, we expect at least 3 calls for the first success: + // 1. Structured (rejected) + // 2. String (rejected) + // 3. Plain (accepted) + // If login does a second pass (upgrade), it might repeat this sequence. assert.ok( env.enableCalls.length >= 3, - "should eventually succeed with plain enable()" + "should eventually succeed with plain enable()", ); + // Verify the first sequence structure const [objectCall, stringCall, plainCall] = env.enableCalls; assert.ok(objectCall !== undefined); assert.ok(stringCall !== undefined); + // plainCall is the one that succeeded (called with undefined/no args) assert.equal(plainCall, undefined, "successful attempt should use plain enable()"); } finally { env.restore(); @@ -407,8 +429,11 @@ describe("NIP-07 Login Permissions", () => { assert.equal(pubkey, HEX_PUBKEY); assert.ok( duration < 1000, - `login should fall back quickly from stalled enable payloads (duration: ${duration}ms)` + `login should fall back quickly from stalled enable payloads (duration: ${duration}ms)`, ); + // Structured (stalls) -> String (resolves) + // Plain is skipped because String resolved. + // Login might do this twice (Core then Full), so 2 or 4 calls. assert.ok(env.enableCalls.length >= 2, "should attempt structured (stalls) and then string payload"); const [objectCall, stringCall] = env.enableCalls; @@ -448,13 +473,13 @@ describe("NIP-07 Login Permissions", () => { assert.equal(result.pubkey, HEX_PUBKEY); assert.ok( duration < 200, - `login should resolve before deferred permission grants (duration: ${duration}ms)` + `login should resolve before deferred permission grants (duration: ${duration}ms)`, ); assert.ok(completionPromise, "completionPromise should be captured"); await assert.rejects( completionPromise, /permission denied/, - "deferred permission grants should still reject" + "deferred permission grants should still reject", ); } finally { nostrClient.ensureExtensionPermissions = originalEnsurePermissions; diff --git a/tests/nostr-private-key-signer.test.mjs b/tests/nostr-private-key-signer.test.mjs index 5645ecb7..610c66a9 100644 --- a/tests/nostr-private-key-signer.test.mjs +++ b/tests/nostr-private-key-signer.test.mjs @@ -45,14 +45,14 @@ describe("Nostr Private Key Signer", () => { const outboundCipher = await signer.nip04Encrypt(recipientPubkey, message); assert.equal(typeof outboundCipher, "string"); const decryptedByRecipient = await nostrTools.nip04.decrypt( - recipientKeyBytes, + recipientPrivateHex, signer.pubkey, outboundCipher, ); assert.equal(decryptedByRecipient, message); const inboundCipher = await nostrTools.nip04.encrypt( - recipientKeyBytes, + recipientPrivateHex, signer.pubkey, message, ); diff --git a/tests/nostr-send-direct-message.test.mjs b/tests/nostr-send-direct-message.test.mjs index 4ca31a46..2b23e1a7 100644 --- a/tests/nostr-send-direct-message.test.mjs +++ b/tests/nostr-send-direct-message.test.mjs @@ -92,9 +92,10 @@ test("sendDirectMessage succeeds with private key signer and no extension", asyn nostrClient.readRelays = [RELAY_URL]; nostrClient.writeRelays = [RELAY_URL]; - const senderSecret = canonicalTools.generateSecretKey(); - const senderPrivateKey = canonicalTools.utils.bytesToHex(senderSecret); - const senderPubkey = canonicalTools.getPublicKey(senderSecret); + const senderPrivateKey = canonicalTools.utils.bytesToHex( + canonicalTools.generateSecretKey(), + ); + const senderPubkey = canonicalTools.getPublicKey(senderPrivateKey); const recipientSecret = canonicalTools.generateSecretKey(); const recipientPrivateKey = canonicalTools.utils.bytesToHex(recipientSecret); const recipientPubkey = canonicalTools.getPublicKey(recipientSecret); @@ -111,7 +112,7 @@ test("sendDirectMessage succeeds with private key signer and no extension", asyn const originalNip44 = signer?.nip44Encrypt; if (signer) { signer.signEvent = (event) => { - const finalized = canonicalTools.finalizeEvent(event, senderSecret); + const finalized = canonicalTools.finalizeEvent(event, senderPrivateKey); return { ...event, id: finalized.id, sig: finalized.sig }; }; delete signer.nip04Encrypt; @@ -132,7 +133,7 @@ test("sendDirectMessage succeeds with private key signer and no extension", asyn assert.notEqual(event.content, "", "ciphertext should not be empty"); const decrypted = await canonicalTools.nip04.decrypt( - recipientSecret, + recipientPrivateKey, senderPubkey, event.content, ); diff --git a/tests/nostr/dm-direct-message-flow.test.mjs b/tests/nostr/dm-direct-message-flow.test.mjs index ccf8fdfe..25759eeb 100644 --- a/tests/nostr/dm-direct-message-flow.test.mjs +++ b/tests/nostr/dm-direct-message-flow.test.mjs @@ -151,24 +151,24 @@ async function setupDmScenario() { const senderPrivateKey = nostrTools.utils.bytesToHex(senderSecret); const receiverPrivateKey = nostrTools.utils.bytesToHex(receiverSecret); - const senderPubkey = nostrTools.getPublicKey(senderSecret); - const receiverPubkey = nostrTools.getPublicKey(receiverSecret); + const senderPubkey = nostrTools.getPublicKey(senderPrivateKey); + const receiverPubkey = nostrTools.getPublicKey(receiverPrivateKey); - const createSigner = (privateKeyBytes, pubkey) => ({ + const createSigner = (privateKey, pubkey) => ({ type: "test", pubkey, signEvent: (event) => { - const finalized = nostrTools.finalizeEvent(event, privateKeyBytes); + const finalized = nostrTools.finalizeEvent(event, privateKey); return { ...event, id: finalized.id, sig: finalized.sig }; }, nip04Encrypt: (target, plaintext) => - nostrTools.nip04.encrypt(privateKeyBytes, target, plaintext), + nostrTools.nip04.encrypt(privateKey, target, plaintext), nip04Decrypt: (target, ciphertext) => - nostrTools.nip04.decrypt(privateKeyBytes, target, ciphertext), + nostrTools.nip04.decrypt(privateKey, target, ciphertext), }); - const senderSigner = createSigner(senderSecret, senderPubkey); - const receiverSigner = createSigner(receiverSecret, receiverPubkey); + const senderSigner = createSigner(senderPrivateKey, senderPubkey); + const receiverSigner = createSigner(receiverPrivateKey, receiverPubkey); const relayPool = new FakeRelayPool(); diff --git a/tests/nostr/nip46Client.test.js b/tests/nostr/nip46Client.test.js index 450f4e2e..bb47a053 100644 --- a/tests/nostr/nip46Client.test.js +++ b/tests/nostr/nip46Client.test.js @@ -25,8 +25,8 @@ test("Nip46RpcClient encrypts payloads with nip44 conversation keys", async () = const clientSecret = utils.bytesToHex(generateSecretKey()); const remoteSecret = utils.bytesToHex(generateSecretKey()); - const clientPublicKey = getPublicKey(utils.hexToBytes(clientSecret)); - const remotePubkey = getPublicKey(utils.hexToBytes(remoteSecret)); + const clientPublicKey = getPublicKey(clientSecret); + const remotePubkey = getPublicKey(remoteSecret); const rpcClient = new Nip46RpcClient({ nostrClient: { relays: [] }, @@ -43,8 +43,8 @@ test("Nip46RpcClient encrypts payloads with nip44 conversation keys", async () = const conversationKey = typeof nip44.v2.getConversationKey === "function" - ? nip44.v2.getConversationKey(utils.hexToBytes(clientSecret), remotePubkey) - : nip44.v2.utils.getConversationKey(utils.hexToBytes(clientSecret), remotePubkey); + ? nip44.v2.getConversationKey(clientSecret, remotePubkey) + : nip44.v2.utils.getConversationKey(clientSecret, remotePubkey); const decryptedWithTools = nip44.v2.decrypt(ciphertext, conversationKey); assert.equal( decryptedWithTools, @@ -71,13 +71,13 @@ test("decryptNip46PayloadWithKeys handles nip44.v2 ciphertext", async () => { const clientSecret = utils.bytesToHex(generateSecretKey()); const remoteSecret = utils.bytesToHex(generateSecretKey()); - const remotePubkey = getPublicKey(utils.hexToBytes(remoteSecret)); + const remotePubkey = getPublicKey(remoteSecret); const serialized = JSON.stringify({ message: "hello", count: 42 }); const conversationKey = typeof nip44.v2.getConversationKey === "function" - ? nip44.v2.getConversationKey(utils.hexToBytes(clientSecret), remotePubkey) - : nip44.v2.utils.getConversationKey(utils.hexToBytes(clientSecret), remotePubkey); + ? nip44.v2.getConversationKey(clientSecret, remotePubkey) + : nip44.v2.utils.getConversationKey(clientSecret, remotePubkey); const ciphertext = nip44.v2.encrypt(serialized, conversationKey); const { plaintext, algorithm } = await decryptNip46PayloadWithKeys( @@ -104,13 +104,13 @@ test("decryptNip46PayloadWithKeys coerces structured handshake payloads", async const clientSecret = utils.bytesToHex(generateSecretKey()); const remoteSecret = utils.bytesToHex(generateSecretKey()); - const remotePubkey = getPublicKey(utils.hexToBytes(remoteSecret)); + const remotePubkey = getPublicKey(remoteSecret); const serialized = JSON.stringify({ message: "hello", count: 42 }); const conversationKey = typeof nip44.v2.getConversationKey === "function" - ? nip44.v2.getConversationKey(utils.hexToBytes(clientSecret), remotePubkey) - : nip44.v2.utils.getConversationKey(utils.hexToBytes(clientSecret), remotePubkey); + ? nip44.v2.getConversationKey(clientSecret, remotePubkey) + : nip44.v2.utils.getConversationKey(clientSecret, remotePubkey); const ciphertext = nip44.v2.encrypt(serialized, conversationKey); const [ciphertextPart, nonce] = ciphertext.split("\n"); @@ -154,13 +154,13 @@ test("decryptNip46PayloadWithKeys decodes buffer-based handshake payloads", asyn const clientSecret = utils.bytesToHex(generateSecretKey()); const remoteSecret = utils.bytesToHex(generateSecretKey()); - const remotePubkey = getPublicKey(utils.hexToBytes(remoteSecret)); + const remotePubkey = getPublicKey(remoteSecret); const serialized = JSON.stringify({ message: "hello", count: 42 }); const conversationKey = typeof nip44.v2.getConversationKey === "function" - ? nip44.v2.getConversationKey(utils.hexToBytes(clientSecret), remotePubkey) - : nip44.v2.utils.getConversationKey(utils.hexToBytes(clientSecret), remotePubkey); + ? nip44.v2.getConversationKey(clientSecret, remotePubkey) + : nip44.v2.utils.getConversationKey(clientSecret, remotePubkey); const ciphertext = nip44.v2.encrypt(serialized, conversationKey); const [ciphertextPart, nonce] = ciphertext.split("\n"); const ciphertextBuffer = Buffer.from(ciphertext, "utf8"); @@ -197,10 +197,10 @@ test("decryptNip46PayloadWithKeys supports nip04-style ciphertext wrappers", asy const clientSecret = utils.bytesToHex(generateSecretKey()); const remoteSecret = utils.bytesToHex(generateSecretKey()); - const remotePubkey = getPublicKey(utils.hexToBytes(remoteSecret)); + const remotePubkey = getPublicKey(remoteSecret); const serialized = JSON.stringify({ message: "hello", count: 42 }); - const ciphertext = await nip04.encrypt(utils.hexToBytes(clientSecret), remotePubkey, serialized); + const ciphertext = await nip04.encrypt(clientSecret, remotePubkey, serialized); const [ciphertextPart, iv] = ciphertext.split("?iv="); const structuredPayload = { @@ -254,8 +254,8 @@ test("parseNip46ConnectionString handles remote signer key hints", async () => { const signerSecret = utils.bytesToHex(generateSecretKey()); const userSecret = utils.bytesToHex(generateSecretKey()); - const signerPubkey = getPublicKey(utils.hexToBytes(signerSecret)).toLowerCase(); - const userPubkey = getPublicKey(utils.hexToBytes(userSecret)).toLowerCase(); + const signerPubkey = getPublicKey(signerSecret).toLowerCase(); + const userPubkey = getPublicKey(userSecret).toLowerCase(); const signerNpub = nip19.npubEncode(signerPubkey); const userNpub = nip19.npubEncode(userPubkey); @@ -295,13 +295,13 @@ test( const remoteSignerSecret = utils.bytesToHex(generateSecretKey()); const userSecret = utils.bytesToHex(generateSecretKey()); - const remoteSignerPubkey = getPublicKey(utils.hexToBytes(remoteSignerSecret)).toLowerCase(); - const userPubkey = getPublicKey(utils.hexToBytes(userSecret)).toLowerCase(); + const remoteSignerPubkey = getPublicKey(remoteSignerSecret).toLowerCase(); + const userPubkey = getPublicKey(userSecret).toLowerCase(); const conversationKey = typeof nip44.v2.getConversationKey === "function" - ? nip44.v2.getConversationKey(utils.hexToBytes(clientSecret), remoteSignerPubkey) - : nip44.v2.utils.getConversationKey(utils.hexToBytes(clientSecret), remoteSignerPubkey); + ? nip44.v2.getConversationKey(clientSecret, remoteSignerPubkey) + : nip44.v2.utils.getConversationKey(clientSecret, remoteSignerPubkey); const payload = JSON.stringify({ id: "ack", result: "ok" }); const ciphertext = nip44.v2.encrypt(payload, conversationKey); @@ -333,12 +333,12 @@ test( const { attemptDecryptNip46HandshakePayload } = await loadNip46Module(); const clientSecret = utils.bytesToHex(generateSecretKey()); - const clientPubkey = getPublicKey(utils.hexToBytes(clientSecret)).toLowerCase(); + const clientPubkey = getPublicKey(clientSecret).toLowerCase(); const remoteSecret = utils.bytesToHex(generateSecretKey()); - const remotePubkey = getPublicKey(utils.hexToBytes(remoteSecret)).toLowerCase(); + const remotePubkey = getPublicKey(remoteSecret).toLowerCase(); const payload = JSON.stringify({ id: "ack", result: "ack" }); - const ciphertext = nip04.encrypt(utils.hexToBytes(remoteSecret), clientPubkey, payload); + const ciphertext = nip04.encrypt(remoteSecret, clientPubkey, payload); const [ciphertextPart, ivPart] = ciphertext.split("?iv="); assert.ok(ivPart, "nip04 encryption should emit an iv segment"); diff --git a/tests/smoke/smoke-test.mjs b/tests/smoke/smoke-test.mjs deleted file mode 100644 index 880566d0..00000000 --- a/tests/smoke/smoke-test.mjs +++ /dev/null @@ -1,196 +0,0 @@ -import { chromium } from 'playwright'; -import { spawn } from 'child_process'; -import fs from 'fs'; -import path from 'path'; -import { fileURLToPath } from 'url'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -const PORT = 8000; -const URL = `http://localhost:${PORT}`; -const ARTIFACTS_DIR = path.resolve('artifacts'); -const LOG_FILE = path.join(ARTIFACTS_DIR, `smoke-${new Date().toISOString().split('T')[0].replace(/-/g, '')}.log`); -const SUMMARY_FILE = path.join(ARTIFACTS_DIR, 'smoke-summary.json'); - -if (!fs.existsSync(ARTIFACTS_DIR)) { - fs.mkdirSync(ARTIFACTS_DIR); -} - -function log(msg) { - const timestamp = new Date().toISOString(); - const line = `[${timestamp}] ${msg}`; - console.log(line); - try { - fs.appendFileSync(LOG_FILE, line + '\n'); - } catch (e) { - // Ignore log write errors - } -} - -async function main() { - log('Starting smoke test...'); - let serverProcess; - let browser; - - try { - // Start Python Server - log('Starting local server (python3 -m http.server)...'); - serverProcess = spawn('python3', ['-m', 'http.server', String(PORT)], { - stdio: 'ignore' - }); - - // Give it time to start - await new Promise(r => setTimeout(r, 2000)); - - // Launch Browser - log('Launching browser...'); - browser = await chromium.launch({ headless: true }); - const context = await browser.newContext(); - const page = await context.newPage(); - - // Capture console logs from page - page.on('console', msg => { - if (msg.type() === 'error') { - log(`[BROWSER ERROR] ${msg.text()}`); - } else { - // log(`[BROWSER] ${msg.text()}`); // verbose - } - }); - page.on('pageerror', err => log(`[BROWSER EXCEPTION] ${err}`)); - - log(`Navigating to ${URL}...`); - await page.goto(URL); - await page.waitForLoadState('networkidle'); - log('Page loaded.'); - - // Inject Test Logic - log('Running in-browser smoke test...'); - - const result = await page.evaluate(async () => { - const logs = []; - function browserLog(msg) { - console.log(msg); - logs.push(msg); - } - - try { - browserLog('Step 1: Importing Client...'); - // Note: In browser context, paths are relative to the served root (http://localhost:8000/) - const { nostrClient } = await import('./js/nostrClientFacade.js'); - const { encodeHexToNpub } = await import('./js/nostr/nip46Client.js'); - - if (!nostrClient) throw new Error('nostrClient not found'); - browserLog('nostrClient imported.'); - - browserLog('Step 2: Generating Key & Login...'); - const randomBytes = new Uint8Array(32); - window.crypto.getRandomValues(randomBytes); - const privateKey = Array.from(randomBytes) - .map((b) => b.toString(16).padStart(2, "0")) - .join(""); - - const { pubkey } = await nostrClient.registerPrivateKeySigner({ privateKey, persist: false }); - nostrClient.pubkey = pubkey; // Explicitly set it on the client instance - - if (!nostrClient.pubkey) throw new Error('Login failed: pubkey is null'); - browserLog(`Logged in as: ${nostrClient.pubkey}`); - - browserLog('Step 3: Connecting to Relays...'); - const connectResults = await nostrClient.connectToRelays(); - const successful = connectResults.filter(r => r.success); - browserLog(`Connected to ${successful.length} relays.`); - - if (successful.length === 0) { - browserLog('Warning: Failed to connect to any relay. Tests depending on network will likely fail or use local cache.'); - } - - browserLog('Step 4: Publishing Video...'); - const seriesId = `smoke-test-${Date.now()}-${Math.floor(Math.random()*1000)}`; - - const videoPayload = { - title: "Smoke Test Video " + Date.now(), - description: "Automated smoke test artifact.", - url: "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4", - videoRootId: seriesId, - mimeType: "video/mp4" - }; - - const videoEvent = await nostrClient.publishVideo(videoPayload, nostrClient.pubkey); - browserLog(`Video published. Event ID: ${videoEvent.id}`); - - browserLog('Step 5: Verifying Video Presence...'); - const fetched = await nostrClient.getEventById(videoEvent.id); - if (!fetched) throw new Error('Failed to retrieve video event from client cache/network.'); - browserLog('Video event verified.'); - - browserLog('Step 6: DM Flow (Send to Self)...'); - const message = "Smoke test DM " + Date.now(); - const npub = encodeHexToNpub(nostrClient.pubkey); - browserLog(`Sending DM to self (${npub})...`); - - const dmResult = await nostrClient.sendDirectMessage(npub, message); - if (!dmResult.ok) throw new Error(`DM send failed: ${dmResult.error} ${JSON.stringify(dmResult.details || {})}`); - browserLog('DM sent.'); - - browserLog('Step 7: Verifying DM Decryption...'); - let found = null; - for (let i = 0; i < 5; i++) { - await new Promise(r => setTimeout(r, 1000)); - const dms = await nostrClient.listDirectMessages(nostrClient.pubkey, { limit: 10 }); - found = dms.find(dm => dm.plaintext === message); - if (found) break; - } - - if (!found) { - throw new Error('DM verification failed: Message not found in decrypted list after retries.'); - } - - browserLog('DM verified and decrypted successfully.'); - - return { success: true, logs }; - } catch (err) { - browserLog(`Error: ${err.message}`); - return { success: false, error: err.message, stack: err.stack, logs }; - } - }); - - if (result.success) { - log('SUCCESS: All smoke tests passed.'); - fs.writeFileSync(SUMMARY_FILE, JSON.stringify({ - status: 'success', - timestamp: new Date().toISOString(), - logs: result.logs - }, null, 2)); - } else { - log('FAILURE: Smoke tests failed.'); - log(`Error: ${result.error}`); - fs.writeFileSync(SUMMARY_FILE, JSON.stringify({ - status: 'failure', - timestamp: new Date().toISOString(), - error: result.error, - stack: result.stack, - logs: result.logs - }, null, 2)); - - const screenshotPath = path.join(ARTIFACTS_DIR, 'smoke-failure.png'); - await page.screenshot({ path: screenshotPath }); - log(`Screenshot saved to ${screenshotPath}`); - - process.exitCode = 1; - } - - } catch (error) { - log(`CRITICAL ERROR: ${error.message}`); - console.error(error); - process.exitCode = 1; - } finally { - if (browser) await browser.close(); - if (serverProcess) { - log('Stopping server...'); - serverProcess.kill(); - } - } -} - -main();