Merge branch 'unstable' into telemetry-aggregation-fix-6349074633801704985

This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2026-01-30 09:19:03 -05:00
commit 667b48c301
19 files changed
+1042 -797

No files matched your search

+5 -2
View File
@@ -431,8 +431,11 @@ artifacts now that the deploy pipeline owns the build step.
preparing a new deployment.
- See [`docs/instance-config.md`](docs/instance-config.md) for a full
reference of every setting and how to tune it.
- Flip `IS_DEV_MODE` to `false` before shipping production builds. The flag
flows into `js/config.js` as `isDevMode`, seeds the
- `IS_DEV_MODE` and `IS_VERBOSE_DEV_MODE` default to `false` for production.
If you need dev logging in a staging or QA build without editing source,
inject `window.__BITVID_DEV_MODE_OVERRIDE__` and/or
`window.__BITVID_VERBOSE_DEV_MODE_OVERRIDE__` before `js/config.js` loads.
The flag flows into `js/config.js` as `isDevMode`, seeds the
`window.__BITVID_DEV_MODE__` global for inline scripts, and gates whether the
dev logging channel emits to the console. See
[`docs/logging.md`](docs/logging.md) for rollout guidance.
+9 -5
View File
@@ -17,11 +17,13 @@
* Whether the current deployment should enable developer-centric behavior.
*
* When `true`, bitvid surfaces extra logging, debug helpers, and experimental
* UI affordances intended for development environments. Set the value to
* `false` before deploying to production so that end users receive the stable
* experience. Downstream modules read this flag via `js/config.js`.
* UI affordances intended for development environments. Leave the value
* `false` for production so end users receive the stable experience; use the
* runtime overrides documented in `js/config.js` when you need to toggle dev
* mode without editing this file. Downstream modules read this flag via
* `js/config.js`.
*/
export const IS_DEV_MODE = true;
export const IS_DEV_MODE = false;
/**
* Whether development builds should surface extra verbose diagnostics.
@@ -29,8 +31,10 @@ export const IS_DEV_MODE = true;
* Set this to `true` to keep high-volume development warnings (like COUNT
* request failures) visible in the console. Flip it to `false` when you want
* a quieter dev mode without disabling other development helpers entirely.
* Use the runtime override documented in `js/config.js` if you need to toggle
* verbosity without editing this file.
*/
export const IS_VERBOSE_DEV_MODE = true;
export const IS_VERBOSE_DEV_MODE = false;
/**
* Whether the current deployment should operate in lockdown mode.
+18 -2
View File
@@ -28,14 +28,30 @@ an operator might change it.
debug affordances. The value is re-exported as `isDevMode` in
`js/config.js` and controls `window.__BITVID_DEV_MODE__` as well as
logger routing.
- **Why change it:** Set to `false` for production builds so only the stable
UX and user-facing logging remain.
- **Why change it:** Defaults to `false` for production builds. Set to `true`
for local development, or use the runtime override described below to
toggle dev mode without editing this file.
- **`IS_VERBOSE_DEV_MODE`**
- **What it does:** Enables high-volume diagnostics inside dev mode (for
example, repeated COUNT failures).
- **Why change it:** Keep `true` while debugging noisy workflows and set to
`false` when you want a quieter dev console without disabling dev mode.
Defaults to `false` for production.
To override the dev flags at runtime (for example in a staging build) without
changing `config/instance-config.js`, inject one or both of the following
globals **before** `js/config.js` loads:
```html
<script>
window.__BITVID_DEV_MODE_OVERRIDE__ = true;
window.__BITVID_VERBOSE_DEV_MODE_OVERRIDE__ = true;
</script>
```
The overrides accept boolean values or the strings `"true"` / `"false"` so they
can be injected at build time.
- **`IS_LOCKDOWN_MODE`**
- **What it does:** Toggles a lockdown posture that limits access and switches
+16 -3
View File
@@ -59,10 +59,23 @@ The source of truth lives in `config/instance-config.js`:
- Set `IS_DEV_MODE = true` while developing locally so the dev logger and other
diagnostics are available.
- Flip `IS_DEV_MODE = false` before shipping a production build. This change
propagates to `isDevMode` (the module export), `window.__BITVID_DEV_MODE__`,
and `logger.dev`, silencing development-only messages.
- Keep `IS_DEV_MODE = false` for production builds. This value propagates to
`isDevMode` (the module export), `window.__BITVID_DEV_MODE__`, and
`logger.dev`, silencing development-only messages.
Always commit the flag change alongside deployment-ready configuration tweaks.
Forgetting to update the flag leaves verbose traces in user consoles and can
expose experimental behavior intended only for QA environments.
If you need to enable dev logging without editing `config/instance-config.js`,
inject a runtime override before `js/config.js` loads:
```html
<script>
window.__BITVID_DEV_MODE_OVERRIDE__ = true;
window.__BITVID_VERBOSE_DEV_MODE_OVERRIDE__ = true;
</script>
```
Overrides accept boolean values or the strings `"true"` / `"false"` so they can
be injected by a build pipeline.
+25 -148
View File
@@ -173,6 +173,7 @@ import EngagementController from "./ui/engagementController.js";
import SimilarContentController from "./ui/similarContentController.js";
import UrlHealthController from "./ui/urlHealthController.js";
import VideoModalCommentController from "./ui/videoModalCommentController.js";
import VideoModalController from "./ui/videoModalController.js";
import TorrentStatusController from "./ui/torrentStatusController.js";
import ShareNostrController from "./ui/shareNostrController.js";
import ModerationActionController from "./services/moderationActionController.js";
@@ -223,6 +224,14 @@ class Application {
setStoredCurrentVideo(value ?? null);
}
get activeProfilePubkey() {
return getActiveProfilePubkey();
}
get savedProfiles() {
return getSavedProfiles();
}
constructor({ services = {}, ui = {}, helpers = {}, loadView: viewLoader } = {}) {
this.loadView = typeof viewLoader === "function" ? viewLoader : null;
@@ -251,6 +260,16 @@ class Application {
const { modalManager } = this.bootstrapper.initialize();
this.modalManager = modalManager;
this.videoModalController = new VideoModalController({
getVideoModal: () => this.videoModal,
callbacks: {
showError: (msg) => this.showError(msg),
getLastModalTrigger: () => this.lastModalTrigger,
setLastModalTrigger: (val) => this.setLastModalTrigger(val),
getCurrentVideo: () => this.currentVideo,
},
});
this.modalCreatorProfileRequestToken = null;
this.dmRecipientPubkey = null;
this.dmRelayHints = new Map();
@@ -1176,162 +1195,21 @@ class Application {
* Show the modal and set the "Please stand by" poster on the video.
*/
async showModalWithPoster(video = this.currentVideo, options = {}) {
if (!this.videoModal) {
return null;
}
if (Object.prototype.hasOwnProperty.call(options || {}, "trigger")) {
this.setLastModalTrigger(options.trigger);
}
const targetVideo = video || this.currentVideo;
if (!targetVideo) {
this.log(
"[Application] Skipping video modal open; no target video is available.",
);
return null;
}
try {
const { root } = await this.ensureVideoModalReady({
ensureVideoElement: true,
});
if (!this.videoModal) {
return root || null;
}
this.videoModal.open(targetVideo, {
triggerElement: this.lastModalTrigger,
});
this.applyModalLoadingPoster();
return (
root ||
(typeof this.videoModal.getRoot === "function"
? this.videoModal.getRoot()
: null)
);
} catch (error) {
devLogger.error(
"[Application] Failed to open the video modal before playback:",
error
);
this.showError("Could not open the video player. Please try again.");
return null;
}
return this.videoModalController.showModalWithPoster(video, options);
}
applyModalLoadingPoster() {
if (!this.videoModal) {
return;
}
this.videoModal.applyLoadingPoster();
this.videoModalController.applyModalLoadingPoster();
}
forceRemoveModalPoster(reason = "manual-clear") {
if (!this.videoModal) {
return false;
}
return this.videoModal.forceRemovePoster(reason);
return this.videoModalController.forceRemoveModalPoster(reason);
}
async ensureVideoModalReady({ ensureVideoElement = false } = {}) {
if (!this.videoModal) {
throw new Error("Video modal instance is not available.");
}
const getRoot = () =>
typeof this.videoModal.getRoot === "function"
? this.videoModal.getRoot()
: null;
const getVideoElement = () =>
typeof this.videoModal.getVideoElement === "function"
? this.videoModal.getVideoElement()
: null;
const existingRoot = getRoot();
const existingVideoElement = getVideoElement();
const rootConnected = Boolean(existingRoot && existingRoot.isConnected);
const hasVideoElement = Boolean(existingVideoElement);
const videoConnected = Boolean(
existingVideoElement && existingVideoElement.isConnected,
);
const needsRehydrate =
!rootConnected || !hasVideoElement || !videoConnected;
if (!needsRehydrate) {
this.modalVideo = existingVideoElement;
if (
existingVideoElement &&
this.videoModal &&
typeof this.videoModal.setVideoElement === "function"
) {
const modalVideo =
typeof this.videoModal.getVideoElement === "function"
? this.videoModal.getVideoElement()
: null;
if (modalVideo !== existingVideoElement) {
this.videoModal.setVideoElement(existingVideoElement);
}
}
return {
root: existingRoot,
videoElement: existingVideoElement,
};
}
if (!videoConnected) {
this.modalVideo = null;
}
if (!this.videoModalReadyPromise) {
if (typeof this.videoModal.load !== "function") {
throw new Error("Video modal does not expose a load() method.");
}
this.videoModalReadyPromise = Promise.resolve(this.videoModal.load());
}
try {
await this.videoModalReadyPromise;
} catch (error) {
this.videoModalReadyPromise = null;
throw error;
}
this.videoModalReadyPromise = null;
const readyRoot = getRoot();
const readyVideoElement = getVideoElement();
const readyVideoConnected = Boolean(
readyVideoElement && readyVideoElement.isConnected,
);
if (readyVideoConnected) {
this.modalVideo = readyVideoElement;
if (
this.videoModal &&
typeof this.videoModal.setVideoElement === "function"
) {
this.videoModal.setVideoElement(readyVideoElement);
}
} else {
this.modalVideo = null;
}
if (ensureVideoElement && !readyVideoConnected) {
throw new Error(
"Video modal video element is missing after load().",
);
}
return {
root: readyRoot,
videoElement: readyVideoConnected ? readyVideoElement : null,
};
const result = await this.videoModalController.ensureVideoModalReady({ ensureVideoElement });
this.modalVideo = result.videoElement;
return result;
}
initializeSimilarContentController() {
@@ -10024,7 +9902,6 @@ class Application {
destroy() {
this.clearActiveIntervals();
this.teardownModalViewCountSubscription();
this.videoModalReadyPromise = null;
if (this.moderationActionController) {
try {
+31 -2
View File
@@ -47,10 +47,39 @@ import {
export const WATCH_HISTORY_VERSION_TAG_VALUE = "2";
export const isDevMode = Boolean(IS_DEV_MODE);
const runtimeScope = typeof globalThis === "undefined" ? null : globalThis;
const parseBooleanOverride = (value) => {
if (typeof value === "boolean") {
return value;
}
if (typeof value === "string") {
const normalized = value.trim().toLowerCase();
if (normalized === "true") {
return true;
}
if (normalized === "false") {
return false;
}
}
return null;
};
const devModeOverride = parseBooleanOverride(
runtimeScope?.__BITVID_DEV_MODE_OVERRIDE__
);
const verboseDevModeOverride = parseBooleanOverride(
runtimeScope?.__BITVID_VERBOSE_DEV_MODE_OVERRIDE__
);
export const isDevMode =
devModeOverride === null ? Boolean(IS_DEV_MODE) : devModeOverride;
export { IS_DEV_MODE };
export const isVerboseDevMode = Boolean(IS_VERBOSE_DEV_MODE);
export const isVerboseDevMode =
verboseDevModeOverride === null
? Boolean(IS_VERBOSE_DEV_MODE)
: verboseDevModeOverride;
export { IS_VERBOSE_DEV_MODE };
export const isLockdownMode = Boolean(IS_LOCKDOWN_MODE);
+12 -1
View File
@@ -6,6 +6,10 @@ import {
resolveInfoJsonUrl,
} from "../utils/storagePointer.js";
import { getCachedNostrTools } from "./toolkit.js";
import {
validateEventAgainstSchema,
NOTE_TYPES,
} from "../nostrEventSchemas.js";
function stringFromInput(value) {
if (value === null || value === undefined) {
@@ -934,13 +938,20 @@ export function buildNip71VideoEvent({
tags.push(...pointerTags);
}
return {
const event = {
kind: normalizeNip71Kind(metadata.kind),
pubkey: normalizedPubkey,
created_at: timestamp,
tags,
content: summary,
};
validateEventAgainstSchema(
event.kind === 22 ? NOTE_TYPES.NIP71_SHORT_VIDEO : NOTE_TYPES.NIP71_VIDEO,
event,
);
return event;
}
export function collectNip71PointerRequests(videos = []) {
+202
View File
@@ -0,0 +1,202 @@
import { devLogger } from "../utils/logger.js";
export default class VideoModalController {
constructor({
getVideoModal,
callbacks: {
showError,
getLastModalTrigger,
setLastModalTrigger,
getCurrentVideo,
} = {},
}) {
this.getVideoModal = getVideoModal;
this.showError = showError;
this.getLastModalTrigger = getLastModalTrigger;
this.setLastModalTrigger = setLastModalTrigger;
this.getCurrentVideo = getCurrentVideo;
this.videoModalReadyPromise = null;
}
get videoModal() {
return this.getVideoModal ? this.getVideoModal() : null;
}
log(message, ...args) {
devLogger.log("[VideoModalController]", message, ...args);
}
/**
* Show the modal and set the "Please stand by" poster on the video.
*/
async showModalWithPoster(video = null, options = {}) {
if (!this.videoModal) {
return null;
}
if (Object.prototype.hasOwnProperty.call(options || {}, "trigger")) {
if (this.setLastModalTrigger) {
this.setLastModalTrigger(options.trigger);
}
}
const targetVideo =
video || (this.getCurrentVideo ? this.getCurrentVideo() : null);
if (!targetVideo) {
this.log("Skipping video modal open; no target video is available.");
return null;
}
try {
const { root } = await this.ensureVideoModalReady({
ensureVideoElement: true,
});
if (!this.videoModal) {
return root || null;
}
const trigger = this.getLastModalTrigger
? this.getLastModalTrigger()
: null;
this.videoModal.open(targetVideo, {
triggerElement: trigger,
});
this.applyModalLoadingPoster();
return (
root ||
(typeof this.videoModal.getRoot === "function"
? this.videoModal.getRoot()
: null)
);
} catch (error) {
devLogger.error(
"[VideoModalController] Failed to open the video modal before playback:",
error,
);
if (this.showError) {
this.showError("Could not open the video player. Please try again.");
}
return null;
}
}
applyModalLoadingPoster() {
if (!this.videoModal) {
return;
}
this.videoModal.applyLoadingPoster();
}
forceRemoveModalPoster(reason = "manual-clear") {
if (!this.videoModal) {
return false;
}
return this.videoModal.forceRemovePoster(reason);
}
async ensureVideoModalReady({ ensureVideoElement = false } = {}) {
if (!this.videoModal) {
throw new Error("Video modal instance is not available.");
}
const getRoot = () =>
typeof this.videoModal.getRoot === "function"
? this.videoModal.getRoot()
: null;
const getVideoElement = () =>
typeof this.videoModal.getVideoElement === "function"
? this.videoModal.getVideoElement()
: null;
const existingRoot = getRoot();
const existingVideoElement = getVideoElement();
const rootConnected = Boolean(existingRoot && existingRoot.isConnected);
const hasVideoElement = Boolean(existingVideoElement);
const videoConnected = Boolean(
existingVideoElement && existingVideoElement.isConnected,
);
const needsRehydrate =
!rootConnected || !hasVideoElement || !videoConnected;
if (!needsRehydrate) {
if (
existingVideoElement &&
this.videoModal &&
typeof this.videoModal.setVideoElement === "function"
) {
const currentModalVideo =
typeof this.videoModal.getVideoElement === "function"
? this.videoModal.getVideoElement()
: null;
if (currentModalVideo !== existingVideoElement) {
this.videoModal.setVideoElement(existingVideoElement);
}
}
return {
root: existingRoot,
videoElement: existingVideoElement,
};
}
if (!videoConnected) {
if (
this.videoModal &&
typeof this.videoModal.setVideoElement === "function"
) {
this.videoModal.setVideoElement(null);
}
}
if (!this.videoModalReadyPromise) {
if (typeof this.videoModal.load !== "function") {
throw new Error("Video modal does not expose a load() method.");
}
this.videoModalReadyPromise = Promise.resolve(this.videoModal.load());
}
try {
await this.videoModalReadyPromise;
} catch (error) {
this.videoModalReadyPromise = null;
throw error;
}
this.videoModalReadyPromise = null;
const readyRoot = getRoot();
const readyVideoElement = getVideoElement();
const readyVideoConnected = Boolean(
readyVideoElement && readyVideoElement.isConnected,
);
if (readyVideoConnected) {
if (
this.videoModal &&
typeof this.videoModal.setVideoElement === "function"
) {
this.videoModal.setVideoElement(readyVideoElement);
}
} else {
if (
this.videoModal &&
typeof this.videoModal.setVideoElement === "function"
) {
this.videoModal.setVideoElement(null);
}
}
if (ensureVideoElement && !readyVideoConnected) {
throw new Error("Video modal video element is missing after load().");
}
return {
root: readyRoot,
videoElement: readyVideoConnected ? readyVideoElement : null,
};
}
}
+11
View File
@@ -1,3 +1,14 @@
[build]
command = "npm run build"
publish = "dist"
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
force = true
[[headers]]
for = "/*"
[headers.values]
Content-Security-Policy = "frame-ancestors *"
+5 -88
View File
@@ -11,14 +11,16 @@
"dependencies": {
"@floating-ui/dom": "^1.7.4",
"@noble/hashes": "^2.0.1",
"@tailwindcss/typography": "^0.5.19",
"autoprefixer": "^10.4.23",
"jsdom": "^27.4.0",
"postcss": "^8.5.6",
"tailwindcss": "^3.4.19",
"ws": "^8.19.0"
},
"devDependencies": {
"@noble/hashes": "^2.0.1",
"@playwright/test": "^1.58.0",
"@tailwindcss/typography": "^0.5.15",
"autoprefixer": "^10.4.23",
"cross-env": "^7.0.3",
"esbuild": "^0.25.12",
"fake-indexeddb": "^6.2.5",
@@ -26,12 +28,10 @@
"pixelmatch": "^5.3.0",
"playwright": "^1.58.0",
"pngjs": "^7.0.0",
"postcss": "^8.4.41",
"prettier": "^3.8.1",
"prettier-plugin-tailwindcss": "^0.6.5",
"serve": "^14.2.5",
"stylelint": "^16.26.1",
"tailwindcss": "^3.4.19"
"stylelint": "^16.26.1"
}
},
"node_modules/@acemir/cssom": {
@@ -44,7 +44,6 @@
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
"integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
@@ -783,7 +782,6 @@
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0",
@@ -794,7 +792,6 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
@@ -804,14 +801,12 @@
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"dev": true,
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.31",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
@@ -895,7 +890,6 @@
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@nodelib/fs.stat": "2.0.5",
@@ -909,7 +903,6 @@
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 8"
@@ -919,7 +912,6 @@
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@nodelib/fs.scandir": "2.1.5",
@@ -1043,7 +1035,6 @@
"version": "0.5.19",
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz",
"integrity": "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==",
"dev": true,
"license": "MIT",
"dependencies": {
"postcss-selector-parser": "6.0.10"
@@ -1173,14 +1164,12 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
"integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
"dev": true,
"license": "MIT"
},
"node_modules/anymatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
"dev": true,
"license": "ISC",
"dependencies": {
"normalize-path": "^3.0.0",
@@ -1215,7 +1204,6 @@
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
"dev": true,
"license": "MIT"
},
"node_modules/argparse": {
@@ -1249,7 +1237,6 @@
"version": "10.4.23",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz",
"integrity": "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==",
"dev": true,
"funding": [
{
"type": "opencollective",
@@ -1293,7 +1280,6 @@
"version": "2.9.18",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.18.tgz",
"integrity": "sha512-e23vBV1ZLfjb9apvfPk4rHVu2ry6RIr2Wfs+O324okSidrX7pTAnEJPCh/O5BtRlr7QtZI7ktOP3vsqr7Z5XoA==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"baseline-browser-mapping": "dist/cli.js"
@@ -1312,7 +1298,6 @@
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -1372,7 +1357,6 @@
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true,
"license": "MIT",
"dependencies": {
"fill-range": "^7.1.1"
@@ -1385,7 +1369,6 @@
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
"integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
"dev": true,
"funding": [
{
"type": "opencollective",
@@ -1466,7 +1449,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
"integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 6"
@@ -1476,7 +1458,6 @@
"version": "1.0.30001766",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001766.tgz",
"integrity": "sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==",
"dev": true,
"funding": [
{
"type": "opencollective",
@@ -1530,7 +1511,6 @@
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
"dev": true,
"license": "MIT",
"dependencies": {
"anymatch": "~3.1.2",
@@ -1613,7 +1593,6 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
"integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 6"
@@ -1794,7 +1773,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
"dev": true,
"license": "MIT",
"bin": {
"cssesc": "bin/cssesc"
@@ -1877,7 +1855,6 @@
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
"integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/dir-glob": {
@@ -1897,7 +1874,6 @@
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
"dev": true,
"license": "MIT"
},
"node_modules/eastasianwidth": {
@@ -1911,7 +1887,6 @@
"version": "1.5.279",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.279.tgz",
"integrity": "sha512-0bblUU5UNdOt5G7XqGiJtpZMONma6WAfq9vsFmtn9x1+joAObr6x1chfqyxFSDCAFwFhCQDrqeAr6MYdpwJ9Hg==",
"dev": true,
"license": "ISC"
},
"node_modules/emoji-regex": {
@@ -1999,7 +1974,6 @@
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -2057,7 +2031,6 @@
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
"integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@nodelib/fs.stat": "^2.0.2",
@@ -2084,7 +2057,6 @@
"version": "1.20.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
"integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
"dev": true,
"license": "ISC",
"dependencies": {
"reusify": "^1.0.4"
@@ -2104,7 +2076,6 @@
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true,
"license": "MIT",
"dependencies": {
"to-regex-range": "^5.0.1"
@@ -2136,7 +2107,6 @@
"version": "5.3.4",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
"integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": "*"
@@ -2150,7 +2120,6 @@
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
@@ -2165,7 +2134,6 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -2188,7 +2156,6 @@
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.1"
@@ -2303,7 +2270,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
@@ -2433,7 +2399,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
"dev": true,
"license": "MIT",
"dependencies": {
"binary-extensions": "^2.0.0"
@@ -2446,7 +2411,6 @@
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
"dev": true,
"license": "MIT",
"dependencies": {
"hasown": "^2.0.2"
@@ -2478,7 +2442,6 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -2498,7 +2461,6 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-extglob": "^2.1.1"
@@ -2511,7 +2473,6 @@
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.12.0"
@@ -2583,7 +2544,6 @@
"version": "1.21.7",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
"dev": true,
"license": "MIT",
"bin": {
"jiti": "bin/jiti.js"
@@ -2705,7 +2665,6 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
"integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14"
@@ -2718,7 +2677,6 @@
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
"dev": true,
"license": "MIT"
},
"node_modules/lodash.truncate": {
@@ -2778,7 +2736,6 @@
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 8"
@@ -2788,7 +2745,6 @@
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
"license": "MIT",
"dependencies": {
"braces": "^3.0.3",
@@ -2874,7 +2830,6 @@
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
"integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"any-promise": "^1.0.0",
@@ -2886,7 +2841,6 @@
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"dev": true,
"funding": [
{
"type": "github",
@@ -2915,14 +2869,12 @@
"version": "2.0.27",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
"integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
"dev": true,
"license": "MIT"
},
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -2989,7 +2941,6 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -2999,7 +2950,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 6"
@@ -3096,7 +3046,6 @@
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true,
"license": "MIT"
},
"node_modules/path-to-regexp": {
@@ -3120,14 +3069,12 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true,
"license": "ISC"
},
"node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8.6"
@@ -3140,7 +3087,6 @@
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -3150,7 +3096,6 @@
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
"integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 6"
@@ -3225,7 +3170,6 @@
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"dev": true,
"funding": [
{
"type": "opencollective",
@@ -3254,7 +3198,6 @@
"version": "15.1.0",
"resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
"integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
"dev": true,
"license": "MIT",
"dependencies": {
"postcss-value-parser": "^4.0.0",
@@ -3272,7 +3215,6 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz",
"integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==",
"dev": true,
"funding": [
{
"type": "opencollective",
@@ -3298,7 +3240,6 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
"integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
"dev": true,
"funding": [
{
"type": "opencollective",
@@ -3341,7 +3282,6 @@
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
"integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
"dev": true,
"funding": [
{
"type": "opencollective",
@@ -3367,7 +3307,6 @@
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
"dev": true,
"license": "MIT",
"dependencies": {
"cssesc": "^3.0.0",
@@ -3415,7 +3354,6 @@
"version": "6.0.10",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
"integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
"dev": true,
"license": "MIT",
"dependencies": {
"cssesc": "^3.0.0",
@@ -3429,7 +3367,6 @@
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"dev": true,
"license": "MIT"
},
"node_modules/prettier": {
@@ -3561,7 +3498,6 @@
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
"dev": true,
"funding": [
{
"type": "github",
@@ -3608,7 +3544,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
"integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
"dev": true,
"license": "MIT",
"dependencies": {
"pify": "^2.3.0"
@@ -3618,7 +3553,6 @@
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dev": true,
"license": "MIT",
"dependencies": {
"picomatch": "^2.2.1"
@@ -3664,7 +3598,6 @@
"version": "1.22.11",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
"integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-core-module": "^2.16.1",
@@ -3695,7 +3628,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
"integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
"dev": true,
"license": "MIT",
"engines": {
"iojs": ">=1.0.0",
@@ -3706,7 +3638,6 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
"dev": true,
"funding": [
{
"type": "github",
@@ -4094,7 +4025,6 @@
"version": "3.35.1",
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
"integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.2",
@@ -4147,7 +4077,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -4234,7 +4163,6 @@
"version": "3.4.19",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
"integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
@@ -4272,7 +4200,6 @@
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
"dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.3"
@@ -4285,7 +4212,6 @@
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
"dev": true,
"license": "MIT",
"dependencies": {
"cssesc": "^3.0.0",
@@ -4299,7 +4225,6 @@
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
"integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
"dev": true,
"license": "MIT",
"dependencies": {
"any-promise": "^1.0.0"
@@ -4309,7 +4234,6 @@
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
"integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
"dev": true,
"license": "MIT",
"dependencies": {
"thenify": ">= 3.1.0 < 4"
@@ -4322,7 +4246,6 @@
"version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"fdir": "^6.5.0",
@@ -4339,7 +4262,6 @@
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12.0.0"
@@ -4357,7 +4279,6 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
@@ -4388,7 +4309,6 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-number": "^7.0.0"
@@ -4425,7 +4345,6 @@
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/type-fest": {
@@ -4445,7 +4364,6 @@
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
"integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
"dev": true,
"funding": [
{
"type": "opencollective",
@@ -4497,7 +4415,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"dev": true,
"license": "MIT"
},
"node_modules/vary": {
+5 -5
View File
@@ -35,8 +35,6 @@
"devDependencies": {
"@noble/hashes": "^2.0.1",
"@playwright/test": "^1.58.0",
"@tailwindcss/typography": "^0.5.15",
"autoprefixer": "^10.4.23",
"cross-env": "^7.0.3",
"esbuild": "^0.25.12",
"fake-indexeddb": "^6.2.5",
@@ -44,18 +42,20 @@
"pixelmatch": "^5.3.0",
"playwright": "^1.58.0",
"pngjs": "^7.0.0",
"postcss": "^8.4.41",
"prettier": "^3.8.1",
"prettier-plugin-tailwindcss": "^0.6.5",
"serve": "^14.2.5",
"stylelint": "^16.26.1",
"tailwindcss": "^3.4.19"
"stylelint": "^16.26.1"
},
"license": "GPL-3.0-or-later",
"dependencies": {
"@floating-ui/dom": "^1.7.4",
"@noble/hashes": "^2.0.1",
"@tailwindcss/typography": "^0.5.19",
"autoprefixer": "^10.4.23",
"jsdom": "^27.4.0",
"postcss": "^8.5.6",
"tailwindcss": "^3.4.19",
"ws": "^8.19.0"
}
}
+240 -269
View File
@@ -1,286 +1,257 @@
import './setup-test-env.js';
import { indexedDB, IDBKeyRange } from "fake-indexeddb";
import { WebSocket } from 'ws';
import * as NostrTools from 'nostr-tools';
import fs from 'node:fs';
import path from 'node:path';
import { spawn } from 'node:child_process';
import { fileURLToPath } from 'url';
// --- Polyfills ---
if (!global.indexedDB) {
global.indexedDB = indexedDB;
}
if (!global.IDBKeyRange) {
global.IDBKeyRange = IDBKeyRange;
}
if (!global.NostrTools) {
global.NostrTools = NostrTools;
}
// setup-test-env.js handles WebSocket, crypto, localStorage, window, self, navigator
// --- Imports from Codebase ---
// Note: We need to use relative paths from scripts/agent/
import { startRelay } from './load-test-relay.mjs';
import { NostrClient } from '../../js/nostr/client.js';
import { decryptDM } from '../../js/dmDecryptor.js';
import { startRelay } from './load-test-relay.mjs';
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';
// --- Configuration ---
const ARTIFACTS_DIR = 'artifacts';
if (!fs.existsSync(ARTIFACTS_DIR)) {
fs.mkdirSync(ARTIFACTS_DIR, { recursive: true });
}
const RELAY_PORT = 8890;
const RELAY_PORT = 8899;
const RELAY_URL = `ws://localhost:${RELAY_PORT}`;
const HTTP_PORT = 8000;
const HTTP_URL = `http://localhost:${HTTP_PORT}`;
// --- Logging ---
const dateStr = new Date().toISOString().slice(0, 10).replace(/-/g, ''); // YYYYMMDD
const logFile = path.join(ARTIFACTS_DIR, `smoke-${dateStr}.log`);
const logStream = fs.createWriteStream(logFile, { flags: 'a' });
// 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');
function log(msg, ...args) {
const timestamp = new Date().toISOString();
const message = `[${timestamp}] ${msg} ${args.map(a => typeof a === 'object' ? JSON.stringify(a) : a).join(' ')}`;
console.log(message);
logStream.write(message + '\n');
// Logger
function log(message) {
const timestamp = new Date().toISOString();
const line = `[${timestamp}] ${message}`;
console.log(line);
fs.appendFileSync(LOG_FILE, line + '\n');
}
function error(msg, ...args) {
const timestamp = new Date().toISOString();
const message = `[${timestamp}] [ERROR] ${msg} ${args.map(a => (a instanceof Error ? a.stack : (typeof a === 'object' ? JSON.stringify(a) : a))).join(' ')}`;
console.error(message);
logStream.write(message + '\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.');
}
// --- Helpers ---
async function waitForServer(url, timeoutMs = 10000) {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
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 {
const res = await fetch(url);
if (res.ok) return true;
} catch (e) {
// ignore
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:<pubkey>: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 ---');
} 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);
}
await new Promise(r => setTimeout(r, 200));
}
return false;
}
// --- Main Test Flow ---
async function run() {
let relayServer = null;
let httpServer = null;
let client = null;
let success = true;
try {
log('--- Starting Smoke Test ---');
// 1. Start Relay
log(`Starting local relay on port ${RELAY_PORT}...`);
relayServer = startRelay(RELAY_PORT);
// Wait a bit for relay to be ready (startRelay is synchronous in setup but async in effect?)
// It uses WebSocketServer which starts immediately.
await new Promise(r => setTimeout(r, 500));
log('Relay started.');
// 2. Start HTTP Server
log(`Starting HTTP server (npx serve) on port ${HTTP_PORT}...`);
httpServer = spawn('npx', ['serve', '-p', String(HTTP_PORT)], {
stdio: 'ignore', // or 'inherit' to see output
detached: false
});
const serverReady = await waitForServer(HTTP_URL);
if (!serverReady) {
throw new Error(`HTTP server did not start on ${HTTP_URL} within timeout.`);
}
log('HTTP server is ready.');
// 3. Init NostrClient
log('Initializing NostrClient...');
client = new NostrClient();
client.relays = [RELAY_URL];
client.readRelays = [RELAY_URL];
client.writeRelays = [RELAY_URL];
// Override dev logger to avoid pollution if needed, or let it log
// We can rely on standard logging.
await client.init();
log(`NostrClient initialized and connected to ${RELAY_URL}`);
// 4. Login (Ephemeral)
log('Generating ephemeral keys for Sender...');
const senderSk = NostrTools.generateSecretKey();
const senderSkHex = Buffer.from(senderSk).toString('hex');
const senderPk = NostrTools.getPublicKey(senderSk);
log(`Sender Pubkey: ${senderPk}`);
await client.registerPrivateKeySigner({ privateKey: senderSkHex, persist: false });
client.pubkey = senderPk; // Manually set logged-in user
if (client.pubkey !== senderPk) {
throw new Error(`Client pubkey mismatch. Expected ${senderPk}, got ${client.pubkey}`);
}
log('Sender logged in.');
// 5. Publish Video
log('Publishing test video...');
const videoPayload = {
title: `Smoke Test Video ${Date.now()}`,
description: 'Smoke test description',
magnet: `magnet:?xt=urn:btih:${Math.random().toString(16).slice(2)}`,
url: `${HTTP_URL}/test.mp4`, // Fake URL
thumbnail: `${HTTP_URL}/thumb.jpg`,
mode: 'live',
videoRootId: `smoke-${Date.now()}`,
isNsfw: false,
isForKids: true
};
const publishedEvent = await client.publishVideo(videoPayload, senderPk);
if (!publishedEvent || !publishedEvent.id) {
throw new Error('Failed to publish video event.');
}
log(`Video published. Event ID: ${publishedEvent.id}`);
// Verify Read Back
log('Verifying video can be fetched...');
// We use subscribeVideos for main feed, or getEventById.
// Let's use getEventById for direct verification.
const fetched = await client.getEventById(publishedEvent.id);
if (!fetched) {
throw new Error('Failed to fetch published video.');
}
if (fetched.title !== videoPayload.title) {
throw new Error(`Fetched video title mismatch. Expected "${videoPayload.title}", got "${fetched.title}"`);
}
log('Video fetched successfully.');
// 6. DM Flow
log('Testing DM Flow...');
const recipientSk = NostrTools.generateSecretKey();
const recipientSkHex = Buffer.from(recipientSk).toString('hex');
const recipientPk = NostrTools.getPublicKey(recipientSk);
const recipientNpub = NostrTools.nip19.npubEncode(recipientPk);
log(`Recipient Pubkey: ${recipientPk}`);
const dmMessage = `Smoke Test Message ${Date.now()}`;
// Send DM
const sendResult = await client.sendDirectMessage(recipientNpub, dmMessage);
if (!sendResult.ok) {
throw new Error(`Failed to send DM: ${sendResult.error}`);
}
log('DM sent successfully.');
// Verify Decryption (Simulate Recipient)
log('Verifying DM decryption (Recipient view)...');
// We need to find the DM event.
// Since we don't have a second client syncing, we can manually query the pool/relay.
const filter = {
kinds: [4, 1059], // 4 for legacy, 1059 for NIP-17
'#p': [recipientPk],
limit: 1
};
// wait a moment for propagation
await new Promise(r => setTimeout(r, 200));
const events = await client.pool.list([RELAY_URL], [filter]);
if (events.length === 0) {
throw new Error('Recipient could not find DM event on relay.');
}
const dmEvent = events[0];
log(`Found DM event kind ${dmEvent.kind} id ${dmEvent.id}`);
// Construct decryption context for decryptDM
// We need a decryptor for the recipient.
// Since we have the recipient's private key, we can use NostrTools.nip04 or nip44.
const decryptors = [];
// NIP-04 Decryptor
decryptors.push({
scheme: 'nip04',
decrypt: async (remotePubkey, ciphertext) => {
return await NostrTools.nip04.decrypt(recipientSkHex, remotePubkey, ciphertext);
}
});
// NIP-44 Decryptor
decryptors.push({
scheme: 'nip44',
supportsGiftWrap: true,
decrypt: (remotePubkey, ciphertext) => {
const conversationKey = NostrTools.nip44.v2.utils.getConversationKey(recipientSk, remotePubkey);
return NostrTools.nip44.v2.decrypt(ciphertext, conversationKey);
}
});
const context = {
actorPubkey: recipientPk,
decryptors
};
const decryptResult = await decryptDM(dmEvent, context);
if (!decryptResult.ok) {
throw new Error(`DM decryption failed: ${JSON.stringify(decryptResult.errors)}`);
}
if (decryptResult.plaintext !== dmMessage) {
throw new Error(`DM plaintext mismatch. Expected "${dmMessage}", got "${decryptResult.plaintext}"`);
}
log('DM verified successfully.');
} catch (err) {
success = false;
error('Smoke Test Failed!', err);
} finally {
// Report
const summary = {
timestamp: new Date().toISOString(),
success,
logFile
};
const summaryFile = path.join(ARTIFACTS_DIR, `smoke-summary-${dateStr}.json`);
fs.writeFileSync(summaryFile, JSON.stringify(summary, null, 2));
log(`Summary saved to ${summaryFile}`);
// Teardown
log('Teardown...');
if (client) {
// client.logout(); // Cleans up
// Close pool connections?
// client.pool.close(client.relays); // SimplePool doesn't have explicit close usually?
// NostrTools SimplePool usually manages connections.
// We can just rely on process exit.
}
if (relayServer) {
relayServer.close();
log('Relay stopped.');
}
if (httpServer) {
httpServer.kill();
log('HTTP server stopped.');
}
log('Done.');
process.exit(success ? 0 : 1);
}
}
run();
runSmokeTest();
+246 -247
View File
@@ -1,9 +1,48 @@
#!/usr/bin/env node
// scripts/agent/validate-events.mjs
import WebSocket from 'ws';
import { webcrypto } from 'node:crypto';
// Polyfill WebSocket
if (typeof globalThis.WebSocket === 'undefined') {
globalThis.WebSocket = WebSocket;
}
// Polyfill window and self
if (typeof globalThis.window === 'undefined') {
globalThis.window = globalThis;
}
if (typeof globalThis.self === 'undefined') {
globalThis.self = globalThis;
}
// Polyfill crypto
if (typeof globalThis.crypto === 'undefined') {
globalThis.crypto = webcrypto;
}
// Polyfill localStorage
if (typeof globalThis.localStorage === 'undefined') {
const storage = new Map();
globalThis.localStorage = {
getItem: (k) => storage.get(k) || null,
setItem: (k, v) => storage.set(String(k), String(v)),
removeItem: (k) => storage.delete(k),
clear: () => storage.clear(),
key: (i) => Array.from(storage.keys())[i] || null,
get length() { return storage.size; }
};
}
if (typeof globalThis.window.localStorage === 'undefined') {
globalThis.window.localStorage = globalThis.localStorage;
}
// Mock navigator
if (typeof globalThis.navigator === 'undefined') {
globalThis.navigator = { userAgent: 'node' };
}
import { execSync } from "child_process";
import {
NOTE_TYPES,
validateEventStructure,
buildVideoPostEvent,
buildHttpAuthEvent,
buildReportEvent,
@@ -28,352 +67,312 @@ import {
buildBlockListEvent,
buildHashtagPreferenceEvent,
buildAdminListEvent,
} from "../../js/nostrEventSchemas.js";
validateEventStructure,
NOTE_TYPES,
ADMIN_LIST_IDENTIFIERS
} from '../../js/nostrEventSchemas.js';
import { buildNip71VideoEvent } from '../../js/nostr/nip71.js';
const PUBKEY = "0000000000000000000000000000000000000000000000000000000000000001";
const CREATED_AT = 1700000000;
// Note: Most builders are defined in js/nostrEventSchemas.js and are already instrumented
// with validation checks (validateEventAgainstSchema) when running in Dev Mode.
// buildNip71VideoEvent was instrumented in js/nostr/nip71.js as part of this work.
// This script verifies that all builders produce valid events according to the schema.
const IGNORED_BUILDERS = new Set([
"buildShareUrlFromEventId", // Method in js/app.js, not an event builder
"buildShareUrlFromEvent", // Substring match or alias
"buildListEvent", // Local helper in js/adminListStore.js wrapping buildAdminListEvent
"buildProfileFromEvent", // Local helper in js/services/authService.js
]);
const KNOWN_BUILDERS = {
buildVideoPostEvent: {
builder: buildVideoPostEvent,
const testCases = [
{
name: 'Video Post',
type: NOTE_TYPES.VIDEO_POST,
builder: buildVideoPostEvent,
params: {
pubkey: PUBKEY,
created_at: CREATED_AT,
dTagValue: "test-video-id",
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
created_at: 1234567890,
dTagValue: 'test-video',
content: {
version: 3,
title: "Test Video",
videoRootId: "test-video-id",
infoHash: "0123456789abcdef0123456789abcdef01234567",
},
title: 'Test Video',
videoRootId: 'test-root',
}
}
},
buildHttpAuthEvent: {
builder: buildHttpAuthEvent,
{
name: 'HTTP Auth',
type: NOTE_TYPES.HTTP_AUTH,
builder: buildHttpAuthEvent,
params: {
pubkey: PUBKEY,
created_at: CREATED_AT,
url: "https://example.com/auth",
method: "GET",
payload: "hash",
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
created_at: 1234567890,
url: 'https://example.com',
method: 'GET'
}
},
buildReportEvent: {
builder: buildReportEvent,
{
name: 'Report',
type: NOTE_TYPES.REPORT,
builder: buildReportEvent,
params: {
pubkey: PUBKEY,
created_at: CREATED_AT,
eventId: "e".repeat(64),
reportType: "nudity",
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
created_at: 1234567890,
eventId: '0000000000000000000000000000000000000000000000000000000000000002',
reportType: 'nudity'
}
},
buildVideoMirrorEvent: {
builder: buildVideoMirrorEvent,
{
name: 'Video Mirror',
type: NOTE_TYPES.VIDEO_MIRROR,
builder: buildVideoMirrorEvent,
params: {
pubkey: PUBKEY,
created_at: CREATED_AT,
tags: [["url", "https://example.com/video.mp4"], ["m", "video/mp4"]],
content: "Mirroring video",
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
created_at: 1234567890,
content: 'Alt text'
}
},
buildRepostEvent: {
builder: buildRepostEvent,
{
name: 'Repost',
type: NOTE_TYPES.REPOST,
builder: buildRepostEvent,
params: {
pubkey: PUBKEY,
created_at: CREATED_AT,
eventId: "e".repeat(64),
eventRelay: "wss://relay.example.com",
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
created_at: 1234567890,
eventId: '0000000000000000000000000000000000000000000000000000000000000002',
targetKind: 1
}
},
buildShareEvent: {
builder: buildShareEvent,
{
name: 'Share',
type: NOTE_TYPES.SHARE,
builder: buildShareEvent,
params: {
pubkey: PUBKEY,
created_at: CREATED_AT,
video: { id: "e".repeat(64), pubkey: PUBKEY },
relays: ["wss://relay.example.com"],
content: "Check this out",
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
created_at: 1234567890,
content: 'Check this out',
video: { id: '0000000000000000000000000000000000000000000000000000000000000002' }
}
},
buildRelayListEvent: {
builder: buildRelayListEvent,
{
name: 'Relay List',
type: NOTE_TYPES.RELAY_LIST,
builder: buildRelayListEvent,
params: {
pubkey: PUBKEY,
created_at: CREATED_AT,
relays: [{ url: "wss://relay.example.com", read: true, write: true }],
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
created_at: 1234567890,
relays: ['wss://relay.example.com']
}
},
buildDmRelayListEvent: {
builder: buildDmRelayListEvent,
{
name: 'DM Relay List',
type: NOTE_TYPES.DM_RELAY_LIST,
builder: buildDmRelayListEvent,
params: {
pubkey: PUBKEY,
created_at: CREATED_AT,
relays: ["wss://relay.example.com"],
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
created_at: 1234567890,
relays: ['wss://relay.example.com']
}
},
buildProfileMetadataEvent: {
builder: buildProfileMetadataEvent,
{
name: 'Profile Metadata',
type: NOTE_TYPES.PROFILE_METADATA,
builder: buildProfileMetadataEvent,
params: {
pubkey: PUBKEY,
created_at: CREATED_AT,
metadata: { name: "Test User", about: "Testing" },
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
created_at: 1234567890,
metadata: { name: 'Alice' }
}
},
buildMuteListEvent: {
builder: buildMuteListEvent,
{
name: 'Mute List',
type: NOTE_TYPES.MUTE_LIST,
builder: buildMuteListEvent,
params: {
pubkey: PUBKEY,
created_at: CREATED_AT,
pTags: [PUBKEY],
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
created_at: 1234567890,
pTags: ['0000000000000000000000000000000000000000000000000000000000000002']
}
},
buildDeletionEvent: {
builder: buildDeletionEvent,
{
name: 'Deletion',
type: NOTE_TYPES.DELETION,
builder: buildDeletionEvent,
params: {
pubkey: PUBKEY,
created_at: CREATED_AT,
eventIds: ["e".repeat(64)],
reason: "mistake",
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
created_at: 1234567890,
eventIds: ['0000000000000000000000000000000000000000000000000000000000000002']
}
},
buildLegacyDirectMessageEvent: {
builder: buildLegacyDirectMessageEvent,
{
name: 'Legacy DM',
type: NOTE_TYPES.LEGACY_DM,
builder: buildLegacyDirectMessageEvent,
params: {
pubkey: PUBKEY,
created_at: CREATED_AT,
recipientPubkey: PUBKEY,
ciphertext: "encrypted",
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
created_at: 1234567890,
recipientPubkey: '0000000000000000000000000000000000000000000000000000000000000002',
ciphertext: 'base64ciphertext'
}
},
buildDmAttachmentEvent: {
builder: buildDmAttachmentEvent,
{
name: 'DM Attachment',
type: NOTE_TYPES.DM_ATTACHMENT,
builder: buildDmAttachmentEvent,
params: {
pubkey: PUBKEY,
created_at: CREATED_AT,
recipientPubkey: PUBKEY,
attachment: { url: "https://example.com/file", x: "hash", type: "image/jpeg" },
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
created_at: 1234567890,
recipientPubkey: '0000000000000000000000000000000000000000000000000000000000000002',
attachment: { url: 'https://example.com/file.jpg', x: 'hash' }
}
},
buildDmReadReceiptEvent: {
builder: buildDmReadReceiptEvent,
{
name: 'DM Read Receipt',
type: NOTE_TYPES.DM_READ_RECEIPT,
builder: buildDmReadReceiptEvent,
params: {
pubkey: PUBKEY,
created_at: CREATED_AT,
recipientPubkey: PUBKEY,
eventId: "e".repeat(64),
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
created_at: 1234567890,
recipientPubkey: '0000000000000000000000000000000000000000000000000000000000000002',
eventId: '0000000000000000000000000000000000000000000000000000000000000002'
}
},
buildDmTypingIndicatorEvent: {
builder: buildDmTypingIndicatorEvent,
{
name: 'DM Typing Indicator',
type: NOTE_TYPES.DM_TYPING,
builder: buildDmTypingIndicatorEvent,
params: {
pubkey: PUBKEY,
created_at: CREATED_AT,
recipientPubkey: PUBKEY,
expiresAt: CREATED_AT + 60,
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
created_at: 1234567890,
recipientPubkey: '0000000000000000000000000000000000000000000000000000000000000002',
expiresAt: 1234567900
}
},
buildViewEvent: {
builder: buildViewEvent,
{
name: 'View Event',
type: NOTE_TYPES.VIEW_EVENT,
builder: buildViewEvent,
params: {
pubkey: PUBKEY,
created_at: CREATED_AT,
pointerValue: "e".repeat(64),
pointerTag: ["e", "e".repeat(64)],
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
created_at: 1234567890,
pointerValue: '0000000000000000000000000000000000000000000000000000000000000002'
}
},
buildZapRequestEvent: {
builder: buildZapRequestEvent,
{
name: 'Zap Request',
type: NOTE_TYPES.ZAP_REQUEST,
builder: buildZapRequestEvent,
params: {
pubkey: PUBKEY,
created_at: CREATED_AT,
recipientPubkey: PUBKEY,
amountSats: 100,
relays: ["wss://relay.example.com"],
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
created_at: 1234567890,
recipientPubkey: '0000000000000000000000000000000000000000000000000000000000000002',
amountSats: 100
}
},
buildReactionEvent: {
builder: buildReactionEvent,
{
name: 'Reaction',
type: NOTE_TYPES.VIDEO_REACTION,
builder: buildReactionEvent,
params: {
pubkey: PUBKEY,
created_at: CREATED_AT,
pointerValue: "e".repeat(64),
targetPointer: { type: "e", value: "e".repeat(64) },
content: "+",
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
created_at: 1234567890,
pointerValue: '0000000000000000000000000000000000000000000000000000000000000002',
content: '+'
}
},
buildCommentEvent: {
builder: buildCommentEvent,
{
name: 'Comment',
type: NOTE_TYPES.VIDEO_COMMENT,
builder: buildCommentEvent,
params: {
pubkey: PUBKEY,
created_at: CREATED_AT,
videoEventId: "e".repeat(64),
content: "Nice video",
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
created_at: 1234567890,
videoEventId: '0000000000000000000000000000000000000000000000000000000000000002',
content: 'Nice video'
}
},
buildWatchHistoryEvent: {
builder: buildWatchHistoryEvent,
{
name: 'Watch History',
type: NOTE_TYPES.WATCH_HISTORY,
builder: buildWatchHistoryEvent,
params: {
pubkey: PUBKEY,
created_at: CREATED_AT,
monthIdentifier: "2023-01",
content: { version: 2, month: "2023-01", items: [] },
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
created_at: 1234567890,
monthIdentifier: 'watch-history:2025-01',
content: { version: 2, month: '2025-01', items: [] }
}
},
buildSubscriptionListEvent: {
builder: buildSubscriptionListEvent,
{
name: 'Subscription List',
type: NOTE_TYPES.SUBSCRIPTION_LIST,
builder: buildSubscriptionListEvent,
params: {
pubkey: PUBKEY,
created_at: CREATED_AT,
content: "encrypted-json",
encryption: "nip44_v2",
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
created_at: 1234567890,
content: 'encrypted-payload'
}
},
buildBlockListEvent: {
builder: buildBlockListEvent,
{
name: 'Block List',
type: NOTE_TYPES.USER_BLOCK_LIST,
builder: buildBlockListEvent,
params: {
pubkey: PUBKEY,
created_at: CREATED_AT,
content: "encrypted-json",
encryption: "nip44_v2",
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
created_at: 1234567890,
content: 'encrypted-payload'
}
},
buildHashtagPreferenceEvent: {
builder: buildHashtagPreferenceEvent,
{
name: 'Hashtag Preferences',
type: NOTE_TYPES.HASHTAG_PREFERENCES,
builder: buildHashtagPreferenceEvent,
params: {
pubkey: PUBKEY,
created_at: CREATED_AT,
content: "encrypted-json",
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
created_at: 1234567890,
content: JSON.stringify({ version: 1, interests: [], disinterests: [] })
}
},
buildAdminListEvent: {
builder: buildAdminListEvent,
{
name: 'Admin List (Moderation)',
type: NOTE_TYPES.ADMIN_MODERATION_LIST,
// Special handling for args
args: ["moderation", {
pubkey: PUBKEY,
created_at: CREATED_AT,
hexPubkeys: [PUBKEY],
}]
builder: (params) => buildAdminListEvent(ADMIN_LIST_IDENTIFIERS.moderation, params),
params: {
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
created_at: 1234567890,
hexPubkeys: []
}
},
};
function getUsedBuilders() {
console.log("Scanning repo for event builder usage...");
try {
const output = execSync('grep -r "build[A-Za-z]\\+Event" js/', { encoding: 'utf-8' });
const lines = output.split('\n');
const usedBuilders = new Set();
const regex = /(build[A-Za-z]+Event)/g;
lines.forEach(line => {
// Skip the schemas file itself
if (line.includes('js/nostrEventSchemas.js')) return;
const matches = line.match(regex);
if (matches) {
matches.forEach(match => usedBuilders.add(match));
{
name: 'NIP-71 Video',
type: NOTE_TYPES.NIP71_VIDEO,
builder: buildNip71VideoEvent,
params: {
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
title: 'Test NIP-71',
metadata: { kind: 21, title: 'Test NIP-71' },
pointerIdentifiers: { videoRootId: 'test-root' }
}
});
return usedBuilders;
} catch (error) {
// Grep returns status 1 if no matches found, which might throw an error in execSync
// depending on the version/platform, or just empty output.
console.error("Grep failed or found nothing:", error.message);
return new Set();
}
}
];
async function run() {
const usedBuilders = getUsedBuilders();
console.log(`Found ${usedBuilders.size} unique builder(s) used in the codebase.`);
// also check if we missed any exported builder in our KNOWN_BUILDERS list
// by assuming KNOWN_BUILDERS contains all we want to support.
let failureCount = 0;
const missingTests = [];
usedBuilders.forEach(builderName => {
if (!KNOWN_BUILDERS[builderName] && !IGNORED_BUILDERS.has(builderName)) {
missingTests.push(builderName);
}
});
if (missingTests.length > 0) {
console.warn("\n[WARNING] The following builders are used in the repo but have no test case in this script:");
missingTests.forEach(name => console.warn(` - ${name}`));
// We don't fail the build for this yet, but we warn.
// Or maybe we should fail? The prompt said "Check runtime event construction...".
// Let's count it as a "gap" but allow passing if validation of known ones works.
}
console.log("\nValidating builders...");
for (const [name, config] of Object.entries(KNOWN_BUILDERS)) {
// If we want to be strict, we could check if it is used. But validation is good regardless.
const isUsed = usedBuilders.has(name);
const usageLabel = isUsed ? "(USED)" : "(UNUSED)";
let failed = false;
for (const testCase of testCases) {
try {
let event;
if (config.args) {
event = config.builder(...config.args);
} else {
event = config.builder(config.params);
}
const { valid, errors } = validateEventStructure(config.type, event);
if (valid) {
console.log(`[PASS] ${name} ${usageLabel}`);
} else {
console.error(`[FAIL] ${name} ${usageLabel}`);
errors.forEach((err) => console.error(` - ${err}`));
failureCount++;
}
} catch (e) {
console.error(`[ERROR] ${name} ${usageLabel} threw an exception:`, e);
failureCount++;
const event = testCase.builder(testCase.params);
if (!event) {
console.error(`[FAIL] ${testCase.name}: Builder returned null`);
failed = true;
continue;
}
const { valid, errors } = validateEventStructure(testCase.type, event);
if (!valid) {
console.error(`[FAIL] ${testCase.name}:`);
errors.forEach(err => console.error(` - ${err}`));
failed = true;
} else {
console.log(`[PASS] ${testCase.name}`);
}
} catch (err) {
console.error(`[FAIL] ${testCase.name} threw error:`, err);
failed = true;
}
}
if (failureCount > 0) {
console.error(`\nValidation failed with ${failureCount} errors.`);
process.exit(1);
} else {
console.log("\nAll known builders validated successfully.");
process.exit(0);
}
}
run();
if (failed) {
process.exit(1);
}
+92 -24
View File
@@ -26,9 +26,8 @@ function runCommand(command, args = [], options = {}) {
return result;
}
function getChangedFiles() {
function getChangedFiles(baseRef) {
try {
const baseRef = process.env.GITHUB_BASE_REF || 'main';
const baseUrl = process.env.GITHUB_BASE_REPO_URL;
let remote = 'origin';
@@ -64,7 +63,6 @@ function getChangedFiles() {
console.error('Error getting changed files:', error.stderr || error.message);
// Fallback: try 2-dot diff if 3-dot failed (e.g. shallow clone issues)
try {
const baseRef = process.env.GITHUB_BASE_REF || 'main';
const remote = process.env.GITHUB_BASE_REPO_URL ? 'upstream' : 'origin';
const cmd = `git diff --name-only ${remote}/${baseRef} HEAD`;
console.log(`Retrying with 2-dot diff: ${cmd}`);
@@ -77,8 +75,7 @@ function getChangedFiles() {
}
}
function checkReleaseChannel(changedFiles) {
const baseRef = process.env.GITHUB_BASE_REF || 'main';
function checkReleaseChannel(changedFiles, baseRef) {
const warnings = [];
if (baseRef === 'main') {
@@ -100,13 +97,11 @@ function checkReleaseChannel(changedFiles) {
return warnings;
}
async function main() {
try {
console.log('Starting Automated PR Review...');
let commentBody = '## 🤖 Automated PR Review\n\n';
let hasFailures = false;
async function reviewPR(prNumber, baseRef) {
console.log(`Starting Automated PR Review for PR #${prNumber} (base: ${baseRef})...`);
let commentBody = '## 🤖 Automated PR Review\n\n';
let hasFailures = false;
let hasFormatChanges = false;
let microFixesStatus = '';
// 1. Install Dependencies
if (process.env.CI || process.argv.includes('--force-ci')) {
@@ -149,13 +144,13 @@ async function main() {
}
// 5. Security/Protocol Review Flags
const changedFiles = getChangedFiles();
const changedFiles = getChangedFiles(baseRef);
const sensitiveChanges = changedFiles.filter(file =>
SENSITIVE_PATHS.some(pattern => pattern.test(file))
);
// Release Channel Check
const channelWarnings = checkReleaseChannel(changedFiles);
const channelWarnings = checkReleaseChannel(changedFiles, baseRef);
if (channelWarnings.length > 0) {
commentBody += '### 🚀 Release Channel Checks\n\n';
channelWarnings.forEach(w => commentBody += `${w}\n\n`);
@@ -182,7 +177,7 @@ async function main() {
commentBody += '\n';
// 6. Post Comment (if in CI)
if (process.env.GITHUB_TOKEN && process.env.PR_NUMBER) {
if (process.env.GITHUB_TOKEN && prNumber) {
// Verify gh CLI is available
console.log('Verifying GitHub CLI...');
runCommand('gh', ['--version']);
@@ -194,7 +189,7 @@ async function main() {
execSync('git config user.name "bitvid-agent"');
execSync('git config user.email "[email protected]"');
execSync('git add .');
execSync('git commit -m "fix(ai): formatting (agent)"');
execSync('git commit -m "fix(ai): formatting (agent) suggested"');
execSync('git push');
commentBody += '> **✅ Micro-fixes applied:** Formatting fixes have been committed to this branch.\n\n';
@@ -204,13 +199,11 @@ async function main() {
// Fallback: Create a new branch and open a PR
try {
console.log('Attempting to create a follow-up PR with fixes...');
const prNumber = process.env.PR_NUMBER;
const fixBranchName = `agent-fix/pr-${prNumber}-${Date.now()}`;
// Push the current HEAD (with fixes) to a new branch on origin
execSync(`git push origin HEAD:refs/heads/${fixBranchName}`);
const baseRef = process.env.GITHUB_BASE_REF || 'main';
const prTitle = `fix(ai): formatting fixes for PR #${prNumber}`;
const prBody = `This PR applies automated formatting fixes for PR #${prNumber}.`;
@@ -231,9 +224,9 @@ async function main() {
}
}
console.log('Posting comment to PR #' + process.env.PR_NUMBER);
console.log('Posting comment to PR #' + prNumber);
try {
const gh = spawnSync('gh', ['pr', 'comment', process.env.PR_NUMBER, '-F', '-'], {
const gh = spawnSync('gh', ['pr', 'comment', prNumber, '-F', '-'], {
input: commentBody,
stdio: ['pipe', 'inherit', 'inherit']
});
@@ -247,17 +240,92 @@ async function main() {
}
// Always print the report to stdout so it's visible in logs (especially if posting fails or for debugging)
// Always print the report to stdout
console.log('---------------------------------------------------');
console.log('PR Review Report:');
console.log(`PR Review Report for PR #${prNumber}:`);
console.log(commentBody);
console.log('---------------------------------------------------');
if (hasFailures) {
process.exit(1);
if (hasFailures) {
throw new Error('Lint or Test failures detected.');
}
}
async function main() {
try {
const isAll = process.argv.includes('--all');
if (isAll) {
console.log('Fetching all open PRs...');
// Get list of open PRs
const listCmd = ['pr', 'list', '--state', 'open', '--json', 'number,baseRefName,headRefName'];
const result = spawnSync('gh', listCmd, { encoding: 'utf-8' });
if (result.error || result.status !== 0) {
throw new Error(`Failed to list PRs: ${result.stderr || result.error?.message}`);
}
const prs = JSON.parse(result.stdout);
console.log(`Found ${prs.length} open PRs.`);
let hasGlobalFailures = false;
// Save current branch to restore later
let originalBranch;
try {
originalBranch = execSync('git branch --show-current', { encoding: 'utf-8' }).trim();
} catch (e) {
// Ignore
}
for (const pr of prs) {
try {
console.log(`\n=== Processing PR #${pr.number} ===`);
// Checkout PR
runCommand('gh', ['pr', 'checkout', pr.number]);
// Run Review
await reviewPR(pr.number, pr.baseRefName);
} catch (e) {
console.error(`❌ Failed to process PR #${pr.number}:`, e.message);
hasGlobalFailures = true;
// Continue to next PR
} finally {
// Clean up or reset if needed?
// Since we use gh pr checkout, we are on a different branch.
// Next iteration will checkout another branch.
}
}
if (originalBranch) {
console.log(`Restoring original branch: ${originalBranch}`);
try {
execSync(`git checkout ${originalBranch}`);
} catch(e) {
console.error('Failed to restore original branch');
}
}
if (hasGlobalFailures) {
process.exit(1);
}
} else {
const prNumber = process.env.PR_NUMBER;
const baseRef = process.env.GITHUB_BASE_REF || 'main';
if (prNumber) {
await reviewPR(prNumber, baseRef);
} else {
// Local run or manual invocation without specific PR env vars
console.log('No PR_NUMBER found and no --all flag. Running local review against ' + baseRef);
await reviewPR('local', baseRef);
}
}
} catch (error) {
console.error('Automated PR Review failed:', error);
console.error('Automated PR Review failed:', error.message);
process.exit(1);
}
}
+2 -1
View File
@@ -38,7 +38,8 @@ function copyFile(src, dest) {
if (fs.existsSync(src)) {
fs.copyFileSync(src, dest);
} else {
console.warn(`Warning: Source file ${src} does not exist.`);
console.error(`Error: Source file ${src} does not exist.`);
process.exit(1);
}
}
+3
View File
@@ -4,6 +4,9 @@ import fs from "node:fs";
import { fileURLToPath } from "node:url";
import path from "node:path";
// Ensure NODE_ENV is set to "test" so that nostrToolsBootstrap.js uses the mock toolkit
process.env.NODE_ENV = "test";
const rootDir = path.dirname(fileURLToPath(new URL("../package.json", import.meta.url)));
const testsDir = path.join(rootDir, "tests");
const testFiles = [];
+5
View File
@@ -49,6 +49,9 @@ test("batchFetchProfiles handles fast and failing relays", async () => {
const originalRelays = Array.isArray(nostrClient.relays)
? [...nostrClient.relays]
: [];
const originalReadRelays = Array.isArray(nostrClient.readRelays)
? [...nostrClient.readRelays]
: [];
const originalWriteRelays = Array.isArray(nostrClient.writeRelays)
? [...nostrClient.writeRelays]
: nostrClient.writeRelays;
@@ -60,6 +63,7 @@ test("batchFetchProfiles handles fast and failing relays", async () => {
const poolListCalls = [];
nostrClient.relays = [FAST_RELAY, FAIL_RELAY];
nostrClient.readRelays = [FAST_RELAY, FAIL_RELAY];
nostrClient.writeRelays = [FAST_RELAY, FAIL_RELAY];
nostrClient.readRelays = [FAST_RELAY, FAIL_RELAY];
nostrClient.pool = {
@@ -96,6 +100,7 @@ test("batchFetchProfiles handles fast and failing relays", async () => {
});
} finally {
nostrClient.relays = originalRelays;
nostrClient.readRelays = originalReadRelays;
nostrClient.writeRelays = originalWriteRelays;
nostrClient.readRelays = originalReadRelays;
nostrClient.pool = originalPool;
+2
View File
@@ -273,9 +273,11 @@ function createController(options = {}) {
}
return hashtagStore.disinterests.delete(normalized);
},
load: async () => {},
publish: async () => ({ ok: true }),
load: async () => {},
on: () => () => {},
load: async () => {},
};
const hashtagPreferences = {
+113
View File
@@ -0,0 +1,113 @@
import test from "node:test";
import assert from "node:assert/strict";
import VideoModalController from "../js/ui/videoModalController.js";
test("VideoModalController", async (t) => {
await t.test("ensureVideoModalReady throws if modal is missing", async () => {
const controller = new VideoModalController({
getVideoModal: () => null,
});
await assert.rejects(
controller.ensureVideoModalReady(),
/Video modal instance is not available/
);
});
await t.test("ensureVideoModalReady loads modal if needs rehydrate", async () => {
let loadCalled = false;
let setVideoElementCalled = false;
const mockRoot = { isConnected: true };
const mockVideoElement = { isConnected: true };
const mockModal = {
// First call returns null (simulate missing root), subsequent calls return valid
getRoot: (() => {
let calls = 0;
return () => {
calls++;
return calls === 1 ? null : mockRoot;
};
})(),
getVideoElement: () => mockVideoElement,
load: async () => {
loadCalled = true;
},
setVideoElement: (el) => {
setVideoElementCalled = true;
assert.equal(el, mockVideoElement);
},
};
const controller = new VideoModalController({
getVideoModal: () => mockModal,
});
const result = await controller.ensureVideoModalReady();
assert.equal(loadCalled, true, "Should call load()");
assert.equal(setVideoElementCalled, true, "Should set video element");
assert.equal(result.root, mockRoot);
assert.equal(result.videoElement, mockVideoElement);
});
await t.test("showModalWithPoster uses provided video", async () => {
const mockVideo = { id: "v1" };
let openedVideo = null;
let appliedPoster = false;
const mockModal = {
getRoot: () => ({ isConnected: true }),
getVideoElement: () => ({ isConnected: true }),
load: async () => {},
open: (video) => { openedVideo = video; },
applyLoadingPoster: () => { appliedPoster = true; },
setVideoElement: () => {},
};
const controller = new VideoModalController({
getVideoModal: () => mockModal,
});
await controller.showModalWithPoster(mockVideo);
assert.deepEqual(openedVideo, mockVideo);
assert.equal(appliedPoster, true);
});
await t.test("showModalWithPoster falls back to current video", async () => {
const currentVideo = { id: "current" };
let openedVideo = null;
const mockModal = {
getRoot: () => ({ isConnected: true }),
getVideoElement: () => ({ isConnected: true }),
load: async () => {},
open: (video) => { openedVideo = video; },
applyLoadingPoster: () => {},
setVideoElement: () => {},
};
const controller = new VideoModalController({
getVideoModal: () => mockModal,
callbacks: {
getCurrentVideo: () => currentVideo,
}
});
await controller.showModalWithPoster(); // No video passed
assert.deepEqual(openedVideo, currentVideo);
});
await t.test("forceRemoveModalPoster calls modal method", async () => {
let removeReason = null;
const mockModal = {
forceRemovePoster: (reason) => { removeReason = reason; return true; }
};
const controller = new VideoModalController({
getVideoModal: () => mockModal,
});
controller.forceRemoveModalPoster("test-reason");
assert.equal(removeReason, "test-reason");
});
});