Compare commits

..
Author SHA1 Message Date
thePR0M3TH3AN 93ffe552a5 update 2026-01-03 13:39:01 -05:00
thePR0M3TH3AN 071d78dd6c update 2025-11-12 11:06:10 -05:00
thePR0M3TH3AN 000127f88c update 2025-10-27 10:57:35 -04:00
thePR0M3TH3AN 7cf334de8b update 2025-10-26 22:18:10 -04:00
thePR0M3TH3AN 42edebcf6d update 2025-10-26 21:50:09 -04:00
thePR0M3TH3AN 281a6511f1 update 2025-10-26 13:12:17 -04:00
thePR0M3TH3AN b85a101749 update 2025-10-21 12:51:01 -04:00
thePR0M3TH3AN b33f7f5f23 Merge pull request #756 from PR0M3TH3AN/codex/implement-nip-46-authentication-provider
Add NIP-46 remote signer auth provider
2025-10-21 12:28:43 -04:00
thePR0M3TH3AN 5328168e99 Add NIP-46 remote signer auth provider 2025-10-21 12:25:41 -04:00
thePR0M3TH3AN 73a8d42174 Merge pull request #755 from PR0M3TH3AN/codex/implement-nsec-auth-provider
Add manual nsec authentication provider
2025-10-21 11:55:30 -04:00
thePR0M3TH3AN a6b889fc0d Add nsec manual key authentication provider 2025-10-21 11:55:06 -04:00
thePR0M3TH3AN 44f450a90c Merge pull request #754 from PR0M3TH3AN/codex/refactor-nip-07-integration-and-apis
Refactor Nostr active signer integration for NIP-07 provider
2025-10-21 11:31:14 -04:00
thePR0M3TH3AN fbe6f4a649 Add active signer support and NIP-07 provider refactor 2025-10-21 11:30:28 -04:00
thePR0M3TH3AN edc47631bb Merge pull request #753 from PR0M3TH3AN/codex/refactor-login-modal-and-controller-setup
Refactor login modal to provider-driven controller
2025-10-21 10:36:36 -04:00
thePR0M3TH3AN 1353254445 Refactor login modal to provider-driven controller 2025-10-21 10:36:05 -04:00
thePR0M3TH3AN 97b6e9c11d Merge pull request #752 from PR0M3TH3AN/codex/update-authtype-handling-in-cache
Handle flexible auth provider IDs in cache
2025-10-21 10:02:50 -04:00
thePR0M3TH3AN 3d1486ef79 Handle flexible auth provider IDs in cache 2025-10-21 09:59:46 -04:00
thePR0M3TH3AN 147e57845b Merge pull request #751 from PR0M3TH3AN/codex/create-authproviders-and-update-authservice
Add pluggable auth provider registry
2025-10-21 09:47:38 -04:00
thePR0M3TH3AN 494b00b926 Refactor auth login providers 2025-10-21 09:46:19 -04:00
thePR0M3TH3AN 8f316f915b Merge pull request #397 from PR0M3TH3AN/unstable
Unstable
2025-10-09 21:36:01 -04:00
thePR0M3TH3AN e77db9d07d Merge pull request #263 from PR0M3TH3AN/unstable
Unstable
2025-10-02 21:27:33 -04:00
thePR0M3TH3AN cdff3efe23 Merge pull request #191 from PR0M3TH3AN/unstable
Added watch list.
2025-09-30 22:12:37 -04:00
thePR0M3TH3AN 1d1655d2a7 Merge pull request #166 from PR0M3TH3AN/unstable
Unstable
2025-09-30 09:10:39 -04:00
thePR0M3TH3AN 79e5267524 Merge pull request #156 from PR0M3TH3AN/unstable
Unstable
2025-09-29 13:49:42 -04:00
thePR0M3TH3AN 97c4ccff50 Merge pull request #112 from PR0M3TH3AN/unstable
Unstable
2025-09-25 20:41:49 -04:00
thePR0M3TH3AN fb194f7144 Merge pull request #14 from PR0M3TH3AN/unstable
updated guide
2025-02-15 15:40:30 -05:00
thePR0M3TH3AN b47bd36278 Merge pull request #13 from PR0M3TH3AN/unstable
added profile/channel pages, subscription lists and channel subscription feed, removed "private" video flags
2025-02-10 08:10:37 -05:00
thePR0M3TH3AN 1066c6c74a Merge pull request #12 from PR0M3TH3AN/unstable
increased nostr profile info fetch speed and reliability
2025-02-09 09:45:59 -05:00
thePR0M3TH3AN 5faf572040 Merge pull request #11 from PR0M3TH3AN/unstable
added whitelist filter to video list
2025-02-08 12:06:28 -05:00
thePR0M3TH3AN 40be8da9aa Merge pull request #10 from PR0M3TH3AN/unstable
updated site text and beacon
2025-02-07 17:33:35 -05:00
thePR0M3TH3AN 3001e956c1 Merge pull request #9 from PR0M3TH3AN/unstable
Added sidebar and massive performance improvements
2025-02-07 13:12:22 -05:00
thePR0M3TH3AN c4ee73d8c0 Merge pull request #8 from PR0M3TH3AN/unstable
added block filter for link path to blacklisted npub content
2025-02-04 12:20:01 -05:00
thePR0M3TH3AN 1c486e2799 Merge pull request #7 from PR0M3TH3AN/unstable
overall much better.
2025-02-04 11:38:40 -05:00
thePR0M3TH3AN 587f2b5f96 Merge pull request #6 from PR0M3TH3AN/unstable
fixed bug when editing older videos without the needed fields
2025-02-03 14:20:38 -05:00
thePR0M3TH3AN 87e5ccdee5 Merge pull request #5 from PR0M3TH3AN/unstable
Added custom Nostr DM form submissions for: Whitelisting, Bug Reporting, Feature Requesting and Content Appeals.
2025-02-03 09:39:16 -05:00
thePR0M3TH3AN 77a4d53e54 Merge pull request #4 from PR0M3TH3AN/unstable
Added better UI modal flow and reliable revert/edit/delete functionality
2025-02-01 21:34:45 -05:00
thePR0M3TH3AN 518428fe99 Merge pull request #3 from PR0M3TH3AN/unstable
adjusted footer, added community guidelines and created banner for forms
2025-01-30 15:41:54 -05:00
thePR0M3TH3AN 65ff3ba841 Merge pull request #2 from PR0M3TH3AN/unstable
Made a ton of changes. IDK. seems to work well.
2025-01-30 09:04:50 -05:00
1685 changed files with 75149 additions and 584923 deletions

No files matched your search

-20
View File
@@ -1,20 +0,0 @@
{
"name": "bitvid",
"image": "mcr.microsoft.com/devcontainers/javascript-node:22",
"features": {
"ghcr.io/devcontainers/features/github-cli:1": {}
},
"forwardPorts": [
3000
],
"postCreateCommand": "npm ci && npm run build && npx playwright install --with-deps",
"customizations": {
"vscode": {
"extensions": [
"esbenp.prettier-vscode",
"bradlc.vscode-tailwindcss",
"stylelint.vscode-stylelint"
]
}
}
}
-7
View File
@@ -1,7 +0,0 @@
node_modules
.git
.gitignore
artifacts
Dockerfile
scripts
test-results
-54
View File
@@ -1,54 +0,0 @@
name: Automated Agent PR Review
on:
pull_request:
types: [opened, synchronize, reopened]
permissions:
contents: write
pull-requests: write
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
agent-review:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.ref }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
fetch-depth: 0
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'npm'
- name: Add upstream remote
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
git remote add upstream https://x-access-token:${GITHUB_TOKEN}@github.com/PR0M3TH3AN/bitvid.git || true
git fetch upstream
- name: Verify upstream branch and diff
run: |
if git ls-remote --heads upstream "${{ github.base_ref }}" | grep "${{ github.base_ref }}" > /dev/null; then
git fetch upstream "${{ github.base_ref }}"
git diff --name-only upstream/${{ github.base_ref }}...HEAD
else
echo "::warning::Branch upstream/${{ github.base_ref }} not found. Skipping diff."
fi
- name: Run Agent Review
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
GITHUB_BASE_REF: ${{ github.base_ref }}
GITHUB_BASE_REPO_URL: ${{ github.event.pull_request.base.repo.clone_url }}
run: node scripts/automated-pr-review.mjs
+10 -365
View File
@@ -4,26 +4,18 @@ on:
push:
branches:
- main
- unstable
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 45
strategy:
matrix:
node-version: [22]
node-version: [20]
steps:
- name: Check out repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
@@ -52,43 +44,17 @@ jobs:
run: npm ci
- name: Install ripgrep
run: |
sudo rm -f /etc/apt/sources.list.d/microsoft-prod.list
sudo apt-get update && sudo apt-get install -y ripgrep
run: sudo apt-get update && sudo apt-get install -y ripgrep
- name: Run lint suite
run: npm run lint
- name: Build assets
run: npm run build
- name: Verify deploy artifact markup and hashed references
run: npm run verify:dist:deploy-artifact
- name: Smoke check hashed dist asset references
run: npm run smoke:dist-assets
- name: Build CSS
run: npm run build:css
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium
- name: Run cache-upgrade smoke test
if: github.event_name == 'push' && github.ref == 'refs/heads/unstable'
run: npm run test:e2e:cache-upgrade
- name: Generate dist hash state
if: github.event_name == 'push' && github.ref == 'refs/heads/unstable'
run: |
mkdir -p artifacts
node scripts/hash-dist.mjs --dist dist --output artifacts/dist-hashes-current.json
- name: Upload dist hash artifact
if: github.event_name == 'push' && github.ref == 'refs/heads/unstable'
uses: actions/upload-artifact@v4
with:
name: dist-hashes-current
path: artifacts/dist-hashes-current.json
if-no-files-found: error
- name: Run visual regression tests
id: visual
run: npm run test:visual
@@ -104,333 +70,12 @@ jobs:
test-results
if-no-files-found: ignore
- name: Run unit tests
run: npm run test:unit
- name: Check CSS artifacts are clean
run: npm run check:css
- name: Fail if visual regressions detected
if: steps.visual.outcome == 'failure'
run: exit 1
deploy-cache-state:
if: github.event_name == 'push' && github.ref == 'refs/heads/unstable'
needs: build
runs-on: ubuntu-latest
timeout-minutes: 20
concurrency:
group: deploy-cache-state-unstable
cancel-in-progress: false
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Download dist hash artifact
uses: actions/download-artifact@v4
with:
name: dist-hashes-current
path: artifacts
- name: Fetch prior dist hash state and metadata
env:
DIST_HASH_STATE_URL: ${{ secrets.DIST_HASH_STATE_URL }}
run: |
mkdir -p artifacts
if [ -n "$DIST_HASH_STATE_URL" ]; then
curl --fail --silent --show-error --dump-header artifacts/dist-hashes-previous.headers "$DIST_HASH_STATE_URL" -o artifacts/dist-hashes-previous.json
DIST_HASH_STATE_ETAG=$(awk 'tolower($1)=="etag:" {print $2}' artifacts/dist-hashes-previous.headers | tr -d '\r' | tail -n 1)
DIST_HASH_STATE_VERSION=$(awk 'tolower($1)=="x-dist-hash-version:" {print $2}' artifacts/dist-hashes-previous.headers | tr -d '\r' | tail -n 1)
DIST_HASH_STATE_LAST_MODIFIED=$(awk 'BEGIN{IGNORECASE=1} /^Last-Modified:/ {sub(/^Last-Modified:[[:space:]]*/, ""); sub(/\r$/, ""); print; exit}' artifacts/dist-hashes-previous.headers)
echo "Loaded prior dist hash state from authoritative store."
else
echo '{"combined":"","files":[]}' > artifacts/dist-hashes-previous.json
DIST_HASH_STATE_ETAG=''
DIST_HASH_STATE_VERSION=''
DIST_HASH_STATE_LAST_MODIFIED=''
echo "No DIST_HASH_STATE_URL configured; using empty baseline."
fi
{
echo "DIST_HASH_STATE_ETAG=$DIST_HASH_STATE_ETAG"
echo "DIST_HASH_STATE_VERSION=$DIST_HASH_STATE_VERSION"
echo "DIST_HASH_STATE_LAST_MODIFIED=$DIST_HASH_STATE_LAST_MODIFIED"
} >> "$GITHUB_ENV"
echo "State metadata snapshot: etag='${DIST_HASH_STATE_ETAG:-<none>}', version='${DIST_HASH_STATE_VERSION:-<none>}', last-modified='${DIST_HASH_STATE_LAST_MODIFIED:-<none>}'"
- name: Diff dist hash states
run: |
node scripts/diff-dist-hashes.mjs \
--current artifacts/dist-hashes-current.json \
--previous artifacts/dist-hashes-previous.json > artifacts/dist-hashes-changed.txt
echo "Changed dist paths (initial baseline):"
cat artifacts/dist-hashes-changed.txt || true
- name: Plan cache purge scope
run: |
node scripts/plan-cache-purge.mjs \
--changed-file artifacts/dist-hashes-changed.txt \
--output-file artifacts/dist-purge-targets.txt > artifacts/dist-purge-plan.json
echo "Initial purge plan:"
cat artifacts/dist-purge-plan.json
- name: Record authoritative dist hash state with CAS and resolve races
env:
DIST_HASH_STATE_URL: ${{ secrets.DIST_HASH_STATE_URL }}
DIST_HASH_STATE_UPLOAD_URL: ${{ secrets.DIST_HASH_STATE_UPLOAD_URL }}
run: |
if [ -z "$DIST_HASH_STATE_UPLOAD_URL" ]; then
echo "No DIST_HASH_STATE_UPLOAD_URL configured; skipping authoritative hash-state update."
exit 0
fi
set +e
node scripts/store-dist-hash-state.mjs \
--input artifacts/dist-hashes-current.json \
--upload-url "$DIST_HASH_STATE_UPLOAD_URL" \
--if-match "$DIST_HASH_STATE_ETAG" \
--if-version "$DIST_HASH_STATE_VERSION" \
--if-unmodified-since "$DIST_HASH_STATE_LAST_MODIFIED"
STORE_EXIT=$?
set -e
if [ "$STORE_EXIT" -eq 0 ]; then
echo "CAS write succeeded on first attempt; retaining initial purge plan."
exit 0
fi
if [ "$STORE_EXIT" -ne 3 ]; then
echo "Hash-state upload failed with exit code $STORE_EXIT"
exit "$STORE_EXIT"
fi
echo "CAS conflict detected. Re-fetching state and recomputing purge targets before any CDN purge."
if [ -z "$DIST_HASH_STATE_URL" ]; then
echo "DIST_HASH_STATE_URL is required for race resolution after CAS conflict."
exit 1
fi
curl --fail --silent --show-error --dump-header artifacts/dist-hashes-previous.headers "$DIST_HASH_STATE_URL" -o artifacts/dist-hashes-previous.json
DIST_HASH_STATE_ETAG=$(awk 'tolower($1)=="etag:" {print $2}' artifacts/dist-hashes-previous.headers | tr -d '\r' | tail -n 1)
DIST_HASH_STATE_VERSION=$(awk 'tolower($1)=="x-dist-hash-version:" {print $2}' artifacts/dist-hashes-previous.headers | tr -d '\r' | tail -n 1)
DIST_HASH_STATE_LAST_MODIFIED=$(awk 'BEGIN{IGNORECASE=1} /^Last-Modified:/ {sub(/^Last-Modified:[[:space:]]*/, ""); sub(/\r$/, ""); print; exit}' artifacts/dist-hashes-previous.headers)
echo "Race-resolution metadata: etag='${DIST_HASH_STATE_ETAG:-<none>}', version='${DIST_HASH_STATE_VERSION:-<none>}', last-modified='${DIST_HASH_STATE_LAST_MODIFIED:-<none>}'"
node scripts/diff-dist-hashes.mjs \
--current artifacts/dist-hashes-current.json \
--previous artifacts/dist-hashes-previous.json > artifacts/dist-hashes-changed.txt
echo "Changed dist paths (race-resolved baseline):"
cat artifacts/dist-hashes-changed.txt || true
node scripts/plan-cache-purge.mjs \
--changed-file artifacts/dist-hashes-changed.txt \
--output-file artifacts/dist-purge-targets.txt > artifacts/dist-purge-plan.json
echo "Race-resolved purge plan:"
cat artifacts/dist-purge-plan.json
node scripts/store-dist-hash-state.mjs \
--input artifacts/dist-hashes-current.json \
--upload-url "$DIST_HASH_STATE_UPLOAD_URL" \
--if-match "$DIST_HASH_STATE_ETAG" \
--if-version "$DIST_HASH_STATE_VERSION" \
--if-unmodified-since "$DIST_HASH_STATE_LAST_MODIFIED"
echo "CAS write succeeded after race resolution; proceeding with recomputed purge targets."
- name: Purge changed Cloudflare URLs
env:
CLOUDFLARE_ZONE_ID: ${{ secrets.CLOUDFLARE_ZONE_ID }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_PURGE_BASE_URL: ${{ secrets.CLOUDFLARE_PURGE_BASE_URL }}
run: |
if [ ! -s artifacts/dist-purge-targets.txt ]; then
echo "No purge targets detected. Skipping purge."
exit 0
fi
if [ -z "$CLOUDFLARE_ZONE_ID" ] || [ -z "$CLOUDFLARE_API_TOKEN" ] || [ -z "$CLOUDFLARE_PURGE_BASE_URL" ]; then
echo "Cloudflare secrets not configured; skipping targeted purge."
exit 0
fi
node scripts/purge-cloudflare-changed.mjs \
--changed-file artifacts/dist-purge-targets.txt \
--base-url "$CLOUDFLARE_PURGE_BASE_URL"
- name: Verify deployed index.html version markup
env:
CLOUDFLARE_PURGE_BASE_URL: ${{ secrets.CLOUDFLARE_PURGE_BASE_URL }}
run: |
if [ -z "$CLOUDFLARE_PURGE_BASE_URL" ]; then
echo "CLOUDFLARE_PURGE_BASE_URL is required to verify deployed index.html"
exit 1
fi
DEPLOY_INDEX_URL="${CLOUDFLARE_PURGE_BASE_URL%/}/index.html" npm run verify:deployed:index-html
unit-tests:
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
node-version: [22]
shard: [1, 2, 3]
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Set up Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Cache npm directory
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ matrix.node-version }}-npm-${{ hashFiles('package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-${{ matrix.node-version }}-npm-
- name: Cache node_modules
id: node-modules-cache
uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.os }}-node-${{ matrix.node-version }}-modules-${{ hashFiles('package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-${{ matrix.node-version }}-modules-
- name: Install dependencies
if: steps.node-modules-cache.outputs.cache-hit != 'true'
run: npm ci
- name: Run unit tests (shard ${{ matrix.shard }}/3)
env:
UNIT_TEST_TIMEOUT_MS: 300000
run: npm run test:unit:shard${{ matrix.shard }}
dm-unit-tests:
runs-on: ubuntu-latest
timeout-minutes: 20
strategy:
matrix:
node-version: [22]
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Set up Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Cache npm directory
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ matrix.node-version }}-npm-${{ hashFiles('package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-${{ matrix.node-version }}-npm-
- name: Cache node_modules
id: node-modules-cache
uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.os }}-node-${{ matrix.node-version }}-modules-${{ hashFiles('package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-${{ matrix.node-version }}-modules-
- name: Install dependencies
if: steps.node-modules-cache.outputs.cache-hit != 'true'
run: npm ci
- name: Run DM unit tests
run: npm run test:dm:unit
dm-integration-tests:
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
matrix:
node-version: [22]
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Set up Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Cache npm directory
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ matrix.node-version }}-npm-${{ hashFiles('package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-${{ matrix.node-version }}-npm-
- name: Cache node_modules
id: node-modules-cache
uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.os }}-node-${{ matrix.node-version }}-modules-${{ hashFiles('package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-${{ matrix.node-version }}-modules-
- name: Install dependencies
if: steps.node-modules-cache.outputs.cache-hit != 'true'
run: npm ci
- name: Run DM integration tests
run: npm run test:dm:integration
e2e-headless:
runs-on: ubuntu-latest
timeout-minutes: 45
strategy:
matrix:
node-version: [22]
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Set up Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Cache npm directory
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ matrix.node-version }}-npm-${{ hashFiles('package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-${{ matrix.node-version }}-npm-
- name: Cache node_modules
id: node-modules-cache
uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.os }}-node-${{ matrix.node-version }}-modules-${{ hashFiles('package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-${{ matrix.node-version }}-modules-
- name: Install dependencies
if: steps.node-modules-cache.outputs.cache-hit != 'true'
run: npm ci
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium
- name: Run headless E2E tests
run: npm run test:e2e
-72
View File
@@ -1,72 +0,0 @@
name: Cleanup stuck workflow runs
on:
schedule:
- cron: '*/30 * * * *'
workflow_dispatch:
permissions:
actions: write
jobs:
cleanup:
runs-on: ubuntu-latest
steps:
- name: Cancel stuck workflow runs
uses: actions/github-script@v7
env:
STALE_THRESHOLD_HOURS: '2'
with:
script: |
const thresholdHours = Number(process.env.STALE_THRESHOLD_HOURS || 2);
const thresholdMs = thresholdHours * 60 * 60 * 1000;
const now = Date.now();
const statuses = ['queued', 'in_progress'];
const owner = context.repo.owner;
const repo = context.repo.repo;
const staleRuns = [];
for (const status of statuses) {
const runs = await github.paginate(github.rest.actions.listWorkflowRunsForRepo, {
owner,
repo,
status,
per_page: 100,
});
for (const run of runs) {
const createdAt = Date.parse(run.created_at);
if (Number.isNaN(createdAt)) {
core.warning(`Skipping run ${run.id} (${run.name}) due to invalid created_at.`);
continue;
}
const ageMs = now - createdAt;
if (ageMs > thresholdMs) {
staleRuns.push({
id: run.id,
name: run.name,
status: run.status,
created_at: run.created_at,
html_url: run.html_url,
});
}
}
}
if (staleRuns.length === 0) {
core.info(`No runs older than ${thresholdHours} hours found in queued/in_progress.`);
return;
}
core.info(`Found ${staleRuns.length} stale runs older than ${thresholdHours} hours.`);
for (const run of staleRuns) {
core.info(`Canceling run ${run.id} (${run.name}) status=${run.status} created_at=${run.created_at} url=${run.html_url}`);
await github.request('POST /repos/{owner}/{repo}/actions/runs/{run_id}/force-cancel', {
owner,
repo,
run_id: run.id,
});
}
@@ -1,75 +0,0 @@
name: Daily Design System Audit
on:
schedule:
- cron: '0 9 * * *' # Run daily at 09:00 UTC
workflow_dispatch:
permissions:
contents: write
issues: write
pull-requests: write
jobs:
audit:
name: Audit Design System
runs-on: ubuntu-latest
steps:
- name: Checkout unstable branch
uses: actions/checkout@v4
with:
ref: unstable
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
- name: Verify gh CLI availability
run: gh --version
- name: Install dependencies
run: npm ci
- name: Run Design System Audit (with fix)
id: audit
run: node scripts/daily-design-system-audit.mjs --fix
continue-on-error: true
- name: Check for changes
id: git-check
run: |
if [ -n "$(git status --porcelain)" ]; then
echo "changes=true" >> $GITHUB_OUTPUT
else
echo "changes=false" >> $GITHUB_OUTPUT
fi
- name: Create Pull Request
if: steps.git-check.outputs.changes == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git checkout -b "ds-audit/$(date +%Y-%m-%d)"
git add .
git commit -m "chore: Apply daily design system remediation"
git push origin HEAD
gh pr create \
--title "Daily Design System Remediation" \
--body-file REMEDIATION_REPORT.md \
--base unstable \
--head "ds-audit/$(date +%Y-%m-%d)" \
--label "design-system"
- name: Create Issue on Failure (No Fixes)
if: steps.audit.outcome == 'failure' && steps.git-check.outputs.changes == 'false'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh issue create \
--title "⚠️ Design System Violations Detected" \
--body-file REMEDIATION_REPORT.md \
--label "design-system"
-8
View File
@@ -5,11 +5,3 @@ src/webtorrent-docs/
playwright-report/
test-results/
tests/visual/__snapshots__/
artifacts/
!artifacts/load-report-*.json
css/tailwind.generated.css
ai/reports/
*.log
coverage/
dist/
TEST_LOG.mdtest_logs/nip-hits.txt
-1
View File
@@ -1 +0,0 @@
engine-strict=true
-1
View File
@@ -1 +0,0 @@
22
+11 -387
View File
@@ -2,9 +2,7 @@
This guide tells AI agents how to keep bitvid aligned with the current product direction. Follow it whenever you touch code, content, or documentation inside this repository.
**Important:** Before starting any development or debugging, please review `KNOWN_ISSUES.md`. This file documents pre-existing test failures, environmental quirks, and architectural limitations that you should be aware of to avoid unnecessary investigation.
For any nostr related work, please review the nip documentation located in /docs/nips before beginning work.
For any nostr related work, please review the nip documentation located in /docs/nips before begining work.
**Comment publishing guardrail:** Session actors are limited to passive telemetry (e.g., view counters). Always require the logged-in Nostr signer when sending video comments through [`js/nostr/commentEvents.js`](js/nostr/commentEvents.js). See the reminder in [`docs/nostr-event-schemas.md`](docs/nostr-event-schemas.md#event-catalogue) for context.
@@ -14,7 +12,7 @@ For any nostr related work, please review the nip documentation located in /docs
* **Main** is the production track. Anything merged here must preserve today’s UX and magnet safety guarantees. Rollbacks should be painless: keep commits atomic, avoid destructive migrations, and leave feature flags in a known-good default (`false` unless product explicitly flips them).
* **Unstable** is our experimentation lane. Gate risky behavior behind feature flags defined in `js/constants.js` and document the toggle/rollback plan in PR descriptions.
* **Emergency response:** If a change regresses playback or breaks magnet parsing, revert immediately, call this out in the PR, and annotate the AGENTS.md changelog with remediation tips. **Note:** These regressions usually surface in dev logs and tests around playback/magnet helpers—reference them when triaging so future agents don’t repeat the mistake.
* **Emergency response:** If a change regresses URL-first playback or breaks magnet parsing, revert immediately, call this out in the PR, and annotate the AGENTS.md changelog with remediation tips. **Note:** These regressions usually surface in dev logs and tests around playback/magnet helpers—reference them when triaging so future agents don’t repeat the mistake.
### Browser logging policy
@@ -23,11 +21,12 @@ For any nostr related work, please review the nip documentation located in /docs
* Before promoting a build, flip `IS_DEV_MODE` in `config/instance-config.js` to match the target environment so `window.__BITVID_DEV_MODE__` and the logger channels behave correctly.
* Review and follow [`docs/logging.md`](docs/logging.md) whenever you add new logging or touch inline scripts.
## 2. Mission: Hybrid Playback Strategy
## 2. Mission: URL‑First Playback with WebTorrent Fallback
* **Goal:** Always deliver smooth playback while optimizing for the configured priority (hosted URL or P2P).
* **Configuration:** `DEFAULT_PLAYBACK_SOURCE` in `config/instance-config.js` determines whether `url` or `torrent` is attempted first.
* **Runtime behavior:** Call `bitvidApp.playVideoWithFallback({ url, magnet })` (implemented in `js/app/playbackCoordinator.js` and bound to the app instance in `js/app.js`). This function attempts the preferred source first and falls back to the alternative if the primary fails or stalls. `js/playbackUtils.js` now provides magnet/session helper utilities rather than the playback entry point itself.
* **Goal:** Always deliver smooth playback while keeping hosting costs low.
* **Primary transport:** A hosted video URL (MP4/WebM/HLS/DASH) that the `<video>` element can stream directly.
* **Fallback transport:** A WebTorrent magnet that includes browser‑safe trackers plus optional HTTP hints.
* **Runtime behavior:** Call `bitvidApp.playVideoWithFallback({ url, magnet })` in `js/app.js`. This function orchestrates URL probing and magnet fallback, delegating low-level stream handling to `js/services/playbackService.js`. `js/playbackUtils.js` now provides magnet/session helper utilities rather than the playback entry point itself.
* **Content contract:** bitvid posts (Nostr kind `30078`) must include a `title` and at least one of `url` or `magnet`. Prefer to publish both along with optional `thumbnail`, `description`, and `mode` fields. When mirroring to NIP‑94 (`kind 1063`), copy the hosted URL and (optionally) the magnet so other clients can discover the same asset.
## Styling & Theming Rules (token-first)
@@ -58,7 +57,7 @@ For any nostr related work, please review the nip documentation located in /docs
1. **Required fields:** Validation must ensure a title exists and that either a hosted URL or a magnet (or both) is supplied. Optional `ws` / `xs` inputs should be appended only when a magnet is present.
2. **Magnet parsing errors:** When a user pastes encoded magnets, run `safeDecodeMagnet()` before normalizing. Show inline errors if decoding fails and confirm the raw `magnet:?xt=` string survives the round trip.
3. **Playback source promise:** After submission, confirm the resulting feed item carries `data-play-url` and `data-play-magnet` attributes so playback utilities can attempt the configured source first.
3. **URL-first promise:** After submission, confirm the resulting feed item carries `data-play-url` and `data-play-magnet` attributes so playback utilities can attempt the URL first.
4. **Mixed-content warnings:** If the page is served over HTTPS, reject `ws=` or `xs=` hints that begin with `http://` and show guidance to upgrade to HTTPS. Document the copy in README/UX strings so operators can align messaging.
5. **Telemetry hooks:** Keep existing analytics/logging untouched unless instructed. If you add new modal states, annotate them clearly in code comments.
6. **Modal regressions:** If validation or helper wiring breaks in Main, disable new feature flags and ship a revert PR immediately. Note the rollback steps in AGENTS.md for posterity.
@@ -73,10 +72,10 @@ Run this script before shipping notable UI changes, especially around upload/pla
* Launch the site, open the Upload modal, and confirm required-field validation (title + one of URL/magnet).
* Submit with URL only, magnet only, and both; ensure success states are clear.
2. **Hybrid playback**
2. **URL-first playback**
* Publish a post with both URL and magnet. Load the card and verify the player fetches the configured preferred source first (check network tab or logs).
* Temporarily block the primary source (e.g., devtools throttling or offline). Confirm playback seamlessly falls back to the secondary source.
* Publish a post with both URL and magnet. Load the card and verify the player fetches the hosted URL first (check network tab or logs).
* Temporarily block the URL (e.g., devtools throttling or offline). Confirm playback seamlessly falls back to the magnet.
3. **Magnet hygiene**
* Paste an encoded magnet; ensure `safeDecodeMagnet()` returns the raw value and the final string still includes the original `xt` hash.
@@ -171,381 +170,6 @@ When moving a feature to a controller, follow these steps:
---
## 9. Nostr Addressing & NIP-33 (Lessons Learned)
* **Addressing Rule:** NIP-33 events (e.g., videos, long-form content) are uniquely identified by the combination of `kind`, `pubkey`, and the `d` tag.
* **The `d` tag is King:** When creating pointers for these events, **always use the `d` tag** as the identifier.
* **Avoid Logical IDs:** Do not confuse logical identifiers (like `videoRootId`) with the network identifier (`d` tag). While they might often be the same, they are not guaranteed to be.
* **Hydration Pitfall:** If you create a pointer using a logical ID that differs from the `d` tag, relays will fail to find the event during hydration because the index relies on the `d` tag.
* **Fix Pattern:** When resolving a pointer from an event, prioritize checking `tags.find(t => t[0] === 'd')?.[1]` over any other ID field.
---
## 10. Direct Messages (NIP-04 vs NIP-44)
* **Dual Support:** We must support both legacy NIP-04 and newer NIP-44 Direct Messages.
* **Reasoning:** A significant portion of the user base still relies on NIP-04 DMs. While NIP-44 offers better security, we cannot drop NIP-04 support until adoption of the new standard is nearly universal.
* **Implementation:** When working on DM features, ensure code paths exist for both encryption/decryption methods. Do not deprecate or remove NIP-04 handling logic. Other legacy methods can be considered for removal, but NIP-04 is critical.
---
## 11. Telemetry & Error Aggregation
* **Tooling:** Use `npm run telemetry:aggregate` (which runs `scripts/agent/telemetry-aggregator.mjs`) to collect, sanitize, and aggregate errors from CI/test runs, smoke tests, and agent logs.
* **Privacy Policy:** The tool automatically strips PII (IPs, emails, hex keys, bech32 keys) from logs. **Do not** manually inspect raw logs containing PII unless necessary for debugging in a secure environment; rely on the sanitized reports in `ai/reports/`.
* **Opt-in:** Telemetry generation is opt-in. You must set the environment variable `ENABLE_TELEMETRY=true` when running the aggregator.
* **Reports:** Aggregated reports are generated in `ai/reports/telemetry-YYYYMMDD.md` and `artifacts/error-aggregates.json`. These files provide prioritized issues for triage.
---
## 12. Multi-Agent Coordination
This project uses multiple AI agents (Claude Code, OpenAI Codex, Google Jules) working in parallel. Without coordination, agents will create conflicting PRs, duplicate work, and cause painful merge conflicts. Follow these rules to keep things clean.
### Before Starting Work
1. **Check open PRs.** Run the following command to list all open PRs:
```
curl -s "https://api.github.com/repos/PR0M3TH3AN/bitvid/pulls?state=open&per_page=100" | jq '{count: length, titles: [.[].title]}'
```
If another agent already has a PR touching the same files or subsystem, do not create a competing PR. Instead, note the conflict and ask the maintainer how to proceed.
2. **Check this section for in-flight work.** The maintainer may list active work areas below. Respect these reservations.
3. **Read KNOWN_ISSUES.md.** Do not open PRs to fix issues already documented there unless explicitly asked.
### Subsystem Boundaries
To minimize merge conflicts, treat these as independent work zones. An agent should avoid touching files outside its assigned zone in a single PR:
| Zone | Key Files | Description |
|------|-----------|-------------|
| **Nostr Core** | `js/nostr/client.js`, `js/nostr/adapters/`, `js/nostrClientFacade.js` | Protocol client, signers, relay management |
| **Event Schemas** | `js/nostrEventSchemas.js`, `docs/nostr-event-schemas.md` | Event definitions and documentation |
| **Playback** | `js/services/playbackService.js`, `js/playbackUtils.js`, `js/magnetUtils.js` | Video streaming, magnet handling |
| **UI Controllers** | `js/ui/` | Modal controllers, notification, components |
| **State** | `js/state/` | Profile cache, application state |
| **DMs** | `js/ui/dm/`, `js/nostr/dm/` | Direct messaging subsystem |
| **Moderation** | `js/moderation/`, `docs/moderation/` | Content moderation, reports, admin lists |
| **Build & CI** | `.github/workflows/`, `scripts/`, `tailwind.config.cjs` | CI pipeline, lint scripts, build tooling |
| **Styling** | `css/tokens.css`, `css/tailwind.source.css` | Design tokens and Tailwind source |
| **App Orchestrator** | `js/app.js`, `js/app/` | Main app wiring (high conflict risk — only one PR at a time) |
**`js/app.js` is the highest-risk file.** It wires everything together. Never have two agent PRs modifying it simultaneously.
### PR Discipline
- **One subsystem per PR.** A PR that touches Nostr Core and UI Controllers is two PRs. Split them.
- **Small and mergeable.** Target PRs that can be reviewed in one sitting. If a refactor spans 10+ files, break it into sequential PRs.
- **Merge before branching.** Before starting new work on a subsystem, ensure all open PRs for that subsystem are merged or closed.
- **Rebase, don't stack.** Long-lived branches diverge fast with multiple agents. Rebase onto `unstable` frequently.
### Signaling Work to Other Agents
When you create a PR or start work, leave a clear signal:
- **PR title prefix**: Use a descriptive prefix like `[nostr-core]`, `[playback]`, `[ui]`, `[ci]` so the scope is visible at a glance.
- **PR description**: Include a "Files Modified" section listing the key files touched, so other agents can quickly detect conflicts.
### Task Claiming Protocol (TORCH)
Task coordination uses **TORCH** (Task Orchestration via Relay-Coordinated Handoff) — a decentralized locking protocol built on Nostr. Agents publish ephemeral lock events to public relays; locks auto-expire via NIP-40. No tokens, no secrets, no git push required. Works across all platforms (Claude Code, Codex, Jules). See `docs/agents/TORCH.md` for the full protocol documentation.
**Before starting any task:**
1. **Check for existing claims:**
```bash
node scripts/agent/nostr-lock.mjs check --cadence <daily|weekly>
```
Returns JSON with `locked` (claimed agents) and `available` (free agents) arrays. If an agent appears in the `locked` list, **skip it**.
2. **Claim the task:**
```bash
AGENT_PLATFORM=<jules|claude-code|codex> \
node scripts/agent/nostr-lock.mjs lock \
--agent <agent-name> \
--cadence <daily|weekly>
```
The script generates an ephemeral keypair, publishes a lock event to public Nostr relays, and performs a built-in race check. The key is used once and discarded. Locks auto-expire after 2 hours (configurable via `NOSTR_LOCK_TTL`).
- **Exit 0** = lock acquired, begin work.
- **Exit 3** = race lost, another agent claimed first. Go back to step 1.
- **Exit 2** = relay error. Write a `_failed.md` log and stop.
3. **View all active locks** (optional, for debugging):
```bash
node scripts/agent/nostr-lock.mjs list
```
**Scheduler agents:** Daily and weekly scheduler agents must follow this protocol in addition to their directory-based rotation logic (`docs/agents/task-logs/daily/` and `docs/agents/task-logs/weekly/`). The lock check happens _after_ determining the next task but _before_ executing it. See the scheduler prompts for the specific implementation steps.
### Currently In-Flight Work
<!-- Maintainer: update this list when assigning work to agents. Agents: check this before starting. -->
_No reservations currently active._
---
## 13. Code Health Rules (CI-Enforced)
Three automated lint checks protect the codebase from common growth problems. These run in CI via `npm run lint` and will block your PR if violated.
### File Size Limits
**Script:** `npm run lint:file-size` (`scripts/check-file-size.mjs`)
- New `.js` files under `js/` must stay **under 1,000 lines**.
- Existing oversized files are grandfathered with a recorded line count. They must **not grow** beyond their recorded count + 50 lines.
- If your change would push a grandfathered file over its limit, **extract logic into a new module first**, then make your change.
**Decomposition strategy for agents:**
1. Identify a cohesive block of logic (e.g., a group of related methods, a subsection of a controller).
2. Extract it into a new file in the same directory.
3. Export the extracted functions/class.
4. Import and re-wire from the original file.
5. Run `npm run lint:file-size` to confirm the original file shrank.
6. Update the grandfathered count if needed (the maintainer will handle this when merging).
### innerHTML Baseline
**Script:** `npm run lint:innerhtml` (`scripts/check-innerhtml.mjs`)
- Every file has a **baseline** innerHTML assignment count. Adding new `innerHTML` usage beyond the baseline fails the lint.
- For new UI code, use **safe DOM APIs** instead:
```javascript
// PREFERRED: auto-escaped, no XSS risk
element.textContent = userProvidedString;
const el = document.createElement("div");
el.className = "card";
parent.appendChild(el);
// ACCEPTABLE: innerHTML with escapeHtml for all user data
import { escapeHtml } from "../utils/domUtils.js";
container.innerHTML = `<span>${escapeHtml(title)}</span>`;
// BLOCKED: innerHTML with unescaped user data
container.innerHTML = `<span>${title}</span>`; // XSS vulnerability
```
- If you must add innerHTML (e.g., loading a view template), update the baseline: `node scripts/check-innerhtml.mjs --update` and include the updated BASELINE in your PR.
### Constants — No Duplication
- Relay timing constants live in **`js/nostr/relayConstants.js`** — always import from there.
- Before defining a new numeric constant, search for existing ones with the same value. If a match exists, import it.
- Feature flags live in **`js/constants.js`**.
- Cache policies live in **`js/nostr/cachePolicies.js`**.
- Do not define the same constant in two files. If two modules need the same value, create a shared module and import from it.
---
## 14. Playwright Agent Testing Infrastructure
bitvid includes a test harness and fixtures that let Playwright CLI agents programmatically log in, seed relay data, and inspect app state — no browser extensions or real relays required.
### Activating Test Mode
Add `?__test__=1` to the URL, or set `localStorage.__bitvidTestMode__ = "1"` via `addInitScript`. This installs `window.__bitvidTest__` on the page.
### Overriding Relays
Point the app at a local mock relay instead of production:
```
?__test__=1&__testRelays__=ws://127.0.0.1:8877
```
Or set `localStorage.__bitvidTestRelays__` to a JSON array of relay URLs.
### Test Harness API (`window.__bitvidTest__`)
| Method | Returns | Purpose |
|--------|---------|---------|
| `loginWithNsec(hexKey)` | `Promise<string>` (pubkey) | Programmatic login, bypasses the modal |
| `logout()` | `void` | Clear active signer |
| `getAppState()` | `{ isLoggedIn, activePubkey, relays, ... }` | Inspect current state |
| `getFeedItems()` | `Array<{ title, pubkey, dTag, hasUrl, hasMagnet }>` | Scrape video cards from DOM |
| `waitForFeedItems(n, ms)` | `Promise<Array>` | Wait for N cards to appear |
| `waitForSelector(sel, ms)` | `Promise<true>` | Wait for a DOM element |
| `getRelayHealth()` | `{ relays, unreachable, backoff }` | Relay connection status |
| `applyRelayOverrides(urls)` | `boolean` | Redirect relay connections |
| `nostrClient` | `NostrClient` | Direct access for advanced use |
### Mock Relay (`scripts/agent/simple-relay.mjs`)
Start with `startRelay(port, { httpPort })`. Alongside the Nostr WebSocket protocol, exposes an HTTP API:
| Endpoint | Method | Purpose |
|----------|--------|---------|
| `/seed` | `POST` | Inject signed events (single object or array) |
| `/events` | `GET` | List all stored events |
| `/events` | `DELETE` | Clear all events |
| `/health` | `GET` | Event count + connection count |
Programmatic API: `relay.seedEvent(event)`, `relay.clearEvents()`, `relay.getEvents()`.
### Playwright Fixture (`tests/e2e/helpers/bitvidTestFixture.ts`)
Import `test` and `expect` from the fixture for tests that need the full stack:
```typescript
import { test, expect } from "./helpers/bitvidTestFixture";
test("agent can seed and view videos", async ({ page, gotoApp, loginAs, seedEvent }) => {
await seedEvent({ title: "Test Video", url: "https://example.com/v.mp4" });
await gotoApp();
await loginAs(page);
// ... assertions
});
```
**Available fixture values:**
| Fixture | Type | Purpose |
|---------|------|---------|
| `relay` | Relay instance | Auto-started/stopped per test |
| `seedEvent(video)` | `(TestVideoEvent) => Promise` | Create a signed kind 30078 event and inject it |
| `seedRawEvent(event)` | `(any) => Promise` | Inject a pre-built event |
| `clearRelay()` | `() => Promise` | Wipe all relay events |
| `gotoApp(path?)` | `(string?) => Promise` | Navigate with test mode + relay overrides |
| `loginAs(page)` | `(Page) => Promise<string>` | Login with deterministic test key |
| `testPubkey` | `string` | Hex pubkey of the test key |
| `relayUrl` | `string` | WebSocket URL of the mock relay |
### `data-testid` Selectors
Use these for stable element targeting:
| Selector | Element |
|----------|---------|
| `[data-testid="login-button"]` | Header login button |
| `[data-testid="upload-button"]` | Header upload button (hidden until logged in) |
| `[data-testid="profile-button"]` | Header profile button (hidden until logged in) |
| `[data-testid="search-input"]` | Header search field |
| `[data-testid="login-modal"]` | Login modal container |
| `[data-testid="login-provider-button"]` | Login provider option buttons |
| `[data-testid="nsec-secret-input"]` | Private key textarea in nsec login |
| `[data-testid="nsec-submit"]` | Nsec login submit button |
| `[data-testid="upload-modal"]` | Upload modal container |
| `[data-testid="upload-title"]` | Video title input |
| `[data-testid="upload-url"]` | Video URL input |
| `[data-testid="upload-magnet"]` | Magnet link input |
| `[data-testid="upload-submit"]` | Publish button |
| `[data-testid="video-modal"]` | Video player modal |
| `[data-testid="video-card"]` | Individual video card in the feed |
| `[data-testid="video-list"]` | Video feed grid container |
### Key Files
| File | Purpose |
|------|---------|
| `js/testHarness.js` | Test harness module (installs `window.__bitvidTest__`) |
| `scripts/agent/simple-relay.mjs` | Mock relay with HTTP seeding API |
| `tests/e2e/helpers/bitvidTestFixture.ts` | Reusable Playwright fixture |
| `tests/e2e/agent-testability.spec.ts` | Infrastructure validation tests |
---
## 15. Agent Execution Protocol
This section defines the meta-workflow that **every AI agent** must follow when executing tasks in this codebase. It ensures safe, incremental changes, a clear decision trail, and continuity across context resets or agent hand-offs.
### Mindset
- Implement changes safely. Match existing conventions. Leave a clear trail of decisions.
- Correctness > cleverness. Consistent conventions > personal preference. Explicit tradeoffs > "should work."
- Prefer minimal, incremental changes over rewrites. Keep the build and tests green.
- Do not invent files, APIs, libraries, or behaviors. If unsure, inspect the codebase first.
### Scoping the Work
Before writing any code, establish clarity on three things:
1. **Primary goal** — What is being built or fixed?
2. **Success criteria** — How will we know it's done? (e.g., tests pass, feature visible in UI, lint clean)
3. **Non-goals** — What is explicitly out of scope? (Prevents scope creep across agent sessions.)
### External Notes (Persistent State Files)
Agents lose context across sessions. These files act as persistent memory that any agent (or human) can read to resume work or understand project status. **Update them before any likely context reset.**
**Storage Rule:** To prevent conflicts, these files are stored in dedicated directories with timestamps.
- **Read:** Always look for the **latest** timestamped file in the directory (e.g., `ls context/ | sort | tail -n 1`).
- **Write:** Never overwrite an existing file. Create a **new** file with a new timestamp (e.g., `context/CONTEXT_YYYY-MM-DD_HH-MM-SS.md`).
| Directory | File Pattern | Purpose | When to Update |
|-----------|--------------|---------|----------------|
| `context/` | `CONTEXT_<timestamp>.md` | Current goal, scope, assumptions, constraints, and "Definition of Done" | At start; when scope changes |
| `todo/` | `TODO_<timestamp>.md` | Checkbox task list with "Done" and "Blocked/Questions" sections | After planning; after tasks |
| `decisions/` | `DECISIONS_<timestamp>.md` | Key choices made, alternatives considered, and rationale | When making tradeoffs |
| `test_logs/` | `TEST_LOG_<timestamp>.md` | Commands run and their results (including failures) | After every lint/test/build run |
**Rules for external notes:**
- These files are working documents. They should reflect the *current* state of work.
- When starting a fresh task, clear stale content (by writing a fresh file) and re-scope.
- Keep entries concise — bullet points and checklists, not prose.
- `decisions/` is especially important: when you make a tradeoff, write it down so the next agent doesn't undo it.
### Work Loop
For each task item, repeat this cycle:
#### A) Locate
- Identify the relevant code and existing patterns. Find the "right place" to make the change.
- Write a short plan (3–7 bullets) before editing. If the task touches multiple subsystem zones (see Section 12), flag this — it may need to be split into separate PRs.
#### B) Implement
- Make the smallest change that satisfies the requirement.
- Keep edits cohesive. Avoid unrelated refactors in the same commit.
- Follow all conventions already documented in this file (logging via `logger`, token-first styling, magnet safety, schema definitions in `nostrEventSchemas.js`, etc.).
#### C) Verify
- Run the most relevant lint/test commands (see CLAUDE.md "Available Scripts" for the full list):
```bash
npm run lint # Required
npm run test:unit:shard1 # Fast feedback; run full suite before PR
```
- If there's no test coverage for your change, add focused tests or document in `decisions/` why tests aren't feasible.
- Log all commands and results in a new `test_logs/TEST_LOG_<timestamp>.md` file.
#### D) Update Notes
- Check off completed items in the latest `todo/` file (by creating a new one).
- Record decisions in a new `decisions/` file.
- Log verification results in a new `test_logs/` file.
- **Do this before any likely context/memory reset** — if you're about to hit a token limit, finish a long session, or hand off to another agent, update notes first.
### Context Recovery
If an agent starts a session and finds existing work-in-progress:
1. **Immediately read the latest files** in `context/`, `todo/`, `decisions/`, and `test_logs/`.
2. Resume from the next unchecked item in the latest `todo/` file.
3. Verify the current state by running `git status`, `git log --oneline -10`, and a quick lint/test pass.
4. Do not re-do work that the latest `test_logs/` shows already passed — unless the code has changed since.
### Output Requirements
When finishing a task or handing off, provide:
- **Summary** of what changed and why.
- **Files changed** (list).
- **Verification results** (from the latest `test_logs/` file).
- **Follow-ups / risks / remaining TODOs**.
- **If blocked:** Ask specific questions and propose 1–2 options.
### Quality Bar
| Principle | Meaning |
|-----------|---------|
| Correctness over cleverness | Simple, working code beats elegant abstractions |
| Conventions over preference | Match what's already in the repo, even if you'd do it differently |
| Explicit tradeoffs | Write down *why* in `decisions/`, don't leave the next agent guessing |
| Logged verification | Every claim of "tests pass" must have a `test_logs/` entry to back it up |
| Incremental over sweeping | Small, reviewable changes that keep the build green at every step |
---
## Next
Please read these documents next.
-26
View File
@@ -1,26 +0,0 @@
# Weekly Audit Report
**Branch:** `unstable`
**Date:** 2024-02-12
## Metrics
### 1. File Size Audit
- **Grandfathered oversized files:** 44
- **Sum of excess lines:** 50,025 (lines exceeding the 1000-line threshold)
- **Status:** **PASS** (No new violations beyond grandfathered list)
### 2. InnerHTML Audit
- **Total innerHTML assignments:** 99
- **Status:** **PASS** (No new assignments found)
### 3. Lint Status
- **Result:** **PASS**
- **Notes:** All checks passed. `lint:assets` and `lint:sw-compat` were skipped due to the environment (missing build artifacts/git history), which is expected.
## Comparison
- **Previous Week:** No previous report was found in the PR comments or issues for comparison.
## Recommendations
- Continue decomposition efforts for the largest grandfathered files (e.g., `js/ui/profileModalController.js`, `js/ui/components/VideoModal.js`).
- Maintain strict checks on innerHTML usage to prevent new security risks.
-32
View File
@@ -1,32 +0,0 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
- New views for Kids, History, Search, Subscriptions, and Docs. (PR #2129, PR #2153 by PR0M3TH3AN)
- Comprehensive test suite for Nostr features and UI components. (PR #2153 by PR0M3TH3AN)
- Provider-agnostic S3 multipart upload helpers to support large file uploads and bucket management. (PR #1620 by PR0M3TH3AN)
### Changed
- Documentation alignment and updates. (PR #2563 by PR0M3TH3AN)
- Cleanup of Search Filter Modal comments. (PR #2562 by PR0M3TH3AN)
- Performance improvements for documentation scroll spy. (PR #2129 by PR0M3TH3AN)
- Optimized Nostr login speed. (PR #2153 by PR0M3TH3AN)
- Updated `nostr-tools`, `floating-ui`, and `crypto-helpers` dependencies. (PR #2153 by PR0M3TH3AN)
- Major repository update and synchronization (Commit `b429b1c` by thePR0M3TH3AN)
- Removed runtime `ASSET_VERSION` query-string cache busting for static assets. Deployment freshness is now driven by build-time hashed filenames and `asset-manifest.json` rewrites.
### Fixed
- Reverted webseed regression fix (PR #2002 by thePR0M3TH3AN)
### Security
- Fixed magnet parameter injection vulnerability via `xt` parameter decoding. (PR #2345 by PR0M3TH3AN)
-549
View File
@@ -1,549 +0,0 @@
# CLAUDE.md — AI Assistant Guide for bitvid
This document provides context and guidance for AI assistants working with the bitvid codebase. It explains the project structure, development workflows, conventions, and key architectural decisions.
---
## Project Overview
**bitvid** is a decentralized video sharing platform built on the Nostr protocol. It operates as a static client — no backend server, no custody of user keys, no server-side signing. All authentication and state management happens client-side or via connected Nostr signers.
### Key Technologies
- **Nostr**: Decentralized identity and event protocol (NIP-07, NIP-33, NIP-44, NIP-46, NIP-51, NIP-71, etc.)
- **WebTorrent**: P2P video streaming fallback
- **Tailwind CSS**: Utility-first styling with custom design tokens
- **Playwright**: E2E and visual regression testing
- **Node.js 22+**: Development environment
### License
GPL-3.0-or-later (see `LICENSE`)
---
## Codebase Structure
```
bitvid/
├── js/ # Main application JavaScript
│ ├── app.js # Main orchestrator (bitvidApp)
│ ├── nostr/ # Nostr protocol implementation
│ │ ├── client.js # Core Nostr client with signer adapters
│ │ ├── nip71.js # NIP-71 video note parsing
│ │ ├── sessionActor.js # Anonymous session actors for telemetry
│ │ └── adapters/ # NIP-07, NIP-46, nsec signer adapters
│ ├── services/ # Business logic services
│ │ └── playbackService.js # Video playback orchestration
│ ├── ui/ # UI components and controllers
│ ├── state/ # State management (cache.js)
│ ├── utils/ # Utility functions
│ ├── constants.js # Feature flags and WSS trackers
│ ├── config.js # Re-exports from instance config
│ ├── magnetUtils.js # Magnet URI helpers
│ ├── nostrEventSchemas.js # Event schema definitions (source of truth)
│ └── nostrClientFacade.js # Main Nostr client entry point
├── config/
│ └── instance-config.js # Deployment configuration
├── css/
│ ├── tokens.css # Design token definitions
│ ├── tailwind.source.css # Tailwind input file
│ └── tailwind.generated.css # Generated (gitignored)
├── tests/ # Test suites
│ ├── unit/ # Unit tests
│ ├── e2e/ # End-to-end tests
│ ├── visual/ # Visual regression tests
│ ├── nostr/ # Nostr-specific tests
│ └── moderation/ # Moderation feature tests
├── docs/ # Documentation
│ ├── nostr-event-schemas.md # Event schema reference
│ ├── playback-fallback.md # Playback architecture
│ ├── moderation/ # Moderation system docs
│ └── nips/ # NIP reference documentation
├── views/ # HTML view templates
├── components/ # HTML component templates
├── content/ # Markdown content (docs, roadmap)
├── torrent/ # WebTorrent integration
├── scripts/ # Build and utility scripts
└── .github/workflows/ # CI/CD configuration
```
---
## Essential Files to Know
| File | Purpose |
|------|---------|
| `AGENTS.md` | AI agent guide with architectural rules and mission |
| `KNOWN_ISSUES.md` | Pre-existing test failures and environmental quirks |
| `CONTRIBUTING.md` | Developer setup and contribution guidelines |
| `js/app.js` | Main application orchestrator (`bitvidApp`) |
| `js/nostr/client.js` | Core Nostr client with signer registry |
| `js/nostrEventSchemas.js` | **Source of truth** for all Nostr event schemas |
| `js/constants.js` | Feature flags and runtime configuration |
| `config/instance-config.js` | Deployment-specific settings |
| `js/testHarness.js` | Playwright test harness (`window.__bitvidTest__`) |
| `tests/e2e/helpers/bitvidTestFixture.ts` | Reusable Playwright fixture for agent testing |
| `scripts/agent/simple-relay.mjs` | Mock Nostr relay with HTTP seeding API |
| `docs/nostr-event-schemas.md` | Event schema documentation |
| `context/` | Agent working state: current goal, scope, assumptions (see AGENTS.md §15) |
| `todo/` | Agent task checklist with done/blocked sections (see AGENTS.md §15) |
| `decisions/` | Agent decision log: choices, alternatives, rationale (see AGENTS.md §15) |
| `test_logs/` | Agent verification log: commands run and results (see AGENTS.md §15) |
---
## Development Workflow
### Initial Setup
```bash
# Clone and install
git clone https://github.com/PR0M3TH3AN/bitvid.git
cd bitvid
npm ci # Use ci for exact lockfile deps
# Build (generates Tailwind CSS)
npm run build
# Start local server
python -m http.server 8000
# or
npx serve
```
### Before Every PR
```bash
# Required checks
npm run format # Format CSS/HTML/MD
npm run lint # All lint checks
npm run test:unit # Or use shards for faster feedback
# Recommended for UI changes
npm run test:visual
```
### Available Scripts
| Script | Purpose |
|--------|---------|
| `npm run build` | Full build (Tailwind + dist + verify) |
| `npm run build:css` | Regenerate Tailwind only |
| `npm run format` | Format with Prettier |
| `npm run lint` | Run all linting checks |
| `npm run lint:css` | CSS token validation |
| `npm run lint:inline-styles` | Block inline styles |
| `npm run lint:tokens` | Design token enforcement |
| `npm run lint:file-size` | Enforce file size limits |
| `npm run lint:innerhtml` | Enforce innerHTML baseline |
| `npm run test:unit` | Full unit test suite |
| `npm run test:unit:shard1` | Shard 1/3 (faster local dev) |
| `npm run test:unit:shard2` | Shard 2/3 |
| `npm run test:unit:shard3` | Shard 3/3 |
| `npm run test:smoke` | Run critical path smoke tests |
| `npm run test:dm:unit` | Direct message unit tests |
| `npm run test:dm:integration` | DM integration tests |
| `npm run test:e2e` | Headless E2E tests |
| `npm run test:visual` | Visual regression tests |
| `node scripts/agent/validate-events.mjs` | Validate Nostr event schemas |
---
## CI/CD Pipeline
The CI workflow (`.github/workflows/ci.yml`) runs on every push to `main` and all PRs:
| Job | Timeout | Description |
|-----|---------|-------------|
| `build` | 45 min | Lint, build, visual regression |
| `unit-tests` | 30 min | Sharded unit tests (3 shards) |
| `dm-unit-tests` | 20 min | Direct message unit tests |
| `dm-integration-tests` | 30 min | DM integration tests |
| `e2e-headless` | 45 min | Playwright E2E tests |
**Concurrency**: `cancel-in-progress: true` — new commits cancel running jobs.
---
## Code Conventions
### Logging
**Always use the logger utility**, never call `console.*` directly:
```javascript
import { logger } from './utils/logger.js';
logger.user.error('User-facing error'); // Production visible
logger.dev.log('Debug info'); // Dev mode only (IS_DEV_MODE)
```
### Styling Rules
1. **Token-first**: Use design tokens from `css/tokens.css`, never raw hex colors
2. **No inline styles**: `style=` attributes and `element.style` are blocked by lint
3. **Semantic tokens**: Use `bg`, `text`, `accent`, `border`, `danger`, etc.
4. **Theme scopes**: Toggle themes via `data-theme` attribute, not code
### Nostr Event Schemas
All Nostr events must be defined in `js/nostrEventSchemas.js`:
```javascript
import { buildVideoPostEvent, NOTE_TYPES } from './nostrEventSchemas.js';
const event = buildVideoPostEvent({
pubkey: hexPubkey,
created_at: Math.floor(Date.now() / 1000),
dTagValue: 'video-id',
content: {
version: 3,
title: 'My Video',
videoRootId: 'video-id',
url: 'https://example.com/video.mp4'
}
});
```
### Magnet Handling
**Critical rules** — magnets are fragile:
```javascript
import { safeDecodeMagnet, normalizeAndAugmentMagnet } from './magnetUtils.js';
// Always decode first
const raw = safeDecodeMagnet(userInput);
// Then normalize with hints
const enriched = normalizeAndAugmentMagnet(raw, { wsHint, xsHint });
// NEVER use new URL() on magnets — it corrupts hashes!
```
- WSS trackers only (from `js/constants.js`)
- Persist raw magnet strings, decode only at playback
- Include HTTPS `ws=`/`xs=` hints when available
### Architecture Pattern
**Separation of concerns**:
- `bitvidApp` (js/app.js) — Orchestrator, manages app-wide state
- UI Controllers — Handle specific UI components (e.g., `ProfileModalController`)
- Services — Business logic (e.g., `playbackService.js`)
- State — Central truth in `js/state/cache.js`
**Data flow**:
1. User action triggers controller event handler
2. Controller invokes callback provided by `bitvidApp`
3. `bitvidApp` executes logic, updates central state
4. `bitvidApp` notifies controller to re-render
---
## Feature Flags
Feature flags in `js/constants.js` gate experimental behavior:
| Flag | Default | Purpose |
|------|---------|---------|
| `URL_FIRST_ENABLED` | Depends on config | Try URL before magnet |
| `FEATURE_WATCH_HISTORY_V2` | true | Watch history system |
| `FEATURE_PUBLISH_NIP71` | false | NIP-71 video publishing |
| `FEATURE_SEARCH_FILTERS` | Dev mode only | Advanced search |
| `FEATURE_TRUST_SEEDS` | true | Baseline trust seeds |
| `FEATURE_TRUSTED_HIDE_CONTROLS` | true | Trusted mute/spam hide controls |
| `FEATURE_IMPROVED_COMMENT_FETCHING` | true | Improved comment fetching logic |
Toggle at runtime:
```javascript
import { setUrlFirstEnabled } from './constants.js';
setUrlFirstEnabled(false); // Disable URL-first playback
```
---
## Nostr Integration
### Key NIPs Used
| NIP | Purpose |
|-----|---------|
| NIP-07 | Browser extension signing |
| NIP-33 | Parameterized replaceable events (videos) |
| NIP-04 | Legacy DM encryption |
| NIP-44 | Modern encryption |
| NIP-46 | Remote signing |
| NIP-51 | Lists (mute, admin) |
| NIP-56 | Moderation reports |
| NIP-71 | Video events |
### Signer Adapters
```javascript
import { nostrClient } from './nostrClientFacade.js';
import { setActiveSigner } from './nostr/client.js';
// Register a signer
setActiveSigner({
pubkey,
signEvent: (event) => signer.sign(event),
nip04Encrypt: (target, text) => signer.nip04.encrypt(target, text),
nip04Decrypt: (actor, cipher) => signer.nip04.decrypt(actor, cipher),
// Optional: nip44Encrypt, nip44Decrypt
});
```
### NIP-33 Addressing (Important!)
Video events are addressed by `kind + pubkey + d-tag`:
```javascript
// CORRECT: Use the d-tag for addressing
const dTag = event.tags.find(t => t[0] === 'd')?.[1];
// WRONG: Don't use logical IDs like videoRootId for relay lookups
```
---
## Testing Guidelines
### Before Starting Work
1. Read `KNOWN_ISSUES.md` for pre-existing failures
2. Run relevant test shard to verify baseline
### Running Tests
```bash
# Fast feedback during development
npm run test:unit:shard1
# Full suite before PR
npm run test:unit
# Visual changes
npm run test:visual
# DM features
npm run test:dm:unit && npm run test:dm:integration
# E2E journeys
npm run test:e2e
```
### Test Artifacts
Failed visual tests store artifacts in `artifacts/test-results/`:
- Screenshots
- Videos
- Traces
View with: `./scripts/show-artifacts.sh`
### Agent E2E Testing (Playwright)
bitvid includes a test harness for programmatic Playwright testing without browser extensions or real relays. See `AGENTS.md` Section 14 for the full reference. Quick start:
```typescript
import { test, expect } from "./helpers/bitvidTestFixture";
test("my test", async ({ page, gotoApp, loginAs, seedEvent }) => {
await seedEvent({ title: "Test Video", url: "https://example.com/v.mp4" });
await gotoApp();
await loginAs(page);
// interact with the app using data-testid selectors
await expect(page.locator('[data-testid="upload-button"]')).toBeVisible();
});
```
Key capabilities:
- **Programmatic login**: `loginAs(page)` uses `window.__bitvidTest__.loginWithNsec()` — no modal interaction needed
- **Mock relay**: Each test gets an isolated in-memory relay with HTTP seeding
- **Relay override**: `?__test__=1&__testRelays__=ws://localhost:8877` redirects all connections
- **State inspection**: `window.__bitvidTest__.getAppState()` returns login status, relays, etc.
- **Stable selectors**: All key elements have `data-testid` attributes (see AGENTS.md Section 14)
---
## Common Tasks
### Adding a New Nostr Event Type
1. Define schema in `js/nostrEventSchemas.js`
2. Add builder function
3. Document in `docs/nostr-event-schemas.md`
4. Add tests
### Creating UI Components
1. Create controller in `js/ui/`
2. Wire callbacks through `bitvidApp`
3. Read state via provided getters, never directly
4. Use design tokens for styling
### Modifying Playback Behavior
1. Check `js/services/playbackService.js` for stream handling
2. Update `js/constants.js` feature flags if needed
3. Test with both URL and magnet sources
4. Document rollback steps in PR
---
## Troubleshooting
### Build Issues
```bash
# Browserslist warning
npx update-browserslist-db@latest
# Tailwind not regenerating
rm css/tailwind.generated.css && npm run build:css
```
### Test Hangs
- Unit tests may hang after completion — use `Ctrl+C`
- Use shards for faster local runs
- Set `UNIT_TEST_TIMEOUT_MS=120000` for slow tests
### Lint Failures
- **Inline styles**: Move to CSS classes or tokens
- **Hex colors**: Use design tokens
- **Design tokens**: Use `theme()` or existing utilities
---
## Release Channels & Promotion Pipeline
Code flows through three branches with increasing stability:
```
unstable → beta → main
(dev) (soak) (prod)
```
| Branch | Purpose | CI Required | Manual QA |
|--------|---------|-------------|-----------|
| `unstable` | Active development, AI agent PRs land here | Yes | No |
| `beta` | Stabilization soak (hosted for testing) | Yes | Yes — hosted domain |
| `main` | Production | Yes | Verified via beta |
**Promotion rules:**
- `unstable → beta`: After a batch of improvements passes CI and local testing
- `beta → main`: After weeks of soak time on the beta hosted domain with no regressions
- Never push directly to `main` or `beta` — always promote from the previous stage
### Emergency Response
If a change breaks playback or magnet handling:
1. Revert immediately
2. Note rollback in PR
3. Document in AGENTS.md
---
## Multi-Agent Development Workflow
This project uses multiple AI coding agents working in parallel:
- **Claude Code** — Refactoring, convention enforcement, codebase-wide changes
- **OpenAI Codex** — Isolated feature implementation with clear specs
- **Google Jules** — Issue triage and straightforward bug fixes
### Coordination Rules
All agents **must** follow the subsystem boundaries and PR discipline rules in `AGENTS.md` Section 12. The key principles:
1. **Check before you start.** Look at open PRs before beginning work by running:
```
curl -s "https://api.github.com/repos/PR0M3TH3AN/bitvid/pulls?state=open&per_page=100" | jq '{count: length, titles: [.[].title]}'
```
If another agent has an open PR touching the same files, stop and flag the conflict.
2. **One subsystem per PR.** Don't mix unrelated changes. A lint fix and a feature addition are two separate PRs.
3. **`js/app.js` is single-writer.** Only one PR at a time should modify the main orchestrator.
4. **Merge fast, branch short.** Long-lived branches cause exponential merge pain with multiple agents. Keep PRs small and mergeable in one sitting.
### For Human Maintainers
When assigning work to agents:
- Update the "Currently In-Flight Work" section in `AGENTS.md` to reserve subsystems
- Avoid sending two agents at the same subsystem simultaneously
- Review and merge agent PRs promptly to keep the queue short — stale PRs compound conflicts
- Use PR title prefixes (`[nostr-core]`, `[ui]`, `[playback]`, etc.) to make scope visible
---
## Key Documentation
| Document | What It Covers |
|----------|---------------|
| `AGENTS.md` | AI agent rules, mission, architectural decisions |
| `CONTRIBUTING.md` | Setup, PR process, DCO |
| `KNOWN_ISSUES.md` | Pre-existing test failures |
| `docs/nostr-event-schemas.md` | Event definitions |
| `docs/playback-fallback.md` | URL-first strategy |
| `docs/moderation/README.md` | Moderation system |
| `docs/logging.md` | Logger usage |
| `docs/agents/TORCH.md` | TORCH distributed task locking protocol |
| `context/` / `todo/` / `decisions/` / `test_logs/` | Agent persistent state files (see AGENTS.md §15) |
---
## Quick Reference
### Import Patterns
```javascript
// Nostr client
import { nostrClient } from './nostrClientFacade.js';
// View events (analytics)
import { recordVideoView } from './nostrViewEventsFacade.js';
// Watch history
import { updateWatchHistoryListWithDefaultClient } from './nostrWatchHistoryFacade.js';
// Event building
import { buildVideoPostEvent } from './nostrEventSchemas.js';
// Feature flags
import { URL_FIRST_ENABLED, setUrlFirstEnabled } from './constants.js';
// Logger
import { logger } from './utils/logger.js';
```
### Content Schema v3
```javascript
{
version: 3,
title: string, // Required
url?: string, // HTTPS video URL
magnet?: string, // WebTorrent magnet
thumbnail?: string,
description?: string,
mode: 'live' | 'dev',
isPrivate: boolean,
deleted: boolean,
videoRootId: string // Stable identifier
}
```
Validation: Must have `title` + at least one of `url` or `magnet`.
---
## Final Notes
1. **Read AGENTS.md first** — it contains mission-critical architectural decisions and the Agent Execution Protocol (§15)
2. **Check KNOWN_ISSUES.md** — avoid investigating pre-existing failures
3. **Use sharded tests** — faster feedback during development
4. **Never commit generated CSS** — `css/tailwind.generated.css` is gitignored
5. **Token-first styling** — no raw colors, no inline styles
6. **Keep magnets raw** — decode only at playback time
7. **Document rollback steps** — especially for playback changes
+5 -209
View File
@@ -22,218 +22,14 @@ See [https://developercertificate.org/](https://developercertificate.org/) for t
## (Optional) Contributor License Agreement (CLA)
If we later introduce **dual-licensing** options (e.g., commercial licenses for enterprises), we may add an _optional_ CLA for contributors who want to grant relicense rights. You will be asked to agree explicitly in any PR that requires it. We will keep community use under GPL-3.0-or-later.
If we later introduce **dual-licensing** options (e.g., commercial licenses for enterprises), we may add an *optional* CLA for contributors who want to grant relicense rights. You will be asked to agree explicitly in any PR that requires it. We will keep community use under GPL-3.0-or-later.
## Code Guidelines
- Prefer small, focused PRs.
- Add tests where reasonable; keep UI accessible.
- Follow existing formatting/lint rules.
- Update docs and user-facing text when behavior changes.
- Do not commit `css/tailwind.generated.css`; it is generated at build time.
## Agent PR Conventions
Automated agents contributing to this repository should follow these rules:
- **Atomic Commits**: Keep changes small, focused, and self-contained.
- **Descriptive Titles**: Use clear, semantic titles (e.g., `fix(ui): resolve upload modal overflow`).
- **Commit Messages**: Use the convention `type(scope): description (agent)` (e.g., `fix(ai): formatting (agent)` or `docs(ai): update quickstart (agent)`).
- **Reference Issues**: Link to relevant issues in the PR description.
- **Review AGENTS.md**: Always consult `AGENTS.md` for specific architectural guidelines (like URL-first playback, token-first design system) and constraints before starting work.
- **Review KNOWN_ISSUES.md**: Before starting development or debugging, review `KNOWN_ISSUES.md` to avoid investigating pre-existing failures or limitations.
- **Dependency Upgrades**: Major or risky dependency upgrades must be documented by creating a Markdown file in the `issues/` directory (e.g., `issues/upgrade-<pkg>.md`) with a plan and risk assessment, rather than opening an immediate PR.
- **Efficiency**: Prefer running targeted or sharded tests (e.g., `npm run test:unit:shard1`, `shard2`, or `shard3`) to conserve resources during iteration.
## Submitting a Pull Request
1. Fork the repository and create a new branch from `main`.
2. Make your changes, adding tests if applicable.
3. Run `npm run format`, `npm run lint`, and unit tests (e.g. `npm run test:unit` or a shard like `npm run test:unit:shard1`) to ensure quality before pushing.
4. (Optional) Run `npm run test:visual` if you made UI changes.
5. (Optional) Run domain-specific tests if relevant (e.g., `npm run test:dm:unit` for Direct Messages).
6. Push your branch and open a Pull Request against the `main` branch.
7. Provide a clear description of the problem and solution.
## CI Behavior and Operations
### Concurrency (superseded runs)
The `CI` workflow is configured with `concurrency.cancel-in-progress: true`, which means when you push new commits to the same PR or branch, any in-flight run for the same workflow group is canceled automatically. This keeps the latest changes running and stops superseded runs from burning capacity. If you need to cancel runs manually (for example, stuck or obviously outdated runs), operators can use the GitHub CLI:
```bash
gh run list --workflow=CI --limit 10
gh run list --workflow=CI --limit 10 | awk 'NR>1 {print $1}' | xargs -n1 gh run cancel
```
If a run refuses to cancel normally, GitHub provides a `force-cancel` endpoint. We only use this in the automated cleanup workflow (below) or when a normal `gh run cancel` does not take effect.
### Job timeouts
Each job in the `CI` workflow has an explicit timeout:
- `build`: 45 minutes
- `unit-tests` (sharded): 30 minutes
- `dm-unit-tests`: 20 minutes
- `dm-integration-tests`: 30 minutes
- `e2e-headless`: 45 minutes
If a job exceeds its timeout, the runner terminates it automatically, so keep an eye on the longest-running suites when adjusting tests.
### Cleanup workflow (manual invocation)
The `Cleanup stuck workflow runs` workflow cancels queued or in-progress runs older than the configured stale threshold (currently 2 hours). It runs on a schedule every 30 minutes, but you can also trigger it manually:
```bash
gh workflow run "Cleanup stuck workflow runs"
```
This workflow uses the `force-cancel` endpoint to ensure stalled runs are terminated reliably, so prefer it for true stuck runs instead of normal cancellation.
## Development Setup
To set up the project locally:
1. **Prerequisites**:
- **Node.js**: v22 or higher (enforced by `.npmrc`).
- **NPM**: v10 or higher (included with Node 22).
2. **Install Dependencies**:
Use `npm ci` to ensure you get the exact dependencies from `package-lock.json`. If you intend to run smoke, visual, or end-to-end tests, also install Playwright browsers.
```bash
npm ci
npx playwright install
```
3. **Start the Application**:
```bash
npm start
```
This command runs a full production build (generating `dist/` and `css/tailwind.generated.css`) and starts a local server.
**Or, build manually:**
```bash
npm run build
npx serve dist
```
(The build command populates the `dist/` directory, which you can then serve locally.)
**For CSS-only changes:**
If you are only iterating on styles, you can run the faster CSS build:
```bash
npm run build:css
```
4. **Run Tests**:
```bash
npm run test:unit
```
_Note: Running the full suite (`npm run test:unit`) is resource-intensive, runs sequentially, and may time out in some environments. We strongly recommend using sharded runs for local development to save time: `npm run test:unit:shard1`, `shard2`, or `shard3`._
You can also run end-to-end, smoke, and visual tests:
```bash
npm run test:e2e
npm run test:smoke # Critical path verification
npm run test:visual
```
To run load tests (agent-driven):
```bash
npm run test:load
```
To update visual regression baselines:
```bash
npm run test:visual:update
```
For Direct Message features, run:
```bash
npm run test:dm:unit
npm run test:dm:integration
```
To aggregate telemetry from test logs:
```bash
npm run telemetry:aggregate
```
5. **Format & Lint**:
```bash
npm run format
npm run lint
```
- **Format**: Targets CSS, HTML, Markdown, and config files. (Note: JavaScript files are not currently auto-formatted by this command).
- **Lint**: Checks for CSS errors, hex color usage, inline styles, design tokens, Tailwind guards, file size limits, innerHTML usage, asset references, and Service Worker compatibility. (Note: There is no ESLint configuration for JavaScript logic; this step focuses on style and design system guards).
- **Note**: `npm run lint` includes an asset verification step (`lint:assets`) which checks the `dist/` directory. For full coverage, run `npm run build` before linting.
- **Audit**: `npm run audit` runs a design system audit and generates `REMEDIATION_REPORT.md` with auto-fix suggestions.
6. **Git Hooks (Optional)**:
We provide a script to set up a git pre-commit hook that runs linting and CSS builds automatically before you commit.
```bash
./scripts/setup-pre-commit.sh
```
For a full guide, see the [Local Setup section in README.md](./README.md#local-setup).
### Event Schema Validation
When modifying event schemas or builders, verify your changes against the codebase and test suite:
```bash
node scripts/agent/validate-events.mjs
```
This script checks that all event builders produce valid events according to the definitions in `js/nostrEventSchemas.js`.
### Troubleshooting
- **Browserslist Warning**: If you see `Browserslist: caniuse-lite is outdated` during the build, run:
```bash
npx update-browserslist-db@latest
```
- **Unit Test Hangs**: `npm run test:unit` may occasionally hang after completion due to open handles. If this happens, use `Ctrl+C` to exit. For CI or faster local runs, consider running specific shards (e.g., `npm run test:unit:shard1`).
- **Linting Failures**: `npm run lint` includes checks for inline styles (e.g., `style="..."` or `.style.prop = ...`). Move these styles to CSS classes or design tokens to pass linting.
## Dev Container
This project includes a `.devcontainer` configuration for VS Code. It provides a pre-configured environment with Node.js 22, the GitHub CLI, and necessary extensions.
To use it:
1. Open the project in VS Code.
2. When prompted, re-open in Container (or use the command palette: "Dev Containers: Reopen in Container").
3. The container will automatically:
- Install NPM dependencies (`npm ci`)
- Run the build (`npm run build`)
- Install Playwright browsers (so you can run `npm run test:visual`)
## Key Documentation
Before starting your work, please review these key documents:
- **[AGENTS.md](./AGENTS.md)**: Architectural guidelines, mission statement, and troubleshooting tips.
- **[Documentation Index](./docs/README.md)**: A complete list of system documentation, including architecture and feed logic.
- **[Nostr Event Schemas](./docs/nostr-event-schemas.md)**: The source of truth for all Nostr events published by the application.
- **[Playback Fallback](./docs/playback-fallback.md)**: Details on the URL-first playback strategy and WebTorrent fallback mechanism.
* Prefer small, focused PRs.
* Add tests where reasonable; keep UI accessible.
* Follow existing formatting/lint rules.
* Update docs and user-facing text when behavior changes.
## Security
-18
View File
@@ -1,18 +0,0 @@
# Base image matching the Playwright version in package.json
FROM mcr.microsoft.com/playwright:v1.58.0-jammy
# Set working directory
WORKDIR /app
# Copy package files
COPY package.json package-lock.json ./
# Install dependencies
# We use npm ci for a clean, deterministic install
RUN npm ci
# Copy the rest of the application source code
COPY . .
# Default command
CMD ["npm", "run", "test:visual"]
-13
View File
@@ -1,13 +0,0 @@
# Initial Baseline - Performance & Docs (Feb 2026)
## Performance Bottlenecks
- **Unbounded Concurrency (P0):** `js/nostr/commentEvents.js` uses `Promise.all` for relay requests in `publishComment` and `listVideoComments`. This can saturate network connections if a user has many relays (20+), causing UI freeze or timeouts.
- **Remediation:** Apply `pMap` with `RELAY_BACKGROUND_CONCURRENCY` (3).
- **VideoEventBuffer (P1):** Flushes every 75ms on event ingest regardless of visibility.
- **Remediation:** Implemented visibility gating (2026-02-13).
- **WatchHistory (P2):** Republish loop has backoff but no visibility check.
## Documentation Gaps
- **CORS Configuration:** `content/docs/guides/upload-content.md` suggests `http://localhost:5500` for CORS, but the default dev port is `3000`.
- **Remediation:** Update port to `3000`.
- **Upload Limit:** 2GB (documented and code-verified).
-21
View File
@@ -1,21 +0,0 @@
# Known Issues
## Tests
### Skipped Tests
The following tests are skipped due to flakiness or environmental issues and should be investigated:
_No skipped tests currently documented._ (Last checked: 2026-02-17)
### Visual Regression Tests (`test:visual`)
- **Artifact Retention**: Visual tests are configured to retain screenshots, traces, and videos on failure to assist with debugging. These artifacts can be found in `artifacts/test-results/`.
- **Note**: Visual tests are passing as of 2026-02-12. (Agent verified 2026-02-17: Tests fail if browsers are missing, consistent with Environment issue).
## Environment
### Playwright Browsers
- **Issue**: Visual and E2E tests (`npm run test:visual`, `npm run test:e2e`) may fail with `browserType.launch: Executable doesn't exist`.
- **Workaround**: Run `npx playwright install` to download the required browser binaries if you are running tests outside of the pre-configured dev container or CI environment. (Last checked: 2026-02-17)
-37
View File
@@ -1,37 +0,0 @@
# NIP Inventory
| NIP / Kind | Description | Repo Locations | Status | Notes |
|------------|-------------|----------------|--------|-------|
| **NIP-01** | Basic Protocol Flow | `js/nostr/client.js`, `docs/nips/01.md` | Compliant | Base implementation verified via `client.js`. |
| **NIP-04** | Encrypted Direct Message | `js/nostr/dmDecryptWorker.js`, `js/nostr/client.js` | Compliant | Verified by `tests/nostr-specs/nip04-nip44.test.mjs`. |
| **NIP-07** | `window.nostr` Capability | `js/nostr/nip07Permissions.js`, `js/nostr/adapters/nip07Adapter.js` | Compliant | Retry logic and timeouts verified in `nip07_compliance.test.mjs`. |
| **NIP-09** | Event Deletion | `js/nostr/client.js`, `docs/nips/09.md` | Partial | `deleteAllVersions` implements Kind 5 logic. |
| **NIP-10** | Text Notes & Threads | `js/nostr/commentEvents.js`, `docs/nips/10.md` | Compliant | `buildCommentEvent` implements NIP-10 threading markers (E, P, K tags). |
| **NIP-17** | Private Direct Messages | `js/nostr/client.js`, `js/ui/profileModal/ProfileDirectMessageRenderer.js` | Compliant | Explicit support for Gift Wraps (Kind 1059) and Seals (Kind 13). |
| **NIP-19** | bech32-encoded entities | `js/utils/nostrHelpers.js`, `js/nostr/nip71.js` | Compliant | Uses `nostr-tools` for correct encoding/decoding. |
| **NIP-21** | `nostr:` URI scheme | `docs/nips/21.md` | Unknown | Usage in content parsing verified in `js/utils/nostrHelpers.js`. |
| **NIP-33** | Addressable Events | `js/nostr/client.js` | Compliant | `d` tag usage verified in `video_note_compliance.test.mjs`. |
| **NIP-44** | Encrypted Payloads (Versioned) | `js/nostr/dmDecryptWorker.js`, `js/nostr/client.js` | Compliant | Verified by `tests/nostr-specs/nip04-nip44.test.mjs`. |
| **NIP-46** | Nostr Remote Signing | `js/nostr/nip46Client.js` | Compliant | Full client implementation for remote signing. |
| **NIP-51** | Lists | `js/userBlocks.js`, `js/subscriptions.js` | Compliant | Mute list (Kind 10000) and Subscription list (Kind 30000) logic aligns with spec. |
| **NIP-56** | Reporting | `js/services/moderationService.js`, `docs/nips/56.md` | Compliant | `buildReportEvent` implements Kind 1984. |
| **NIP-57** | Zaps | `js/payments/zapRequests.js`, `js/payments/zapSplit.js` | Compliant | Implements Kind 9734/9735 flow. |
| **NIP-59** | Gift Wrap | `js/nostr/client.js` | Compliant | Implemented as part of NIP-17. |
| **NIP-65** | Relay List Metadata | `js/relayManager.js` | Partial | Code supports Kind 10002. Test `nip65_compliance.test.mjs` is missing. |
| **NIP-71** | Video Events | `js/nostr/nip71.js` | Partial | Kinds 21/22 supported in schema, but Kind 30078 is primary. Verified tag generation in `tests/nostr-specs/kind30078.test.mjs`. |
| **NIP-78** | Application-specific data | `js/services/hashtagPreferencesService.js` | Compliant | Used for hashtag preferences (Kind 30015). |
| **NIP-94** | File Metadata | `js/nostr/videoPublisher.js` | Compliant | `buildVideoMirrorEvent` implements Kind 1063. |
| **NIP-96** | HTTP File Storage Integration | `js/nostr/videoPublisher.js` | Partial | References `service nip96` in NIP-71 tags. |
| **NIP-98** | HTTP Auth | `js/services/r2Service.js` | Compliant | Used for R2 service authentication (Kind 27235). |
| **Kind 21** | NIP-71 Video | `js/nostr/nip71.js` | Compliant | Implemented. |
| **Kind 22** | NIP-71 Short Video | `js/nostr/nip71.js` | Compliant | Implemented. |
| **Kind 10000** | Mute List | `js/userBlocks.js` | Compliant | Uses standard NIP-51 (no d tag for main list). |
| **Kind 10002** | Relay List | `js/relayManager.js` | Compliant | Verified. |
| **Kind 1063** | File Header | `js/nostr/videoPublisher.js` | Compliant | Implemented for NIP-94 mirroring. |
| **Kind 30000** | Subscription List | `js/subscriptions.js` | Compliant | Implements NIP-51 follow sets. |
| **Kind 30015** | Hashtag Preferences | `js/services/hashtagPreferencesService.js` | Compliant | Implemented. |
| **Kind 30078** | App Data | `js/nostr/client.js`, `js/nostr/videoPayloadBuilder.js` | Compliant | Verified by `tests/nostr-specs/kind30078.test.mjs`. |
| **Kind 30079** | Watch History | `js/nostr/watchHistory.js`, `config/instance-config.js` | Compliant | Parameterized replaceable list (custom kind). Implements bucketing and encryption. |
| **NIP-25** | Reactions | `js/nostr/reactionEvents.js`, `js/nostrEventSchemas.js` | Compliant | Implements Kind 7 reaction events. |
| **NIP-42** | Authentication of clients to relays | `js/nostr/client.js` | Unknown | Needs verification of AUTH command handling. |
| **NIP-47** | Wallet Connect | `js/payments/nwcClient.js` | Compliant | Implements NWC client for zaps. |
-39
View File
@@ -1,39 +0,0 @@
# Onboarding Audit Report
**Date:** 2026-02-15
**Agent:** onboarding-audit-agent
**Status:** ⚠️ Onboarding failures found (Fixed in this run)
## Environment
- **Node:** v22.22.0
- **NPM:** 11.10.0
- **OS:** Linux
## Executed Steps (Clean Environment)
| Step | Command | Result | Notes |
|------|---------|--------|-------|
| 1 | `npm ci` | ✅ Pass | Installed dependencies successfully. |
| 2 | `npm run build` | ✅ Pass | Build verified manually (simulating `npm start` build step). |
| 3 | `npm run test:unit:shard1` | ✅ Pass | Unit tests passed. |
| 4 | `npm run test:smoke` | ❌ Fail / ✅ Pass | Failed initially due to missing Playwright browsers. Passed after running `npx playwright install`. |
| 5 | `npm run format` | ✅ Pass | Format check passed. |
| 6 | `npm run lint` | ✅ Pass | Lint check passed. |
## Failures & Fixes
### 1. Missing Playwright Browsers
**Command:** `npm run test:smoke`
**Failure:**
```
browserType.launch: Executable doesn't exist at /home/jules/.cache/ms-playwright/chromium_headless_shell-1208/chrome-headless-shell-linux64/chrome-headless-shell
Looks like Playwright Test or Playwright was just installed or updated.
Please run the following command to download new browsers:
npx playwright install
```
**Root Cause:** `npm ci` installs the `@playwright/test` package but does not download the browser binaries required for smoke and visual tests. The documentation did not mention this step for local setup.
**Fix:** Updated `README.md` and `CONTRIBUTING.md` to include `npx playwright install` as a required step for running these tests.
## Documentation Updates
- **README.md**: Added `npx playwright install` note to "Local Setup".
- **CONTRIBUTING.md**: Added `npx playwright install` note to "Development Setup".
-50
View File
@@ -1,50 +0,0 @@
# Optimization Task Stubs
The following tasks were identified and implemented to resolve high CPU/GPU usage and potential crashes on the website.
## Completed Tasks
### 1. Optimize Ambient Background Animation
**Problem:** The `AmbientBackground` controller was calling `resizeCanvas` inside the `requestAnimationFrame` loop. This function accessed `getBoundingClientRect()`, causing synchronous layout thrashing (forced reflow) on every frame, leading to high CPU/GPU usage.
**Fix:**
- Refactor `js/ui/ambientBackground.js` to use `ResizeObserver`.
- Only update canvas dimensions when the element actually resizes.
- Remove layout reads from the animation loop.
### 2. Async Chunking for Explore Data Service
**Problem:** `ExploreDataService` performed heavy synchronous iterations over large datasets (Watch History and Video Tags) on the main thread. This could cause the UI to freeze (long tasks) during initialization or updates.
**Fix:**
- Refactor `buildWatchHistoryTagCounts` and `buildTagIdf` in `js/services/exploreDataService.js`.
- Implement processing in chunks (e.g., 500 items).
- Yield to the main thread using `setTimeout` between chunks to allow user interaction and frame rendering.
### 3. Throttle URL Health Video Probes
**Problem:** The `UrlHealthController` spawned a `<video>` element for every video card to probe connectivity. When loading a grid of 50+ videos, this caused a storm of simultaneous media decoding requests, leading to resource exhaustion and browser instability.
**Fix:**
- Refactor `js/ui/urlHealthController.js`.
- Implement a concurrency limiter (queue) for `probeUrlWithVideoElement`.
- Limit active video probes to a maximum of 3 concurrent instances.
## Future Optimization Tasks
### 4. Virtualize Video List Rendering
**Description:** Implement virtual scrolling (windowing) for the main video grid.
**Benefit:** Reduces DOM node count and memory usage when displaying thousands of videos.
**Action:**
- Create a `VirtualScroller` component.
- Only render video cards that are currently in the viewport (plus a buffer).
### 5. Move Heavy Computation to Web Worker
**Description:** Move the tag IDF and Watch History analysis entirely off the main thread.
**Benefit:** Zero impact on UI responsiveness during data refreshes.
**Action:**
- Create `js/workers/exploreData.worker.js`.
- Move `ExploreDataService` logic into the worker.
- Use `postMessage` to send video data and receive calculated stats.
### 6. Optimize Image Loading with `loading="lazy"` & `decoding="async"`
**Description:** Ensure all thumbnails and avatars use modern browser lazy loading.
**Benefit:** Faster initial page load and lower bandwidth usage.
**Action:**
- Audit `VideoCard` and `VideoModal` templates.
- Ensure `<img>` tags have `loading="lazy"` and `decoding="async"`.
-55
View File
@@ -1,55 +0,0 @@
# Automated PR Review Report
## 1. PR: codex/add-centralized-permission-gate
**Target:** `main`
**Status:** ❌ Request Changes
### ❌ Test Failures
`tests/nostr-login-permissions.test.mjs` failed (6 subtests).
> **Suggestion:** Inspect `tests/nostr-login-permissions.test.mjs`. It seems related to `extension.enable` assertions. Run `npm run test:unit` locally.
### 🚀 Release Channel Checks
⚠️ **Warning:** Targets `main` but modifies critical config (`config/instance-config.js`, `js/constants.js`).
> **Guidance:** Ensure feature flags are safe defaults.
### 🛡️ Guardrails
`requires-security-review` `requires-protocol-review`
- Touched 41 sensitive files including `js/auth/signingAdapter.js`, `js/dmDecryptor.js`, `js/nostr/nip07Permissions.js`.
---
## 2. PR: codex/remove-nip04-and-nip44-from-default-permissions
**Target:** `main`
**Status:** ❌ Request Changes
### ❌ Test Failures
`tests/nostr-login-permissions.test.mjs` failed (6 subtests).
> **Suggestion:** Same failure as above. Likely an environmental issue or regression in NIP-07 logic.
### 🚀 Release Channel Checks
⚠️ **Warning:** Targets `main` but modifies critical config.
### 🛡️ Guardrails
`requires-security-review` `requires-protocol-review`
- Touched 41 sensitive files.
---
## 3. PR: fix/optimize-nip07-login-perf-4235940878368250470
**Target:** `main`
**Status:** ❌ Failure / Timeout
### ❌ Test Failures
Tests timed out during execution.
> **Suggestion:** Run `npm run test:unit:shard1` locally to verify changes without timeout.
---
## Audit Log
| Branch | Lint | Format | Tests | Security |
| :--- | :--- | :--- | :--- | :--- |
| `codex/add-centralized-permission-gate` | ✅ Pass | ✅ Pass | ❌ Fail | 🛡️ Req |
| `codex/remove-nip04-and-nip44...` | ✅ Pass | ✅ Pass | ❌ Fail | 🛡️ Req |
| `fix/optimize-nip07-login-perf...` | ✅ Pass | ✅ Pass | ❌ Timeout | ? |
**Micro-fixes:** No formatting changes detected in the reviewed branches.
+57 -304
View File
@@ -2,7 +2,9 @@
# bitvid - Decentralized Video Sharing
**bitvid** is a decentralized platform where users can share videos and follow creators with privacy and freedom. Built with a static site architecture, it operates entirely as a **static client**—it does not run a backend server, hold custody of user keys, or sign requests on behalf of users. All signing and state management happen client-side or via a connected Nostr signer.
##### IPNS: [k51qzi5uqu5dgwr4oejq9rk41aoe9zcupenby6iqecsk5byc7rx48uecd133a1](https://k51qzi5uqu5dgwr4oejq9rk41aoe9zcupenby6iqecsk5byc7rx48uecd133a1.ipns.dweb.link/)
**bitvid** is a decentralized platform where users can share videos and follow creators with privacy and freedom. Built with a static site architecture, it’s lightweight, efficient, and fully decentralized, making it ideal for hosting or local deployment.
---
@@ -12,7 +14,7 @@
- **Channel profile pages**: The [channel view](views/channel-profile.html) and `js/channelProfile.js` render banners, playlists, links, and follow stats so every creator has a branded landing page.
- **Audience flags**: The [Upload](components/upload-modal.html) and [Edit Video](components/edit-video-modal.html) modals expose **NSFW** and **For Kids** toggles that map straight to note metadata for safer discovery.
- **Richer metadata repeaters**: Configure variants, captions, segments, participants, references, and hashtags directly in the Upload modal so posts ship with structured context.
- **Flexible S3 Uploads**: Publish using browser-held keys, manual uploads, or presigned manifests.
- **Cloudflare R2 Uploads**: Publish through the modal’s Cloudflare flow with progress tracking and credential helpers.
- **Encrypted Watch History**: Sync viewing activity privately through the NIP-04 encrypted pipeline with local fallbacks.
- **Live View Counters**: Subscribe to view events and see totals update in real time on video cards and the video modal.
- **Lightning Zaps**: Tip creators with Lightning payments via the Zap controls in the video modal.
@@ -36,62 +38,21 @@
Open the **Upload** modal from the header toolbar and start by selecting the NIP-71 event type that matches your post. The chooser sets the correct Nostr kind and schema defaults so the published note follows [`docs/nostr-event-schemas.md`](docs/nostr-event-schemas.md).
Both upload modes expose metadata repeaters for **variants** and **hashtags**, while the [Edit Video](components/edit-video-modal.html) modal offers the full suite (including **captions/text tracks**, **segments**, **participants**, and **references**). Use these when you have multiple playback qualities (variants), accessibility tracks (captions), multipart drops (segments), discoverability tags (hashtags), credited collaborators (participants), or cross-posts/threads (references). Skip any repeater you don’t need—the base schema stays valid without them.
Both upload modes expose metadata repeaters for **variants**, **captions/text tracks**, **segments**, **hashtags**, **participants**, and **references**. Use these when you have multiple playback qualities (variants), accessibility tracks (captions), multipart drops (segments), discoverability tags (hashtags), credited collaborators (participants), or cross-posts/threads (references). Skip any repeater you don’t need—the base schema stays valid without them.
Pick the flow that matches your source material. Supported S3 upload modes include:
Pick the flow that matches your source material:
1. **Browser-held S3 keys (trusted operator only):** Enter your S3 credentials in the Storage tab to upload directly from the browser.
> **Security Warning:** This mode requires storing encrypted credentials in the browser's IndexedDB. While keys are encrypted at rest, they are decrypted in memory during use. Use this mode only on self-hosted, trusted deployments where you control the environment. Do not enter high-value credentials on public or untrusted instances.
2. **Manual upload via provider console:** Upload your file to your storage provider (e.g., R2, S3) manually, then paste the public URL into the upload form.
- **Custom (hosted URL or magnet)**: Provide a title plus an HTTPS video URL and/or a WebTorrent magnet. The form requires at least one transport, validates `ws=`/`xs=` hints, keeps magnets raw by decoding them with `safeDecodeMagnet()` before publish, and applies whatever metadata repeaters you configured.
- **Cloudflare (R2 direct upload)**: Enter your Cloudflare credentials in the guided form, optionally expand the **Advanced settings** accordion to override pathing or access controls, then drop media files for bitvid to upload through the R2 API. The modal tracks progress, applies your metadata selections, auto-fills the primary `imeta` variant once the upload completes, and publishes the resulting R2 URL back into the note automatically.
**Upload File (direct S3 upload)**:
If you are using **Mode 1** (Browser-held keys), enter your credentials in the guided form or Storage tab. Optionally expand the **Advanced options** accordion to override pathing or access controls, then drop media files for bitvid to upload through the S3 API. The modal tracks progress, applies your metadata selections, auto-fills the primary `imeta` variant once the upload completes, and publishes the resulting URL back into the note automatically.
**External Link (hosted URL or magnet)**:
For **Mode 2** (Manual upload) or external content, provide a title plus an HTTPS video URL and/or a WebTorrent magnet. The form requires at least one transport, validates `ws=`/`xs=` hints, keeps magnets raw by decoding them with `safeDecodeMagnet()` before publish, and applies whatever metadata repeaters you configured. If you submit a magnet without a hosted URL, the modal warns that availability depends on peers seeding the torrent.
Hosted URLs remain the preferred playback path, and you can still add a magnet or supplemental web seeds when using any mode. Use the **Private** toggle to keep the resulting card visible only to you, and lean on the repeaters whenever you want to surface richer context or alternate assets as outlined in the event schema reference.
### User content identifiers & storage layout
Every video note ships with a stable series identifier so edits and deletes can be stitched together reliably. bitvid sets both the `videoRootId` (content payload) and the NIP-33 `d` tag to the same value. When you publish, the identifier is chosen from the first provided value in `videoRootId`, `seriesId`, or `seriesIdentifier`; if none are supplied, bitvid generates a timestamp-based fallback. Keep this identifier stable across edits so older versions collapse correctly in the UI.【F:js/nostr/client.js†L5348-L5389】
Storage tags hang off that identifier to make asset discovery deterministic:
- **Storage pointer (`s` tag)**: The note includes `['s', '<provider>:<prefix>']`. If you don’t supply a pointer, bitvid derives one from the hosted URL (preferred), the torrent info hash (as `btih:<hash>`), or finally the `videoRootId` fallback (`nostr:<videoRootId>`).【F:js/nostr/client.js†L823-L862】
- **Prefix derivation**: For URL-based assets, the prefix is the public base URL plus the object key **without** its file extension. For example, `/uploads/demo/video.mp4` becomes `/uploads/demo/video`.【F:js/utils/storagePointer.js†L56-L88】
- **`info.json` location**: The companion metadata file lives at `<prefix>.info.json`. If the storage prefix is not an absolute URL, the UI uses the hosted URL’s origin to build the prefix before appending `.info.json` (and falls back to the raw prefix when no URL is available).【F:js/utils/storagePointer.js†L90-L129】
- **`info.json` contents**: The file is expected to describe the stored assets that back the note—typically the hosted URLs and optional WebTorrent metadata (info hash, web seeds, torrent URL) along with the derived prefix—so the UI can hydrate asset metadata even when the note only carries minimal fields.
**How the UI resolves assets**
1. The NIP-71 parser reads the `s` tag and hosted URL from the note, then computes `infoJsonUrl` from the storage pointer and URL fallback rules.【F:js/nostr/nip71.js†L1479-L1510】【F:js/utils/storagePointer.js†L90-L129】
2. Video cards embed the hosted URL, magnet, and `infoJsonUrl` as `data-play-url`, `data-play-magnet`, and `data-info-json` attributes for click handlers to consume.【F:js/ui/components/VideoCard.js†L2641-L2687】
3. When a card is clicked, the view extracts those attributes; if `infoJsonUrl` is missing, playback still proceeds using the hosted URL/magnet from the note without the extra metadata file.【F:js/ui/views/VideoListView.js†L1032-L1072】
**Example object key layout**
```
Storage pointer (s tag): s3:https://cdn.example.com/uploads/demo/video
Hosted asset: https://cdn.example.com/uploads/demo/video.mp4
Torrent metadata: https://cdn.example.com/uploads/demo/video.torrent
Info JSON: https://cdn.example.com/uploads/demo/video.info.json
```
Hosted URLs remain the preferred playback path, and you can still add a magnet or supplemental web seeds when using either mode. Use the **Private** toggle to keep the resulting card visible only to you, and lean on the repeaters whenever you want to surface richer context or alternate assets as outlined in the event schema reference.
### How playback works
1. **Configurable Priority**: `playVideoWithFallback({ url, magnet })` attempts the preferred source (URL or WebTorrent) based on the `DEFAULT_PLAYBACK_SOURCE` configuration.
2. **Seamless Fallback**: If the preferred source fails, stalls, or is unavailable, bitvid automatically switches to the alternative source.
1. **URL-first**: `playVideoWithFallback({ url, magnet })` attempts the hosted URL immediately. Healthy URLs deliver the full experience without touching P2P resources.
2. **WebTorrent fallback**: If the URL probe fails or returns an error status, bitvid falls back to WebTorrent using the raw magnet. The helpers append HTTPS `ws=`/`xs=` hints so peers seed quickly.
3. **Safety checks**: Magnets are decoded with `safeDecodeMagnet()` and normalized via `normalizeAndAugmentMagnet()` before reaching WebTorrent. Trackers remain WSS-only to satisfy browser constraints.
4. **Operator playbook**: If a deployment causes playback regressions, flip the relevant feature flags back to their default values in `js/constants.js` and redeploy. Capture the rollback steps in AGENTS.md and the PR description so the Main channel stays stable.
5. **Deep dive**: See [`docs/playback-fallback.md`](docs/playback-fallback.md) for the call flow into `playbackService`, magnet normalization details, and fallback hand-off points.
### Embed player
- **Embed URL format**: `/embed.html?pointer=<naddr-or-nevent>&playback=<url|torrent>`. The `pointer` can be a NIP-19 `naddr`/`nevent`, a `kind:pubkey:d` string, or a raw hex event id. The `playback` query param forces CDN (`url`) or WebTorrent (`torrent`) mode; omit it to let bitvid choose automatically.
- **Same-origin requirement**: To reuse session-actor storage for view counters, the embed must be served from the same origin as the parent page (so the iframe can access the same storage bucket).
- **Framing headers**: If you intend to embed across sites, ensure your hosting headers allow framing (e.g., Netlify `_headers` with `Content-Security-Policy: frame-ancestors *` or a narrowed allowlist).
- **X-Frame-Options**: Do not set `X-Frame-Options: DENY` or `SAMEORIGIN` if cross-site embedding is required.
### Watch history & view counts
@@ -141,156 +102,20 @@ To run **bitvid** locally:
cd bitvid
```
2. Install dependencies:
2. Start a local server:
- Using Python:
```bash
python -m http.server 8000
```
- Or with Node.js:
```bash
npx serve
```
Use `npm ci` to install dependencies exactly as specified in `package-lock.json`. If you intend to run smoke, visual, or end-to-end tests, also install Playwright browsers.
3. Open the site in your browser:
```bash
npm ci
npx playwright install
```
3. Start the application:
```bash
npm start
```
This command runs a full production build (generating `dist/` and `css/tailwind.generated.css`) and starts a local server.
4. Open the site in your browser:
```
http://localhost:3000
```
_(Note: The application runs via `npx serve`. The default port is 3000, but it may select a different port if 3000 is occupied. Check your terminal output for the correct URL.)_
_Note: If you prefer manual steps, run `npm run build` followed by `npx serve dist`. Because `dist` is the document root in this mode, always open `/` rather than appending `/dist`._
_If you use a static server from the repository root (for example VS Code Live Server or `python3 -m http.server`), open `http://127.0.0.1:8000/dist/` with the trailing slash._
5. Verify setup:
Run tests and linting to ensure everything is working correctly.
```bash
npm run test:unit:shard1 # Run a subset of tests for speed
npm run test:smoke # Critical path verification
npm run format
npm run lint
```
_Tip: Use `npm run test:unit` to run the full suite (can be slow)._
#### Quick Reference
- **Run unit tests**: `npm run test:unit`
- **Run smoke tests**: `npm run test:smoke`
- **Format code**: `npm run format` (CSS, HTML, MD, Config - no JS logic)
- **Lint code**: `npm run lint` (Styles, Tokens, Hex, Tailwind guards, file size, innerHTML, assets, SW compat - no ESLint/logic linting)
- **Visual tests**: `npm run test:visual` (Playwright snapshots)
### Dev Container
This project includes a `.devcontainer` configuration. If you use VS Code, you can open the project in a container to get a pre-configured environment with all dependencies installed.
### Developer Quickstart
See [`CONTRIBUTING.md`](./CONTRIBUTING.md) for the full developer guide.
For detailed architecture and system documentation, see the [Documentation Index](docs/README.md).
**Verify your work:**
- **Run unit tests**: `npm run test:unit` (Required before PRs). _Tip: Use `npm run test:unit:shard1`, `shard2`, or `shard3` for faster local feedback._
- **Format code**: `npm run format` (Required before PRs - only formats CSS, HTML, MD, and Config)
- **Lint code**: `npm run lint` (Checks for CSS, hex colors, inline styles, design tokens, Tailwind guards, file size, innerHTML, assets, and SW compat - no ESLint/logic linting)
**Other commands:**
- **Run load tests**: `npm run test:load`
- **Run DM unit tests**: `npm run test:dm:unit`
- **Run DM integration tests**: `npm run test:dm:integration`
- **Run headless E2E tests**: `npm run test:e2e`
- **Run visual regression tests**: `npm run test:visual`
- **Update visual baselines**: `npm run test:visual:update`
- **Run design system audit**: `npm run audit` (Generates remediation report)
- **Aggregate telemetry**: `npm run telemetry:aggregate`
- **Cancel CI runs**: See [`docs/cancelling-ci-runs.md`](docs/cancelling-ci-runs.md) for a script to clear pending workflows.
### Docs navigation & TOC updates
The docs viewer reads `content/docs/toc.json` to build the sidebar tree and map `doc` slugs to Markdown files. Each entry defines the label shown in the UI and the slug used in deep links. Add new docs content by creating a Markdown file under `content/` (or a subfolder) and then adding a matching entry in `content/docs/toc.json` that points to the new slug. Keep the slug and filename in sync so the reader can resolve the Markdown content and the sidebar link. Deep links follow the format `#view=docs&doc=<slug>` (for example, `#view=docs&doc=getting-started`). QA acceptance criteria for new docs include: the sidebar link appears, the deep link opens the correct document, the mobile drawer lists the new item, and the Markdown renders as expected (headings, lists, links, etc.).【F:content/docs/toc.json†L1-L5】
### Running Tests in Docker
To run Playwright tests in a consistent containerized environment, use the provided Docker script:
```bash
./scripts/run-playwright-docker.sh [command]
```
- **Default**: Runs `npm run test:visual`.
- **Custom command**: Pass any command as an argument, e.g., `./scripts/run-playwright-docker.sh npm run test:e2e`.
The script:
1. Builds the `bitvid-playwright` Docker image (based on `mcr.microsoft.com/playwright`).
2. Mounts the current directory to `/app` (allowing source changes to be reflected immediately).
3. Uses an anonymous volume for `/app/node_modules` to preserve the container's Linux-specific dependencies.
4. Outputs test artifacts to `artifacts/test-results` (accessible on the host).
### Visual Regression Debugging
Visual regression tests (`test:visual`) are configured to retain screenshots, videos, and traces when they fail. These artifacts are stored in the `artifacts/test-results` directory.
To quickly view the location of these artifacts:
```bash
./scripts/show-artifacts.sh
```
Use these artifacts to inspect the UI state at the moment of failure and diagnose rendering issues or regressions.
### Quickstart: Send your first video post
To programmatically publish videos, use the event builders in `js/nostrEventSchemas.js` (the source of truth for all event definitions) to construct valid video notes. See [`docs/nostr-event-schemas.md`](docs/nostr-event-schemas.md) for full schema documentation.
```javascript
import { buildVideoPostEvent } from "./js/nostrEventSchemas.js";
// Ensure dependencies are installed (npm ci)
// 1. Build the event object (useful for inspection or custom publishing)
const event = buildVideoPostEvent({
// Provide your hex pubkey (not npub)
pubkey: "your_pubkey_hex",
created_at: Math.floor(Date.now() / 1000),
dTagValue: "my-first-video", // The stable identifier (d-tag) for this video series
content: {
version: 3,
title: "My First Video",
videoRootId: "my-first-video", // Logical ID, matches the d-tag
url: "https://example.com/video.mp4", // The builder automatically derives the required 's' storage tag from this URL
description: "This is a test video post sent via the SDK."
// magnet: "magnet:?xt=urn:btih:..." // Optional fallback (provide the raw magnet string)
}
});
console.log("Event constructed:", event);
// 2. Publish the event (requires browser/extension or active signer)
// In an application context (e.g. browser console or module):
import { nostrClient } from "./js/nostrClientFacade.js";
// Ensure client is connected
await nostrClient.init();
// Login (prompts NIP-07 extension)
await nostrClient.loginWithExtension();
// Publish the built event
await nostrClient.signAndPublishEvent(event);
http://localhost:8000
```
### CSS build pipeline
@@ -306,9 +131,9 @@ package scripts to keep formatting, linting, and generated output consistent:
- **Core scripts:**
```bash
npm run format # normalize CSS/HTML/MD/Config sources with Prettier + tailwindcss plugin
npm run format # normalize CSS/HTML/JS/MD sources with Prettier + tailwindcss plugin
npm run lint:css # enforce design token usage and guard against raw hex colors
npm run build # run the full production build (generates dist/ and css/tailwind.generated.css)
npm run build # run the Tailwind build locally (output remains gitignored)
```
- **No hard-coded colors:** Follow the token-first rules in `AGENTS.md`—reach
@@ -318,11 +143,11 @@ package scripts to keep formatting, linting, and generated output consistent:
```bash
npm install # install Prettier, Stylelint, and Tailwind toolchain
npm run format # format CSS/HTML/MD with Prettier + tailwindcss plugin
npm run lint # run CSS, hex, inline-style, tokens, Tailwind, file-size, innerHTML, assets, and SW compat guards
npm run format # format CSS/HTML/JS/MD with Prettier + tailwindcss plugin
npm run lint # run CSS, hex color, inline-style, design-token, and Tailwind color/bracket guards in one pass
npm run lint:css # enforce token usage and forbid raw hex colors
npm run lint:inline-styles # fail CI if inline style attributes or element.style usage slip in
npm run build # run the full production build (cleans dist/, runs build:css, and copies assets)
npm run build # run the Tailwind build (delegates to npm run build:css)
npm run build:beacon # bundle torrent/dist assets and re-run the inline-style guard
npm run build:beacon:bundle # bundle beacon assets without running the guard (rarely needed)
```
@@ -335,7 +160,7 @@ commands from the repo root:
1. Install dependencies (only required after cloning or when packages change):
```bash
npm ci
npm install
```
2. Rebuild the Tailwind bundle for local verification (the output stays
@@ -349,46 +174,6 @@ The generated `css/tailwind.generated.css` artifact is ignored in git. CI
workflows and Netlify deploys run `npm run build` to produce the compiled
stylesheet during deployment, so source changes are all you need to commit.
Deploy targets must publish the `dist/` directory as the web root (never the
repository root). CI enforces deploy artifact integrity with
`npm run verify:dist:deploy-artifact` immediately after `npm run build`, and
hosts that support a publish/output directory (for example Netlify and Vercel)
should be configured to use `dist`.
#### Netlify cache policy for safe fast rollouts
bitvid uses path-class cache rules so deployments propagate quickly without
breaking long-term static asset caching:
- `index.html`, `embed.html`, and other `*.html` shell pages are served with
`Cache-Control: no-cache, must-revalidate` so clients always revalidate the
app entry document before booting a new session.
- Hashed bundles under `/css/*` and `/js/*` are served with
`Cache-Control: public, max-age=31536000, immutable` so browsers and CDNs can
keep them for a year with zero revalidation churn.
- Service-worker-related files (`/sw.min.js`, `/site.webmanifest`) use short,
revalidation-friendly cache controls (`max-age=0, must-revalidate`) so rollout
metadata and worker updates are picked up promptly.
This split is required for safe fast rollouts: HTML needs rapid update pickup,
while fingerprinted static assets should remain aggressively cacheable for
performance and cost control.
> Migration note: runtime `ASSET_VERSION` query-parameter mutation has been
> removed. Static asset freshness now comes from build-time hashed filenames
> recorded in `dist/asset-manifest.json` and rewritten into `dist/index.html`
> / `dist/embed.html` during `npm run build`.
#### Service worker rollback playbook
If a deploy regresses playback and you suspect `sw.min.js`, use this rollback runbook:
1. **Freeze impact**: redeploy the last known-good commit so clients fetch the previous worker script immediately.
2. **Invalidate active workers**: from DevTools > Application > Service Workers, click **Unregister** for `sw.min.js` and hard reload once to verify playback without stale worker state.
3. **Confirm claim flow**: in the browser console, confirm worker lifecycle messages (`BITVID_SW_LIFECYCLE`) appear and that requests to `/webtorrent/*` carry the `X-Bitvid-SW-Version` response header.
4. **Check compatibility guardrails**: run `npm run lint:sw-compat` before shipping the fix. This fails deploys when service-worker runtime paths/scope changes are not accompanied by `_headers`, `index.html`, and `embed.html` updates in the same commit.
5. **Redeploy + monitor**: publish the fix, then replay the URL-first fallback QA script in `docs/qa.md` and capture logs/screenshots in the PR.
Inline styles are intentionally blocked. `npm run lint:inline-styles` scans HTML
and scripts for `style=` attributes, `element.style`, or `style.cssText` usage
and will fail CI until offending markup is moved into the shared CSS/token
@@ -422,40 +207,9 @@ the entry point that matches their feature set:
2. **View counters ([NIP-71](https://github.com/nostr-protocol/nips/blob/master/71.md)):** `import { recordVideoView, subscribeVideoViewEvents } from './nostrViewEventsFacade.js';`
3. **Watch-history lists ([NIP-51](https://github.com/nostr-protocol/nips/blob/master/51.md) + [NIP-04](https://github.com/nostr-protocol/nips/blob/master/04.md) encryption):** `import { updateWatchHistoryListWithDefaultClient } from './nostrWatchHistoryFacade.js';`
The legacy `js/nostr.js` shim has been removed. Update any remaining
`import { ... } from './nostr.js';` calls to use the facades above so upgrades
stay painless.
### Signer Adapters
bitvid exposes a stable signer API through `js/nostrClientFacade.js`, which
wraps the shared client created in `js/nostr/defaultClient.js` and registers
signer adapters in `js/nostr/client.js`. Import the facade whenever you need to
sign events or encrypt payloads so the adapter registry can select the active
signer, fall back to supported capabilities, and surface permission prompts.
Use the facade for the common, stable entry points:
```js
import {
nostrClient,
requestDefaultExtensionPermissions
} from "./nostrClientFacade.js";
```
Custom auth providers should still register their signer object through the
adapter registry in `js/nostr/client.js`, but downstream modules should only
call the facade to keep API surfaces consistent.
### Nostr video fetch limits
The `nostrClient` uses `subscribeVideos(callback, options)` for the main feed to support buffering and streaming. It accepts an optional `options.limit` to control how many video events are requested per relay. The request size defaults
to `DEFAULT_VIDEO_REQUEST_LIMIT` (150) and is clamped to
`MAX_VIDEO_REQUEST_LIMIT` (500) to avoid accidental firehose requests as the
network grows. Use a smaller limit for lightweight surfaces or a larger value
when running batch jobs that can tolerate additional load.
`nostrClient.fetchVideos(options)` is a legacy wrapper around subscription and is deprecated.
`js/nostr.js` remains as a compatibility shim while existing plugins migrate, but
it will be removed in a future release. Move any legacy `import { ... } from './nostr.js';`
calls over to the facades above so upgrades stay painless.
The build command compiles Tailwind with `tailwind.config.cjs`, runs it through
the PostCSS pipeline defined in `postcss.config.cjs` (for autoprefixing), and
@@ -491,13 +245,8 @@ artifacts now that the deploy pipeline owns the build step.
- Central place for instance-specific values like the Super Admin npub and the
default whitelist-only mode setting. Update the documented exports here when
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.
- `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
- Flip `IS_DEV_MODE` to `false` before shipping production builds. 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.
@@ -545,33 +294,37 @@ placeholder at “—” and development builds log a warning—so mixed deploym
### How to Contribute
Please see [`CONTRIBUTING.md`](./CONTRIBUTING.md) for detailed setup instructions, code guidelines, and agent PR conventions.
1. **Fork and Clone**:
```bash
git clone https://github.com/PR0M3TH3AN/bitvid.git
cd bitvid
```
2. **Create a Branch**:
```bash
git checkout -b feature/your-feature-name
```
3. **Make Changes**:
- Ensure your code follows best practices and is well-documented.
4. **Test**:
- Validate the site functionality locally before submitting.
5. **Submit a Pull Request**:
- Explain your changes and reference any related issues.
1. **Fork and Clone** the repository.
2. **Create a Branch** for your feature or fix.
3. **Install Dependencies** with `npm ci`.
4. **Make Changes** and ensure tests pass.
5. **Submit a Pull Request** with a clear description.
### Contribution Guidelines
- Follow the [GPL-3.0-or-later License](LICENSE).
- Use clear, concise commit messages.
- Respect the existing coding style and architecture.
- Run the manual QA script (see below) and note results in PR descriptions for changes that affect upload or playback.
---
## Testing
Continuous integration runs CSS linting/builds, the DM unit/integration suites, the Playwright kitchen-sink snapshots, headless E2E flows, and the Node-based unit tests on every push.
Continuous integration runs CSS linting/builds, the Playwright kitchen-sink snapshots, and the Node-based unit tests on every push.
Before pushing, run `npm run build` locally so the Tailwind bundle regenerates.
- **Unit Tests**: Run `npm run test:unit` for application logic changes.
- _Tip_: Use `npm run test:unit:shard1`, `shard2`, or `shard3` for faster local feedback.
- Custom shards: `cross-env UNIT_TEST_SHARD=1/3 node scripts/run-unit-tests.mjs` or pass `--shard=1/3`.
- Timeout override: `cross-env UNIT_TEST_TIMEOUT_MS=120000` to abort stalled files.
- **Visual Regression**: Run `npm run test:visual` to verify UI presentation against baselines.
- Update baselines: `npm run test:visual:update`.
- **Direct Messages**: Run `npm run test:dm:unit` and `npm run test:dm:integration` for DM flows.
- **End-to-End**: Run `npm run test:e2e` for headless Playwright journeys.
Pair that with `npm run test:unit` for application logic changes and
`npm run test:visual` for presentation updates to mirror the CI surface area.
### Manual QA checklist
-32
View File
@@ -1,32 +0,0 @@
# Daily Design System Audit Report (2026-02-16)
Headline: ✓ No violations
## CSS
Total count: 0
## Hex Colors
Total count: 0
## Inline Styles
Total count: 0
## Raw Lengths
Total count: 0
## Bracket Utilities
Total count: 0
## Tailwind Palette
Total count: 0
## Next Steps
- [ ] Review violations
- [ ] Apply auto-fixes (if safe)
- [ ] Open remediation PR
-69
View File
@@ -1,69 +0,0 @@
# Validation Report
This report documents the verification of user-facing documentation in `/content` against the codebase.
## 1. Storage Configuration (CORS)
**Claim:** Users must configure CORS with specific headers to allow browser uploads.
**Source:** `content/docs/guides/upload-content.md`
**Code:** `js/storage/s3-multipart.js` (function `ensureBucketCors`)
**Verification:**
The `ensureBucketCors` function hardcodes the following CORS rules:
- `AllowedHeaders`: `["*"]` (Matches docs)
- `AllowedMethods`: `["GET", "HEAD", "PUT", "POST", "DELETE", "OPTIONS"]` (Matches docs, order irrelevant)
- `ExposeHeaders`: `["ETag", "Content-Length", "Content-Range", "Accept-Ranges"]` (Matches docs)
- `MaxAgeSeconds`: `3600` (Matches docs)
The `AllowedOrigins` in code is dynamic (`allowedOrigins` variable), while the docs provide a concrete example (`["http://localhost:5500", "https://bitvid.network"]`) which is correct for users.
## 2. File Type Limits
**Claim:** Accepted file types are `.mp4, .webm, .mov, .mkv, .ts, .m3u8, .mpg, .mpeg`.
**Source:** `content/docs/guides/upload-content.md`
**Code:** `components/upload-modal.html`
**Verification:**
The input element is defined as:
```html
<input id="input-file" type="file" class="hidden" accept="video/*,.m3u8,.ts,.mp4,.webm,.mov,.mkv,.mpg,.mpeg" />
```
The documentation matches the specific extensions listed in the code. The docs now also clarify that "other standard video formats" are accepted via `video/*`.
## 3. File Size Limit
**Claim:** Recommended limit is 2GB due to browser memory.
**Source:** `content/docs/guides/upload-content.md`
**Code:** `js/utils/torrentHash.js`
**Verification:**
The client-side hashing logic uses `FileReader.readAsArrayBuffer(file)`. This loads the entire file into memory. Browser implementations (especially Chrome) typically crash or fail when allocating ArrayBuffers larger than ~2GB. This technical constraint confirms the documentation's warning.
## 4. Moderation (Whitelist Mode)
**Claim:** "Whitelist Mode" restricts visibility to approved accounts.
**Source:** `content/docs/guides/upload-content.md` & `getting-started.md`
**Code:** `js/accessControl.js`
**Verification:**
The `AccessControl` class implements `canAccess(candidate)`:
```javascript
if (this.whitelistEnabled && !this.whitelistPubkeys.has(hex)) {
return false;
}
```
If `whitelistEnabled` is true (Whitelist Mode), non-whitelisted pubkeys return `false`, blocking their content from being displayed in feeds that use this check. This confirms the documentation.
## 5. Upload Lifecycle
**Claim:** Direct upload to bucket -> Client-side hashing -> NIP-71/Nostr publication.
**Source:** `content/docs/guides/upload-content.md`
**Code:** `js/ui/components/UploadModal.js`
**Verification:**
`handleVideoSelection` in `UploadModal.js`:
1. Calls `service.uploadFile` (Direct upload to R2/S3).
2. Calls `generateTorrentMetadata` (Client-side hashing).
3. Calls `publish` -> `publishVideoNote` (Nostr publication).
This flow matches the documentation exactly.
-24
View File
@@ -1,24 +0,0 @@
/*
Content-Security-Policy: frame-ancestors *
/index.html
Cache-Control: no-cache, must-revalidate
/embed.html
Cache-Control: no-cache, must-revalidate
/*.html
Cache-Control: no-cache, must-revalidate
/css/*
Cache-Control: public, max-age=31536000, immutable
/js/*
Cache-Control: public, max-age=31536000, immutable
/sw.min.js
Cache-Control: public, max-age=0, must-revalidate
X-Bitvid-SW-Version: bitvid-sw-2026-02-10
/site.webmanifest
Cache-Control: public, max-age=0, must-revalidate
-1
View File
@@ -1 +0,0 @@
/* /index.html 200
-50
View File
@@ -1,50 +0,0 @@
# Onboarding Audit Report
**Date:** 2023-10-26
**Agent:** Jules
**Subject:** Developer Onboarding Experience Audit
## Executive Summary
The onboarding process defined in `README.md` and `CONTRIBUTING.md` was audited by performing a fresh simulation of a developer setup. The core build commands (`npm ci`, `npm run build`, `npm run format`) succeeded without issues. However, the validation step (`npm run lint`) failed due to strict code quality checks flagging existing patterns in the codebase. These issues have been remediated to ensure a smooth onboarding experience for new contributors.
## Validation Steps
| Step | Command | Status | Notes |
|------|---------|--------|-------|
| 1 | `npm ci` | ✅ Passed | Dependencies installed successfully (291 packages). |
| 2 | `npm run build` | ✅ Passed | CSS build completed. Warning regarding `caniuse-lite` observed (known issue). |
| 3 | `npm run format` | ✅ Passed | Prettier formatting verification succeeded. |
| 4 | `npm run lint` | ❌ Failed | Failed on hex colors, inline styles, and raw Tailwind colors. |
## Failures and Remediation
### 1. Strict Hex Color Checking
**Issue:** `npm run lint:hex` failed due to hardcoded `#ff6b6b` (fallback accent color) in `js/embed.js` and associated tests.
**Fix:** Updated `scripts/check-hex.js` to include the following files in the ignored list, as these usages are intentional fallbacks:
- `js/embed.js`
- `tests/unit/embed-accent.test.mjs`
- `tests/visual/embed-layout.spec.ts`
### 2. Inline Style Violations
**Issue:** `npm run lint:inline-styles` failed due to direct `.style` property usage in `js/embed.js` (for CSS variables) and `js/ui/components/VideoModal.js` (layout logic).
**Fix:** Updated `scripts/check-inline-styles.mjs` to add these files to the `VIOLATION_ALLOWLIST`.
### 3. Raw Tailwind Colors
**Issue:** `npm run lint:tailwind-colors` failed due to usage of `text-red-400` and `text-green-400` in `js/ui/components/EmbedPlayerModal.js`.
**Fix:** Replaced raw color classes with semantic design tokens:
- `text-red-400` -> `text-status-danger`
- `text-green-400` -> `text-status-success`
## Devcontainer Review
The existing `.devcontainer/devcontainer.json` was reviewed and found to be robust.
- **Base Image:** `mcr.microsoft.com/devcontainers/javascript-node:22` (Up-to-date Node.js version).
- **Post Create Command:** `npm ci && npm run build` (Correctly matches onboarding steps).
- **Extensions:** Prettier, Tailwind CSS, and Stylelint extensions are pre-installed.
No changes were required for the devcontainer configuration.
## Conclusion
The repository setup is healthy. The strict linting rules were the primary barrier to a "green" onboarding run, and these have been addressed. Future contributors should be able to run `npm run lint` successfully immediately after setup.
-45
View File
@@ -1,45 +0,0 @@
# Onboarding Audit Report
Date: 2024-10-24
Environment: Node.js v22.22.0
## Execution Results
| Command | Status | Notes |
| :--- | :--- | :--- |
| `npm ci` | Success | Installed dependencies correctly. |
| `npm run build:css` | Success | Generated `css/tailwind.generated.css`. Warnings observed: `Browserslist: caniuse-lite is outdated` and Tailwind mixed units warning. |
| `npm run format` | Success | No files were modified (codebase is clean). |
| `npm run lint` | Success | All lint checks passed. |
| `npm run test:unit` | Success | Tests ran successfully (100+ tests passed), though execution time is significant (> 5 minutes). |
## Documentation Alignment
- **README.md**:
- Mentions `npm run build`. Correct (delegates to `build:css`).
- Mentions `npm run test:unit`. Correct.
- Mentions starting server `python -m http.server 8000`. Correct.
- **CONTRIBUTING.md**:
- Mentions `npm run build`. Correct.
- Mentions `npm run test:unit`. Correct.
- Mentions `npm run format` and `npm run lint`. Correct.
## Devcontainer
- `.devcontainer/devcontainer.json` exists and uses `mcr.microsoft.com/devcontainers/javascript-node:22`.
- This aligns with the local environment (Node v22) used for validation.
- `postCreateCommand` runs `npm ci && npm run build`. This is efficient.
## Recommendations
1. **Documentation**:
- `CONTRIBUTING.md`: The section "Development Setup" is accurate. However, we can add a note about `npm run build:css` explicitly if users want to just build CSS without the alias, but `npm run build` is better.
- We could add a note about the expected warnings during build to reassure new developers.
- The `caniuse-lite` warning suggests we should run `npx update-browserslist-db@latest` and commit the lockfile change, but that might be out of scope for "docs". However, I can try it.
2. **Devcontainer**:
- The setup seems solid. No changes required.
3. **General**:
- Run `npx update-browserslist-db@latest` to fix the build warning.
-38
View File
@@ -1,38 +0,0 @@
# Onboarding Audit Report - 2026-01-18
## Summary
Performed a fresh checkout validation of the `bitvid` repository to audit the developer onboarding experience.
## Execution Log
1. **Dependency Installation**: `npm ci` was used instead of `npm install` (implied by README) to ensure a clean state.
- Result: Success.
2. **Build**: `npm run build:css`.
- Result: Success.
3. **Format**: `npm run format`.
- Result: Success.
4. **Testing**: `npm run test:unit`.
- Result: Timed out in the audit environment (400s limit), but individual tests pass. The test runner executes tests sequentially which can be slow on constrained resources.
## Findings & Fixes
### Documentation Gaps
- **README.md**:
- The "Local Setup" section implied `npm install` but `npm ci` is preferred for consistent CI/CD and fresh setups.
- The "For Contributors" section completely missed the dependency installation step.
- **Fix**: Updated `README.md` to explicitly list `npm ci` in both sections and added a "Test" step with `npm run test:unit`.
- **CONTRIBUTING.md**:
- Lacked technical setup instructions, relying on README.
- **Fix**: Added a "Development Setup" section summarizing `npm ci`, testing, and linting commands, linking back to README.
### Environment Brittleness
- The reliance on system-wide Node.js versions was unidentified.
- **Fix**: Added a `.devcontainer/devcontainer.json` configuration to standardize the development environment using a Docker-based Node.js 20 container.
### Linting Failures
- `npm run lint` fails on a fresh checkout due to pre-existing inline style violations in `js/ui/components/EventDetailsModal.js` and `js/ui/components/SimilarContentCard.js`.
- **Recommendation**: These legacy style violations should be addressed in a separate refactoring PR.
## Recommendations
- Contributors should use the provided Devcontainer or ensure Node.js 20+ is installed.
- `npm ci` should be the standard instruction for new environments.
-215
View File
@@ -1,215 +0,0 @@
# Telemetry Report - 2026-01-23
**Total Unique Issues:** 16
**Generated:** 2026-01-23T06:26:53.283Z
## Top 10 Priority Issues
| Priority | Count | Issue | Owner | Sources |
| :--- | :---: | :--- | :--- | :--- |
| **High** | 2 | [batchFetchProfiles] Failed to fetch profiles from relay ... | QA Team | test_unit.log, test_unit_debug.log |
| **High** | 1 | [ChannelProfile] Failed to prepare channel videos for mod... | QA Team | test_unit.log |
| **High** | 1 | [ChannelProfile] Failed to decorate channel video moderat... | QA Team | test_unit.log |
| **High** | 1 | [ChannelProfile] Failed to decorate channel video moderat... | QA Team | test_unit.log |
| **High** | 1 | [ChannelProfile] Failed to decorate channel video moderat... | QA Team | test_unit.log |
| **High** | 1 | [ChannelProfile] Failed to decorate channel video moderat... | QA Team | test_unit.log |
| **High** | 1 | [ChannelProfile] Failed to decorate channel video moderat... | QA Team | test_unit.log |
| **High** | 1 | [ChannelProfile] Failed to decorate channel video moderat... | QA Team | test_unit.log |
| **High** | 1 | [ChannelProfile] Failed to decorate channel video moderat... | QA Team | test_unit.log |
| **High** | 1 | [ChannelProfile] Failed to decorate channel video moderat... | Unassigned | test_unit.log |
## Detailed Breakdown
### [High] [batchFetchProfiles] Failed to fetch profiles from relay wss://fail.example: Error: relay timed out
- **Occurrences:** 2
- **Sources:** test_unit.log, test_unit_debug.log
- **Suggested Owner:** QA Team
- **Fingerprint:** `b6566eea`
**Stack Trace / Details:**
```
[batchFetchProfiles] Failed to fetch profiles from relay wss://fail.example: Error: relay timed out
at Object.list (file://$REPO/tests/app-batch-fetch-profiles.test.mjs:80:31)
at file://$REPO/js/utils/profileBatchFetcher.js:115:10
at Array.map (<anonymous>)
at batchFetchProfilesFromRelays (file://$REPO/js/utils/profileBatchFetcher.js:112:75)
at TestContext.<anonymous> (file://$REPO/tests/app-batch-fetch-profiles.test.mjs:87:11)
at Test.runInAsyncScope (node:async_hooks:214:14)
at Test.run (node:internal/test_runner/test:1047:25)
at Test.start (node:internal/test_runner/test:944:17)
at startSubtestAfterBootstrap (node:internal/test_runner/harness:296:17)
```
### [High] [ChannelProfile] Failed to prepare channel videos for moderation TypeError: Cannot read properties of undefined (reading 'normalizeModerationSettings')
- **Occurrences:** 1
- **Sources:** test_unit.log
- **Suggested Owner:** QA Team
- **Fingerprint:** `c18193e3`
**Stack Trace / Details:**
```
[ChannelProfile] Failed to prepare channel videos for moderation TypeError: Cannot read properties of undefined (reading 'normalizeModerationSettings')
at Application.getActiveModerationThresholds (file://$REPO/js/app.js:6593:56)
at applyChannelModerationToVideos (file://$REPO/js/channelProfile.js:282:21)
at renderChannelVideosFromList (file://$REPO/js/channelProfile.js:4490:28)
at TestContext.<anonymous> (file://$REPO/tests$REPO/channel-profile-moderation.test.mjs:135:26)
at async Test.run (node:internal/test_runner/test:1054:7)
at async startSubtestAfterBootstrap (node:internal/test_runner/harness:296:3)
```
### [High] [ChannelProfile] Failed to decorate channel video moderation TypeError: Cannot read properties of undefined (reading 'decorateVideo')
- **Occurrences:** 1
- **Sources:** test_unit.log
- **Suggested Owner:** QA Team
- **Fingerprint:** `1e73620e`
**Stack Trace / Details:**
```
[ChannelProfile] Failed to decorate channel video moderation TypeError: Cannot read properties of undefined (reading 'decorateVideo')
at Application.decorateVideoModeration (file://$REPO/js/app.js:6598:37)
at decorateChannelVideo (file://$REPO/js/channelProfile.js:210:29)
at applyChannelModerationToVideos (file://$REPO/js/channelProfile.js:363:26)
at async renderChannelVideosFromList (file://$REPO/js/channelProfile.js:4490:22)
at async TestContext.<anonymous> (file://$REPO/tests$REPO/channel-profile-moderation.test.mjs:216:20)
at async Test.run (node:internal/test_runner/test:1054:7)
at async Test.processPendingSubtests (node:internal/test_runner/test:744:7)
```
### [High] [ChannelProfile] Failed to decorate channel video moderation TypeError: Cannot read properties of undefined (reading 'decorateVideo')
- **Occurrences:** 1
- **Sources:** test_unit.log
- **Suggested Owner:** QA Team
- **Fingerprint:** `21dc1236`
**Stack Trace / Details:**
```
[ChannelProfile] Failed to decorate channel video moderation TypeError: Cannot read properties of undefined (reading 'decorateVideo')
at Application.decorateVideoModeration (file://$REPO/js/app.js:6598:37)
at decorateChannelVideo (file://$REPO/js/channelProfile.js:210:29)
at resolveChannelModeration (file://$REPO/js/channelProfile.js:917:23)
at applyChannelVisualBlur (file://$REPO/js/channelProfile.js:962:22)
at updateChannelModerationVisuals (file://$REPO/js/channelProfile.js:1027:18)
at renderChannelVideosFromList (file://$REPO/js/channelProfile.js:4893:3)
at async TestContext.<anonymous> (file://$REPO/tests$REPO/channel-profile-moderation.test.mjs:216:20)
at async Test.run (node:internal/test_runner/test:1054:7)
at async Test.processPendingSubtests (node:internal/test_runner/test:744:7)
```
### [High] [ChannelProfile] Failed to decorate channel video moderation TypeError: Cannot read properties of undefined (reading 'decorateVideo')
- **Occurrences:** 1
- **Sources:** test_unit.log
- **Suggested Owner:** QA Team
- **Fingerprint:** `9cebf1f5`
**Stack Trace / Details:**
```
[ChannelProfile] Failed to decorate channel video moderation TypeError: Cannot read properties of undefined (reading 'decorateVideo')
at Application.decorateVideoModeration (file://$REPO/js/app.js:6598:37)
at decorateChannelVideo (file://$REPO/js/channelProfile.js:210:29)
at applyChannelModerationToVideos (file://$REPO/js/channelProfile.js:363:26)
at async renderChannelVideosFromList (file://$REPO/js/channelProfile.js:4490:22)
at async TestContext.<anonymous> (file://$REPO/tests$REPO/channel-profile-moderation.test.mjs:309:3)
at async Test.run (node:internal/test_runner/test:1054:7)
at async Test.processPendingSubtests (node:internal/test_runner/test:744:7)
```
### [High] [ChannelProfile] Failed to decorate channel video moderation TypeError: Cannot read properties of undefined (reading 'decorateVideo')
- **Occurrences:** 1
- **Sources:** test_unit.log
- **Suggested Owner:** QA Team
- **Fingerprint:** `a8230c7d`
**Stack Trace / Details:**
```
[ChannelProfile] Failed to decorate channel video moderation TypeError: Cannot read properties of undefined (reading 'decorateVideo')
at Application.decorateVideoModeration (file://$REPO/js/app.js:6598:37)
at decorateChannelVideo (file://$REPO/js/channelProfile.js:210:29)
at resolveChannelModeration (file://$REPO/js/channelProfile.js:917:23)
at applyChannelVisualBlur (file://$REPO/js/channelProfile.js:962:22)
at updateChannelModerationVisuals (file://$REPO/js/channelProfile.js:1027:18)
at renderChannelVideosFromList (file://$REPO/js/channelProfile.js:4893:3)
at async TestContext.<anonymous> (file://$REPO/tests$REPO/channel-profile-moderation.test.mjs:309:3)
at async Test.run (node:internal/test_runner/test:1054:7)
at async Test.processPendingSubtests (node:internal/test_runner/test:744:7)
```
### [High] [ChannelProfile] Failed to decorate channel video moderation TypeError: Cannot read properties of undefined (reading 'decorateVideo')
- **Occurrences:** 1
- **Sources:** test_unit.log
- **Suggested Owner:** QA Team
- **Fingerprint:** `07c6b0b2`
**Stack Trace / Details:**
```
[ChannelProfile] Failed to decorate channel video moderation TypeError: Cannot read properties of undefined (reading 'decorateVideo')
at Application.decorateVideoModeration (file://$REPO/js/app.js:6598:37)
at decorateChannelVideo (file://$REPO/js/channelProfile.js:210:29)
at applyChannelModerationToVideos (file://$REPO/js/channelProfile.js:363:26)
at async renderChannelVideosFromList (file://$REPO/js/channelProfile.js:4490:22)
at async TestContext.<anonymous> (file://$REPO/tests$REPO/channel-profile-moderation.test.mjs:383:3)
at async Test.run (node:internal/test_runner/test:1054:7)
at async Test.processPendingSubtests (node:internal/test_runner/test:744:7)
```
### [High] [ChannelProfile] Failed to decorate channel video moderation TypeError: Cannot read properties of undefined (reading 'decorateVideo')
- **Occurrences:** 1
- **Sources:** test_unit.log
- **Suggested Owner:** QA Team
- **Fingerprint:** `abfcb1f2`
**Stack Trace / Details:**
```
[ChannelProfile] Failed to decorate channel video moderation TypeError: Cannot read properties of undefined (reading 'decorateVideo')
at Application.decorateVideoModeration (file://$REPO/js/app.js:6598:37)
at decorateChannelVideo (file://$REPO/js/channelProfile.js:210:29)
at resolveChannelModeration (file://$REPO/js/channelProfile.js:917:23)
at applyChannelVisualBlur (file://$REPO/js/channelProfile.js:962:22)
at updateChannelModerationVisuals (file://$REPO/js/channelProfile.js:1027:18)
at renderChannelVideosFromList (file://$REPO/js/channelProfile.js:4893:3)
at async TestContext.<anonymous> (file://$REPO/tests$REPO/channel-profile-moderation.test.mjs:383:3)
at async Test.run (node:internal/test_runner/test:1054:7)
at async Test.processPendingSubtests (node:internal/test_runner/test:744:7)
```
### [High] [ChannelProfile] Failed to decorate channel video moderation TypeError: Cannot read properties of undefined (reading 'decorateVideo')
- **Occurrences:** 1
- **Sources:** test_unit.log
- **Suggested Owner:** QA Team
- **Fingerprint:** `ff2d20dc`
**Stack Trace / Details:**
```
[ChannelProfile] Failed to decorate channel video moderation TypeError: Cannot read properties of undefined (reading 'decorateVideo')
at Application.decorateVideoModeration (file://$REPO/js/app.js:6598:37)
at decorateChannelVideo (file://$REPO/js/channelProfile.js:210:29)
at resolveChannelModeration (file://$REPO/js/channelProfile.js:917:23)
at applyChannelVisualBlur (file://$REPO/js/channelProfile.js:962:22)
at TestContext.<anonymous> (file://$REPO/tests$REPO/channel-profile-moderation.test.mjs:390:3)
at async Test.run (node:internal/test_runner/test:1054:7)
at async Test.processPendingSubtests (node:internal/test_runner/test:744:7)
```
### [High] [ChannelProfile] Failed to decorate channel video moderation TypeError: Cannot read properties of undefined (reading 'decorateVideo')
- **Occurrences:** 1
- **Sources:** test_unit.log
- **Suggested Owner:** Unassigned
- **Fingerprint:** `812d9b22`
**Stack Trace / Details:**
```
[ChannelProfile] Failed to decorate channel video moderation TypeError: Cannot read properties of undefined (reading 'decorateVideo')
at Application.decorateVideoModeration (file://$REPO/js/app.js:6598:37)
at decorateChannelVideo (file://$REPO/js/channelProfile.js:210:29)
at resolveChannelModeration (file://$REPO/js/channelProfile.js:917:23)
at applyChannelVisualBlur (file://$REPO/js/channelProfile.js:962:22)
at updateChannelModerationVisuals (file://$REPO/js/channelProfile.js:1027:18)
at handleChannelModerationOverride (file://$REPO/js/channelProfile.js:804:5)
at channelModerationBadgeState.boundOverride (file://$REPO/js/channelProfile.js:570:7)
at HTMLButtonElement.callTheUserObjectsOperation ($REPO/node_modules/jsdom/lib/jsdom/living/generated/EventListener.js:26:30)
at innerInvokeEventListeners ($REPO/node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:350:25)
at invokeEventListeners ($REPO/node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:286:3)
```
---
*Privacy Notice: PII (IPs, emails, keys) has been sanitized from this report.*
-31
View File
@@ -1,31 +0,0 @@
# Telemetry Report - 2026-01-25
**Total Unique Issues:** 1
**Generated:** 2026-01-25T06:32:46.314Z
## Top 10 Priority Issues
| Priority | Count | Issue | Owner | Sources |
| :--- | :---: | :--- | :--- | :--- |
| **High** | 1 | handleModerationBlock requests a block, clears overrides,... | QA Team | unit-test:test_unit_debug.log |
## Detailed Breakdown
### [High] handleModerationBlock requests a block, clears overrides, and refreshes hidden state
- **Occurrences:** 1
- **Sources:** unit-test:test_unit_debug.log
- **Suggested Owner:** QA Team
- **Fingerprint:** `56489eaa`
**Stack Trace / Details:**
```
TestContext.<anonymous> (file://$REPO/tests$REPO/moderation-overrides.test.mjs:228:10)
async Test.run (node:internal/test_runner/test:1054:7)
async Test.processPendingSubtests (node:internal/test_runner/test:744:7)
...
# Subtest: handleModerationBlock returns false when viewer is logged out
```
---
*Privacy Notice: PII (IPs, emails, keys) has been sanitized from this report.*
-111
View File
@@ -1,111 +0,0 @@
# Telemetry Report - 2026-01-28
**Total Unique Issues:** 6
**Generated:** 2026-01-28T06:17:38.223Z
## Top 10 Priority Issues
| Priority | Count | Issue | Owner | Sources |
| :--- | :---: | :--- | :--- | :--- |
| **High** | 6 | handleModerationBlock requests a block, clears overrides,... | QA Team | unit-test:test_unit_debug_2.log, unit-test:test_unit_debug_3.log, unit-test:test_unit_pre_commit.log, unit-test:test_unit_retry.log, unit-test:test_unit_retry_3.log, unit-test:test_unit_retry_4.log |
| **High** | 2 | handleModerationOverride resumes deferred playback | QA Team | unit-test:test_unit_retry_2.log, unit-test:test_unit_retry_3.log |
| **High** | 1 | handleModerationBlock requests a block, clears overrides,... | QA Team | unit-test:test_unit_debug_4.log |
| **High** | 1 | handleModerationBlock requests a block, clears overrides,... | QA Team | unit-test:test_unit_final.log |
| **High** | 1 | handleModerationOverride decorates stored and current vid... | QA Team | unit-test:test_unit_retry_2.log |
| **High** | 1 | handleModerationBlock requests a block, clears overrides,... | QA Team | unit-test:test_unit_retry_2.log |
## Detailed Breakdown
### [High] handleModerationBlock requests a block, clears overrides, and refreshes hidden state
- **Occurrences:** 6
- **Sources:** unit-test:test_unit_debug_2.log, unit-test:test_unit_debug_3.log, unit-test:test_unit_pre_commit.log, unit-test:test_unit_retry.log, unit-test:test_unit_retry_3.log, unit-test:test_unit_retry_4.log
- **Suggested Owner:** QA Team
- **Fingerprint:** `56489eaa`
**Stack Trace / Details:**
```
TestContext.<anonymous> (file://$REPO/tests$REPO/moderation-overrides.test.mjs:228:10)
async Test.run (node:internal/test_runner/test:1054:7)
async Test.processPendingSubtests (node:internal/test_runner/test:744:7)
...
# Subtest: handleModerationBlock returns false when viewer is logged out
```
### [High] handleModerationOverride resumes deferred playback
- **Occurrences:** 2
- **Sources:** unit-test:test_unit_retry_2.log, unit-test:test_unit_retry_3.log
- **Suggested Owner:** QA Team
- **Fingerprint:** `bcbb97f7`
**Stack Trace / Details:**
```
TestContext.<anonymous> (file://$REPO/tests$REPO/moderation-overrides.test.mjs:104:10)
async Test.run (node:internal/test_runner/test:1054:7)
async Test.processPendingSubtests (node:internal/test_runner/test:744:7)
...
# Subtest: handleModerationBlock requests a block, clears overrides, and refreshes hidden state
```
### [High] handleModerationBlock requests a block, clears overrides, and refreshes hidden state
- **Occurrences:** 1
- **Sources:** unit-test:test_unit_debug_4.log
- **Suggested Owner:** QA Team
- **Fingerprint:** `80a22b69`
**Stack Trace / Details:**
```
TestContext.<anonymous> (file://$REPO/tests$REPO/moderation-overrides.test.mjs:229:10)
async Test.run (node:internal/test_runner/test:1054:7)
async Test.processPendingSubtests (node:internal/test_runner/test:744:7)
...
# Subtest: handleModerationBlock returns false when viewer is logged out
```
### [High] handleModerationBlock requests a block, clears overrides, and refreshes hidden state
- **Occurrences:** 1
- **Sources:** unit-test:test_unit_final.log
- **Suggested Owner:** QA Team
- **Fingerprint:** `5e159618`
**Stack Trace / Details:**
```
TestContext.<anonymous> (file://$REPO/tests$REPO/moderation-overrides.test.mjs:232:10)
async Test.run (node:internal/test_runner/test:1054:7)
async Test.processPendingSubtests (node:internal/test_runner/test:744:7)
...
# Subtest: handleModerationBlock returns false when viewer is logged out
```
### [High] handleModerationOverride decorates stored and current videos then refreshes UI
- **Occurrences:** 1
- **Sources:** unit-test:test_unit_retry_2.log
- **Suggested Owner:** QA Team
- **Fingerprint:** `4a2440b4`
**Stack Trace / Details:**
```
TestContext.<anonymous> (file://$REPO/tests$REPO/moderation-overrides.test.mjs:67:10)
async Test.run (node:internal/test_runner/test:1054:7)
async startSubtestAfterBootstrap (node:internal/test_runner/harness:296:3)
...
# Subtest: handleModerationOverride resumes deferred playback
```
### [High] handleModerationBlock requests a block, clears overrides, and refreshes hidden state
- **Occurrences:** 1
- **Sources:** unit-test:test_unit_retry_2.log
- **Suggested Owner:** QA Team
- **Fingerprint:** `a879eb91`
**Stack Trace / Details:**
```
TestContext.<anonymous> (file://$REPO/tests$REPO/moderation-overrides.test.mjs:202:10)
async Test.run (node:internal/test_runner/test:1054:7)
async Test.processPendingSubtests (node:internal/test_runner/test:744:7)
...
# Subtest: handleModerationBlock returns false when viewer is logged out
```
---
*Privacy Notice: PII (IPs, emails, keys) has been sanitized from this report.*
-49
View File
@@ -1,49 +0,0 @@
# Weekly Agent Report: 2026-01-18
**Date Range:** 2026-01-11 to 2026-01-18
## 1. Weekly Summary
### PRs Opened (Agent)
The following branches were created and work submitted by the AI agent in the past week:
| PR/Branch | Title | Status |
| :--- | :--- | :--- |
| `ai/smoke-tests-20260117-...` | `chore(ai): add smoke test script (agent)` | Merged (`a8647c1`) |
| `ai/load-test-20260117-...` | `Add load testing scripts and report` | Merged (`898f305`) |
### Tests Added & Coverage Improvements
- **Smoke Testing:** Added `scripts/agent/smoke-test.mjs` to verify critical user flows (login, publish, DM encryption) in a headless environment using `JSDOM` and `ws`.
- **Load Testing:** Added `scripts/agent/load-test.mjs` to measure relay performance (latency, throughput) and generate reports.
### Security & Dependency Suggestions
- **Dependency:** `fake-indexeddb` has been added to `devDependencies`.
- *Suggestion:* Ensure this mock is explicitly utilized in unit tests for the new `UnifiedDB` implementation to prevent regression of the storage layer during CI.
### High-Risk Items (Requires Review)
The following items merged in the past week involve significant architectural changes or security protocols and require human attention:
- **`requires-protocol-review`**: **Watch History v2** (Commit `cbd8af4`)
- Enforces `["v", "2"]` tag and mandatory NIP-44 encryption for watch history events.
- *Risk:* Potential backward compatibility issues with legacy v1 history if not properly migrated or handled.
- **`requires-review`**: **UnifiedDB & SyncEngine** (Commit `fa84089`)
- Replaces `localStorage` with `IndexedDB` for robust data persistence.
- *Risk:* Data loss during migration for existing users if the `localStorage` -> `IndexedDB` handover fails.
- **`requires-security-review`**: **NIP-46 Encryption Exposure** (Commit `8c042d7`)
- Exposes `nip04_encrypt`/`decrypt` and `nip44_encrypt`/`decrypt` via RPC for remote signers.
- *Risk:* Unauthorized access to encryption methods if the RPC interface is not strictly scoped or validated.
- **`requires-security-review`**: **Session Actor Isolation** (Commit `b49a294`)
- Restricts ephemeral session actors from publishing public events.
- *Risk:* Ensure legitimate use cases for ephemeral keys (if any) are not unintentionally blocked.
---
## 2. Top 5 Recommended Human Actions
| Priority | Action | Description |
| :--- | :--- | :--- |
| **P0** | **Verify Data Migration** | Manually test the **UnifiedDB** migration path. Load a profile with existing `localStorage` data and confirm it persists to `IndexedDB` without data loss. |
| **P0** | **Security Audit (NIP-46)** | Review `js/nostr/nip46Client.js` changes (Commit `8c042d7`). Ensure encryption RPC calls are secure and only accessible to authorized origins. |
| **P1** | **Protocol Check (Watch History)** | Verify that new watch history events carry the `["v", "2"]` tag and are correctly encrypted. Confirm the client gracefully handles legacy v1 events. |
| **P1** | **Run Smoke Tests** | Execute `node scripts/agent/smoke-test.mjs` against a test relay to verify that core flows (login, publish, DM) remain functional after the storage refactor. |
| **P2** | **Review Load Test Report** | Analyze the report generated by the new load test suite to understand relay performance baselines and identify potential bottlenecks. |
-51
View File
@@ -1,51 +0,0 @@
# Weekly Report: 2026-01-24
## Summary
This week focused on infrastructure improvements (load testing, smoke testing), dependency updates (Playwright), and creating minimal reproducers for reported UI issues. Several maintenance tasks were also performed, including linting autofixes and schema validation hardening.
## PRs / Branches Opened
| Title | Branch | Status |
| :--- | :--- | :--- |
| `chore(ai): event schema validation tool + fixes` | `ai/schema-validate-latest...` | Needs Review |
| `chore(ai): bump playwright to 1.58.0 and dependency scan` | `ai/deps-playwright...` | Ready for Merge |
| `docs(ai): weekly changelog draft` | `ai/changelog...` | For Review |
| `Add minimal reproducers for Moderation UI and CSS Lint bugs` | `ai/reproducers-multi...` | Merged/Added |
| `chore(ai): apply formatting/lint autofixes` | `ai/lint...` | Maintenance |
| `feat(infra): add load testing script (agent)` | `ai/load-test...` | Ready for Testing |
| `chore(ai): add smoke test script (agent)` | `ai/smoke-tests...` | Ready for Testing |
## Issues & Artifacts Created
| Title | Priority | Link |
| :--- | :--- | :--- |
| Moderation UI Visual Glitch | Medium | `examples/reproducers/issue-1-moderation-ui` |
| CSS Lint False Positives | Low | `examples/reproducers/issue-2-css-lint` |
| Relay Load Testing Infra | High | `scripts/agent/load-test.mjs` |
| E2E Regression Infra | High | `scripts/agent/smoke-test.mjs` |
## Tests Added / Coverage
* **Visual Regression:** Added reproducers for Moderation UI issues.
* **Load Testing:** Added `scripts/agent/load-test.mjs` for relay stress testing.
* **Smoke Testing:** Added `scripts/agent/smoke-test.mjs` for critical flow verification (Login, Video Publish).
## Security & Dependency Suggestions
* **Dependency Update:** Playwright bumped to `1.58.0`.
* **Security Hardening:** `js/nostrEventSchemas.js` updated to improve validation against malicious inputs.
## High-Risk Items
> **Do Not Auto-Merge**
* `requires-protocol-review`: **Schema Validation Fixes** (`ai/schema-validate-latest...`). Changes to event validation logic must be reviewed to ensure no legitimate events are rejected and no invalid events are accepted.
* `requires-human-review`: **Load Testing Script** (`ai/load-test...`). Ensure the script is configured to only target test relays and respects rate limits before running against any production infrastructure.
## Top 5 Recommended Actions
1. **Security Review:** Audit `js/nostrEventSchemas.js` changes in the schema validation branch to prevent protocol regressions.
2. **Infrastructure Review:** Verify `scripts/agent/load-test.mjs` target safety configuration.
3. **Merge:** Merge the Playwright 1.58.0 upgrade if CI is green.
4. **Triage:** Investigate the Moderation UI visual bug using the provided reproducer (`examples/reproducers/issue-1-moderation-ui`).
5. **Test:** Execute `npm run smoke-test` locally to establish a baseline for system stability.
-130
View File
@@ -1,130 +0,0 @@
import os
filepath = 'js/dmDecryptor.js'
with open(filepath, 'r') as f:
content = f.read()
search_block = """ for (const decryptor of ordered) {
for (const remotePubkey of remoteCandidates) {
try {
const plaintext = await decryptor.decrypt(remotePubkey, ciphertext, {
event,
stage: "content",
remotePubkey,
});
if (typeof plaintext === "string") {
const resolvedScheme =
normalizeScheme(hints.algorithms?.[0]) ||
normalizeScheme(decryptor.scheme) ||
"";
return buildDecryptResult({
ok: true,
event,
message: {
...cloneEvent(event),
content: plaintext,
},
plaintext,
recipients,
senderPubkey,
actorPubkey,
decryptor,
scheme: resolvedScheme,
});
}
} catch (error) {
errors.push({
scheme: decryptor.scheme || "",
source: decryptor.source || "",
stage: "content",
remotePubkey,
error,
});
}
}
}
return buildDecryptResult({
ok: false,
event,
actorPubkey,
errors,
});"""
replace_block = """ const attemptDecryption = async (decryptor, remotePubkey) => {
try {
const plaintext = await decryptor.decrypt(remotePubkey, ciphertext, {
event,
stage: "content",
remotePubkey,
});
if (typeof plaintext === "string") {
const resolvedScheme =
normalizeScheme(hints.algorithms?.[0]) ||
normalizeScheme(decryptor.scheme) ||
"";
return buildDecryptResult({
ok: true,
event,
message: {
...cloneEvent(event),
content: plaintext,
},
plaintext,
recipients,
senderPubkey,
actorPubkey,
decryptor,
scheme: resolvedScheme,
});
}
throw new Error("Decrypted content was not a valid string.");
} catch (error) {
throw {
scheme: decryptor.scheme || "",
source: decryptor.source || "",
stage: "content",
remotePubkey,
error,
};
}
};
const attempts = [];
for (const decryptor of ordered) {
for (const remotePubkey of remoteCandidates) {
attempts.push(attemptDecryption(decryptor, remotePubkey));
}
}
try {
return await Promise.any(attempts);
} catch (aggregateError) {
const aggregatedErrors = aggregateError.errors || [];
errors.push(...aggregatedErrors);
return buildDecryptResult({
ok: false,
event,
actorPubkey,
errors,
});
}"""
if search_block in content:
new_content = content.replace(search_block, replace_block)
with open(filepath, 'w') as f:
f.write(new_content)
print("Successfully replaced content.")
else:
print("Search block not found.")
# Debug: print what we have roughly where it should be
start_idx = content.find("for (const decryptor of ordered) {")
if start_idx != -1:
print("Found start of block, but full block mismatch.")
print("Existing content snippet:")
print(content[start_idx:start_idx+500])
-29
View File
@@ -1,29 +0,0 @@
# Onboarding Audit Report
Date: 2025-02-11
## Summary
The onboarding process was audited by simulating a fresh checkout and running the documented commands. All steps passed successfully. To further improve reliability, strict Node version enforcement was added.
## Environment
- **Node Version**: v22.22.0
- **NPM Version**: 11.7.0
- **OS**: Linux (Sandbox)
## Commands Executed
1. `npm ci` - **PASS**
- Clean install of dependencies.
- Added 424 packages.
2. `npm run build:css` - **PASS**
- Built Tailwind CSS successfully.
3. `npm run format` - **PASS**
- Checked formatting for CSS, HTML, MD, Config.
- No files were modified (compliance verified).
## Configuration Checks
- **package.json**: `engines.node` is set to `>=22`.
- **.devcontainer/devcontainer.json**: Uses `mcr.microsoft.com/devcontainers/javascript-node:22`, consistent with `package.json`.
- **Dockerfile**: Uses `mcr.microsoft.com/playwright:v1.58.0-jammy`. Verified compatible with Node 22 requirement.
## Improvements Implemented
- **Enforce Node Version**: created `.npmrc` with `engine-strict=true`. This ensures developers are using the required Node version (>=22) during `npm install`/`ci`.
- **Documentation**: Updated `README.md` and `CONTRIBUTING.md` to explicitly mention the Node version requirement and its enforcement.
@@ -1,224 +0,0 @@
{
"grandfathered_files": 43,
"grandfathered_excess_lines": 49463,
"new_oversized_files": 0,
"new_excess_lines": 0,
"grandfathered_list": [
{
"path": "js/adminListStore.js",
"lines": 1033,
"excess": 33
},
{
"path": "js/app/authSessionCoordinator.js",
"lines": 1743,
"excess": 743
},
{
"path": "js/app/feedCoordinator.js",
"lines": 1767,
"excess": 767
},
{
"path": "js/app/playbackCoordinator.js",
"lines": 1217,
"excess": 217
},
{
"path": "js/app.js",
"lines": 5245,
"excess": 4245
},
{
"path": "js/channelProfile.js",
"lines": 5530,
"excess": 4530
},
{
"path": "js/feedEngine/stages.js",
"lines": 1350,
"excess": 350
},
{
"path": "js/historyView.js",
"lines": 2547,
"excess": 1547
},
{
"path": "js/index.js",
"lines": 1407,
"excess": 407
},
{
"path": "js/nostr/client.js",
"lines": 3990,
"excess": 2990
},
{
"path": "js/nostr/managers/SignerManager.js",
"lines": 1106,
"excess": 106
},
{
"path": "js/nostr/nip46Client.js",
"lines": 2102,
"excess": 1102
},
{
"path": "js/nostr/nip46Connector.js",
"lines": 1094,
"excess": 94
},
{
"path": "js/nostr/nip71.js",
"lines": 1531,
"excess": 531
},
{
"path": "js/nostr/publishHelpers.js",
"lines": 1415,
"excess": 415
},
{
"path": "js/nostr/watchHistory.js",
"lines": 2211,
"excess": 1211
},
{
"path": "js/nostrEventSchemas.js",
"lines": 2908,
"excess": 1908
},
{
"path": "js/payments/nwcClient.js",
"lines": 1642,
"excess": 642
},
{
"path": "js/services/authService.js",
"lines": 1321,
"excess": 321
},
{
"path": "js/services/commentThreadService.js",
"lines": 1122,
"excess": 122
},
{
"path": "js/services/hashtagPreferencesService.js",
"lines": 1435,
"excess": 435
},
{
"path": "js/services/moderationService.js",
"lines": 2513,
"excess": 1513
},
{
"path": "js/services/nostrService.js",
"lines": 2227,
"excess": 1227
},
{
"path": "js/services/playbackService.js",
"lines": 1289,
"excess": 289
},
{
"path": "js/services/r2Service.js",
"lines": 1194,
"excess": 194
},
{
"path": "js/state/cache.js",
"lines": 1829,
"excess": 829
},
{
"path": "js/subscriptions.js",
"lines": 2374,
"excess": 1374
},
{
"path": "js/ui/applicationBootstrap.js",
"lines": 1169,
"excess": 169
},
{
"path": "js/ui/components/EditModal.js",
"lines": 1130,
"excess": 130
},
{
"path": "js/ui/components/RevertModal.js",
"lines": 1663,
"excess": 663
},
{
"path": "js/ui/components/SimilarContentCard.js",
"lines": 1303,
"excess": 303
},
{
"path": "js/ui/components/UploadModal.js",
"lines": 1320,
"excess": 320
},
{
"path": "js/ui/components/VideoCard.js",
"lines": 3130,
"excess": 2130
},
{
"path": "js/ui/components/VideoModal.js",
"lines": 6285,
"excess": 5285
},
{
"path": "js/ui/loginModalController.js",
"lines": 2552,
"excess": 1552
},
{
"path": "js/ui/moreMenuController.js",
"lines": 1601,
"excess": 601
},
{
"path": "js/ui/overlay/popoverEngine.js",
"lines": 1070,
"excess": 70
},
{
"path": "js/ui/profileModalController.js",
"lines": 8757,
"excess": 7757
},
{
"path": "js/ui/views/VideoListView.js",
"lines": 1392,
"excess": 392
},
{
"path": "js/ui/zapController.js",
"lines": 1206,
"excess": 206
},
{
"path": "js/userBlocks.js",
"lines": 2305,
"excess": 1305
},
{
"path": "js/watchHistoryService.js",
"lines": 1400,
"excess": 400
},
{
"path": "js/webtorrent.js",
"lines": 1038,
"excess": 38
}
],
"new_list": []
}
@@ -1,177 +0,0 @@
{
"total_assignments": 87,
"files_count": 34,
"top_offenders": [
{
"path": "js/channelProfile.js",
"count": 10,
"is_new": false
},
{
"path": "js/ui/components/RevertModal.js",
"count": 10,
"is_new": false
},
{
"path": "js/docsView.js",
"count": 8,
"is_new": false
},
{
"path": "js/subscriptions.js",
"count": 7,
"is_new": false
},
{
"path": "js/ui/subscriptionHistoryController.js",
"count": 7,
"is_new": false
},
{
"path": "js/historyView.js",
"count": 4,
"is_new": false
},
{
"path": "js/ui/components/DeleteModal.js",
"count": 3,
"is_new": false
},
{
"path": "js/ui/components/ShareNostrModal.js",
"count": 3,
"is_new": false
},
{
"path": "js/ui/dm/DMSettingsModalController.js",
"count": 3,
"is_new": false
},
{
"path": "js/ui/loginModalController.js",
"count": 3,
"is_new": false
},
{
"path": "js/ui/views/VideoListView.js",
"count": 3,
"is_new": false
},
{
"path": "js/viewManager.js",
"count": 3,
"is_new": false
},
{
"path": "js/utils/qrcode.js",
"count": 2,
"is_new": false
},
{
"path": "js/app/feedCoordinator.js",
"count": 1,
"is_new": false
},
{
"path": "js/app.js",
"count": 1,
"is_new": false
},
{
"path": "js/exploreView.js",
"count": 1,
"is_new": false
},
{
"path": "js/forYouView.js",
"count": 1,
"is_new": false
},
{
"path": "js/index.js",
"count": 1,
"is_new": false
},
{
"path": "js/kidsView.js",
"count": 1,
"is_new": false
},
{
"path": "js/ui/components/EditModal.js",
"count": 1,
"is_new": false
},
{
"path": "js/ui/components/EmbedVideoModal.js",
"count": 1,
"is_new": false
},
{
"path": "js/ui/components/EventDetailsModal.js",
"count": 1,
"is_new": false
},
{
"path": "js/ui/components/UploadModal.js",
"count": 1,
"is_new": false
},
{
"path": "js/ui/components/VideoModal.js",
"count": 1,
"is_new": false
},
{
"path": "js/ui/components/nip71FormManager.js",
"count": 1,
"is_new": false
},
{
"path": "js/ui/dm/AppShell.js",
"count": 1,
"is_new": false
},
{
"path": "js/ui/dm/Composer.js",
"count": 1,
"is_new": false
},
{
"path": "js/ui/dm/ConversationList.js",
"count": 1,
"is_new": false
},
{
"path": "js/ui/dm/DMRelaySettings.js",
"count": 1,
"is_new": false
},
{
"path": "js/ui/dm/MessageThread.js",
"count": 1,
"is_new": false
},
{
"path": "js/ui/dm/NotificationCenter.js",
"count": 1,
"is_new": false
},
{
"path": "js/ui/moreMenuController.js",
"count": 1,
"is_new": false
},
{
"path": "js/ui/profileModalController.js",
"count": 1,
"is_new": false
},
{
"path": "js/ui/videoListViewController.js",
"count": 1,
"is_new": false
}
],
"violations": []
}
@@ -1,11 +0,0 @@
{
"total_failures": 0,
"files_with_errors": [],
"skipped_checks": [
{
"check": "lint:assets",
"reason": "Missing dist/asset-manifest.json.",
"details": "asset reference check (build required)."
}
]
}
-40
View File
@@ -1,40 +0,0 @@
Title: Audit Report — 2026-02-12 (unstable)
**Summary**
* Date: 2026-02-12
* Branch: unstable
**Metrics**
* Grandfathered oversized files: 43 files (total excess lines: 49463)
* New oversized files: 0 files (total excess lines: 0)
* Total innerHTML assignments: 87 across 34 files
* Top offenders:
1. js/channelProfile.js — 10
2. js/ui/components/RevertModal.js — 10
3. js/docsView.js — 8
4. js/subscriptions.js — 7
5. js/ui/subscriptionHistoryController.js — 7
6. js/historyView.js — 4
7. js/ui/components/DeleteModal.js — 3
8. js/ui/components/ShareNostrModal.js — 3
9. js/ui/dm/DMSettingsModalController.js — 3
10. js/ui/loginModalController.js — 3
* Lint failures: 0
* Skipped checks: 1
* lint:assets: Missing dist/asset-manifest.json.
**High-priority items**
* None. Keep it up!
**Artifacts**
* file-size-report.json
* innerhtml-report.json
* lint-report.json
* raw logs
-1
View File
@@ -1 +0,0 @@
{"dependencies":[],"devDependencies":["autoprefixer","cross-env","postcss","prettier","serve","stylelint","tailwindcss"],"missing":{"@scure/base":["/app/vendor/crypto-helpers.entry.js","/app/tests/zap-split.test.mjs"],"https:":["/app/js/nostrToolsBootstrap.js"]},"using":{"playwright":["/app/debug_sidebar.mjs","/app/tests/embed_playback_check.spec.mjs"],"@playwright/test":["/app/playwright.config.ts","/app/tests/visual/embed-layout.spec.ts","/app/tests/visual/kitchen-sink.spec.ts","/app/tests/visual/moderation.spec.ts","/app/tests/visual/overlay-layers.spec.ts","/app/tests/visual/reduced-motion.spec.ts","/app/tests/e2e/popover.spec.ts","/app/tests/e2e/repro_issue.spec.ts","/app/tests/e2e/toast-service.spec.ts","/app/tests/e2e/ui-modals.spec.ts","/app/tests/e2e/video-modal-share-bug.spec.ts","/app/tests/e2e/webseed-playback.spec.ts","/app/tests/e2e/helpers/uiTestUtils.ts","/app/examples/reproducers/playwright.config.ts","/app/examples/reproducers/issue-sidebar-layout/repro.spec.ts","/app/examples/reproducers/issue-popover-viewport/repro.spec.ts","/app/examples/reproducers/issue-moderation-visibility/repro.spec.ts","/app/examples/reproducers/issue-moderation-ui/playwright.config.ts","/app/examples/reproducers/issue-moderation-ui/repro.spec.ts","/app/examples/reproducers/issue-1-moderation-ui/playwright.config.ts","/app/examples/reproducers/issue-1-moderation-ui/repro.spec.ts","/app/examples/reproducers/issue-003-popover-ref-error/repro.spec.ts","/app/examples/reproducers/issue-002-login-modal-nsec/repro.spec.ts","/app/examples/reproducers/003-video-modal-reg/repro.spec.ts","/app/examples/reproducers/002-sidebar-layout/repro.spec.ts","/app/examples/reproducers/001-popover-404/repro.spec.ts"],"prettier-plugin-tailwindcss":["/app/prettier.config.cjs"],"@tailwindcss/typography":["/app/tailwind.config.cjs"],"@scure/base":["/app/vendor/crypto-helpers.entry.js","/app/tests/zap-split.test.mjs"],"@noble/hashes":["/app/vendor/crypto-helpers.entry.js"],"jsdom":["/app/tests/design-system-metrics.test.mjs","/app/tests/discussion-count-service.test.mjs","/app/tests/edit-modal-submit-state.test.mjs","/app/tests/login-modal-controller.test.mjs","/app/tests/media-loader.test.mjs","/app/tests/modal-accessibility.test.mjs","/app/tests/nip71-form-manager.test.mjs","/app/tests/position-floating-panel.test.mjs","/app/tests/profile-modal-controller.test.mjs","/app/tests/subscriptions-manager.test.mjs","/app/tests/test-setup.mjs","/app/tests/video-card-source-visibility.test.mjs","/app/tests/video-modal-accessibility.test.mjs","/app/tests/ui/app-chrome-controller.test.mjs","/app/tests/ui/hashtag-strip-helper-bounce.test.mjs","/app/tests/ui/popoverEngine.test.mjs","/app/tests/ui/profile-modal-moderation-settings.test.mjs","/app/tests/ui/tag-pill-list.test.mjs","/app/tests/ui/tag-preference-menu.test.mjs","/app/tests/ui/url-health-controller.test.mjs","/app/tests/ui/video-list-view-popular-tags.test.mjs","/app/tests/ui/video-list-view-sorting.test.mjs","/app/tests/ui/helpers/jsdom-test-helpers.mjs","/app/tests/torrent/style-helpers.test.mjs","/app/tests/torrent/toast-service.test.mjs","/app/tests/services/link-preview-service.test.mjs","/app/tests/moderation/video-card.test.mjs","/app/tests/app/channel-profile-moderation.test.mjs","/app/tests/app/hash-change-handler.test.mjs","/app/tests/app/hydrate-sidebar-navigation.test.mjs","/app/examples/reproducers/issue-channel-moderation-test-crash/repro.mjs","/app/examples/reproducers/issue-001-video-card-logic/repro.mjs"],"fake-indexeddb":["/app/tests/dm-db.test.mjs","/app/tests/dm-normalization.test.mjs","/app/tests/nostr-session-actor.test.mjs","/app/tests/storage-service.test.mjs","/app/tests/ui/storageService.test.mjs","/app/tests/services/link-preview-service.test.mjs","/app/tests/nostr/dm-direct-message-flow.test.mjs","/app/tests/nostr/session-actor.test.mjs","/app/scripts/agent/smoke-test.mjs"],"nostr-tools":["/app/tests/dm-decryptor.test.mjs","/app/tests/nostr-private-key-signer.test.mjs","/app/tests/nostr-publish-rejection.test.mjs","/app/tests/nostr-send-direct-message.test.mjs","/app/tests/nostr/adapters.test.mjs","/app/tests/nostr/dm-direct-message-flow.test.mjs","/app/tests/nostr/dmSignalEvents.test.mjs","/app/tests/nostr/integration-remote-flow.test.mjs","/app/tests/nostr/nip46Client.test.js","/app/tests/e2e/repro_issue.spec.ts","/app/scripts/agent/interop-test.mjs","/app/scripts/agent/load-test-relay.mjs","/app/scripts/agent/load-test.mjs","/app/scripts/agent/simple-relay.mjs","/app/scripts/agent/smoke-test.mjs","/app/js/nostrToolsBootstrap.js"],"pixelmatch":["/app/tests/visual/kitchen-sink.spec.ts"],"pngjs":["/app/tests/visual/kitchen-sink.spec.ts"],"ws":["/app/tests/e2e/repro_issue.spec.ts","/app/scripts/agent/interop-test.mjs","/app/scripts/agent/load-test-relay.mjs","/app/scripts/agent/load-test.mjs","/app/scripts/agent/setup-test-env.js","/app/scripts/agent/simple-relay.mjs","/app/scripts/agent/smoke-test.mjs"],"esbuild":["/app/scripts/build-beacon.mjs"],"@floating-ui/domLine truncated
-28
View File
@@ -1,28 +0,0 @@
# Dependency Audit Report
## 1. Vulnerability Summary
- Total Vulnerabilities: 0
- Critical: 0
- High: 0
- Moderate: 0
- Low: 0
No vulnerabilities found.
## 2. Outdated Packages
### Safe Upgrades (Patch/Minor)
- **jsdom** (dependencies): 28.0.0 -> 28.1.0 (Latest: 28.1.0)
- **stylelint** (dependencies): 16.12.0 -> 16.26.1 (Latest: 17.3.0)
### Major Upgrades (Risky)
- **pixelmatch** (dependencies): 5.3.0 -> 7.1.0
- **postcss-import** (dependencies): 15.1.0 -> 16.1.1
- **prettier-plugin-tailwindcss** (dependencies): 0.6.14 -> 0.7.2
- **stylelint-config-standard** (dependencies): 36.0.1 -> 40.0.0
- **tailwindcss** (dependencies): 3.4.19 -> 4.1.18
### Security/Protocol Libraries (Manual Review Required)
- **nostr-tools** (dependencies): 2.19.4 -> 2.23.1 (Latest: 2.23.1)
## 3. Recommendations
- **Action Item**: Attempt safe upgrade of `jsdom` from `28.0.0` to `28.1.0`.
-13
View File
@@ -1,13 +0,0 @@
[
{
"fingerprint": "00257328",
"title": "Load Test Bottleneck: Cryptographic Bottleneck (Avg Sign Time 5.87ms > 5ms)",
"count": 1,
"sources": [
"load-test:load-report-20260204.json"
],
"severity": "Medium",
"stack": "Bottleneck: Cryptographic Bottleneck (Avg Sign Time 5.87ms > 5ms)\nRemediation: Client-side event signing is slow. Review cryptographic library (nostr-tools) usage or client hardware capabilities.",
"owner": "P2P Team"
}
]
-25
View File
@@ -1,25 +0,0 @@
# todo: Investigate why visibility check fails in fixture environment
Found TODOs in `tests/visual/moderation.spec.ts` related to Playwright test failures in headless mode.
## Context
Several tests in `tests/visual/moderation.spec.ts` have commented-out assertions and interactions due to visibility checks failing or clicks timing out when running in the fixture environment (headless).
## TODOs
- `// TODO: Investigate why visibility check fails in fixture environment (headless)`
- `// TODO: Investigate why click times out in fixture environment (headless)`
- `// TODO: Investigate visibility check failure`
- `// TODO: Investigate click timeout failure`
## Code Excerpt
```typescript
// TODO: Investigate why visibility check fails in fixture environment (headless)
// await expect(showAnywayButton).toBeVisible();
await expect(restoreButtonQuery).toHaveCount(0);
// TODO: Investigate why click times out in fixture environment (headless)
// await showAnywayButton.click({ force: true });
```
## Suggested Next Step
Investigate the `tests/visual/moderation.spec.ts` execution in headless mode. It might be related to rendering timing, fixture loading state, or specific CSS properties affecting visibility in the headless renderer.
@@ -1,37 +0,0 @@
{
"timestamp": "2026-01-25T19:08:25.628Z",
"config": {
"duration": 5,
"clients": 10,
"rate": 1,
"relay": "ws://localhost:8889"
},
"metrics": {
"total_sent": 51,
"total_received": 51,
"errors": 0,
"throughput_sent": 10.2,
"throughput_recv": 10.2,
"latency_ms": {
"p50": 0.8110580000000027,
"p95": 1.366897999999992,
"p99": 3.3182230000002164
},
"operation_times_ms": {
"build_avg": 0.160458607843158,
"sign_avg": 6.053091784313705
},
"resource_usage": [
{
"timestamp": 1769368105533,
"cpuUser": 778796,
"cpuSystem": 118523,
"rss": 88248320,
"heapUsed": 12950504
}
],
"error_breakdown": {}
},
"bottlenecks": [],
"remediation": []
}
-97
View File
@@ -1,97 +0,0 @@
{
"timestamp": "2026-01-25T19:12:13.640Z",
"config": {
"duration": 60,
"clients": 100,
"rate": 1,
"relay": "ws://localhost:8889",
"force": false
},
"metrics": {
"total_sent": 6219,
"total_received": 6219,
"errors": 0,
"throughput_sent": 103.65,
"throughput_recv": 103.65,
"latency_ms": {
"p50": 0.4525159999998323,
"p95": 0.9470979999969131,
"p99": 7.444698999999673
},
"operation_times_ms": {
"build_avg": 0.05414333365494229,
"sign_avg": 5.486003140054635
},
"resource_usage": [
{
"timestamp": 1769368278643,
"cpuPercent": 63.94,
"rss": 97239040,
"heapUsed": 16337816
},
{
"timestamp": 1769368283643,
"cpuPercent": 64.36,
"rss": 98402304,
"heapUsed": 20259576
},
{
"timestamp": 1769368288642,
"cpuPercent": 63.86,
"rss": 98762752,
"heapUsed": 17659632
},
{
"timestamp": 1769368293643,
"cpuPercent": 61.32,
"rss": 99790848,
"heapUsed": 18155016
},
{
"timestamp": 1769368298645,
"cpuPercent": 61.19,
"rss": 100999168,
"heapUsed": 15496016
},
{
"timestamp": 1769368303646,
"cpuPercent": 63.12,
"rss": 102313984,
"heapUsed": 16290368
},
{
"timestamp": 1769368308649,
"cpuPercent": 61.61,
"rss": 105914368,
"heapUsed": 22963352
},
{
"timestamp": 1769368313653,
"cpuPercent": 60.4,
"rss": 106831872,
"heapUsed": 21787144
},
{
"timestamp": 1769368318653,
"cpuPercent": 60.86,
"rss": 120463360,
"heapUsed": 17511208
},
{
"timestamp": 1769368323657,
"cpuPercent": 60.52,
"rss": 120987648,
"heapUsed": 19454880
},
{
"timestamp": 1769368328657,
"cpuPercent": 59.65,
"rss": 121643008,
"heapUsed": 21375032
}
],
"error_breakdown": {}
},
"bottlenecks": [],
"remediation": []
}
File diff suppressed because it is too large. Load diff
-392
View File
@@ -1,392 +0,0 @@
{
"meta": {
"clients": 100,
"duration": 60,
"targetRatePerClient": 0.5
},
"results": {
"totalSent": 2900,
"totalReceivedByObserver": 2958,
"totalErrors": 0,
"errorRate": 0,
"avgLatencyMs": 8.5,
"maxLatencyMs": 42,
"avgSignTimeMs": 5.869976568218849,
"hot_functions": [
{
"name": "Event Signing (Client)",
"avgTimeMs": 5.869976568218849,
"calls": 2900
}
]
},
"bottlenecks": [
"Cryptographic Bottleneck (Avg Sign Time 5.87ms > 5ms)"
],
"metrics": {
"timestamps": [
"2026-02-04T19:06:10.186Z",
"2026-02-04T19:06:11.827Z",
"2026-02-04T19:06:12.831Z",
"2026-02-04T19:06:14.256Z",
"2026-02-04T19:06:15.314Z",
"2026-02-04T19:06:16.285Z",
"2026-02-04T19:06:17.281Z",
"2026-02-04T19:06:18.280Z",
"2026-02-04T19:06:19.280Z",
"2026-02-04T19:06:20.280Z",
"2026-02-04T19:06:21.281Z",
"2026-02-04T19:06:22.284Z",
"2026-02-04T19:06:23.281Z",
"2026-02-04T19:06:24.282Z",
"2026-02-04T19:06:25.282Z",
"2026-02-04T19:06:26.282Z",
"2026-02-04T19:06:27.288Z",
"2026-02-04T19:06:28.283Z",
"2026-02-04T19:06:29.290Z",
"2026-02-04T19:06:30.284Z",
"2026-02-04T19:06:31.289Z",
"2026-02-04T19:06:32.283Z",
"2026-02-04T19:06:33.288Z",
"2026-02-04T19:06:34.283Z",
"2026-02-04T19:06:35.303Z",
"2026-02-04T19:06:36.287Z",
"2026-02-04T19:06:37.287Z",
"2026-02-04T19:06:38.288Z",
"2026-02-04T19:06:39.288Z",
"2026-02-04T19:06:40.288Z",
"2026-02-04T19:06:41.288Z",
"2026-02-04T19:06:42.288Z",
"2026-02-04T19:06:43.289Z",
"2026-02-04T19:06:44.289Z",
"2026-02-04T19:06:45.289Z",
"2026-02-04T19:06:46.289Z",
"2026-02-04T19:06:47.290Z",
"2026-02-04T19:06:48.290Z",
"2026-02-04T19:06:49.290Z",
"2026-02-04T19:06:50.291Z",
"2026-02-04T19:06:51.292Z",
"2026-02-04T19:06:52.292Z",
"2026-02-04T19:06:53.292Z",
"2026-02-04T19:06:54.292Z",
"2026-02-04T19:06:55.292Z",
"2026-02-04T19:06:56.292Z",
"2026-02-04T19:06:57.292Z",
"2026-02-04T19:06:58.293Z",
"2026-02-04T19:06:59.292Z",
"2026-02-04T19:07:00.292Z",
"2026-02-04T19:07:01.293Z",
"2026-02-04T19:07:02.293Z",
"2026-02-04T19:07:03.293Z",
"2026-02-04T19:07:04.294Z",
"2026-02-04T19:07:05.293Z",
"2026-02-04T19:07:06.302Z",
"2026-02-04T19:07:07.293Z",
"2026-02-04T19:07:08.294Z"
],
"cpu": [
1.2891,
75.4605,
1.1246,
110.9914,
13.9201,
98.5274,
6.3944,
64.1486,
6.0738,
63.4177,
6.2085,
66.0653,
5.9001,
62.525,
5.7544,
69.5987,
6.6041,
60.1274,
6.8777,
66.1227,
6.6576,
61.7225,
6.5692,
62.586,
10.3549,
69.6933,
5.8595,
62.5952,
5.8479,
69.4633,
5.9227,
59.5779,
5.9434,
58.9378,
5.7777,
65.1409,
5.7253,
58.8193,
6.3755,
64.0124,
6.9732,
61.4041,
5.724,
58.3382,
5.5851,
58.7335,
6.0968,
59.5833,
5.6418,
60.6467,
5.742,
60.2272,
5.617,
59.7526,
6.6313,
63.0884,
5.8035,
61.0101
],
"memory": [
14.21099853515625,
12.053665161132812,
13.577583312988281,
11.293220520019531,
13.9764404296875,
13.597808837890625,
12.902915954589844,
17.456069946289062,
15.924842834472656,
13.108901977539062,
11.620040893554688,
16.124671936035156,
14.659011840820312,
11.750862121582031,
17.709686279296875,
14.97283935546875,
15.023307800292969,
17.9583740234375,
18.069061279296875,
13.631416320800781,
13.709136962890625,
16.736785888671875,
16.66020965576172,
12.300872802734375,
19.781394958496094,
15.29693603515625,
13.89215087890625,
18.39887237548828,
16.973892211914062,
14.178886413574219,
12.703651428222656,
17.223411560058594,
15.726631164550781,
12.830970764160156,
18.700477600097656,
15.878158569335938,
14.343246459960938,
18.87677764892578,
17.446983337402344,
14.474784851074219,
13.017341613769531,
17.500404357910156,
16.13226318359375,
13.164146423339844,
19.092933654785156,
16.134384155273438,
14.735084533691406,
19.123802185058594,
17.696151733398438,
14.804244995117188,
13.346839904785156,
17.821983337402344,
16.359207153320312,
13.470359802246094,
19.442214965820312,
16.46966552734375,
15.026473999023438,
19.590980529785156
],
"latency": [
12,
10,
11,
11,
42,
11,
6,
7,
6,
7,
7,
10,
6,
7,
6,
6,
11,
7,
14,
8,
13,
6,
12,
7,
23,
6,
7,
7,
6,
7,
7,
6,
6,
7,
7,
6,
7,
6,
7,
7,
7,
7,
7,
7,
6,
7,
7,
7,
7,
6,
7,
6,
6,
6,
7,
15,
6,
7
],
"throughput": [
1,
101,
1,
101,
11,
91,
9,
93,
9,
93,
9,
93,
9,
93,
9,
93,
9,
93,
9,
93,
9,
93,
9,
93,
9,
93,
9,
93,
9,
93,
9,
93,
9,
93,
9,
93,
9,
93,
9,
93,
9,
93,
9,
93,
9,
93,
9,
93,
9,
93,
9,
93,
9,
93,
9,
93,
9,
93
],
"avgSignTime": [
0,
6.090444670000015,
0,
10.09357892,
10.334544000000195,
9.704539101123578,
5.840137249999998,
5.749988358695646,
5.691442000000279,
5.735636086956576,
5.9182750000002216,
5.890629923913069,
5.588866250000137,
5.850060869565309,
5.676161750001484,
5.6353527065217035,
5.762921555554688,
5.728557747252825,
6.10907399999956,
5.90219461538456,
5.862068444444351,
5.8458819999998,
5.776876333333348,
5.937595384615213,
9.20721633333354,
6.482018351648579,
5.615201250000155,
5.785851652173891,
5.64396275000081,
5.771455956521964,
5.550119749998885,
5.582937739129746,
5.7709488749997035,
5.643515130434737,
5.749425124999107,
6.210867510869914,
5.636921000000257,
5.659433999999949,
6.089611750000586,
5.987438336956668,
6.957270124999013,
5.891856141305059,
5.555353749998176,
5.548610880434985,
5.590769124999497,
5.557453586956794,
5.865140124999016,
5.646670826087163,
5.6239324999996825,
5.723344728261272,
5.6432206250010495,
5.7358725434777345,
5.5553546250012005,
5.647155880434224,
5.598165125001287,
5.917939250000058,
5.63759575000131,
5.651182891304653
]
},
"remediation": [
"Client-side event signing is slow. Review cryptographic library (nostr-tools) usage or client hardware capabilities."
],
"requiresSecurityReview": true
}
-129
View File
@@ -1,129 +0,0 @@
{
"config": {
"clients": 1000,
"duration": 60,
"rate": 0.2,
"relay": null,
"outputDir": "artifacts"
},
"timestamp": "2026-02-12T19:10:35.621Z",
"durationSec": 63.068,
"throughput": 158.78099828756262,
"totalSent": 10014,
"totalAccepted": 10014,
"totalRejected": 0,
"totalErrors": 0,
"latency": {
"p50": 0,
"p95": 1,
"p99": 1,
"min": 0,
"max": 6,
"avg": 0.2518474136209307
},
"signingTime": {
"avg": 0.7027462706211202,
"max": 3.145286999999371
},
"resources": [
{
"time": 7994,
"cpuUser": 2231161,
"cpuSystem": 437532,
"rss": 119869440,
"heapTotal": 55013376,
"heapUsed": 28269464
},
{
"time": 12994,
"cpuUser": 3142379,
"cpuSystem": 522268,
"rss": 122753024,
"heapTotal": 56848384,
"heapUsed": 26284072
},
{
"time": 17994,
"cpuUser": 3977010,
"cpuSystem": 616834,
"rss": 125243392,
"heapTotal": 58683392,
"heapUsed": 24239504
},
{
"time": 22993,
"cpuUser": 4886434,
"cpuSystem": 708145,
"rss": 128258048,
"heapTotal": 60256256,
"heapUsed": 35053272
},
{
"time": 27993,
"cpuUser": 5695214,
"cpuSystem": 818451,
"rss": 131010560,
"heapTotal": 62328832,
"heapUsed": 32881992
},
{
"time": 32994,
"cpuUser": 6598049,
"cpuSystem": 901079,
"rss": 133763072,
"heapTotal": 64163840,
"heapUsed": 29520680
},
{
"time": 37994,
"cpuUser": 7410460,
"cpuSystem": 1001266,
"rss": 135860224,
"heapTotal": 65212416,
"heapUsed": 40101552
},
{
"time": 42994,
"cpuUser": 8255994,
"cpuSystem": 1088486,
"rss": 138219520,
"heapTotal": 67047424,
"heapUsed": 36800544
},
{
"time": 47994,
"cpuUser": 9094489,
"cpuSystem": 1172380,
"rss": 141234176,
"heapTotal": 68882432,
"heapUsed": 34655312
},
{
"time": 52994,
"cpuUser": 9929570,
"cpuSystem": 1275658,
"rss": 143724544,
"heapTotal": 69898240,
"heapUsed": 36593096
},
{
"time": 57995,
"cpuUser": 10749951,
"cpuSystem": 1294454,
"rss": 143724544,
"heapTotal": 69898240,
"heapUsed": 33902488
}
],
"topHotFunctions": [
{
"function": "finalizeEvent (client signing)",
"avgTime": "0.70ms",
"impact": "Low/Moderate CPU usage"
}
],
"errors": 0,
"proposedRemediation": [
"Signing speed is good (<10ms). Focus on network I/O."
]
}
-178
View File
@@ -1,178 +0,0 @@
NIP-01
======
Basic protocol flow description
-------------------------------
`draft` `mandatory` `relay`
This NIP defines the basic protocol that should be implemented by everybody. New NIPs may add new optional (or mandatory) fields and messages and features to the structures and flows described here.
## Events and signatures
Each user has a keypair. Signatures, public key, and encodings are done according to the [Schnorr signatures standard for the curve `secp256k1`](https://bips.xyz/340).
The only object type that exists is the `event`, which has the following format on the wire:
```yaml
{
"id": <32-bytes lowercase hex-encoded sha256 of the serialized event data>,
"pubkey": <32-bytes lowercase hex-encoded public key of the event creator>,
"created_at": <unix timestamp in seconds>,
"kind": <integer between 0 and 65535>,
"tags": [
[<arbitrary string>...],
// ...
],
"content": <arbitrary string>,
"sig": <64-bytes lowercase hex of the signature of the sha256 hash of the serialized event data, which is the same as the "id" field>
}
```
To obtain the `event.id`, we `sha256` the serialized event. The serialization is done over the UTF-8 JSON-serialized string (which is described below) of the following structure:
```
[
0,
<pubkey, as a lowercase hex string>,
<created_at, as a number>,
<kind, as a number>,
<tags, as an array of arrays of non-null strings>,
<content, as a string>
]
```
To prevent implementation differences from creating a different event ID for the same event, the following rules MUST be followed while serializing:
- UTF-8 should be used for encoding.
- Whitespace, line breaks or other unnecessary formatting should not be included in the output JSON.
- The following characters in the content field must be escaped as shown, and all other characters must be included verbatim:
- A line break (`0x0A`), use `\n`
- A double quote (`0x22`), use `\"`
- A backslash (`0x5C`), use `\\`
- A carriage return (`0x0D`), use `\r`
- A tab character (`0x09`), use `\t`
- A backspace, (`0x08`), use `\b`
- A form feed, (`0x0C`), use `\f`
### Tags
Each tag is an array of one or more strings, with some conventions around them. Take a look at the example below:
```jsonc
{
"tags": [
["e", "5c83da77af1dec6d7289834998ad7aafbd9e2191396d75ec3cc27f5a77226f36", "wss://nostr.example.com"],
["p", "f7234bd4c1394dda46d09f35bd384dd30cc552ad5541990f98844fb06676e9ca"],
["a", "30023:f7234bd4c1394dda46d09f35bd384dd30cc552ad5541990f98844fb06676e9ca:abcd", "wss://nostr.example.com"],
["alt", "reply"],
// ...
],
// ...
}
```
The first element of the tag array is referred to as the tag _name_ or _key_ and the second as the tag _value_. So we can safely say that the event above has an `e` tag set to `"5c83da77af1dec6d7289834998ad7aafbd9e2191396d75ec3cc27f5a77226f36"`, an `alt` tag set to `"reply"` and so on. All elements after the second do not have a conventional name.
This NIP defines 3 standard tags that can be used across all event kinds with the same meaning. They are as follows:
- The `e` tag, used to refer to an event: `["e", <32-bytes lowercase hex of the id of another event>, <recommended relay URL, optional>, <32-bytes lowercase hex of the author's pubkey, optional>]`
- The `p` tag, used to refer to another user: `["p", <32-bytes lowercase hex of a pubkey>, <recommended relay URL, optional>]`
- The `a` tag, used to refer to an addressable or replaceable event
- for an addressable event: `["a", "<kind integer>:<32-bytes lowercase hex of a pubkey>:<d tag value>", <recommended relay URL, optional>]`
- for a normal replaceable event: `["a", "<kind integer>:<32-bytes lowercase hex of a pubkey>:", <recommended relay URL, optional>]` (note: include the trailing colon)
As a convention, all single-letter (only english alphabet letters: a-z, A-Z) key tags are expected to be indexed by relays, such that it is possible, for example, to query or subscribe to events that reference the event `"5c83da77af1dec6d7289834998ad7aafbd9e2191396d75ec3cc27f5a77226f36"` by using the `{"#e": ["5c83da77af1dec6d7289834998ad7aafbd9e2191396d75ec3cc27f5a77226f36"]}` filter. Only the first value in any given tag is indexed.
### Kinds
Kinds specify how clients should interpret the meaning of each event and the other fields of each event (e.g. an `"r"` tag may have a meaning in an event of kind 1 and an entirely different meaning in an event of kind 10002). Each NIP may define the meaning of a set of kinds that weren't defined elsewhere. [NIP-10](10.md), for instance, specifies the `kind:1` text note for social media applications.
This NIP defines one basic kind:
- `0`: **user metadata**: the `content` is set to a stringified JSON object `{name: <nickname or full name>, about: <short bio>, picture: <url of the image>}` describing the user who created the event. [Extra metadata fields](24.md#kind-0) may be set. A relay may delete older events once it gets a new one for the same pubkey.
And also a convention for kind ranges that allow for easier experimentation and flexibility of relay implementation:
- for kind `n` such that `1000 <= n < 10000 || 4 <= n < 45 || n == 1 || n == 2`, events are **regular**, which means they're all expected to be stored by relays.
- for kind `n` such that `10000 <= n < 20000 || n == 0 || n == 3`, events are **replaceable**, which means that, for each combination of `pubkey` and `kind`, only the latest event MUST be stored by relays, older versions MAY be discarded.
- for kind `n` such that `20000 <= n < 30000`, events are **ephemeral**, which means they are not expected to be stored by relays.
- for kind `n` such that `30000 <= n < 40000`, events are **addressable** by their `kind`, `pubkey` and `d` tag value -- which means that, for each combination of `kind`, `pubkey` and the `d` tag value, only the latest event MUST be stored by relays, older versions MAY be discarded.
In case of replaceable events with the same timestamp, the event with the lowest id (first in lexical order) should be retained, and the other discarded.
When answering to `REQ` messages for replaceable events such as `{"kinds":[0],"authors":[<hex-key>]}`, even if the relay has more than one version stored, it SHOULD return just the latest one.
These are just conventions and relay implementations may differ.
## Communication between clients and relays
Relays expose a websocket endpoint to which clients can connect. Clients SHOULD open a single websocket connection to each relay and use it for all their subscriptions. Relays MAY limit number of connections from specific IP/client/etc.
### From client to relay: sending events and creating subscriptions
Clients can send 3 types of messages, which must be JSON arrays, according to the following patterns:
* `["EVENT", <event JSON as defined above>]`, used to publish events.
* `["REQ", <subscription_id>, <filters1>, <filters2>, ...]`, used to request events and subscribe to new updates.
* `["CLOSE", <subscription_id>]`, used to stop previous subscriptions.
`<subscription_id>` is an arbitrary, non-empty string of max length 64 chars. It represents a subscription per connection. Relays MUST manage `<subscription_id>`s independently for each WebSocket connection. `<subscription_id>`s are not guaranteed to be globally unique.
`<filtersX>` is a JSON object that determines what events will be sent in that subscription, it can have the following attributes:
```yaml
{
"ids": <a list of event ids>,
"authors": <a list of lowercase pubkeys, the pubkey of an event must be one of these>,
"kinds": <a list of a kind numbers>,
"#<single-letter (a-zA-Z)>": <a list of tag values, for #e — a list of event ids, for #p — a list of pubkeys, etc.>,
"since": <an integer unix timestamp in seconds. Events must have a created_at >= to this to pass>,
"until": <an integer unix timestamp in seconds. Events must have a created_at <= to this to pass>,
"limit": <maximum number of events relays SHOULD return in the initial query>
}
```
Upon receiving a `REQ` message, the relay SHOULD return events that match the filter. Any new events it receives SHOULD be sent to that same websocket until the connection is closed, a `CLOSE` event is received with the same `<subscription_id>`, or a new `REQ` is sent using the same `<subscription_id>` (in which case a new subscription is created, replacing the old one).
Filter attributes containing lists (`ids`, `authors`, `kinds` and tag filters like `#e`) are JSON arrays with one or more values. At least one of the arrays' values must match the relevant field in an event for the condition to be considered a match. For scalar event attributes such as `authors` and `kind`, the attribute from the event must be contained in the filter list. In the case of tag attributes such as `#e`, for which an event may have multiple values, the event and filter condition values must have at least one item in common.
The `ids`, `authors`, `#e` and `#p` filter lists MUST contain exact 64-character lowercase hex values.
The `since` and `until` properties can be used to specify the time range of events returned in the subscription. If a filter includes the `since` property, events with `created_at` greater than or equal to `since` are considered to match the filter. The `until` property is similar except that `created_at` must be less than or equal to `until`. In short, an event matches a filter if `since <= created_at <= until` holds.
All conditions of a filter that are specified must match for an event for it to pass the filter, i.e., multiple conditions are interpreted as `&&` conditions.
A `REQ` message may contain multiple filters. In this case, events that match any of the filters are to be returned, i.e., multiple filters are to be interpreted as `||` conditions.
The `limit` property of a filter is only valid for the initial query and MUST be ignored afterwards. When `limit: n` is present it is assumed that the events returned in the initial query will be the last `n` events ordered by the `created_at`. Newer events should appear first, and in the case of ties the event with the lowest id (first in lexical order) should be first. Relays SHOULD use the `limit` value to guide how many events are returned in the initial response. Returning fewer events is acceptable, but returning (much) more should be avoided to prevent overwhelming clients.
### From relay to client: sending events and notices
Relays can send 5 types of messages, which must also be JSON arrays, according to the following patterns:
* `["EVENT", <subscription_id>, <event JSON as defined above>]`, used to send events requested by clients.
* `["OK", <event_id>, <true|false>, <message>]`, used to indicate acceptance or denial of an `EVENT` message.
* `["EOSE", <subscription_id>]`, used to indicate the _end of stored events_ and the beginning of events newly received in real-time.
* `["CLOSED", <subscription_id>, <message>]`, used to indicate that a subscription was ended on the server side.
* `["NOTICE", <message>]`, used to send human-readable error messages or other things to clients.
This NIP defines no rules for how `NOTICE` messages should be sent or treated.
- `EVENT` messages MUST be sent only with a subscription ID related to a subscription previously initiated by the client (using the `REQ` message above).
- `OK` messages MUST be sent in response to `EVENT` messages received from clients, they must have the 3rd parameter set to `true` when an event has been accepted by the relay, `false` otherwise. The 4th parameter MUST always be present, but MAY be an empty string when the 3rd is `true`, otherwise it MUST be a string formed by a machine-readable single-word prefix followed by a `:` and then a human-readable message. Some examples:
* `["OK", "b1a649ebe8...", true, ""]`
* `["OK", "b1a649ebe8...", true, "pow: difficulty 25>=24"]`
* `["OK", "b1a649ebe8...", true, "duplicate: already have this event"]`
* `["OK", "b1a649ebe8...", false, "blocked: you are banned from posting here"]`
* `["OK", "b1a649ebe8...", false, "blocked: please register your pubkey at https://my-expensive-relay.example.com"]`
* `["OK", "b1a649ebe8...", false, "rate-limited: slow down there chief"]`
* `["OK", "b1a649ebe8...", false, "invalid: event creation date is too far off from the current time"]`
* `["OK", "b1a649ebe8...", false, "pow: difficulty 26 is less than 30"]`
* `["OK", "b1a649ebe8...", false, "restricted: not allowed to write."]`
* `["OK", "b1a649ebe8...", false, "error: could not connect to the database"]`
* `["OK", "b1a649ebe8...", false, "mute: no one was listening to your ephemeral event and it wasn't handled in any way, it was ignored"]`
- `CLOSED` messages MUST be sent in response to a `REQ` when the relay refuses to fulfill it. It can also be sent when a relay decides to kill a subscription on its side before a client has disconnected or sent a `CLOSE`. This message uses the same pattern of `OK` messages with the machine-readable prefix and human-readable message. Some examples:
* `["CLOSED", "sub1", "unsupported: filter contains unknown elements"]`
* `["CLOSED", "sub1", "error: could not connect to the database"]`
* `["CLOSED", "sub1", "error: shutting down idle subscription"]`
- The standardized machine-readable prefixes for `OK` and `CLOSED` are: `duplicate`, `pow`, `blocked`, `rate-limited`, `invalid`, `restricted`, `mute` and `error` for when none of that fits.
-55
View File
@@ -1,55 +0,0 @@
> __Warning__ `unrecommended`: deprecated in favor of [NIP-17](17.md)
NIP-04
======
Encrypted Direct Message
------------------------
`final` `unrecommended` `optional` `relay`
A special event with kind `4`, meaning "encrypted direct message". It is supposed to have the following attributes:
**`content`** MUST be equal to the base64-encoded, aes-256-cbc encrypted string of anything a user wants to write, encrypted using a shared cipher generated by combining the recipient's public-key with the sender's private-key; this appended by the base64-encoded initialization vector as if it was a querystring parameter named "iv". The format is the following: `"content": "<encrypted_text>?iv=<initialization_vector>"`.
**`tags`** MUST contain an entry identifying the receiver of the message (such that relays may naturally forward this event to them), in the form `["p", "<pubkey, as a hex string>"]`.
**`tags`** MAY contain an entry identifying the previous message in a conversation or a message we are explicitly replying to (such that contextual, more organized conversations may happen), in the form `["e", "<event_id>"]`.
**Note**: By default in the [libsecp256k1](https://github.com/bitcoin-core/secp256k1) ECDH implementation, the secret is the SHA256 hash of the shared point (both X and Y coordinates). In Nostr, only the X coordinate of the shared point is used as the secret and it is NOT hashed. If using libsecp256k1, a custom function that copies the X coordinate must be passed as the `hashfp` argument in `secp256k1_ecdh`. See [here](https://github.com/bitcoin-core/secp256k1/blob/master/src/modules/ecdh/main_impl.h#L29).
Code sample for generating such an event in JavaScript:
```js
import crypto from 'crypto'
import * as secp from '@noble/secp256k1'
let sharedPoint = secp.getSharedSecret(ourPrivateKey, '02' + theirPublicKey)
let sharedX = sharedPoint.slice(1, 33)
let iv = crypto.randomFillSync(new Uint8Array(16))
var cipher = crypto.createCipheriv(
'aes-256-cbc',
Buffer.from(sharedX),
iv
)
let encryptedMessage = cipher.update(text, 'utf8', 'base64')
encryptedMessage += cipher.final('base64')
let ivBase64 = Buffer.from(iv.buffer).toString('base64')
let event = {
pubkey: ourPubKey,
created_at: Math.floor(Date.now() / 1000),
kind: 4,
tags: [['p', theirPublicKey]],
content: encryptedMessage + '?iv=' + ivBase64
}
```
## Security Warning
This standard does not go anywhere near what is considered the state-of-the-art in encrypted communication between peers, and it leaks metadata in the events, therefore it must not be used for anything you really need to keep secret, and only with relays that use `AUTH` to restrict who can fetch your `kind:4` events.
## Client Implementation Warning
Clients *should not* search and replace public key or note references from the `.content`. If processed like a regular text note (where `@npub...` is replaced with `#[0]` with a `["p", "..."]` tag) the tags are leaked and the mentioned user will receive the message in their inbox.
-32
View File
@@ -1,32 +0,0 @@
NIP-07
======
`window.nostr` capability for web browsers
------------------------------------------
`draft` `optional`
The `window.nostr` object may be made available by web browsers or extensions and websites or web-apps may make use of it after checking its availability.
That object must define the following methods:
```
async window.nostr.getPublicKey(): string // returns a public key as hex
async window.nostr.signEvent(event: { created_at: number, kind: number, tags: string[][], content: string }): Event // takes an event object, adds `id`, `pubkey` and `sig` and returns it
```
Aside from these two basic above, the following functions can also be implemented optionally:
```
async window.nostr.nip04.encrypt(pubkey, plaintext): string // returns ciphertext and iv as specified in nip-04 (deprecated)
async window.nostr.nip04.decrypt(pubkey, ciphertext): string // takes ciphertext and iv as specified in nip-04 (deprecated)
async window.nostr.nip44.encrypt(pubkey, plaintext): string // returns ciphertext as specified in nip-44
async window.nostr.nip44.decrypt(pubkey, ciphertext): string // takes ciphertext as specified in nip-44
```
### Recommendation to Extension Authors
To make sure that the `window.nostr` is available to nostr clients on page load, the authors who create Chromium and Firefox extensions should load their scripts by specifying `"run_at": "document_end"` in the extension's manifest.
### Implementation
See https://github.com/aljazceru/awesome-nostr#nip-07-browser-extensions.
-84
View File
@@ -1,84 +0,0 @@
NIP-10
======
Text Notes and Threads
----------------------
`draft` `optional`
This NIP defines `kind:1` as a simple plaintext note.
## Abstract
The `.content` property contains some human-readable text.
`e` tags can be used to define note thread roots and replies. They SHOULD be sorted by the reply stack from root to the direct parent.
`q` tags MAY be used when citing events in the `.content` with [NIP-21](21.md).
```json
["q", "<event-id> or <event-address>", "<relay-url>", "<pubkey-if-a-regular-event>"]
```
Authors of the `e` and `q` tags SHOULD be added as `p` tags to notify of a new reply or quote.
Markup languages such as markdown and HTML SHOULD NOT be used.
## Marked "e" tags (PREFERRED)
Kind 1 events with `e` tags are replies to other kind 1 events. Kind 1 replies MUST NOT be used to reply to other kinds, use [NIP-22](22.md) instead.
`["e", <event-id>, <relay-url>, <marker>, <pubkey>]`
Where:
* `<event-id>` is the id of the event being referenced.
* `<relay-url>` is the URL of a recommended relay associated with the reference. Clients SHOULD add a valid `<relay-url>` field, but may instead leave it as `""`.
* `<marker>` is optional and if present is one of `"reply"`, `"root"`.
* `<pubkey>` is optional, SHOULD be the pubkey of the author of the referenced event
Those marked with `"reply"` denote the id of the reply event being responded to. Those marked with `"root"` denote the root id of the reply thread being responded to. For top level replies (those replying directly to the root event), only the `"root"` marker should be used.
A direct reply to the root of a thread should have a single marked "e" tag of type "root".
>This scheme is preferred because it allows events to mention others without confusing them with `<reply-id>` or `<root-id>`.
`<pubkey>` SHOULD be the pubkey of the author of the `e` tagged event, this is used in the outbox model to search for that event from the authors write relays where relay hints did not resolve the event.
## The "p" tag
Used in a text event contains a list of pubkeys used to record who is involved in a reply thread.
When replying to a text event E the reply event's "p" tags should contain all of E's "p" tags as well as the `"pubkey"` of the event being replied to.
Example: Given a text event authored by `a1` with "p" tags [`p1`, `p2`, `p3`] then the "p" tags of the reply should be [`a1`, `p1`, `p2`, `p3`]
in no particular order.
## Deprecated Positional "e" tags
This scheme is not in common use anymore and is here just to keep backward compatibility with older events on the network.
Positional `e` tags are deprecated because they create ambiguities that are difficult, or impossible to resolve when an event references another but is not a reply.
They use simple `e` tags without any marker.
`["e", <event-id>, <relay-url>]` as per NIP-01.
Where:
* `<event-id>` is the id of the event being referenced.
* `<relay-url>` is the URL of a recommended relay associated with the reference. Many clients treat this field as optional.
**The positions of the "e" tags within the event denote specific meanings as follows**:
* No "e" tag: <br>
This event is not a reply to, nor does it refer to, any other event.
* One "e" tag: <br>
`["e", <id>]`: The id of the event to which this event is a reply.
* Two "e" tags: `["e", <root-id>]`, `["e", <reply-id>]` <br>
`<root-id>` is the id of the event at the root of the reply chain. `<reply-id>` is the id of the article to which this event is a reply.
* Many "e" tags: `["e", <root-id>]` `["e", <mention-id>]`, ..., `["e", <reply-id>]`<br>
There may be any number of `<mention-ids>`. These are the ids of events which may, or may not be in the reply chain.
They are citing from this event. `root-id` and `reply-id` are as above.
-205
View File
@@ -1,205 +0,0 @@
NIP-17
======
Private Direct Messages
-----------------------
`draft` `optional` `relay`
This NIP defines an encrypted chat scheme which uses [NIP-44](44.md) encryption and [NIP-59](59.md) seals and gift wraps.
Any event sent to an encrypted chat MUST NOT be signed, and MUST be encrypted as described in [NIP-59](./59.md) and illustrated below. Omitting signatures makes messages deniable in case they are accidentally or maliciously leaked, while still allowing the recipient to authenticate them.
By convention, `kind 14` direct messages, `kind 15` file messages, and [`kind 7` reactions](./25.md) may be sent to an encrypted chat.
## Kind Definitions
### Chat Message
Kind `14` is a chat message. `p` tags identify one or more receivers of the message.
```jsonc
{
"id": "<usual hash>",
"pubkey": "<sender-pubkey>",
"created_at": "<current-time>",
"kind": 14,
"tags": [
["p", "<receiver-1-pubkey>", "<relay-url>"],
["p", "<receiver-2-pubkey>", "<relay-url>"],
["e", "<kind-14-id>", "<relay-url>"] // if this is a reply
["subject", "<conversation-title>"],
// rest of tags...
],
"content": "<message-in-plain-text>",
}
```
`.content` MUST be plain text. Fields `id` and `created_at` are required.
An `e` tag denotes the direct parent message this post is replying to.
`q` tags MAY be used when citing events in the `.content` with [NIP-21](21.md).
```json
["q", "<event-id> or <event-address>", "<relay-url>", "<pubkey-if-a-regular-event>"]
```
## File Message
```jsonc
{
"id": "<usual hash>",
"pubkey": "<sender-pubkey>",
"created_at": "<current-time>",
"kind": 15,
"tags": [
["p", "<receiver-1-pubkey>", "<relay-url>"],
["p", "<receiver-2-pubkey>", "<relay-url>"],
["e", "<kind-14-id>", "<relay-url>", "reply"], // if this is a reply
["subject", "<conversation-title>"],
["file-type", "<file-mime-type>"],
["encryption-algorithm", "<encryption-algorithm>"],
["decryption-key", "<decryption-key>"],
["decryption-nonce", "<decryption-nonce>"],
["x", "<the SHA-256 hexencoded string of the file>"],
// rest of tags...
],
"content": "<file-url>"
}
```
Kind `15` is used for sending encrypted file event messages:
- `file-type`: Specifies the MIME type of the attached file (e.g., `image/jpeg`, `audio/mpeg`, etc.) before encryption.
- `encryption-algorithm`: Indicates the encryption algorithm used for encrypting the file. Supported algorithms: `aes-gcm`.
- `decryption-key`: The decryption key that will be used by the recipient to decrypt the file.
- `decryption-nonce`: The decryption nonce that will be used by the recipient to decrypt the file.
- `content`: The URL of the file (`<file-url>`).
- `x` containing the SHA-256 hexencoded string of the encrypted file.
- `ox` containing the SHA-256 hexencoded string of the file before encryption.
- `size` (optional) size of the encrypted file in bytes
- `dim` (optional) size in pixels in the form `<width>x<height>`
- `blurhash`(optional) the [blurhash](https://github.com/woltapp/blurhash) to show while the client is loading the file
- `thumb` (optional) URL of thumbnail with same aspect ratio (encrypted with the same key, nonce)
- `fallback` (optional) zero or more fallback file sources in case `url` fails (encrypted with the same key, nonce)
## Chat Rooms
The set of `pubkey` + `p` tags defines a chat room. If a new `p` tag is added or a current one is removed, a new room is created with a clean message history.
Clients SHOULD render messages of the same room in a continuous thread.
An optional `subject` tag defines the current name/topic of the conversation. Any member can change the topic by simply submitting a new `subject` to an existing `pubkey` + `p` tags room. There is no need to send `subject` in every message. The newest `subject` in the chat room is the subject of the conversation.
## Encrypting
Following [NIP-59](59.md), the **unsigned** chat messages must be sealed (`kind:13`) and then gift-wrapped (`kind:1059`) to each receiver and the sender individually.
```js
{
"id": "<usual hash>",
"pubkey": randomPublicKey,
"created_at": randomTimeUpTo2DaysInThePast(),
"kind": 1059, // gift wrap
"tags": [
["p", receiverPublicKey, "<relay-url>"] // receiver
],
"content": nip44Encrypt(
{
"id": "<usual hash>",
"pubkey": senderPublicKey,
"created_at": randomTimeUpTo2DaysInThePast(),
"kind": 13, // seal
"tags": [], // no tags
"content": nip44Encrypt(unsignedKind14, senderPrivateKey, receiverPublicKey),
"sig": "<signed by senderPrivateKey>"
},
randomPrivateKey, receiverPublicKey
),
"sig": "<signed by randomPrivateKey>"
}
```
The encryption algorithm MUST use the latest version of [NIP-44](44.md).
Clients MUST verify if pubkey of the `kind:13` is the same pubkey on the `kind:14`, otherwise any sender can impersonate others by simply changing the pubkey on `kind:14`.
Clients SHOULD randomize `created_at` in up to two days in the past in both the seal and the gift wrap to make sure grouping by `created_at` doesn't reveal any metadata.
The gift wrap's `p` tag can be the receiver's main pubkey or an alias key created to receive DMs without exposing the receiver's identity.
Clients MAY offer disappearing messages by setting an `expiration` tag in the gift wrap of each receiver or by not generating a gift wrap to the sender's public key. This tag SHOULD be included on the `kind 13` seal as well, in case it leaks.
## Publishing
Kind `10050` indicates the user's preferred relays to receive DMs. The event MUST include a list of `relay` tags with relay URIs.
```jsonc
{
"kind": 10050,
"tags": [
["relay", "wss://inbox.nostr.wine"],
["relay", "wss://myrelay.nostr1.com"],
],
"content": "",
// other fields...
}
```
Clients SHOULD publish the gift-wrapped `kind 1059` events that contain the sealed rumors to the relays listed in the recipient’s kind 10050 event. If that is not found that indicates the user is not ready to receive messages under this NIP and clients shouldn't try.
## Relays
Relays MAY protect message metadata by only serving `kind:1059` events to users p-tagged on the event (enforced using [NIP 42 AUTH](./42.md)).
Clients SHOULD guide users to keep `kind:10050` lists small (1-3 relays) and SHOULD spread them to as many relays as viable.
## Benefits & Limitations
This NIP offers the following privacy and security features:
1. **No Metadata Leak**: Participant identities, each message's real date and time, event kinds, and other event tags are all hidden from the public. Senders and receivers cannot be linked with public information alone.
2. **No Public Group Identifiers**: There is no public central queue, channel or otherwise converging identifier to correlate or count all messages in the same group.
3. **No Moderation**: There are no group admins: no invitations or bans.
4. **No Shared Secrets**: No secret must be known to all members that can leak or be mistakenly shared
5. **Fully Recoverable**: Messages can be fully recoverable by any client with the user's private key
6. **Optional Forward Secrecy**: Users and clients can opt-in for "disappearing messages".
7. **Uses Public Relays**: Messages can flow through public relays without loss of privacy. Private relays can increase privacy further, but they are not required.
8. **Cold Storage**: Users can unilaterally opt-in to sharing their messages with a separate key that is exclusive for DM backup and recovery.
The main limitation of this approach is having to send a separate encrypted event to each receiver. Group chats with more than 100 participants should find a more suitable messaging scheme.
## Examples
This example sends the message `Hola, que tal?` from `nsec1w8udu59ydjvedgs3yv5qccshcj8k05fh3l60k9x57asjrqdpa00qkmr89m` to `nsec12ywtkplvyq5t6twdqwwygavp5lm4fhuang89c943nf2z92eez43szvn4dt`.
The two final GiftWraps, one to the receiver and the other to the sender, respectively, are:
```json
{
"id":"2886780f7349afc1344047524540ee716f7bdc1b64191699855662330bf235d8",
"pubkey":"8f8a7ec43b77d25799281207e1a47f7a654755055788f7482653f9c9661c6d51",
"created_at":1703128320,
"kind":1059,
"tags":[
["p", "918e2da906df4ccd12c8ac672d8335add131a4cf9d27ce42b3bb3625755f0788"]
],
"content":"AsqzdlMsG304G8h08bE67dhAR1gFTzTckUUyuvndZ8LrGCvwI4pgC3d6hyAK0Wo9gtkLqSr2rT2RyHlE5wRqbCOlQ8WvJEKwqwIJwT5PO3l2RxvGCHDbd1b1o40ZgIVwwLCfOWJ86I5upXe8K5AgpxYTOM1BD+SbgI5jOMA8tgpRoitJedVSvBZsmwAxXM7o7sbOON4MXHzOqOZpALpS2zgBDXSAaYAsTdEM4qqFeik+zTk3+L6NYuftGidqVluicwSGS2viYWr5OiJ1zrj1ERhYSGLpQnPKrqDaDi7R1KrHGFGyLgkJveY/45y0rv9aVIw9IWF11u53cf2CP7akACel2WvZdl1htEwFu/v9cFXD06fNVZjfx3OssKM/uHPE9XvZttQboAvP5UoK6lv9o3d+0GM4/3zP+yO3C0NExz1ZgFmbGFz703YJzM+zpKCOXaZyzPjADXp8qBBeVc5lmJqiCL4solZpxA1865yPigPAZcc9acSUlg23J1dptFK4n3Tl5HfSHP+oZ/QS/SHWbVFCtq7ZMQSRxLgEitfglTNz9P1CnpMwmW/Y4Gm5zdkv0JrdUVrn2UO9ARdHlPsW5ARgDmzaxnJypkfoHXNfxGGXWRk0sKLbz/ipnaQP/eFJv/ibNuSfqL6E4BnN/tHJSHYEaTQ/PdrA2i9laG3vJti3kAl5Ih87ct0w/tzYfp4SRPhEF1zzue9G/16eJEMzwmhQ5Ec7jJVcVGa4RltqnuF8unUu3iSRTQ+/MNNUkK6Mk+YuaJJs6Fjw6tRHuWi57SdKKv7GGkr0zlBUU2Dyo1MwpAqzsCcCTeQSv+8qt4wLf4uhU9Br7F/L0ZY9bFgh6iLDCdB+4iABXyZwT7Ufn762195hrSHcU4Okt0Zns9EeiBOFxnmpXEslYkYBpXw70GmymQfJlFOfoEp93QKCMS2DAEVeI51dJV1e+6t3pCSsQN69Vg6jUCsm1TMxSs2VX4BRbq562+VffchvW2BB4gMjsvHVUSRl8i5/ZSDlfzSPXcSGALLHBRzy+gn0oXXJ/447VHYZJDL3Ig8+QW5oFMgnWYhuwI5QSLEyflUrfSz+Pdwn/5eyjybXKJftePBD9Q+8NQ8zulU5sqvsMeIx/bBUx0fmOXsS3vjqCXW5IjkmSUV7q54GewZqTQBlcx+90xh/LSUxXex7UwZwRnifvyCbZ+zwNTHNb12chYeNjMV7kAIr3cGQv8vlOMM8ajyaZ5KVy7HpSXQjz4PGT2/nXbL5jKt8Lx0erGXsSsazkdoYDG3U",
"sig":"a3c6ce632b145c0869423c1afaff4a6d764a9b64dedaf15f170b944ead67227518a72e455567ca1c2a0d187832cecbde7ed478395ec4c95dd3e71749ed66c480"
}
```
```json
{
"id":"162b0611a1911cfcb30f8a5502792b346e535a45658b3a31ae5c178465509721",
"pubkey":"626be2af274b29ea4816ad672ee452b7cf96bbb4836815a55699ae402183f512",
"created_at":1702711587,
"kind":1059,
"tags":[
["p", "44900586091b284416a0c001f677f9c49f7639a55c3f1e2ec130a8e1a7998e1b"]
],
"content":"AsTClTzr0gzXXji7uye5UB6LYrx3HDjWGdkNaBS6BAX9CpHa+Vvtt5oI2xJrmWLen+Fo2NBOFazvl285Gb3HSM82gVycrzx1HUAaQDUG6HI7XBEGqBhQMUNwNMiN2dnilBMFC3Yc8ehCJT/gkbiNKOpwd2rFibMFRMDKai2mq2lBtPJF18oszKOjA+XlOJV8JRbmcAanTbEK5nA/GnG3eGUiUzhiYBoHomj3vztYYxc0QYHOx0WxiHY8dsC6jPsXC7f6k4P+Hv5ZiyTfzvjkSJOckel1lZuE5SfeZ0nduqTlxREGeBJ8amOykgEIKdH2VZBZB+qtOMc7ez9dz4wffGwBDA7912NFS2dPBr6txHNxBUkDZKFbuD5wijvonZDvfWq43tZspO4NutSokZB99uEiRH8NAUdGTiNb25m9JcDhVfdmABqTg5fIwwTwlem5aXIy8b66lmqqz2LBzJtnJDu36bDwkILph3kmvaKPD8qJXmPQ4yGpxIbYSTCohgt2/I0TKJNmqNvSN+IVoUuC7ZOfUV9lOV8Ri0AMfSr2YsdZ9ofV5o82ClZWlWiSWZwy6ypa7CuT1PEGHzywB4CZ5ucpO60Z7hnBQxHLiAQIO/QhiBp1rmrdQZFN6PUEjFDloykoeHe345Yqy9Ke95HIKUCS9yJurD+nZjjgOxZjoFCsB1hQAwINTIS3FbYOibZnQwv8PXvcSOqVZxC9U0+WuagK7IwxzhGZY3vLRrX01oujiRrevB4xbW7Oxi/Agp7CQGlJXCgmRE8Rhm+Vj2s+wc/4VLNZRHDcwtfejogjrjdi8p6nfUyqoQRRPARzRGUnnCbh+LqhigT6gQf3sVilnydMRScEc0/YYNLWnaw9nbyBa7wFBAiGbJwO40k39wj+xT6HTSbSUgFZzopxroO3f/o4+ubx2+IL3fkev22mEN38+dFmYF3zE+hpE7jVxrJpC3EP9PLoFgFPKCuctMnjXmeHoiGs756N5r1Mm1ffZu4H19MSuALJlxQR7VXE/LzxRXDuaB2u9days/6muP6gbGX1ASxbJd/ou8+viHmSC/ioHzNjItVCPaJjDyc6bv+gs1NPCt0qZ69G+JmgHW/PsMMeL4n5bh74g0fJSHqiI9ewEmOG/8bedSREv2XXtKV39STxPweceIOh0k23s3N6+wvuSUAJE7u1LkDo14cobtZ/MCw/QhimYPd1u5HnEJvRhPxz0nVPz0QqL/YQeOkAYk7uzgeb2yPzJ6DBtnTnGDkglekhVzQBFRJdk740LEj6swkJ",
"sig":"c94e74533b482aa8eeeb54ae72a5303e0b21f62909ca43c8ef06b0357412d6f8a92f96e1a205102753777fd25321a58fba3fb384eee114bd53ce6c06a1c22bab"
}
```
-69
View File
@@ -1,69 +0,0 @@
NIP-19
======
bech32-encoded entities
-----------------------
`draft` `optional`
This NIP standardizes bech32-formatted strings that can be used to display keys, ids and other information in clients. These formats are not meant to be used anywhere in the core protocol, they are only meant for displaying to users, copy-pasting, sharing, rendering QR codes and inputting data.
It is recommended that ids and keys are stored in either hex or binary format, since these formats are closer to what must actually be used the core protocol.
## Bare keys and ids
To prevent confusion and mixing between private keys, public keys and event ids, which are all 32 byte strings. bech32-(not-m) encoding with different prefixes can be used for each of these entities.
These are the possible bech32 prefixes:
- `npub`: public keys
- `nsec`: private keys
- `note`: note ids
Example: the hex public key `3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d` translates to `npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6`.
The bech32 encodings of keys and ids are not meant to be used inside the standard NIP-01 event formats or inside the filters, they're meant for human-friendlier display and input only. Clients should still accept keys in both hex and npub format for now, and convert internally.
## Shareable identifiers with extra metadata
When sharing a profile or an event, an app may decide to include relay information and other metadata such that other apps can locate and display these entities more easily.
For these events, the contents are a binary-encoded list of `TLV` (type-length-value), with `T` and `L` being 1 byte each (`uint8`, i.e. a number in the range of 0-255), and `V` being a sequence of bytes of the size indicated by `L`.
These are the possible bech32 prefixes with `TLV`:
- `nprofile`: a nostr profile
- `nevent`: a nostr event
- `naddr`: a nostr _addressable event_ coordinate
- `nrelay`: a nostr relay (deprecated)
These possible standardized `TLV` types are indicated here:
- `0`: `special`
- depends on the bech32 prefix:
- for `nprofile` it will be the 32 bytes of the profile public key
- for `nevent` it will be the 32 bytes of the event id
- for `naddr`, it is the identifier (the `"d"` tag) of the event being referenced. For normal replaceable events use an empty string.
- `1`: `relay`
- for `nprofile`, `nevent` and `naddr`, _optionally_, a relay in which the entity (profile or event) is more likely to be found, encoded as ascii
- this may be included multiple times
- `2`: `author`
- for `naddr`, the 32 bytes of the pubkey of the event
- for `nevent`, _optionally_, the 32 bytes of the pubkey of the event
- `3`: `kind`
- for `naddr`, the 32-bit unsigned integer of the kind, big-endian
- for `nevent`, _optionally_, the 32-bit unsigned integer of the kind, big-endian
## Examples
- `npub10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg` should decode into the public key hex `7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e` and vice-versa
- `nsec1vl029mgpspedva04g90vltkh6fvh240zqtv9k0t9af8935ke9laqsnlfe5` should decode into the private key hex `67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa` and vice-versa
- `nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhhytnc9e3k7mgpz4mhxue69uhkg6nzv9ejuumpv34kytnrdaksjlyr9p` should decode into a profile with the following TLV items:
- pubkey: `3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d`
- relay: `wss://r.x.com`
- relay: `wss://djbas.sadkb.com`
## Notes
- `npub` keys MUST NOT be used in NIP-01 events or in NIP-05 JSON responses, only the hex format is supported there.
- When decoding a bech32-formatted string, TLVs that are not recognized or supported should be ignored, rather than causing an error.
-9
View File
@@ -1,9 +0,0 @@
NIP-33
======
Parameterized Replaceable Events
--------------------------------
`final` `mandatory`
Renamed to "Addressable events" and moved to [NIP-01](01.md).
-297
View File
@@ -1,297 +0,0 @@
NIP-44
======
Encrypted Payloads (Versioned)
------------------------------
`optional`
The NIP introduces a new data format for keypair-based encryption. This NIP is versioned
to allow multiple algorithm choices to exist simultaneously. This format may be used for
many things, but MUST be used in the context of a signed event as described in NIP-01.
*Note*: this format DOES NOT define any `kind`s related to a new direct messaging standard,
only the encryption required to define one. It SHOULD NOT be used as a drop-in replacement
for NIP-04 payloads.
## Versions
Currently defined encryption algorithms:
- `0x00` - Reserved
- `0x01` - Deprecated and undefined
- `0x02` - secp256k1 ECDH, HKDF, padding, ChaCha20, HMAC-SHA256, base64
## Limitations
Every nostr user has their own public key, which solves key distribution problems present
in other solutions. However, nostr's relay-based architecture makes it difficult to implement
more robust private messaging protocols with things like metadata hiding, forward secrecy,
and post compromise secrecy.
The goal of this NIP is to have a _simple_ way to encrypt payloads used in the context of a signed
event. When applying this NIP to any use case, it's important to keep in mind your users' threat
model and this NIP's limitations. For high-risk situations, users should chat in specialized E2EE
messaging software and limit use of nostr to exchanging contacts.
On its own, messages sent using this scheme have a number of important shortcomings:
- No deniability: it is possible to prove an event was signed by a particular key
- No forward secrecy: when a key is compromised, it is possible to decrypt all previous conversations
- No post-compromise security: when a key is compromised, it is possible to decrypt all future conversations
- No post-quantum security: a powerful quantum computer would be able to decrypt the messages
- IP address leak: user IP may be seen by relays and all intermediaries between user and relay
- Date leak: `created_at` is public, since it is a part of NIP-01 event
- Limited message size leak: padding only partially obscures true message length
- No attachments: they are not supported
Lack of forward secrecy may be partially mitigated by only sending messages to trusted relays, and asking
relays to delete stored messages after a certain duration has elapsed.
## Version 2
NIP-44 version 2 has the following design characteristics:
- Payloads are authenticated using a MAC before signing rather than afterwards because events are assumed
to be signed as specified in NIP-01. The outer signature serves to authenticate the full payload, and MUST
be validated before decrypting.
- ChaCha is used instead of AES because it's faster and has
[better security against multi-key attacks](https://datatracker.ietf.org/doc/draft-irtf-cfrg-aead-limits/).
- ChaCha is used instead of XChaCha because XChaCha has not been standardized. Also, xChaCha's improved collision
resistance of nonces isn't necessary since every message has a new (key, nonce) pair.
- HMAC-SHA256 is used instead of Poly1305 because polynomial MACs are much easier to forge.
- SHA256 is used instead of SHA3 or BLAKE because it is already used in nostr. Also BLAKE's speed advantage
is smaller in non-parallel environments.
- A custom padding scheme is used instead of padmé because it provides better leakage reduction for small messages.
- Base64 encoding is used instead of another encoding algorithm because it is widely available, and is already used in nostr.
### Encryption
1. Calculate a conversation key
- Execute ECDH (scalar multiplication) of public key B by private key A
Output `shared_x` must be unhashed, 32-byte encoded x coordinate of the shared point
- Use HKDF-extract with sha256, `IKM=shared_x` and `salt=utf8_encode('nip44-v2')`
- HKDF output will be a `conversation_key` between two users.
- It is always the same, when key roles are swapped: `conv(a, B) == conv(b, A)`
2. Generate a random 32-byte nonce
- Always use [CSPRNG](https://en.wikipedia.org/wiki/Cryptographically_secure_pseudorandom_number_generator)
- Don't generate a nonce from message content
- Don't re-use the same nonce between messages: doing so would make them decryptable,
but won't leak the long-term key
3. Calculate message keys
- The keys are generated from `conversation_key` and `nonce`. Validate that both are 32 bytes long
- Use HKDF-expand, with sha256, `PRK=conversation_key`, `info=nonce` and `L=76`
- Slice 76-byte HKDF output into: `chacha_key` (bytes 0..32), `chacha_nonce` (bytes 32..44), `hmac_key` (bytes 44..76)
4. Add padding
- Content must be encoded from UTF-8 into byte array
- Validate plaintext length. Minimum is 1 byte, maximum is 65535 bytes
- Padding format is: `[plaintext_length: u16][plaintext][zero_bytes]`
- Padding algorithm is related to powers-of-two, with min padded msg size of 32 bytes
- Plaintext length is encoded in big-endian as first 2 bytes of the padded blob
5. Encrypt padded content
- Use ChaCha20, with key and nonce from step 3
6. Calculate MAC (message authentication code)
- AAD (additional authenticated data) is used - instead of calculating MAC on ciphertext,
it's calculated over a concatenation of `nonce` and `ciphertext`
- Validate that AAD (nonce) is 32 bytes
7. Base64-encode (with padding) params using `concat(version, nonce, ciphertext, mac)`
Encrypted payloads MUST be included in an event's payload, hashed, and signed as defined in NIP 01, using schnorr
signature scheme over secp256k1.
### Decryption
Before decryption, the event's pubkey and signature MUST be validated as defined in NIP 01. The public key MUST be
a valid non-zero secp256k1 curve point, and the signature must be valid secp256k1 schnorr signature. For exact
validation rules, refer to BIP-340.
1. Check if first payload's character is `#`
- `#` is an optional future-proof flag that means non-base64 encoding is used
- The `#` is not present in base64 alphabet, but, instead of throwing `base64 is invalid`,
implementations MUST indicate that the encryption version is not yet supported
2. Decode base64
- Base64 is decoded into `version, nonce, ciphertext, mac`
- If the version is unknown, implementations must indicate that the encryption version is not supported
- Validate length of base64 message to prevent DoS on base64 decoder: it can be in range from 132 to 87472 chars
- Validate length of decoded message to verify output of the decoder: it can be in range from 99 to 65603 bytes
3. Calculate conversation key
- See step 1 of [encryption](#Encryption)
4. Calculate message keys
- See step 3 of [encryption](#Encryption)
5. Calculate MAC (message authentication code) with AAD and compare
- Stop and throw an error if MAC doesn't match the decoded one from step 2
- Use constant-time comparison algorithm
6. Decrypt ciphertext
- Use ChaCha20 with key and nonce from step 3
7. Remove padding
- Read the first two BE bytes of plaintext that correspond to plaintext length
- Verify that the length of sliced plaintext matches the value of the two BE bytes
- Verify that calculated padding from step 3 of the [encryption](#Encryption) process matches the actual padding
### Details
- Cryptographic methods
- `secure_random_bytes(length)` fetches randomness from CSPRNG.
- `hkdf(IKM, salt, info, L)` represents HKDF [(RFC 5869)](https://datatracker.ietf.org/doc/html/rfc5869)
with SHA256 hash function comprised of methods `hkdf_extract(IKM, salt)` and `hkdf_expand(OKM, info, L)`.
- `chacha20(key, nonce, data)` is ChaCha20 [(RFC 8439)](https://datatracker.ietf.org/doc/html/rfc8439) with
starting counter set to 0.
- `hmac_sha256(key, message)` is HMAC [(RFC 2104)](https://datatracker.ietf.org/doc/html/rfc2104).
- `secp256k1_ecdh(priv_a, pub_b)` is multiplication of point B by scalar a (`a ⋅ B`), defined in
[BIP340](https://github.com/bitcoin/bips/blob/e918b50731397872ad2922a1b08a5a4cd1d6d546/bip-0340.mediawiki).
The operation produces a shared point, and we encode the shared point's 32-byte x coordinate, using method
`bytes(P)` from BIP340. Private and public keys must be validated as per BIP340: pubkey must be a valid,
on-curve point, and private key must be a scalar in range `[1, secp256k1_order - 1]`.
NIP44 doesn't do hashing of the output: keep this in mind, because some libraries hash it using sha256.
As an example, in libsecp256k1, unhashed version is available in `secp256k1_ec_pubkey_tweak_mul`
- Operators
- `x[i:j]`, where `x` is a byte array and `i, j <= 0` returns a `(j - i)`-byte array with a copy of the
`i`-th byte (inclusive) to the `j`-th byte (exclusive) of `x`.
- Constants `c`:
- `min_plaintext_size` is 1. 1 byte msg is padded to 32 bytes.
- `max_plaintext_size` is 65535 (64kB - 1). It is padded to 65536 bytes.
- Functions
- `base64_encode(string)` and `base64_decode(bytes)` are Base64 ([RFC 4648](https://datatracker.ietf.org/doc/html/rfc4648), with padding)
- `concat` refers to byte array concatenation
- `is_equal_ct(a, b)` is constant-time equality check of 2 byte arrays
- `utf8_encode(string)` and `utf8_decode(bytes)` transform string to byte array and back
- `write_u8(number)` restricts number to values 0..255 and encodes into Big-Endian uint8 byte array
- `write_u16_be(number)` restricts number to values 0..65535 and encodes into Big-Endian uint16 byte array
- `zeros(length)` creates byte array of length `length >= 0`, filled with zeros
- `floor(number)` and `log2(number)` are well-known mathematical methods
### Implementation pseudocode
The following is a collection of python-like pseudocode functions which implement the above primitives,
intended to guide implementers. A collection of implementations in different languages is available at https://github.com/paulmillr/nip44.
```py
# Calculates length of the padded byte array.
def calc_padded_len(unpadded_len):
next_power = 1 << (floor(log2(unpadded_len - 1))) + 1
if next_power <= 256:
chunk = 32
else:
chunk = next_power / 8
if unpadded_len <= 32:
return 32
else:
return chunk * (floor((len - 1) / chunk) + 1)
# Converts unpadded plaintext to padded bytearray
def pad(plaintext):
unpadded = utf8_encode(plaintext)
unpadded_len = len(plaintext)
if (unpadded_len < c.min_plaintext_size or
unpadded_len > c.max_plaintext_size): raise Exception('invalid plaintext length')
prefix = write_u16_be(unpadded_len)
suffix = zeros(calc_padded_len(unpadded_len) - unpadded_len)
return concat(prefix, unpadded, suffix)
# Converts padded bytearray to unpadded plaintext
def unpad(padded):
unpadded_len = read_uint16_be(padded[0:2])
unpadded = padded[2:2+unpadded_len]
if (unpadded_len == 0 or
len(unpadded) != unpadded_len or
len(padded) != 2 + calc_padded_len(unpadded_len)): raise Exception('invalid padding')
return utf8_decode(unpadded)
# metadata: always 65b (version: 1b, nonce: 32b, max: 32b)
# plaintext: 1b to 0xffff
# padded plaintext: 32b to 0xffff
# ciphertext: 32b+2 to 0xffff+2
# raw payload: 99 (65+32+2) to 65603 (65+0xffff+2)
# compressed payload (base64): 132b to 87472b
def decode_payload(payload):
plen = len(payload)
if plen == 0 or payload[0] == '#': raise Exception('unknown version')
if plen < 132 or plen > 87472: raise Exception('invalid payload size')
data = base64_decode(payload)
dlen = len(d)
if dlen < 99 or dlen > 65603: raise Exception('invalid data size');
vers = data[0]
if vers != 2: raise Exception('unknown version ' + vers)
nonce = data[1:33]
ciphertext = data[33:dlen - 32]
mac = data[dlen - 32:dlen]
return (nonce, ciphertext, mac)
def hmac_aad(key, message, aad):
if len(aad) != 32: raise Exception('AAD associated data must be 32 bytes');
return hmac(sha256, key, concat(aad, message));
# Calculates long-term key between users A and B: `get_key(Apriv, Bpub) == get_key(Bpriv, Apub)`
def get_conversation_key(private_key_a, public_key_b):
shared_x = secp256k1_ecdh(private_key_a, public_key_b)
return hkdf_extract(IKM=shared_x, salt=utf8_encode('nip44-v2'))
# Calculates unique per-message key
def get_message_keys(conversation_key, nonce):
if len(conversation_key) != 32: raise Exception('invalid conversation_key length')
if len(nonce) != 32: raise Exception('invalid nonce length')
keys = hkdf_expand(OKM=conversation_key, info=nonce, L=76)
chacha_key = keys[0:32]
chacha_nonce = keys[32:44]
hmac_key = keys[44:76]
return (chacha_key, chacha_nonce, hmac_key)
def encrypt(plaintext, conversation_key, nonce):
(chacha_key, chacha_nonce, hmac_key) = get_message_keys(conversation_key, nonce)
padded = pad(plaintext)
ciphertext = chacha20(key=chacha_key, nonce=chacha_nonce, data=padded)
mac = hmac_aad(key=hmac_key, message=ciphertext, aad=nonce)
return base64_encode(concat(write_u8(2), nonce, ciphertext, mac))
def decrypt(payload, conversation_key):
(nonce, ciphertext, mac) = decode_payload(payload)
(chacha_key, chacha_nonce, hmac_key) = get_message_keys(conversation_key, nonce)
calculated_mac = hmac_aad(key=hmac_key, message=ciphertext, aad=nonce)
if not is_equal_ct(calculated_mac, mac): raise Exception('invalid MAC')
padded_plaintext = chacha20(key=chacha_key, nonce=chacha_nonce, data=ciphertext)
return unpad(padded_plaintext)
# Usage:
# conversation_key = get_conversation_key(sender_privkey, recipient_pubkey)
# nonce = secure_random_bytes(32)
# payload = encrypt('hello world', conversation_key, nonce)
# 'hello world' == decrypt(payload, conversation_key)
```
### Audit
The v2 of the standard was audited by [Cure53](https://cure53.de) in December 2023.
Check out [audit-2023.12.pdf](https://github.com/paulmillr/nip44/blob/ce63c2eaf345e9f7f93b48f829e6bdeb7e7d7964/audit-2023.12.pdf)
and [auditor's website](https://cure53.de/audit-report_nip44-implementations.pdf).
### Tests and code
A collection of implementations in different languages is available at https://github.com/paulmillr/nip44.
We publish extensive test vectors. Instead of having it in the document directly, a sha256 checksum of vectors is provided:
269ed0f69e4c192512cc779e78c555090cebc7c785b609e338a62afc3ce25040 nip44.vectors.json
Example of a test vector from the file:
```json
{
"sec1": "0000000000000000000000000000000000000000000000000000000000000001",
"sec2": "0000000000000000000000000000000000000000000000000000000000000002",
"conversation_key": "c41c775356fd92eadc63ff5a0dc1da211b268cbea22316767095b2871ea1412d",
"nonce": "0000000000000000000000000000000000000000000000000000000000000001",
"plaintext": "a",
"payload": "AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABee0G5VSK0/9YypIObAtDKfYEAjD35uVkHyB0F4DwrcNaCXlCWZKaArsGrY6M9wnuTMxWfp1RTN9Xga8no+kF5Vsb"
}
```
The file also contains intermediate values. A quick guidance with regards to its usage:
- `valid.get_conversation_key`: calculate conversation_key from secret key sec1 and public key pub2
- `valid.get_message_keys`: calculate chacha_key, chacha_nonce, hmac_key from conversation_key and nonce
- `valid.calc_padded_len`: take unpadded length (first value), calculate padded length (second value)
- `valid.encrypt_decrypt`: emulate real conversation. Calculate pub2 from sec2, verify conversation_key from (sec1, pub2), encrypt, verify payload, then calculate pub1 from sec1, verify conversation_key from (sec2, pub1), decrypt, verify plaintext.
- `valid.encrypt_decrypt_long_msg`: same as previous step, but instead of a full plaintext and payload, their checksum is provided.
- `invalid.encrypt_msg_lengths`
- `invalid.get_conversation_key`: calculating conversation_key must throw an error
- `invalid.decrypt`: decrypting message content must throw an error
-233
View File
@@ -1,233 +0,0 @@
NIP-46
======
Nostr Remote Signing
--------------------
## Changes
`remote-signer-key` is introduced, passed in bunker url, clients must differentiate between `remote-signer-pubkey` and `user-pubkey`, must call `get_public_key` after connect, nip05 login is removed, create_account moved to another NIP.
## Rationale
Private keys should be exposed to as few systems - apps, operating systems, devices - as possible as each system adds to the attack surface.
This NIP describes a method for 2-way communication between a remote signer and a Nostr client. The remote signer could be, for example, a hardware device dedicated to signing Nostr events, while the client is a normal Nostr client.
## Terminology
- **user**: A person that is trying to use Nostr.
- **client**: A user-facing application that _user_ is looking at and clicking buttons in. This application will send requests to _remote-signer_.
- **remote-signer**: A daemon or server running somewhere that will answer requests from _client_, also known as "bunker".
- **client-keypair/pubkey**: The keys generated by _client_. Used to encrypt content and communicate with _remote-signer_.
- **remote-signer-keypair/pubkey**: The keys used by _remote-signer_ to encrypt content and communicate with _client_. This keypair MAY be same as _user-keypair_, but not necessarily.
- **user-keypair/pubkey**: The actual keys representing _user_ (that will be used to sign events in response to `sign_event` requests, for example). The _remote-signer_ generally has control over these keys.
All pubkeys specified in this NIP are in hex format.
## Overview
1. _client_ generates `client-keypair`. This keypair doesn't need to be communicated to _user_ since it's largely disposable. _client_ might choose to store it locally and they should delete it on logout;
2. A connection is established (see below), _remote-signer_ learns `client-pubkey`, _client_ learns `remote-signer-pubkey`.
3. _client_ uses `client-keypair` to send requests to _remote-signer_ by `p`-tagging and encrypting to `remote-signer-pubkey`;
4. _remote-signer_ responds to _client_ by `p`-tagging and encrypting to the `client-pubkey`.
5. _client_ requests `get_public_key` to learn `user-pubkey`.
## Initiating a connection
There are two ways to initiate a connection:
### Direct connection initiated by _remote-signer_
_remote-signer_ provides connection token in the form:
```
bunker://<remote-signer-pubkey>?relay=<wss://relay-to-connect-on>&relay=<wss://another-relay-to-connect-on>&secret=<optional-secret-value>
```
_user_ passes this token to _client_, which then sends `connect` request to _remote-signer_ via the specified relays. Optional secret can be used for single successfully established connection only, _remote-signer_ SHOULD ignore new attempts to establish connection with old secret.
### Direct connection initiated by the _client_
_client_ provides a connection token using `nostrconnect://` as the protocol, and `client-pubkey` as the origin. Additional information should be passed as query parameters:
- `relay` (required) - one or more relay urls on which the _client_ is listening for responses from the _remote-signer_.
- `secret` (required) - a short random string that the _remote-signer_ should return as the `result` field of its response.
- `perms` (optional) - a comma-separated list of permissions the _client_ is requesting be approved by the _remote-signer_
- `name` (optional) - the name of the _client_ application
- `url` (optional) - the canonical url of the _client_ application
- `image` (optional) - a small image representing the _client_ application
Here's an example:
```
nostrconnect://83f3b2ae6aa368e8275397b9c26cf550101d63ebaab900d19dd4a4429f5ad8f5?relay=wss%3A%2F%2Frelay1.example.com&perms=nip44_encrypt%2Cnip44_decrypt%2Csign_event%3A13%2Csign_event%3A14%2Csign_event%3A1059&name=My+Client&secret=0s8j2djs&relay=wss%3A%2F%2Frelay2.example2.com
```
_user_ passes this token to _remote-signer_, which then sends `connect` *response* event to the `client-pubkey` via the specified relays. Client discovers `remote-signer-pubkey` from connect response author. `secret` value MUST be provided to avoid connection spoofing, _client_ MUST validate the `secret` returned by `connect` response.
## Request Events `kind: 24133`
```jsonc
{
"kind": 24133,
"pubkey": <local_keypair_pubkey>,
"content": <nip44(<request>)>,
"tags": [["p", <remote-signer-pubkey>]],
}
```
The `content` field is a JSON-RPC-like message that is [NIP-44](44.md) encrypted and has the following structure:
```jsonc
{
"id": <random_string>,
"method": <method_name>,
"params": [array_of_strings]
}
```
- `id` is a random string that is a request ID. This same ID will be sent back in the response payload.
- `method` is the name of the method/command (detailed below).
- `params` is a positional array of string parameters.
### Methods/Commands
Each of the following are methods that the _client_ sends to the _remote-signer_.
| Command | Params | Result |
| ------------------------ | ------------------------------------------------- | ---------------------------------------------------------------------- |
| `connect` | `[<remote-signer-pubkey>, <optional_secret>, <optional_requested_perms>]` | `"ack"` OR `<required-secret-value>` |
| `sign_event` | `[<{kind, content, tags, created_at}>]` | `json_stringified(<signed_event>)` |
| `ping` | `[]` | `"pong"` |
| `get_public_key` | `[]` | `<user-pubkey>` |
| `nip04_encrypt` | `[<third_party_pubkey>, <plaintext_to_encrypt>]` | `<nip04_ciphertext>` |
| `nip04_decrypt` | `[<third_party_pubkey>, <nip04_ciphertext_to_decrypt>]` | `<plaintext>` |
| `nip44_encrypt` | `[<third_party_pubkey>, <plaintext_to_encrypt>]` | `<nip44_ciphertext>` |
| `nip44_decrypt` | `[<third_party_pubkey>, <nip44_ciphertext_to_decrypt>]` | `<plaintext>` |
| `switch_relays` | `[]` | `["<relay-url>", "<relay-url>", ...]` OR `null` |
### Requested permissions
The `connect` method may be provided with `optional_requested_perms` for user convenience. The permissions are a comma-separated list of `method[:params]`, i.e. `nip44_encrypt,sign_event:4` meaning permissions to call `nip44_encrypt` and to call `sign_event` with `kind:4`. Optional parameter for `sign_event` is the kind number, parameters for other methods are to be defined later. Same permission format may be used for `perms` field of `metadata` in `nostrconnect://` string.
### Switching relays
At all times, the _remote-signer_ should be in control of what relays are being used for the connection between it and the _client_. Therefore it should be possible for it to evolve its set of relays over time as old relays go out of operation and new ones appear. Even more importantly, in the case of the connection initiated by the _client_ the client may pick relays completely foreign to the _remote-signer_'s preferences, so it must be possible for it to switch those immediately.
Therefore, compliant clients should send a `switch_relays` request immediately upon establishing a connection (always, or at reasonable intervals). Upon receiving such requests, the _remote-signer_ should reply with its updated list of relays, or `null` if there is nothing to be changed. Immediately upon receiving an updated relay list, the _client_ should update its local state and send further requests on the new relays. The `remote-signer` should then be free to disconnect from the previous relays if that is desired.
## Response Events `kind:24133`
```json
{
"id": <id>,
"kind": 24133,
"pubkey": <remote-signer-pubkey>,
"content": <nip44(<response>)>,
"tags": [["p", <client-pubkey>]],
"created_at": <unix timestamp in seconds>
}
```
The `content` field is a JSON-RPC-like message that is [NIP-44](44.md) encrypted and has the following structure:
```json
{
"id": <request_id>,
"result": <results_string>,
"error": <optional_error_string>
}
```
- `id` is the request ID that this response is for.
- `results` is a string of the result of the call (this can be either a string or a JSON stringified object)
- `error`, _optionally_, it is an error in string form, if any. Its presence indicates an error with the request.
## Example flow for signing an event
- `remote-signer-pubkey` is `fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52`
- `user-pubkey` is also `fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52`
- `client-pubkey` is `eff37350d839ce3707332348af4549a96051bd695d3223af4aabce4993531d86`
### Signature request
```jsonc
{
"kind": 24133,
"pubkey": "eff37350d839ce3707332348af4549a96051bd695d3223af4aabce4993531d86",
"content": nip44({
"id": <random_string>,
"method": "sign_event",
"params": [json_stringified(<{
content: "Hello, I'm signing remotely",
kind: 1,
tags: [],
created_at: 1714078911
}>)]
}),
"tags": [["p", "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"]], // p-tags the remote-signer-pubkey
}
```
### Response event
```jsonc
{
"kind": 24133,
"pubkey": "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52",
"content": nip44({
"id": <random_string>,
"result": json_stringified(<signed-event>)
}),
"tags": [["p", "eff37350d839ce3707332348af4549a96051bd695d3223af4aabce4993531d86"]], // p-tags the client-pubkey
}
```
### Diagram
![signing-example](https://i.nostr.build/P3gW.png)
## Auth Challenges
An Auth Challenge is a response that a _remote-signer_ can send back when it needs the _user_ to authenticate via other means. The response `content` object will take the following form:
```json
{
"id": <request_id>,
"result": "auth_url",
"error": <URL_to_display_to_end_user>
}
```
_client_ should display (in a popup or new tab) the URL from the `error` field and then subscribe/listen for another response from the _remote-signer_ (reusing the same request ID). This event will be sent once the user authenticates in the other window (or will never arrive if the user doesn't authenticate).
### Example event signing request with auth challenge
![signing-example-with-auth-challenge](https://i.nostr.build/W3aj.png)
## Appendix
### Announcing _remote-signer_ metadata
_remote-signer_ MAY publish its metadata by using [NIP-05](05.md) and [NIP-89](89.md). With NIP-05, a request to `<remote-signer>/.well-known/nostr.json?name=_` MAY return this:
```jsonc
{
"names":{
"_": <remote-signer-app-pubkey>,
},
"nip46": {
"relays": ["wss://relay1","wss://relay2"...],
"nostrconnect_url": "https://remote-signer-domain.example/<nostrconnect>"
}
}
```
The `<remote-signer-app-pubkey>` MAY be used to verify the domain from _remote-signer_'s NIP-89 event (see below). `relays` SHOULD be used to construct a more precise `nostrconnect://` string for the specific `remote-signer`. `nostrconnect_url` template MAY be used to redirect users to _remote-signer_'s connection flow by replacing `<nostrconnect>` placeholder with an actual `nostrconnect://` string.
### Remote signer discovery via NIP-89
_remote-signer_ MAY publish a NIP-89 `kind: 31990` event with `k` tag of `24133`, which MAY also include one or more `relay` tags and MAY include `nostrconnect_url` tag. The semantics of `relay` and `nostrconnect_url` tags are the same as in the section above.
_client_ MAY improve UX by discovering _remote-signers_ using their `kind: 31990` events. _client_ MAY then pre-generate `nostrconnect://` strings for the _remote-signers_, and SHOULD in that case verify that `kind: 31990` event's author is mentioned in signer's `nostr.json?name=_` file as `<remote-signer-app-pubkey>`.
-170
View File
@@ -1,170 +0,0 @@
NIP-51
======
Lists
-----
`draft` `optional`
This NIP defines lists of things that users can create. Lists can contain references to anything, and these references can be **public** or **private**.
Public items in a list are specified in the event `tags` array, while private items are specified in a JSON array that mimics the structure of the event `tags` array, but stringified and encrypted using the same scheme from [NIP-44](44.md) (the shared key is computed using the author's public and private key) and stored in the `.content`. An earlier version of this specification used [NIP-04](04.md) for encryptions. Those are now deprecated. For backward compatibility, Clients can automatically discover if the encryption is NIP-04 or NIP-44 by searching for "iv" in the ciphertext and decrypting accordingly.
When new items are added to an existing list, clients SHOULD append them to the end of the list, so they are stored in chronological order.
## Types of lists
### Standard lists
Standard lists use normal replaceable events, meaning users may only have a single list of each kind. They have special meaning and clients may rely on them to augment a user's profile or browsing experience.
For example, _mute list_ can contain the public keys of spammers and bad actors users don't want to see in their feeds or receive annoying notifications from.
| name | kind | description | expected tag items |
| --- | --- | --- | --- |
| Follow list | 3 | microblogging basic follow list, see [NIP-02](02.md) | `"p"` (pubkeys -- with optional relay hint and petname) |
| Mute list | 10000 | things the user doesn't want to see in their feeds | `"p"` (pubkeys), `"t"` (hashtags), `"word"` (lowercase string), `"e"` (threads) |
| Pinned notes | 10001 | events the user intends to showcase in their profile page | `"e"` (kind:1 notes) |
| Read/write relays | 10002 | where a user publishes to and where they expect mentions | see [NIP-65](65.md) |
| Bookmarks | 10003 | uncategorized, "global" list of things a user wants to save | `"e"` (kind:1 notes), `"a"` (kind:30023 articles) |
| Communities | 10004 | [NIP-72](72.md) communities the user belongs to | `"a"` (kind:34550 community definitions) |
| Public chats | 10005 | [NIP-28](28.md) chat channels the user is in | `"e"` (kind:40 channel definitions) |
| Blocked relays | 10006 | relays clients should never connect to | `"relay"` (relay URLs) |
| Search relays | 10007 | relays clients should use when performing search queries | `"relay"` (relay URLs) |
| Simple groups | 10009 | [NIP-29](29.md) groups the user is in | `"group"` ([NIP-29](29.md) group id + relay URL + optional group name), `"r"` for each relay in use |
| Relay feeds | 10012 | user favorite browsable relays (and relay sets) | `"relay"` (relay URLs) and `"a"` (kind:30002 relay set) |
| Interests | 10015 | topics a user may be interested in and pointers | `"t"` (hashtags) and `"a"` (kind:30015 interest set) |
| Media follows | 10020 | multimedia (photos, short video) follow list | `"p"` (pubkeys -- with optional relay hint and petname) |
| Emojis | 10030 | user preferred emojis and pointers to emoji sets | `"emoji"` (see [NIP-30](30.md)) and `"a"` (kind:30030 emoji set) |
| DM relays | 10050 | Where to receive [NIP-17](17.md) direct messages | `"relay"` (see [NIP-17](17.md)) |
| Good wiki authors | 10101 | [NIP-54](54.md) user recommended wiki authors | `"p"` (pubkeys) |
| Good wiki relays | 10102 | [NIP-54](54.md) relays deemed to only host useful articles | `"relay"` (relay URLs) |
### Sets
Sets are lists with well-defined meaning that can enhance the functionality and the UI of clients that rely on them. Unlike standard lists, users are expected to have more than one set of each kind, therefore each of them must be assigned a different `"d"` identifier.
For example, _relay sets_ can be displayed in a dropdown UI to give users the option to switch to which relays they will publish an event or from which relays they will read the replies to an event; _curation sets_ can be used by apps to showcase curations made by others tagged to different topics.
Aside from their main identifier, the `"d"` tag, sets can optionally have a `"title"`, an `"image"` and a `"description"` tags that can be used to enhance their UI.
| name | kind | description | expected tag items |
| --- | --- | --- | --- |
| Follow sets | 30000 | categorized groups of users a client may choose to check out in different circumstances | `"p"` (pubkeys) |
| Relay sets | 30002 | user-defined relay groups the user can easily pick and choose from during various operations | `"relay"` (relay URLs) |
| Bookmark sets | 30003 | user-defined bookmarks categories , for when bookmarks must be in labeled separate groups | `"e"` (kind:1 notes), `"a"` (kind:30023 articles) |
| Curation sets | 30004 | groups of articles picked by users as interesting and/or belonging to the same category | `"a"` (kind:30023 articles), `"e"` (kind:1 notes) |
| Curation sets | 30005 | groups of videos picked by users as interesting and/or belonging to the same category | `"e"` (kind:21 videos) |
| Curation sets | 30006 | groups of pictures picked by users as interesting and/or belonging to the same category | `"e"` (kind:20 pictures) |
| Kind mute sets | 30007 | mute pubkeys by kinds<br>`"d"` tag MUST be the kind string | `"p"` (pubkeys) |
| Interest sets | 30015 | interest topics represented by a bunch of "hashtags" | `"t"` (hashtags) |
| Emoji sets | 30030 | categorized emoji groups | `"emoji"` (see [NIP-30](30.md)) |
| Release artifact sets | 30063 | group of artifacts of a software release | `"e"` (kind:1063 [file metadata](94.md) events), `"a"` (software application event) |
| App curation sets | 30267 | references to multiple software applications | `"a"` (software application event) |
| Calendar | 31924 | a set of events categorized in any way | `"a"` (calendar event event) |
| Starter packs | 39089 | a named set of profiles to be shared around with the goal of being followed together | `"p"` (pubkeys) |
| Media starter packs | 39092 | same as above, but specific to multimedia (photos, short video) clients | `"p"` (pubkeys) |
### Deprecated standard lists
Some clients have used these lists in the past, but they should work on transitioning to the [standard formats](#standard-lists) above.
| kind | "d" tag | use instead |
| --- | --- | --- |
| 30000 | `"mute"` | kind 10000 _mute list_ |
| 30001 | `"pin"` | kind 10001 _pin list_ |
| 30001 | `"bookmark"` | kind 10003 _bookmarks list_ |
| 30001 | `"communities"` | kind 10004 _communities list_ |
## Examples
### A _mute list_ with some public items and some encrypted items
```json
{
"id": "a92a316b75e44cfdc19986c634049158d4206fcc0b7b9c7ccbcdabe28beebcd0",
"pubkey": "854043ae8f1f97430ca8c1f1a090bdde6488bd5115c7a45307a2a212750ae4cb",
"created_at": 1699597889,
"kind": 10000,
"tags": [
["p", "07caba282f76441955b695551c3c5c742e5b9202a3784780f8086fdcdc1da3a9"],
["p", "a55c15f5e41d5aebd236eca5e0142789c5385703f1a7485aa4b38d94fd18dcc4"]
],
"content": "TJob1dQrf2ndsmdbeGU+05HT5GMnBSx3fx8QdDY/g3NvCa7klfzgaQCmRZuo1d3WQjHDOjzSY1+MgTK5WjewFFumCcOZniWtOMSga9tJk1ky00tLoUUzyLnb1v9x95h/iT/KpkICJyAwUZ+LoJBUzLrK52wNTMt8M5jSLvCkRx8C0BmEwA/00pjOp4eRndy19H4WUUehhjfV2/VV/k4hMAjJ7Bb5Hp9xdmzmCLX9+64+MyeIQQjQAHPj8dkSsRahP7KS3MgMpjaF8nL48Bg5suZMxJayXGVp3BLtgRZx5z5nOk9xyrYk+71e2tnP9IDvSMkiSe76BcMct+m7kGVrRcavDI4n62goNNh25IpghT+a1OjjkpXt9me5wmaL7fxffV1pchdm+A7KJKIUU3kLC7QbUifF22EucRA9xiEyxETusNludBXN24O3llTbOy4vYFsq35BeZl4v1Cse7n2htZicVkItMz3wjzj1q1I1VqbnorNXFgllkRZn4/YXfTG/RMnoK/bDogRapOV+XToZ+IvsN0BqwKSUDx+ydKpci6htDRF2WDRkU+VQMqwM0CoLzy2H6A2cqyMMMD9SLRRzBg==?iv=S3rFeFr1gsYqmQA7bNnNTQ==",
"sig": "1173822c53261f8cffe7efbf43ba4a97a9198b3e402c2a1df130f42a8985a2d0d3430f4de350db184141e45ca844ab4e5364ea80f11d720e36357e1853dba6ca"
}
```
### A _curation set_ of articles and notes about yaks
```json
{
"id": "567b41fc9060c758c4216fe5f8d3df7c57daad7ae757fa4606f0c39d4dd220ef",
"pubkey": "d6dc95542e18b8b7aec2f14610f55c335abebec76f3db9e58c254661d0593a0c",
"created_at": 1695327657,
"kind": 30004,
"tags": [
["d", "jvdy9i4"],
["title", "Yaks"],
["image", "https://cdn.britannica.com/40/188540-050-9AC748DE/Yak-Himalayas-Nepal.jpg"],
["description", "The domestic yak, also known as the Tartary ox, grunting ox, or hairy cattle, is a species of long-haired domesticated cattle found throughout the Himalayan region of the Indian subcontinent, the Tibetan Plateau, Gilgit-Baltistan, Tajikistan and as far north as Mongolia and Siberia."],
["a", "30023:26dc95542e18b8b7aec2f14610f55c335abebec76f3db9e58c254661d0593a0c:95ODQzw3ajNoZ8SyMDOzQ"],
["a", "30023:54af95542e18b8b7aec2f14610f55c335abebec76f3db9e58c254661d0593a0c:1-MYP8dAhramH9J5gJWKx"],
["a", "30023:f8fe95542e18b8b7aec2f14610f55c335abebec76f3db9e58c254661d0593a0c:D2Tbd38bGrFvU0bIbvSMt"],
["e", "d78ba0d5dce22bfff9db0a9e996c9ef27e2c91051de0c4e1da340e0326b4941e"]
],
"content": "",
"sig": "a9a4e2192eede77e6c9d24ddfab95ba3ff7c03fbd07ad011fff245abea431fb4d3787c2d04aad001cb039cb8de91d83ce30e9a94f82ac3c5a2372aa1294a96bd"
}
```
### A _release artifact set_ of an Example App
```jsonc
{
"id": "567b41fc9060c758c4216fe5f8d3df7c57daad7ae757fa4606f0c39d4dd220ef",
"pubkey": "d6dc95542e18b8b7aec2f14610f55c335abebec76f3db9e58c254661d0593a0c",
"created_at": 1695327657,
"kind": 30063,
"content": "Release notes in markdown",
"tags": [
["d", "[email protected]"],
["e", "d78ba0d5dce22bfff9db0a9e996c9ef27e2c91051de0c4e1da340e0326b4941e"], // Windows exe
["e", "f27e2c91051de0c4e1da0d5dce22bfff9db0a9340e0326b4941ed78bae996c9e"], // MacOS dmg
["e", "9d24ddfab95ba3ff7c03fbd07ad011fff245abea431fb4d3787c2d04aad02332"], // Linux AppImage
["e", "340e0326b340e0326b4941ed78ba340e0326b4941ed78ba340e0326b49ed78ba"], // PWA
["a", "32267:d6dc95542e18b8b7aec2f14610f55c335abebec76f3db9e58c254661d0593a0c:com.example.app"] // Reference to parent software application
],
"sig": "a9a4e2192eede77e6c9d24ddfab95ba3ff7c03fbd07ad011fff245abea431fb4d3787c2d04aad001cb039cb8de91d83ce30e9a94f82ac3c5a2372aa1294a96bd"
}
```
### An _app curation set_
```jsonc
{
"id": "d8037fa866eb5acd2159960b3ada7284172f7d687b5289cc72a96ca2b431b611",
"pubkey": "78ce6faa72264387284e647ba6938995735ec8c7d5c5a65737e55130f026307d",
"sig": "c1ce0a04521c020ae7485307cd86285530c1f778766a3fd594d662a73e7c28f307d7cd9a9ab642ae749fce62abbabb3a32facfe8d19a21fba551b60fae863d95",
"kind": 30267,
"created_at": 1729302793,
"content": "My nostr app selection",
"tags": [
["d", "nostr"],
["a", "32267:7579076d9aff0a4cfdefa7e2045f2486c7e5d8bc63bfc6b45397233e1bbfcb19:com.example.app1"],
["a", "32267:045f2486c7e5d8bc63bfc6b45397233e1bbfcb197579076d9aff0a4cfdefa7e2:net.example.app2"],
["a", "32267:264387284e647ba6938995735ec8c7d5c5a6f026307d78ce6faa725737e55130:pl.code.app3"]
]
}
```
## Encryption process pseudocode
```scala
val private_items = [
["p", "07caba282f76441955b695551c3c5c742e5b9202a3784780f8086fdcdc1da3a9"],
["a", "a55c15f5e41d5aebd236eca5e0142789c5385703f1a7485aa4b38d94fd18dcc4"],
]
val base64blob = nip44.encrypt(json.encode_to_string(private_items))
event.content = base64blob
```
-108
View File
@@ -1,108 +0,0 @@
NIP-56
======
Reporting
---------
`optional`
A report is a `kind 1984` event that signals to users and relays that
some referenced content is objectionable. The definition of objectionable is
obviously subjective and all agents on the network (users, apps, relays, etc.)
may consume and take action on them as they see fit.
The `content` MAY contain additional information submitted by the entity
reporting the content.
Tags
----
The report event MUST include a `p` tag referencing the pubkey of the user you
are reporting.
If reporting a note, an `e` tag MUST also be included referencing the note id.
A `report type` string MUST be included as the 3rd entry to the `e`, `p` or `x` tag
being reported, which consists of the following report types:
- `nudity` - depictions of nudity, porn, etc.
- `malware` - virus, trojan horse, worm, robot, spyware, adware, back door, ransomware, rootkit, kidnapper, etc.
- `profanity` - profanity, hateful speech, etc.
- `illegal` - something which may be illegal in some jurisdiction
- `spam` - spam
- `impersonation` - someone pretending to be someone else
- `other` - for reports that don't fit in the above categories
Some report tags only make sense for profile reports, such as `impersonation`.
- `x` tags SHOULD be info hash of a blob which is intended to be report. when the `x` tag is represented client MUST include an `e` tag which is the id of the event that contains the mentioned blob. also, additionally these events can contain a `server` tag to point to media servers which may contain the mentioned media.
`l` and `L` tags MAY be also be used as defined in [NIP-32](32.md) to support
further qualification and querying.
Example events
--------------
```jsonc
{
"kind": 1984,
"tags": [
["p", "<pubkey>", "nudity"],
["L", "social.nos.ontology"],
["l", "NS-nud", "social.nos.ontology"]
],
"content": "",
// other fields...
}
```
```jsonc
{
"kind": 1984,
"tags": [
["e", "<eventId>", "illegal"],
["p", "<pubkey>"]
],
"content": "He's insulting the king!",
// other fields...
}
```
```jsonc
{
"kind": 1984,
"tags": [
["p", "<impersonator pubkey>", "impersonation"]
],
"content": "Profile is impersonating nostr:<victim bech32 pubkey>",
// other fields...
}
```
```jsonc
{
"kind": 1984,
"tags": [
["x", "<blob hash>", "malware"],
["e", "<event id which contains the blob on x tag>", "malware"],
["server", "https://you-may-find-the-blob-here.com/path-to-url.ext"]
],
"content": "This file contains malware software in it.",
// other fields...
}
```
Client behavior
---------------
Clients can use reports from friends to make moderation decisions if they
choose to. For instance, if 3+ of your friends report a profile for `nudity`,
clients can have an option to automatically blur photos from said account.
Relay behavior
--------------
It is not recommended that relays perform automatic moderation using reports,
as they can be easily gamed. Admins could use reports from trusted moderators to
takedown illegal or explicit content if the relay does not allow such things.
-257
View File
@@ -1,257 +0,0 @@
NIP-59
======
Gift Wrap
---------
`optional` `relay`
This NIP defines a protocol for encapsulating any nostr event. This makes it possible to obscure most metadata
for a given event, perform collaborative signing, and more.
This NIP *does not* define any messaging protocol. Applications of this NIP should be defined separately.
This NIP relies on [NIP-44](./44.md)'s versioned encryption algorithms.
## Overview
This protocol uses three main concepts to protect the transmission of a target event: `rumor`s, `seal`s, and `gift wrap`s.
- A `rumor` is a regular nostr event, but is **not signed**. This means that if it is leaked, it cannot be verified.
- A `rumor` is serialized to JSON, encrypted, and placed in the `content` field of a `seal`. The `seal` is then
signed by the author of the note. The only information publicly available on a `seal` is who signed it, but not what was said.
- A `seal` is serialized to JSON, encrypted, and placed in the `content` field of a `gift wrap`.
This allows the isolation of concerns across layers:
- A rumor carries the content but is unsigned, which means if leaked it will be rejected by relays and clients,
and can't be authenticated. This provides a measure of deniability.
- A seal identifies the author without revealing the content or the recipient.
- A gift wrap can add metadata (recipient, tags, a different author) without revealing the true author.
## Protocol Description
### 1. The Rumor Event Kind
A `rumor` is the same thing as an unsigned event. Any event kind can be made a `rumor` by removing the signature.
### 2. The Seal Event Kind
A `seal` is a `kind:13` event that wraps a `rumor` with the sender's regular key. The `seal` is **always** encrypted
to a receiver's pubkey but there is no `p` tag pointing to the receiver. There is no way to know who the rumor is for
without the receiver's or the sender's private key. The only public information in this event is who is signing it.
```json
{
"id": "<id>",
"pubkey": "<real author's pubkey>",
"content": "<encrypted rumor>",
"kind": 13,
"created_at": 1686840217,
"tags": [],
"sig": "<real author's pubkey signature>"
}
```
Tags MUST always be empty in a `kind:13`. The inner event MUST always be unsigned.
### 3. Gift Wrap Event Kind
A `gift wrap` event is a `kind:1059` event that wraps any other event. `tags` SHOULD include any information
needed to route the event to its intended recipient, including the recipient's `p` tag or [NIP-13](13.md) proof of work.
```json
{
"id": "<id>",
"pubkey": "<random, one-time-use pubkey>",
"content": "<encrypted kind 13>",
"kind": 1059,
"created_at": 1686840217,
"tags": [["p", "<recipient pubkey>"]],
"sig": "<random, one-time-use pubkey signature>"
}
```
## Encrypting Payloads
Encryption is done following [NIP-44](44.md) on the JSON-encoded event. Place the encryption payload in the `.content`
of the wrapper event (either a `seal` or a `gift wrap`).
## Other Considerations
If a `rumor` is intended for more than one party, or if the author wants to retain an encrypted copy, a single
`rumor` may be wrapped and addressed for each recipient individually.
The canonical `created_at` time belongs to the `rumor`. All other timestamps SHOULD be tweaked to thwart
time-analysis attacks. Note that some relays don't serve events dated in the future, so all timestamps
SHOULD be in the past.
Relays may choose not to store gift wrapped events due to them not being publicly useful. Clients MAY choose
to attach a certain amount of proof-of-work to the wrapper event per [NIP-13](13.md) in a bid to demonstrate that
the event is not spam or a denial-of-service attack.
To protect recipient metadata, relays SHOULD guard access to `kind 1059` events based on user AUTH. When
possible, clients should only send wrapped events to relays that offer this protection.
To protect recipient metadata, relays SHOULD only serve `kind 1059` events intended for the marked recipient.
When possible, clients should only send wrapped events to `read` relays for the recipient that implement
AUTH, and refuse to serve wrapped events to non-recipients.
When adding expiration tags to both `seal` and `gift wrap` layers, implementations SHOULD use independent random timestamps for each layer. Using different `created_at` values increases timing variance and helps protect against metadata correlation attacks.
Since signing keys are random, relays SHOULD delete `kind 1059` events whose p-tag matches the signer of
[NIP-09](09.md) deletions or [NIP-62](62.md) vanish requests.
## An Example
Let's send a wrapped `kind 1` message between two parties asking "Are you going to the party tonight?"
- Author private key: `0beebd062ec8735f4243466049d7747ef5d6594ee838de147f8aab842b15e273`
- Recipient private key: `e108399bd8424357a710b606ae0c13166d853d327e47a6e5e038197346bdbf45`
- Ephemeral wrapper key: `4f02eac59266002db5801adc5270700ca69d5b8f761d8732fab2fbf233c90cbd`
Note that this messaging protocol should not be used in practice, this is just an example. Refer to other
NIPs for concrete messaging protocols that depend on gift wraps.
### 1. Create an event
Create a `kind 1` event with the message, the receivers, and any other tags you want, signed by the author.
Do not sign the event.
```json
{
"created_at": 1691518405,
"content": "Are you going to the party tonight?",
"tags": [],
"kind": 1,
"pubkey": "611df01bfcf85c26ae65453b772d8f1dfd25c264621c0277e1fc1518686faef9",
"id": "9dd003c6d3b73b74a85a9ab099469ce251653a7af76f523671ab828acd2a0ef9"
}
```
### 2. Seal the rumor
Encrypt the JSON-encoded `rumor` with a conversation key derived using the author's private key and
the recipient's public key. Place the result in the `content` field of a `kind 13` `seal` event. Sign
it with the author's key.
```json
{
"content": "AqBCdwoS7/tPK+QGkPCadJTn8FxGkd24iApo3BR9/M0uw6n4RFAFSPAKKMgkzVMoRyR3ZS/aqATDFvoZJOkE9cPG/TAzmyZvr/WUIS8kLmuI1dCA+itFF6+ULZqbkWS0YcVU0j6UDvMBvVlGTzHz+UHzWYJLUq2LnlynJtFap5k8560+tBGtxi9Gx2NIycKgbOUv0gEqhfVzAwvg1IhTltfSwOeZXvDvd40rozONRxwq8hjKy+4DbfrO0iRtlT7G/eVEO9aJJnqagomFSkqCscttf/o6VeT2+A9JhcSxLmjcKFG3FEK3Try/WkarJa1jM3lMRQqVOZrzHAaLFW/5sXano6DqqC5ERD6CcVVsrny0tYN4iHHB8BHJ9zvjff0NjLGG/v5Wsy31+BwZA8cUlfAZ0f5EYRo9/vKSd8TV0wRb9DQ=",
"kind": 13,
"created_at": 1703015180,
"pubkey": "611df01bfcf85c26ae65453b772d8f1dfd25c264621c0277e1fc1518686faef9",
"tags": [],
"id": "28a87d7c074d94a58e9e89bb3e9e4e813e2189f285d797b1c56069d36f59eaa7",
"sig": "02fc3facf6621196c32912b1ef53bac8f8bfe9db51c0e7102c073103586b0d29c3f39bdaa1e62856c20e90b6c7cc5dc34ca8bb6a528872cf6e65e6284519ad73"
}
```
### 3. Wrap the seal
Encrypt the JSON-encoded `kind 13` event with your ephemeral, single-use random key. Place the result
in the `content` field of a `kind 1059`. Add a single `p` tag containing the recipient's public key.
Sign the `gift wrap` using the random key generated in the previous step.
```json
{
"content": "AhC3Qj/QsKJFWuf6xroiYip+2yK95qPwJjVvFujhzSguJWb/6TlPpBW0CGFwfufCs2Zyb0JeuLmZhNlnqecAAalC4ZCugB+I9ViA5pxLyFfQjs1lcE6KdX3euCHBLAnE9GL/+IzdV9vZnfJH6atVjvBkNPNzxU+OLCHO/DAPmzmMVx0SR63frRTCz6Cuth40D+VzluKu1/Fg2Q1LSst65DE7o2efTtZ4Z9j15rQAOZfE9jwMCQZt27rBBK3yVwqVEriFpg2mHXc1DDwHhDADO8eiyOTWF1ghDds/DxhMcjkIi/o+FS3gG1dG7gJHu3KkGK5UXpmgyFKt+421m5o++RMD/BylS3iazS1S93IzTLeGfMCk+7IKxuSCO06k1+DaasJJe8RE4/rmismUvwrHu/HDutZWkvOAhd4z4khZo7bJLtiCzZCZ74lZcjOB4CYtuAX2ZGpc4I1iOKkvwTuQy9BWYpkzGg3ZoSWRD6ty7U+KN+fTTmIS4CelhBTT15QVqD02JxfLF7nA6sg3UlYgtiGw61oH68lSbx16P3vwSeQQpEB5JbhofW7t9TLZIbIW/ODnI4hpwj8didtk7IMBI3Ra3uUP7ya6vptkd9TwQkd/7cOFaSJmU+BIsLpOXbirJACMn+URoDXhuEtiO6xirNtrPN8jYqpwvMUm5lMMVzGT3kMMVNBqgbj8Ln8VmqouK0DR+gRyNb8fHT0BFPwsHxDskFk5yhe5c/2VUUoKCGe0kfCcX/EsHbJLUUtlHXmTqaOJpmQnW1tZ/siPwKRl6oEsIJWTUYxPQmrM2fUpYZCuAo/29lTLHiHMlTbarFOd6J/ybIbICy2gRRH/LFSryty3Cnf6aae+A9uizFBUdCwTwffc3vCBae802+R92OL78bbqHKPbSZOXNC+6ybqziezwG+OPWHx1Qk39RYaF0aFsM4uZWrFic97WwVrH5i+/Nsf/OtwWiuH0gV/SqvN1hnkxCTF/+XNn/laWKmS3e7wFzBsG8+qwqwmO9aVbDVMhOmeUXRMkxcj4QreQkHxLkCx97euZpC7xhvYnCHarHTDeD6nVK+xzbPNtzeGzNpYoiMqxZ9bBJwMaHnEoI944Vxoodf51cMIIwpTmmRvAzI1QgrfnOLOUS7uUjQ/IZ1Qa3lY08Nqm9MAGxZ2Ou6R0/Z5z30ha/Q71q6meAs3uHQcpSuRaQeV29IASmye2A2Nif+lmbhV7w8hjFYoaLCRsdchiVyNjOEM4VmxUhX4VEvw6KoCAZ/XvO2eBF/SyNU3Of4SO",
"kind": 1059,
"created_at": 1703021488,
"pubkey": "18b1a75918f1f2c90c23da616bce317d36e348bcf5f7ba55e75949319210c87c",
"id": "5c005f3ccf01950aa8d131203248544fb1e41a0d698e846bd419cec3890903ac",
"sig": "35fabdae4634eb630880a1896a886e40fd6ea8a60958e30b89b33a93e6235df750097b04f9e13053764251b8bc5dd7e8e0794a3426a90b6bcc7e5ff660f54259",
"tags": [["p", "166bf3765ebd1fc55decfe395beff2ea3b2a4e0a8946e7eb578512b555737c99"]],
}
```
### 4. Broadcast Selectively
Broadcast the `kind 1059` event to the recipient's relays only. Delete all the other events.
## Code Samples
### JavaScript
```javascript
import {bytesToHex} from "@noble/hashes/utils"
import type {EventTemplate, UnsignedEvent, Event} from "nostr-tools"
import {getPublicKey, getEventHash, nip19, nip44, finalizeEvent, generateSecretKey} from "nostr-tools"
type Rumor = UnsignedEvent & {id: string}
const TWO_DAYS = 2 * 24 * 60 * 60
const now = () => Math.round(Date.now() / 1000)
const randomNow = () => Math.round(now() - (Math.random() * TWO_DAYS))
const nip44ConversationKey = (privateKey: Uint8Array, publicKey: string) =>
nip44.v2.utils.getConversationKey(bytesToHex(privateKey), publicKey)
const nip44Encrypt = (data: EventTemplate, privateKey: Uint8Array, publicKey: string) =>
nip44.v2.encrypt(JSON.stringify(data), nip44ConversationKey(privateKey, publicKey))
const nip44Decrypt = (data: Event, privateKey: Uint8Array) =>
JSON.parse(nip44.v2.decrypt(data.content, nip44ConversationKey(privateKey, data.pubkey)))
const createRumor = (event: Partial<UnsignedEvent>, privateKey: Uint8Array) => {
const rumor = {
created_at: now(),
content: "",
tags: [],
...event,
pubkey: getPublicKey(privateKey),
} as any
rumor.id = getEventHash(rumor)
return rumor as Rumor
}
const createSeal = (rumor: Rumor, privateKey: Uint8Array, recipientPublicKey: string) => {
return finalizeEvent(
{
kind: 13,
content: nip44Encrypt(rumor, privateKey, recipientPublicKey),
created_at: randomNow(),
tags: [],
},
privateKey
) as Event
}
const createWrap = (event: Event, recipientPublicKey: string) => {
const randomKey = generateSecretKey()
return finalizeEvent(
{
kind: 1059,
content: nip44Encrypt(event, randomKey, recipientPublicKey),
created_at: randomNow(),
tags: [["p", recipientPublicKey]],
},
randomKey
) as Event
}
// Test case using the above example
const senderPrivateKey = nip19.decode(`nsec1p0ht6p3wepe47sjrgesyn4m50m6avk2waqudu9rl324cg2c4ufesyp6rdg`).data
const recipientPrivateKey = nip19.decode(`nsec1uyyrnx7cgfp40fcskcr2urqnzekc20fj0er6de0q8qvhx34ahazsvs9p36`).data
const recipientPublicKey = getPublicKey(recipientPrivateKey)
const rumor = createRumor(
{
kind: 1,
content: "Are you going to the party tonight?",
},
senderPrivateKey
)
const seal = createSeal(rumor, senderPrivateKey, recipientPublicKey)
const wrap = createWrap(seal, recipientPublicKey)
// Recipient unwraps with their private key.
const unwrappedSeal = nip44Decrypt(wrap, recipientPrivateKey)
const unsealedRumor = nip44Decrypt(unwrappedSeal, recipientPrivateKey)
```
-43
View File
@@ -1,43 +0,0 @@
NIP-65
======
Relay List Metadata
-------------------
`draft` `optional`
Defines a replaceable event using `kind:10002` to advertise relays where the user generally **writes** to and relays where the user generally **reads** mentions.
The event MUST include a list of `r` tags with relay URLs as value and an optional `read` or `write` marker. If the marker is omitted, the relay is both **read** and **write**.
```jsonc
{
"kind": 10002,
"tags": [
["r", "wss://alicerelay.example.com"],
["r", "wss://brando-relay.com"],
["r", "wss://expensive-relay.example2.com", "write"],
["r", "wss://nostr-relay.example.com", "read"]
],
"content": "",
// other fields...
}
```
When downloading events **from** a user, clients SHOULD use the **write** relays of that user.
When downloading events **about** a user, where the user was tagged (mentioned), clients SHOULD use the user's **read** relays.
When publishing an event, clients SHOULD:
- Send the event to the **write** relays of the author
- Send the event to all **read** relays of each tagged user
- Send the author's `kind:10002` event to all relays the event was published to
### Size
Clients SHOULD guide users to keep `kind:10002` lists small (2-4 relays of each category).
### Discoverability
Clients SHOULD spread an author's `kind:10002` event to as many relays as viable, paying attention to relays that, at any moment, serve naturally as well-known public indexers for these relay lists (where most other clients and users are connecting to in order to publish and fetch those).
-58
View File
@@ -1,58 +0,0 @@
NIP-94
======
File Metadata
-------------
`draft` `optional`
The purpose of this NIP is to allow an organization and classification of shared files. So that relays can filter and organize in any way that is of interest. With that, multiple types of filesharing clients can be created. NIP-94 support is not expected to be implemented by "social" clients that deal with `kind:1` notes or by longform clients that deal with `kind:30023` articles.
## Event format
This NIP specifies the use of the `1063` event kind, having in `content` a description of the file content, and a list of tags described below:
* `url` the url to download the file
* `m` a string indicating the data type of the file. The [MIME types](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types) format must be used, and they should be lowercase.
* `x` containing the SHA-256 hexencoded string of the file.
* `ox` containing the SHA-256 hexencoded string of the original file, before any transformations done by the upload server
* `size` (optional) size of file in bytes
* `dim` (optional) size of file in pixels in the form `<width>x<height>`
* `magnet` (optional) URI to magnet file
* `i` (optional) torrent infohash
* `blurhash`(optional) the [blurhash](https://github.com/woltapp/blurhash) to show while the file is being loaded by the client
* `thumb` (optional) url of thumbnail with same aspect ratio
* `image` (optional) url of preview image with same dimensions
* `summary` (optional) text excerpt
* `alt` (optional) description for accessibility
* `fallback` (optional) zero or more fallback file sources in case `url` fails
* `service` (optional) service type which is serving the file (eg. [NIP-96](96.md))
```jsonc
{
"kind": 1063,
"tags": [
["url",<string with URI of file>],
["m", <MIME type>],
["x", <Hash SHA-256>],
["ox", <Hash SHA-256>],
["size", <size of file in bytes>],
["dim", <size of file in pixels>],
["magnet", <magnet URI> ],
["i", <torrent infohash>],
["blurhash", <value>],
["thumb", <string with thumbnail URI>, <Hash SHA-256>],
["image", <string with preview URI>, <Hash SHA-256>],
["summary", <excerpt>],
["alt", <description>]
],
"content": "<caption>",
// other fields...
}
```
## Suggested use cases
* A relay for indexing shared files. For example, to promote torrents.
* A pinterest-like client where people can share their portfolio and inspire others.
* A simple way to distribute configurations and software updates.
-22
View File
@@ -1,22 +0,0 @@
{
"auditReportVersion": 2,
"vulnerabilities": {},
"metadata": {
"vulnerabilities": {
"info": 0,
"low": 0,
"moderate": 0,
"high": 0,
"critical": 0,
"total": 0
},
"dependencies": {
"prod": 111,
"dev": 337,
"optional": 53,
"peer": 0,
"peerOptional": 0,
"total": 474
}
}
}
-58
View File
@@ -1,58 +0,0 @@
{
"jsdom": {
"current": "28.0.0",
"wanted": "28.1.0",
"latest": "28.1.0",
"dependent": "app",
"location": "/app/node_modules/jsdom"
},
"nostr-tools": {
"current": "2.19.4",
"wanted": "2.23.1",
"latest": "2.23.1",
"dependent": "app",
"location": "/app/node_modules/nostr-tools"
},
"pixelmatch": {
"current": "5.3.0",
"wanted": "5.3.0",
"latest": "7.1.0",
"dependent": "app",
"location": "/app/node_modules/pixelmatch"
},
"postcss-import": {
"current": "15.1.0",
"wanted": "15.1.0",
"latest": "16.1.1",
"dependent": "app",
"location": "/app/node_modules/postcss-import"
},
"prettier-plugin-tailwindcss": {
"current": "0.6.14",
"wanted": "0.6.14",
"latest": "0.7.2",
"dependent": "app",
"location": "/app/node_modules/prettier-plugin-tailwindcss"
},
"stylelint": {
"current": "16.12.0",
"wanted": "16.26.1",
"latest": "17.3.0",
"dependent": "app",
"location": "/app/node_modules/stylelint"
},
"stylelint-config-standard": {
"current": "36.0.1",
"wanted": "36.0.1",
"latest": "40.0.0",
"dependent": "app",
"location": "/app/node_modules/stylelint-config-standard"
},
"tailwindcss": {
"current": "3.4.19",
"wanted": "3.4.19",
"latest": "4.1.18",
"dependent": "app",
"location": "/app/node_modules/tailwindcss"
}
}
-1
View File
@@ -1 +0,0 @@
js/ui/profileModalContract.js:146: "Formats npub strings for display using the canonical short representation (npubXXXX…XXXX).",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

File renamed without changes.
File renamed without changes.
File renamed without changes.
-6
View File
@@ -1,6 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10" />
<path d="M2 12h20" />
<path d="M12 2c2.5 2.7 4 6.2 4 10s-1.5 7.3-4 10" />
<path d="M12 2c-2.5 2.7-4 6.2-4 10s1.5 7.3 4 10" />
</svg>

Before

Width:  |  Height:  |  Size: 334 B

File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2">
<path d="M21.698,10.658L24,12L11.998,19L0,12L2.301,10.658L11.998,16.316L21.698,10.658ZM11.998,21.315L2.301,15.657L0,17L11.998,24L24,17L21.698,15.658L11.998,21.315ZM24,7L11.998,-0L0,7L11.998,14L24,7Z" fill="white" fill-rule="nonzero"/>
<path d="M21,7.702L12.5,12.322L12.5,22C14.067,21.135 18.879,18.483 20.477,17.601C20.8,17.424 21,17.082 21,16.71L21,7.702ZM11.5,12.321L3,7.599L3,16.605C3,16.975 3.197,17.313 3.514,17.492C5.104,18.39 9.93,21.115 11.5,22L11.5,12.321ZM3.421,6.692L12,11.455L20.672,6.742C20.672,6.742 14.041,3.004 12.486,2.128C12.335,2.043 12.167,2 12,2C11.832,2 11.665,2.043 11.514,2.128C9.959,3.004 3.421,6.692 3.421,6.692Z" fill="#5C6F8A" fill-rule="nonzero"/>
</svg>
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
-16
View File
@@ -1,16 +0,0 @@
# Audit Report (Unstable Branch)
## File Size
- **Grandfathered Oversized Files:** 44
- **Total Excess Lines:** 54,278 (sum of lines exceeding the 1000-line limit for each file)
## innerHTML Usage
- **Total Assignments:** 125
## Lint Status
- **Failures:** None
- `lint:assets` skipped (build required)
- `lint:sw-compat` skipped (git history unavailable)
## Comparison
No previous report found in the repository for comparison.
-55
View File
@@ -1,55 +0,0 @@
import { performance } from "perf_hooks";
// Mock IndexedDB if running in Node
if (!globalThis.indexedDB) {
const { indexedDB, IDBKeyRange } = await import("fake-indexeddb");
globalThis.indexedDB = indexedDB;
globalThis.IDBKeyRange = IDBKeyRange;
}
// Mock console to suppress logs
const originalConsoleWarn = console.warn;
console.warn = () => {};
// Import after mocking console
const { writeMessages, clearDmDb } = await import("../js/storage/dmDb.js");
async function runBenchmark() {
await clearDmDb();
const iterations = 1000;
const messages = [];
for (let i = 0; i < iterations; i++) {
messages.push({
id: `msg-${i}`,
conversation_id: "conv-1",
sender_pubkey: "sender",
receiver_pubkey: "receiver",
created_at: Date.now(),
kind: 4,
content: `Message ${i}`,
});
}
console.log(`Starting benchmark with ${iterations} writes...`);
const start = performance.now();
// Simulate N+1 writes
for (const msg of messages) {
await writeMessages(msg);
}
const end = performance.now();
const duration = end - start;
console.log(`Total time: ${duration.toFixed(2)}ms`);
console.log(`Average time per write: ${(duration / iterations).toFixed(2)}ms`);
// Restore console
console.warn = originalConsoleWarn;
}
runBenchmark().catch(console.error);
-85
View File
@@ -1,85 +0,0 @@
import { decryptDM } from "../js/dmDecryptor.js";
// Valid 64-char hex strings
const WRAP_PUBKEY = "0000000000000000000000000000000000000000000000000000000000000001";
const SEAL_PUBKEY = "0000000000000000000000000000000000000000000000000000000000000002";
const SENDER_PUBKEY = "0000000000000000000000000000000000000000000000000000000000000003";
const RECIPIENT_PUBKEY = "0000000000000000000000000000000000000000000000000000000000000004";
const sealEvent = {
id: "seal-event",
pubkey: SEAL_PUBKEY,
content: "seal-ciphertext",
created_at: Math.floor(Date.now() / 1000),
};
const rumorEvent = {
id: "rumor-event",
pubkey: SENDER_PUBKEY,
content: "Hello World",
created_at: Math.floor(Date.now() / 1000),
tags: [["p", RECIPIENT_PUBKEY]],
};
const wrapEvent = {
kind: 1059,
pubkey: WRAP_PUBKEY,
content: "wrap-ciphertext",
created_at: Math.floor(Date.now() / 1000),
tags: [["p", RECIPIENT_PUBKEY]],
};
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const createDecryptor = (name, shouldFail, delayMs) => {
return {
scheme: "nip44",
supportsGiftWrap: true,
decrypt: async (pubkey, ciphertext, context) => {
await delay(delayMs);
if (shouldFail) {
throw new Error(`${name} failed`);
}
// If we are unwrapping the outer wrap, return the seal
if (context.stage === "wrap") {
return JSON.stringify(sealEvent);
}
// If we are unwrapping the seal, return the rumor
if (context.stage === "seal") {
return JSON.stringify(rumorEvent);
}
return "";
},
};
};
async function runBenchmark() {
console.log("Running DM Decrypt Benchmark...");
const decryptors = [
createDecryptor("D1", true, 100),
createDecryptor("D2", true, 100),
createDecryptor("D3", false, 50),
];
const context = {
actorPubkey: RECIPIENT_PUBKEY,
decryptors: decryptors,
};
const start = performance.now();
const result = await decryptDM(wrapEvent, context);
const end = performance.now();
console.log(`Result OK: ${result.ok}`);
if (!result.ok) {
const errorMessages = result.errors.map(e => ({stage: e.stage, message: e.error.message}));
console.log("Errors:", JSON.stringify(errorMessages, null, 2));
}
console.log(`Time taken: ${(end - start).toFixed(2)}ms`);
}
runBenchmark();
-76
View File
@@ -1,76 +0,0 @@
import { createFeedEngine } from "../js/feedEngine/index.js";
import { performance } from "perf_hooks";
// Mock logger
const logger = {
log: () => {},
warn: () => {},
error: () => {},
};
// Polyfill localStorage to silence errors
if (typeof globalThis.localStorage === "undefined") {
globalThis.localStorage = {
getItem: () => null,
setItem: () => {},
removeItem: () => {},
};
}
async function runBenchmark() {
const engine = createFeedEngine({ logger });
const feedName = "bench-feed";
// Create items
const itemCount = 100000;
const items = [];
for (let i = 0; i < itemCount; i++) {
items.push({
video: { id: `v${i}`, created_at: Date.now() },
metadata: { some: "data" },
});
}
// Define stages
const stages = [];
// 10 stages that return null (no change) - STRICT PASS THROUGH
for (let i = 0; i < 10; i++) {
stages.push(async (items) => {
return null;
});
}
// 10 stages that return the same items array (identity) - STRICT PASS THROUGH
for (let i = 0; i < 10; i++) {
stages.push(async (items) => {
return items;
});
}
engine.registerFeed(feedName, {
source: async () => [...items],
stages: stages,
});
console.log(`Starting benchmark with ${itemCount} items and ${stages.length} stages...`);
const iterations = 50;
const start = performance.now();
for (let i = 0; i < iterations; i++) {
await engine.runFeed(feedName);
if (i % 5 === 0) process.stdout.write(".");
}
const end = performance.now();
const totalTime = end - start;
const avgTime = totalTime / iterations;
console.log("\n");
console.log(`Total time: ${totalTime.toFixed(2)}ms`);
console.log(`Average time per run: ${avgTime.toFixed(2)}ms`);
}
runBenchmark().catch(console.error);
-81
View File
@@ -1,81 +0,0 @@
import { normalizeHashtag } from "../js/utils/hashtagNormalization.js";
import { getVideoTags } from "../js/feedEngine/utils.js";
// Mock video object
function createMockVideo(tagCount) {
const tags = [];
for (let i = 0; i < tagCount; i++) {
tags.push(["t", `tag${i}`]);
}
return {
id: "video-" + Math.random(),
tags: tags,
nip71: {
hashtags: ["extra1", "extra2"]
}
};
}
// Legacy Uncached collectVideoTags (for comparison)
function collectVideoTags(video) {
const videoTags = new Set();
if (Array.isArray(video.tags)) {
for (const tag of video.tags) {
if (Array.isArray(tag) && tag[0] === "t" && typeof tag[1] === "string") {
const normalized = normalizeHashtag(tag[1]);
if (normalized) {
videoTags.add(normalized);
}
}
}
}
if (Array.isArray(video.nip71?.hashtags)) {
for (const tag of video.nip71.hashtags) {
if (typeof tag === "string") {
const normalized = normalizeHashtag(tag);
if (normalized) {
videoTags.add(normalized);
}
}
}
}
return videoTags;
}
// Benchmark
const VIDEOS_COUNT = 10000;
const TAGS_PER_VIDEO = 10;
const ITERATIONS = 100;
const videos = [];
for (let i = 0; i < VIDEOS_COUNT; i++) {
videos.push(createMockVideo(TAGS_PER_VIDEO));
}
console.log(`Benchmarking with ${VIDEOS_COUNT} videos, ${ITERATIONS} iterations.`);
console.time("Uncached (collectVideoTags)");
for (let i = 0; i < ITERATIONS; i++) {
for (const video of videos) {
collectVideoTags(video);
}
}
console.timeEnd("Uncached (collectVideoTags)");
console.time("Cached (getVideoTags) - First Run");
for (const video of videos) {
getVideoTags(video);
}
console.timeEnd("Cached (getVideoTags) - First Run");
console.time("Cached (getVideoTags) - Second Run");
for (let i = 0; i < ITERATIONS - 1; i++) {
for (const video of videos) {
getVideoTags(video);
}
}
console.timeEnd("Cached (getVideoTags) - Second Run");
-63
View File
@@ -1,63 +0,0 @@
// benchmarks/kids_scoring_bench.js
import { createKidsScorerStage } from "../js/feedEngine/kidsScoring.js";
import { performance } from "perf_hooks";
async function runBenchmark() {
const itemCount = 200000;
const items = [];
// Create mock items with diverse data to exercise different paths
for (let i = 0; i < itemCount; i++) {
items.push({
video: {
id: `v${i}`,
pubkey: `pubkey${i % 100}`,
duration: 300 + (i % 600), // 5 to 15 minutes
tags: [
["t", "kids"],
["t", "learning"],
["t", i % 2 === 0 ? "preschool" : "science"],
],
viewCount: i * 10,
created_at: Date.now() / 1000 - (i % 10000),
moderation: { trustedCount: i % 5 },
},
metadata: {},
});
}
const context = {
runtime: {
ageGroup: "preschool",
trustedAuthors: ["pubkey1", "pubkey2"],
},
addWhy: () => {}, // Mock addWhy
};
const scorer = createKidsScorerStage();
console.log(`Starting kids scoring benchmark with ${itemCount} items...`);
// Warmup
await scorer([...items.slice(0, 1000)], context);
const iterations = 100;
let totalTime = 0;
for (let i = 0; i < iterations; i++) {
// Reset metadata for each run to be cleaner
items.forEach(item => item.metadata = {});
const start = performance.now();
await scorer(items, context);
const end = performance.now();
totalTime += (end - start);
if (i % 5 === 0) process.stdout.write(".");
}
const avgTime = totalTime / iterations;
console.log(`\nAverage time per run: ${avgTime.toFixed(4)}ms`);
}
runBenchmark().catch(console.error);
-28
View File
@@ -1,28 +0,0 @@
import { normalizeNip46CiphertextPayload } from "../js/nostr/nip46Client.js";
const ITERATIONS = 100;
const ARRAY_SIZE = 200;
function runBenchmark() {
const largeArray = Array.from({ length: ARRAY_SIZE }, (_, i) => `string_${i}`);
// Wrap in a structure that triggers the recursive coerce and then the array handling
const payload = {
data: largeArray
};
const start = performance.now();
for (let i = 0; i < ITERATIONS; i++) {
normalizeNip46CiphertextPayload(payload);
}
const end = performance.now();
const duration = end - start;
console.log(`Time taken for ${ITERATIONS} iterations with array size ${ARRAY_SIZE}: ${duration.toFixed(2)}ms`);
console.log(`Average time per iteration: ${(duration / ITERATIONS).toFixed(2)}ms`);
}
runBenchmark();
-88
View File
@@ -1,88 +0,0 @@
import { profileCache } from "../js/state/profileCache.js";
class LegacyProfileCache {
constructor() {
this.memoryCache = new Map();
}
set(pubkey, section, data) {
this.memoryCache.set(`${pubkey}:${section}`, data);
}
clearMemoryCache(pubkey) {
const prefix = `${pubkey}:`;
for (const key of this.memoryCache.keys()) {
if (key.startsWith(prefix)) {
this.memoryCache.delete(key);
}
}
}
}
function generatePubkey(i) {
return i.toString(16).padStart(64, '0');
}
function runBenchmark() {
const legacy = new LegacyProfileCache();
// Clean actual cache just in case
profileCache.memoryCache.clear();
const PUBKEY_COUNT = 1000;
const SECTIONS_PER_PUBKEY = 20;
const TARGET_PUBKEY = generatePubkey(500); // Middle one
console.log(`Setting up caches with ${PUBKEY_COUNT} pubkeys x ${SECTIONS_PER_PUBKEY} sections...`);
// Populate
for (let i = 0; i < PUBKEY_COUNT; i++) {
const pubkey = generatePubkey(i);
for (let s = 0; s < SECTIONS_PER_PUBKEY; s++) {
const section = `section-${s}`;
const data = { value: `data-${i}-${s}` };
// Legacy
legacy.set(pubkey, section, data);
// Actual (Optimized)
// We use setMemoryDataForPubkey to bypass storage/logic overhead
profileCache.setMemoryDataForPubkey(pubkey, section, data);
}
}
console.log(`Total entries: ${PUBKEY_COUNT * SECTIONS_PER_PUBKEY}`);
// Measure Legacy
const startLegacy = performance.now();
legacy.clearMemoryCache(TARGET_PUBKEY);
const endLegacy = performance.now();
const timeLegacy = endLegacy - startLegacy;
// Measure Optimized
const startOptimized = performance.now();
profileCache.clearMemoryCache(TARGET_PUBKEY);
const endOptimized = performance.now();
const timeOptimized = endOptimized - startOptimized;
console.log(`\nResults:`);
console.log(`Legacy (Linear Scan): ${timeLegacy.toFixed(4)} ms`);
console.log(`Optimized (Nested Map): ${timeOptimized.toFixed(4)} ms`);
if (timeLegacy > timeOptimized) {
const factor = timeLegacy / (timeOptimized || 0.001); // avoid div by zero
console.log(`Improvement: ~${factor.toFixed(1)}x faster`);
} else {
console.log("No improvement detected (dataset might be too small or optimized code has overhead)");
}
// Verification
if (profileCache.memoryCache.has(TARGET_PUBKEY)) {
console.error("FAIL: Optimized cache did not clear the pubkey!");
process.exit(1);
} else {
console.log("PASS: Optimized cache cleared successfully.");
}
}
runBenchmark();
-66
View File
@@ -1,66 +0,0 @@
import { profileCache } from "../js/state/profileCache.js";
// Polyfill localStorage
const store = new Map();
global.localStorage = {
getItem: (key) => store.get(key) || null,
setItem: (key, value) => store.set(key, value),
removeItem: (key) => store.delete(key),
clear: () => store.clear(),
};
// Polyfill requestIdleCallback
global.requestIdleCallback = (cb) => setTimeout(() => {
cb({
timeRemaining: () => 50,
didTimeout: false
});
}, 0);
// Polyfill window (might be needed by logger or others)
if (typeof window === 'undefined') {
global.window = global;
}
function generateLargeData() {
const data = [];
// Generate roughly 2MB of JSON data
for (let i = 0; i < 20000; i++) {
data.push({ id: i, content: "x".repeat(100) });
}
return data;
}
async function runBenchmark() {
const pubkey = "0".repeat(64);
const section = "watchHistory";
const data = generateLargeData();
// Set active profile to ensure logic runs
profileCache.setActiveProfile(pubkey);
console.log("Benchmarking setProfileData with large payload...");
// Warmup
profileCache.setProfileData(pubkey, section, [{id: 1}]);
const start = performance.now();
// We call it multiple times to amplify the effect, but a single call with 2MB is enough to show blocking
profileCache.setProfileData(pubkey, section, data);
const end = performance.now();
console.log(`Execution time (Synchronous part): ${(end - start).toFixed(4)} ms`);
// Wait for idle callback
await new Promise(resolve => setTimeout(resolve, 50));
// Verify data is "stored"
const stored = localStorage.getItem(profileCache.getStorageKey(pubkey, section));
if (stored) {
console.log("Data successfully stored in localStorage (length: " + stored.length + ")");
} else {
console.log("Data NOT found in localStorage");
}
}
runBenchmark();
-50
View File
@@ -1,50 +0,0 @@
import { listVideoReactions } from '../js/nostr/reactionEvents.js';
import { RELAY_BACKGROUND_CONCURRENCY } from '../js/nostr/relayConstants.js';
const NUM_RELAYS = 20;
const DELAY_MS = 100;
const mockRelays = Array.from({ length: NUM_RELAYS }, (_, i) => `wss://mock-relay-${i}.com`);
let activeRequests = 0;
let peakRequests = 0;
const mockPool = {
list: async (relays, filters) => {
activeRequests++;
if (activeRequests > peakRequests) {
peakRequests = activeRequests;
}
// Simulate network delay
await new Promise(resolve => setTimeout(resolve, DELAY_MS));
activeRequests--;
return []; // Return empty events for now
}
};
const mockClient = {
pool: mockPool,
relays: mockRelays, // Allow these relays
ensurePool: async () => mockPool
};
async function runBenchmark() {
console.log(`Starting benchmark with ${NUM_RELAYS} relays...`);
console.log(`Expected concurrency limit (after fix): ${RELAY_BACKGROUND_CONCURRENCY}`);
const start = performance.now();
await listVideoReactions(mockClient, { id: 'test-video-id' }, { relays: mockRelays });
const end = performance.now();
console.log('---------------------------------------------------');
console.log(`Total time: ${(end - start).toFixed(2)}ms`);
console.log(`Peak concurrent requests: ${peakRequests}`);
console.log('---------------------------------------------------');
}
runBenchmark().catch(console.error);
-101
View File
@@ -1,101 +0,0 @@
import { NostrClient } from "../js/nostr/client.js";
import { pMap } from "../js/utils/asyncUtils.js";
// Mock globals for Node environment
globalThis.WebSocket = class MockWebSocket {};
globalThis.localStorage = {
getItem: () => null,
setItem: () => {},
removeItem: () => {},
clear: () => {}
};
globalThis.indexedDB = {
open: () => ({
result: {
createObjectStore: () => {},
transaction: () => ({ objectStore: () => ({ get: () => {}, put: () => {} }) })
},
onupgradeneeded: () => {},
onsuccess: () => {}
})
};
// Mock dependencies
class MockPool {
constructor() {
this.activeRequests = 0;
this.maxConcurrent = 0;
}
async list(relays, filters) {
this.activeRequests++;
if (this.activeRequests > this.maxConcurrent) {
this.maxConcurrent = this.activeRequests;
}
// Simulate network delay
await new Promise(resolve => setTimeout(resolve, 50));
this.activeRequests--;
return [];
}
async get(relays, filter) {
return null;
}
}
async function runBenchmark() {
console.log("Starting Relay Concurrency Benchmark...");
const client = new NostrClient();
const mockPool = new MockPool();
client.pool = mockPool;
// Simulate 20 relays
const relays = Array.from({ length: 20 }, (_, i) => `wss://relay${i}.example.com`);
client.relays = relays;
client.readRelays = relays;
// 1. Benchmark fetchAndCacheNip71Metadata
console.log("\nTesting fetchAndCacheNip71Metadata with 20 relays...");
mockPool.maxConcurrent = 0;
const pointerMap = new Map();
pointerMap.set("e:123", { type: "e", value: "123", relays: [] });
await client.fetchAndCacheNip71Metadata(pointerMap, ["e:123"]);
console.log(`Max concurrent requests: ${mockPool.maxConcurrent}`);
if (mockPool.maxConcurrent > 5) {
console.log("FAIL: Concurrency is unbounded!");
} else {
console.log("PASS: Concurrency is bounded.");
}
// 2. Benchmark hydrateVideoHistory
console.log("\nTesting hydrateVideoHistory with 20 relays...");
mockPool.maxConcurrent = 0;
const video = {
id: "v1",
videoRootId: "root1",
tags: [["d", "d1"]],
pubkey: "pub1"
};
// Prime local cache to force relay fetch
// We need to simulate that we found <= 1 version locally
client.allEvents.set("v1", video);
await client.hydrateVideoHistory(video);
console.log(`Max concurrent requests: ${mockPool.maxConcurrent}`);
if (mockPool.maxConcurrent > 5) {
console.log("FAIL: Concurrency is unbounded!");
} else {
console.log("PASS: Concurrency is bounded.");
}
}
runBenchmark().catch(console.error);
-71
View File
@@ -1,71 +0,0 @@
// benchmarks/render_loop_overhead_bench.js
class WatchHistoryTelemetry {
persistMetadataForVideo(video, pointerInfo) {
// Deprecated - effectively a no-op in real code
}
}
class ModalCoordinator {
constructor() {
this.watchHistoryTelemetry = new WatchHistoryTelemetry();
}
persistWatchHistoryMetadataForVideo(video, pointerInfo) {
if (this.watchHistoryTelemetry) {
this.watchHistoryTelemetry.persistMetadataForVideo(video, pointerInfo);
return;
}
}
}
class App {
constructor() {
this._modal = new ModalCoordinator();
this._coordinatorsReady = true; // Simulating initialized state
}
_initCoordinators() {
if (this._coordinatorsReady) return;
this._coordinatorsReady = true;
}
persistWatchHistoryMetadataForVideo(...args) {
this._initCoordinators();
return this._modal.persistWatchHistoryMetadataForVideo(...args);
}
}
const app = new App();
const video = { id: "test", title: "test" };
const pointerInfo = { key: "test" };
const ITERATIONS = 100_000_000;
console.log(`Benchmarking overhead for ${ITERATIONS.toLocaleString()} iterations...`);
const startWithCall = performance.now();
for (let i = 0; i < ITERATIONS; i++) {
if (
pointerInfo &&
typeof app?.persistWatchHistoryMetadataForVideo === "function"
) {
app.persistWatchHistoryMetadataForVideo(video, pointerInfo);
}
}
const endWithCall = performance.now();
const durationWithCall = endWithCall - startWithCall;
const startWithoutCall = performance.now();
for (let i = 0; i < ITERATIONS; i++) {
// Simulating the removal of the call
if (false) { // Condition becomes false or code is removed
// Removed
}
}
const endWithoutCall = performance.now();
const durationWithoutCall = endWithoutCall - startWithoutCall;
console.log(`With call: ${durationWithCall.toFixed(2)}ms`);
console.log(`Without call: ${durationWithoutCall.toFixed(2)}ms`);
console.log(`Improvement: ${(durationWithCall - durationWithoutCall).toFixed(2)}ms`);
console.log(`Relative improvement: ${(durationWithCall / durationWithoutCall).toFixed(2)}x`);
-61
View File
@@ -1,61 +0,0 @@
import { performance } from 'perf_hooks';
// Mock data
const NUM_HEADINGS = 100;
const SCROLL_EVENTS = 10000;
const headings = [];
for (let i = 0; i < NUM_HEADINGS; i++) {
headings.push({
id: `heading-${i}`,
// Mock getBoundingClientRect
getBoundingClientRect: () => ({
top: (i * 100) - currentScrollY, // Simple layout simulation
})
});
}
let currentScrollY = 0;
let activeId = "";
// The function to benchmark (copied/adapted from js/docsView.js)
const updateActiveFromScroll = () => {
const offset = 96;
let nextId = headings[0]?.id || "";
// In the real code, this runs on every scroll (throttled by rAF)
// We simulate the logic here.
for (const heading of headings) {
const top = heading.getBoundingClientRect().top - offset;
if (top <= 0) {
nextId = heading.id;
} else {
break;
}
}
if (nextId && nextId !== activeId) {
activeId = nextId;
// setActiveSection logic (simplified)
}
};
console.log(`Benchmarking legacy scroll spy logic...`);
console.log(`Headings: ${NUM_HEADINGS}`);
console.log(`Scroll events simulated: ${SCROLL_EVENTS}`);
const start = performance.now();
for (let i = 0; i < SCROLL_EVENTS; i++) {
// Simulate scrolling down
currentScrollY += 5;
updateActiveFromScroll();
}
const end = performance.now();
const duration = end - start;
console.log(`Total execution time: ${duration.toFixed(2)}ms`);
console.log(`Average time per scroll event: ${(duration / SCROLL_EVENTS).toFixed(4)}ms`);
console.log(`Note: This benchmark only measures JS execution time. Real browser performance is worse due to layout thrashing (getBoundingClientRect).`);
-52
View File
@@ -1,52 +0,0 @@
import { finalizeEvent as pureFinalize, generateSecretKey, getPublicKey, getEventHash } from "nostr-tools";
import { finalizeEvent as wasmFinalize, setNostrWasm } from "nostr-tools/wasm";
import { initNostrWasm } from "nostr-wasm";
async function main() {
const secretKey = generateSecretKey();
const pubkey = getPublicKey(secretKey);
const ITERATIONS = 1000;
const baseEvent = {
kind: 1,
created_at: Math.floor(Date.now() / 1000),
tags: [["t", "nostr"], ["t", "benchmark"]],
content: "Hello world, this is a benchmark event.",
pubkey: pubkey,
};
console.log(`Running signing benchmark with ${ITERATIONS} iterations...`);
// 1. Pure JS baseline (finalizeEvent)
{
console.log("Pure JS finalizeEvent...");
const start = performance.now();
for (let i = 0; i < ITERATIONS; i++) {
const event = { ...baseEvent, created_at: baseEvent.created_at + i };
pureFinalize(event, secretKey);
}
const end = performance.now();
const time = end - start;
console.log(`Pure JS time: ${time.toFixed(2)}ms (${(ITERATIONS/time*1000).toFixed(2)} ops/sec)`);
}
// 2. WASM
console.log("Initializing WASM...");
const wasm = await initNostrWasm();
setNostrWasm(wasm);
{
console.log("WASM finalizeEvent...");
const start = performance.now();
for (let i = 0; i < ITERATIONS; i++) {
const event = { ...baseEvent, created_at: baseEvent.created_at + i };
wasmFinalize(event, secretKey);
}
const end = performance.now();
const time = end - start;
console.log(`WASM time: ${time.toFixed(2)}ms (${(ITERATIONS/time*1000).toFixed(2)} ops/sec)`);
}
}
main().catch(console.error);
-45
View File
@@ -1,45 +0,0 @@
import { createExploreDiversitySorter } from "../js/feedEngine/sorters.js";
const NUM_ITEMS = 1000; // Large enough to see the difference between N^3 and N^2
const TAG_POOL = ["cats", "dogs", "nostr", "bitcoin", "art", "music", "news", "tech", "coding", "python", "javascript", "react", "vue", "angular", "svelte", "deno", "node", "linux", "macos", "windows"];
function randomTags() {
const count = Math.floor(Math.random() * 5) + 1;
const tags = new Set();
while (tags.size < count) {
tags.add(TAG_POOL[Math.floor(Math.random() * TAG_POOL.length)]);
}
return Array.from(tags).map(t => ["t", t]);
}
function buildItem(i) {
return {
video: {
id: `vid-${i}`,
tags: randomTags(),
rootCreatedAt: 1000 + i,
},
metadata: {
exploreScore: Math.random(),
},
};
}
async function runBenchmark() {
const items = [];
for (let i = 0; i < NUM_ITEMS; i++) {
items.push(buildItem(i));
}
const sorter = createExploreDiversitySorter({ lambda: 0.7 });
console.log(`Sorting ${NUM_ITEMS} items...`);
const start = performance.now();
await sorter(items, { addWhy: () => {} });
const end = performance.now();
console.log(`Time: ${(end - start).toFixed(2)}ms`);
}
runBenchmark();
-58
View File
@@ -1,58 +0,0 @@
import { createChronologicalSorter } from "../js/feedEngine/sorters.js";
import { performance } from "perf_hooks";
const NUM_ITEMS = 100000;
function buildItem(i) {
return {
video: {
id: `vid-${i}`,
rootCreatedAt: 1000 + i,
},
metadata: {},
};
}
async function runBenchmark() {
const items = [];
for (let i = 0; i < NUM_ITEMS; i++) {
items.push(buildItem(i));
}
// Mock context with hooks to trigger the candidate lookup loop
const context = {
hooks: {
timestamps: {
getKnownVideoPostedAt: () => null,
getKnownPostedAt: () => null,
getVideoPostedAt: () => null,
resolveVideoPostedAt: () => null,
}
}
};
const sorter = createChronologicalSorter({ direction: "desc" });
console.log(`Sorting ${NUM_ITEMS} items with hooks...`);
const iterations = 20; // Increased iterations for stability
let totalTime = 0;
// Warmup
for (let i = 0; i < 5; i++) sorter(items, context);
for (let i = 0; i < iterations; i++) {
const start = performance.now();
sorter(items, context);
const end = performance.now();
totalTime += (end - start);
if (i % 5 === 0) process.stdout.write(".");
}
const avgTime = totalTime / iterations;
console.log("\n");
console.log(`Average time per sort: ${avgTime.toFixed(2)}ms`);
}
runBenchmark();
-69
View File
@@ -1,69 +0,0 @@
import { createWatchHistoryManager } from "../js/nostr/watchHistory.js";
async function main() {
console.log("Starting WatchHistoryManager benchmark...");
// Mock dependencies
const mockSigner = {
signEvent: async (event) => {
// Simulate signing latency
await new Promise(resolve => setTimeout(resolve, 10));
return { ...event, sig: "mock-sig" };
},
nip04Encrypt: async (pubkey, plaintext) => {
await new Promise(resolve => setTimeout(resolve, 5));
return "mock-ciphertext";
}
};
const mockPool = {
publish: (relays, event) => {
return {
on: (status, cb) => {
if (status === 'ok') {
// Simulate network latency
setTimeout(cb, 50);
}
}
}
}
};
const deps = {
ensureNostrTools: async () => ({
nip04: { encrypt: async () => "encrypted" },
utils: { hexToBytes: () => new Uint8Array(32) }
}),
getCachedNostrTools: () => ({
nip04: { encrypt: async () => "encrypted" },
utils: { hexToBytes: () => new Uint8Array(32) }
}),
getActivePubkey: () => "00".repeat(32),
getSessionActor: () => null,
resolveActiveSigner: () => mockSigner,
getPool: () => mockPool,
signEventWithPrivateKey: async () => {
await new Promise(resolve => setTimeout(resolve, 10));
return { sig: "mock-sig" };
},
shouldRequestExtensionPermissions: () => false,
};
const manager = createWatchHistoryManager(deps);
// Create records for 12 months
const records = {};
for (let i = 1; i <= 12; i++) {
const month = `2023-${String(i).padStart(2, '0')}`;
records[month] = [{ type: 'e', value: `item-${i}`, watchedAt: 1672531200 + i * 100 }];
}
const start = Date.now();
await manager.publishRecords(records, { actorPubkey: "00".repeat(32) });
const duration = Date.now() - start;
console.log(`Published 12 months in ${duration}ms`);
}
main().catch(console.error);
@@ -1,100 +0,0 @@
import { Worker } from "worker_threads";
import { performance } from "perf_hooks";
const WORKER_CODE = `
const { parentPort } = require('worker_threads');
parentPort.on('message', (data) => {
parentPort.postMessage('pong');
});
`;
// Implementation of toLightweightVideo to verify performance
function toLightweightVideo(video) {
if (!video || typeof video !== "object") {
return null;
}
const lightweight = {
id: video.id,
kind: video.kind,
pubkey: video.pubkey,
nip71: video.nip71,
tags: [],
};
if (Array.isArray(video.tags)) {
for (const t of video.tags) {
if (Array.isArray(t) && t.length >= 2) {
const type = t[0];
if (type === "d" || type === "t") {
lightweight.tags.push(t);
}
}
}
}
return lightweight;
}
async function runBenchmark() {
const worker = new Worker(WORKER_CODE, { eval: true });
// Create a large list of full video objects
const listSize = 10000;
const fullVideos = [];
for (let i = 0; i < listSize; i++) {
fullVideos.push({
id: `key-${i}`,
content: "some content ".repeat(20), // ~200 chars
tags: [["t", "tag1"], ["t", "tag2"], ["p", "pubkey"], ["d", "identifier"], ["zap", "zapdata"]],
created_at: Date.now(),
pubkey: "some-pubkey",
sig: "some-sig",
kind: 30078,
nip71: { hashtags: ["tag3"], t: ["tag4"] },
relays: ["wss://relay1", "wss://relay2"],
otherBigField: "x".repeat(100),
});
}
console.log(`List size: ${listSize}`);
// Warmup
await new Promise((resolve) => {
worker.once('message', resolve);
worker.postMessage({ type: 'ping', payload: [] });
});
// Measure Full Transfer
let start = performance.now();
for (let i = 0; i < 10; i++) {
await new Promise((resolve) => {
worker.once('message', resolve);
worker.postMessage({ type: 'ping', payload: fullVideos });
});
}
let end = performance.now();
const fullTime = (end - start);
console.log(`Full Transfer (10 runs): ${fullTime.toFixed(2)}ms`);
// Measure Lightweight Transfer (using helper)
start = performance.now();
for (let i = 0; i < 10; i++) {
const lightweightVideos = fullVideos.map(toLightweightVideo);
await new Promise((resolve) => {
worker.once('message', resolve);
worker.postMessage({ type: 'ping', payload: lightweightVideos });
});
}
end = performance.now();
const lightTime = (end - start);
console.log(`Lightweight Transfer + Map Overhead (10 runs): ${lightTime.toFixed(2)}ms`);
const improvement = ((fullTime - lightTime) / fullTime) * 100;
console.log(`Improvement: ${improvement.toFixed(2)}%`);
worker.terminate();
}
runBenchmark().catch(console.error);
+30450
View File
File diff suppressed because it is too large. Load diff
-17
View File
@@ -1,17 +0,0 @@
const fs = require('fs');
const content = fs.readFileSync('file_size.log', 'utf8');
const lines = content.split('\n');
let totalExcess = 0;
let count = 0;
lines.forEach(line => {
const match = line.match(/\((\d+) lines\)/);
if (match) {
const lines = parseInt(match[1], 10);
if (lines > 1000) {
totalExcess += (lines - 1000);
count++;
}
}
});
console.log('Count:', count);
console.log('Total Excess:', totalExcess);
Loaded 100 of 1685 files, more files were not shown because too many files have changed in this diff. Show more