The cold-login REQ storm came from list subsystems subscribing/fetching across
the user's full NIP-65 relay set (often ~20) instead of the documented §17 read
cap. The fetchListIncrementally paths already route through capReadRelays, but
the live subscriptions and direct pool.list calls bypassed it:
- userBlocks.ensureBlockListSubscription: cap the live block-list subscription
- subscriptions.ensureSubscriptionListSubscription: cap the live follows-list
subscription
- hashtagPreferencesService: cap the relay set used by pool.list + fetch
capReadRelays keeps the user's own relays first and guarantees the reserved
defaults, bounded to MAX_SUBSCRIBE_RELAYS (8). Trade-off accepted: a list event
living only on a user's 9th+ relay may sync on a later pass rather than at login.
Regression: tests/user-blocks-relay-cap.test.mjs.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
File-size decomposition (no behavior change). Move the state-free TinyEventEmitter,
the decrypt-retry helpers (isTransientDecryptError, computeRetryDelay + tables),
the normalize helpers (normalizeHexPubkey, normalizeEncryptionToken), and the
subscription-list codec (serializeSubscriptionTagMatrix, parseSubscriptionPlaintext,
parseCachedSubscriptionSnapshot) into js/subscriptionsHelpers.js.
subscriptions.js: 2584 -> 2361 lines (below its original 2426 baseline;
re-recorded to 2361 to lock the reduction).
Build + full unit suite green.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Under a NIP-46 remote signer, every decrypt is a PUBLISHED relay RPC. The list
services (blocks/subscriptions/hashtags) probed all decryptor families
concurrently via Promise.any — so each list event published nip04 AND nip44 RPCs
at once. On login that flooded the relay ("rate-limited: you are noting too
much"), and the racing/queued RPCs blew past the 30s decrypt budget → the
"Decryption timed out" failures.
A NIP-04 payload carries the "?iv=" marker and can ONLY be decrypted by nip04;
otherwise it's nip44. The other family can never succeed on a given ciphertext,
so racing it is pure waste (and a wasted published RPC under NIP-46). Restrict
the decrypt probe to the family matching the ciphertext format (fall back to all
if the format is somehow unknown). Halves cold-load RPCs and stops the racing
publishes that tripped the rate limit; benefits all signer types.
Spec correction: the hashtag "retries decryptors when hinted scheme fails" test
stubbed an impossible scenario (the same ciphertext decrypting under nip04 after
nip44 failed). Rewritten to assert the real contract — the ciphertext format
routes decryption and overrides a misleading `encrypted` tag, invoking only the
matching decryptor.
test_integrity_note:
change_type: ["spec_correction"]
scenarios:
- id: SCN-decrypt-format-routing
given: "a nip04-format ciphertext (?iv=) tagged misleadingly as nip44"
when: "a list service decrypts it"
then: "only the nip04 decryptor is invoked; nip44 is never attempted"
anti_cheat_rationale:
prevents: ["over-mocking internal logic", "asserting impossible cross-family decryption"]
relaxation:
did_relax_any_assertion: false
Co-Authored-By: Claude Opus 4.8 <[email protected]>
A raw window.nostr probe in the user's browser (getPublicKey → nip04.encrypt
→ nip04.decrypt, bypassing ALL of bitvid) HUNG forever, with the extension's
own console showing `ObjectMultiplex - orphaned data for "background-liveness"`
+ `malformed chunk` + listener-leak warnings. That is the NIP-07 extension's
content-script ↔ background-service-worker channel being dead (classic MV3
worker death) — an extension/environment failure, not a bitvid bug. No client
change can force an unresponsive signer to answer; the fix is to reload/unlock
the extension.
What bitvid CAN do is stop failing silently. Add js/utils/signerHealthNotice.js:
after a few consecutive list-decrypt timeouts while the signer reports itself
present, emit ONE rate-limited, actionable user notice ("your signer isn't
responding to decryption — unlock/reload the extension and refresh") instead of
the silent 15-minute retry loop. Reset on any decrypt success so a transient
blip doesn't warn. Wired into blocks, hashtags, and subscriptions.
Also documents the definitive root-cause + all resilience increments in
KNOWN_BUGS #0. Test: tests/signer-health-notice.test.mjs (threshold, rate-limit,
success-reset). Full unit suite green.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Real-env console: signerStatus "present" but every block/hashtag decrypt
timed out at the 15s service budget, forever, retrying every ~15s for
15+ minutes — lists never loaded.
Reproduced deterministically with the channel-sim harness (latency
override): with per-call latency BELOW the service budget all lists load;
the instant it crosses the budget every list fails (NO) and retries
forever. A nip-07 decrypt is a postMessage round-trip to the extension's
single-threaded, possibly-sleeping service worker — under cold-login load
real extensions take 15-25s to answer even though the crypto is instant.
The earlier 6s per-call timeout was killing those in-progress decrypts.
Raise the budget across blocks/hashtags/subscriptions so a slow-but-
responsive signer completes instead of timing out:
- service-level DECRYPT_TIMEOUT_MS 15s → 30s
- per-call nip-07 decrypt timeout 6s → 25s interactive / ~10-12s background
- backoff cap 30s → 60s so a persistently dead signer settles into an
occasional heartbeat instead of pinning the extension every ~15s
Harness: at 16s/call (previously NO across the board) blocks +
subscriptions + hashtags now all decrypt (0 timeouts); healthy mode
unchanged (signer-ready ~1.2s). Full unit suite green. KNOWN_BUGS #0.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Two redundancies were inflating the login REQ burst (kind 30000=19 storm in
the real-env console — the cause of the slow hashtags, KNOWN_BUGS #0),
isolated with the channel-sim harness's REQ-signature + timing capture:
1. Duplicate trigger: ProfileModalController.handleAuthLogin called
subscriptions.loadSubscriptions() AND userBlocks.loadBlocks() DIRECTLY at
login, on top of the auth coordinator's gated list loads — a second,
ungated relay round-trip for each. Route them through ensureLoaded()
instead so they coalesce with the coordinator's in-flight load. Harness:
subscriptions updateFromRelays now fires ONCE at login instead of 3×
(≈3× fewer kind-30000 fetches in prod, where each is multiplied by the
relay set).
2. Fetch/decrypt coupling: a decrypt retry re-ran the full relay fetch even
though only signer availability had changed. Cache the fetched ciphertext
(_pendingDecryptEvent) and have scheduleDecryptRetry re-decrypt it via a
cachedDecryptEvent fast-path in updateFromRelays — no redundant relay
query per retry.
Validated: MODE=overload (channel drops) still decrypts blocks +
subscriptions + hashtags (DMs OK); full unit suite green; subscription loop
+ aggregation tests still pass.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The live subscription-list subscription (kind 30000) delivers our OWN list
event back through handleSubscriptionListEvent, which called updateFromRelays
on EVERY event. updateFromRelays re-fetches the list, the relay echoes the
same event through the live sub, and the handler fires again — a
self-sustaining loop that floods the cold-login burst with redundant
kind-30000 REQs (the kind 30000=19 storm in the real-env console,
KNOWN_BUGS #0). Same shape as the contact-list emit→fetch→emit loop fixed
earlier.
Add an emit-on-change guard: skip the refresh when the event is one we've
already acted on (by id — matters during the pre-decrypt login window where
subsEventCreatedAt isn't set yet) or is not newer than what we've applied.
A genuinely newer list event still refreshes exactly once.
Also extend the channel-sim harness to capture per-filter REQ signatures and
kind-30000 REQ timing, which is how this loop was isolated.
Test: tests/subscriptions-manager.test.mjs — echo of the same event id does
not re-refresh; an older event is ignored; a newer event refreshes once.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Built a deterministic NIP-07 channel-simulation harness
(scripts/perf/nip07-channel-sim.mjs) that reproduces the real cold-login
failure locally with a fake extension whose message channel drops under
concurrent load (MODE=overload), recovers (MODE=slow), or stays dead. It
seeds a real encrypted block list and reports whether it decrypts.
The harness pinned the actual root cause that the relay/breaker/timeout
work hadn't fixed: when the channel briefly drops during the login burst,
the per-scheme decrypt probe (Promise.any) rejects with
"message channel closed" — and every list service SWALLOWED that and
treated it as "the user has no blocks / no subs / no hashtags", or routed
it to a give-up branch. Either way: no retry, so the list stayed empty
until a full page refresh, even though the channel recovered seconds later.
Fix: classify transient failures (channel-death messages, breaker fast-fail
code, our own decrypt-timeout) and route them to the existing keep-stale +
scheduleDecryptRetry path instead of swallowing/abandoning:
- userBlocks: re-throw a transient sub-error out of the Promise.any probe
(was silently returning [] = "no blocks"); route it (and channel/breaker
errors) through the retry branch.
- hashtagPreferencesService + subscriptions: broaden the retry condition
from the timeout code only to any transient error, recursing into the
nested Promise.any AggregateError causes.
Harness verdict (MODE=overload, 16 channel drops): block list
BLOCK LIST DECRYPTED NO -> YES after the fix; signer-ready ~1.9s.
Adds getAppState().lists observability to the test harness. Full unit
suite green. KNOWN_BUGS #0.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Real-env console (2026-06-15) showed [signer-ready] taking ~48s and every
list decrypt timing out before AND after it. Root mechanism: a priority
inversion on the 2-slot NIP-07 request queue.
- List decrypts run at NIP07_PRIORITY.HIGH; the readiness gate's
permission/enable handshake ran at NORMAL. So a flood of HIGH-priority
decrypts jumped ahead of the gate and starved it — the channel never got
warmed, so the decrypts kept failing. Fix: add NIP07_PRIORITY.CRITICAL
(above HIGH) and run the permission/enable handshake at CRITICAL so it
always wins queue ordering.
- Login-time decrypt timeouts were 12-15s even though a healthy
nip04/nip44 decrypt is local crypto (~1s); the long window was only to
tolerate a first-grant prompt, which the gate already pre-grants. A 15s
timeout just monopolizes one of the two queue slots on a doomed call,
blocking the gate for 15s at a time. Cut blocks/hashtags/subscriptions
login decrypt timeouts to 6s so slots cycle ~2.5x faster (and the circuit
breaker trips in ~18s instead of ~45s). Service-level 15s wrappers stay
as a backstop (now strictly longer than the nip07 layer).
Part of KNOWN_BUGS #0 ("cut the burst, then add a safety net"). Full unit
suite green.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Real-env console shows the encrypted lists are FETCHED but DECRYPTION fails on
cold login: hashtags time out at 6s, blocks report signerStatus 'missing', DMs
throw 'helpers unavailable'. Root cause: a relay-connection storm to the user's
~20 dead NIP-65 relays (subsystems use uncapped relayManager read+write) starves
the nip-07 extension. Bumping the decrypt timeout to 15s (matching blocks) is a
partial mitigation for hashtags/subscriptions. The proper fix (bound reads to a
liveness-ranked healthy core incl. defaults + DM decrypt retry) is documented in
KNOWN_BUGS #0 — a first capReadRelays attempt was reverted because capping block
reads broke the security-critical user-blocks integration test.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The two feed-driving list subscriptions (kind-30000 follows, kind-30015/30005
hashtag prefs) now go through the L1 SubscriptionManager instead of the global
relaySubscriptionService singleton: centralized dedup + reconnect re-issue (the
lists self-heal after a relay drop). Handles are stored and closed on teardown;
relaySubscriptionService is retained as the manager-less fallback.
Verified: subscriptions-manager/-feed + hashtag-preferences suites pass (32);
flaky multi-relay harness still all-PASS; pool-access lint clean.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
- `handleVisibility` only scheduled intervals but didn't trigger an immediate refresh when returning to the tab.
- This caused `tests/unit/services/exploreDataService.test.mjs` to fail (assertion `0 !== 1`).
- Added explicit calls to `refreshWatchHistoryTagCounts` and `refreshTagIdf` when `document.hidden` becomes false.
Co-authored-by: PR0M3TH3AN <[email protected]>
The NIP-07 adapter uses type "nip07" but all permission pre-granting
checks only matched type "extension", causing NIP-07 browser extension
users to fail decryption for block/mute lists, subscriptions, DMs, and
watch history. Hashtag preferences appeared to work because their read
path had a separate fix, masking the systemic issue.
Changes:
- shouldRequestExtensionPermissions() now matches both "extension" and
"nip07" signer types (central fix affecting DM signals, comments,
publish helpers, and watch history fetch)
- buildDmDecryptContext() pre-grants extension permissions for NIP-07
- userBlocks.js: remove capabilities-gated decryptor check that failed
with lazy NIP-07 module injection (same fix already in subscriptions.js);
always resolve signer regardless of allowPermissionPrompt; increase
fast-path relay timeout from 6s to 10s
- watchHistoryService: recognize NIP-07 type and always resolve signer
- All publish paths (blocks, subscriptions, hashtags, relays, uploads,
sharing, moderation) now pre-grant permissions for NIP-07 signers
https://claude.ai/code/session_01PipsdaTQmw7wBTJ1LmJjkk
- Remove redundant double signer readiness gate evaluation that added 5-10s
to the login path (authSessionCoordinator)
- Reduce decryption timeouts (10s→6s per scheme, 3s→1.5s retry delay,
8s→5s NIP-07 no-prompt, 45s→25s overall ensureLoaded) for faster scheme
fallback during list sync
- Fix storage tab unlock: remove overly strict capabilities check that
blocked lazy-injecting NIP-07 extensions, and add auto-unlock attempt
when profile modal opens with an active signer
- Fix subscription/hashtag decryption: always resolve signer for background
refreshes (was previously gated behind allowPermissionPrompt), and always
check window.nostr as fallback decryptor regardless of prompt mode
- Fix capabilities check in subscription decryption: use method existence
instead of dynamic capabilities getter that returns false during lazy
extension injection
- Wrap Promise.any decryptor calls in Promise.resolve().then() to catch
synchronous throws from NIP-07 adapter methods when extension modules
haven't loaded yet
- Make watch-history feed registration race-safe with pre-check and
try/catch for concurrent registration attempts
- Improve NIP-07 extension detection with DOMContentLoaded listener for
faster detection of extensions that inject during page load
https://claude.ai/code/session_0174XTB5ADJ9cMBSucqL9Wer
Four issues caused login to be extremely slow and lists to fail syncing:
1. Subscription list decryption excluded NIP-04 when NIP-44 was available,
preventing decryption of NIP-04-encrypted lists entirely. Removed the
allowNip04 gate so NIP-04 is always a fallback (matching userBlocks.js
and hashtagPreferencesService.js which already worked correctly).
2. When the NIP-07 extension permission pre-grant failed (common on page
refresh when the extension injects slowly), all list services were called
with allowPermissionPrompt:false and could never recover. Now list
services are allowed to prompt for permissions themselves when the
pre-grant fails, avoiding 6-10 second retry delays.
3. The profile modal storage tab was never refreshed on login or logout,
so it stayed locked even after authentication succeeded. Added
populateStoragePane() calls to handleAuthLogin and handleAuthLogout.
4. The watch-history feed engine.run() call was missing await, so rejected
promises from unregistered feeds escaped the try/catch and propagated
as unhandled errors instead of falling through to the service fallback.
https://claude.ai/code/session_013puYzbf1cC7a59HLcGQUdJ
The NIP-07 login flow was extremely slow because list decryption (blocks,
subscriptions, hashtag preferences) started before the signer was ready.
On page refresh, all three services failed with "permission-required" and
scheduled 3-second retry delays, adding 6-10 seconds to every login.
Key fixes:
- List loading now waits for the permission pre-grant to complete before
attempting decryption, ensuring the signer is available on first try
- Extension wait timeout increased from 3s to 5s for slower extensions
- Decrypt timeouts reduced from 20s to 10s (signer is guaranteed ready)
- Auto-login now forwards authType/providerId from saved profiles
- Storage tab unlock falls back to ensureActiveSignerForPubkey when the
global signer is not yet registered
- Watch history feed registration adds defensive re-registration and
try/catch fallback to prevent "Feed not registered" errors
- Decryption scheme cache TTL extended from 10 minutes to 2 hours
- ensureExtensionPermissions waits for extension injection when there
are outstanding permissions to request (was returning immediately)
https://claude.ai/code/session_01T4wwPu7m5qp1Nxx3H2v57j
Three performance bottlenecks were causing slow list loading after NIP-07 login:
1. Each list service (blocks, subscriptions, hashtag preferences) maintained
its own isolated decryptionSuccessCache. When blocks decrypted with nip44_v2,
subscriptions and hashtag preferences had no way to know — they each had to
independently probe all schemes through the NIP-07 extension queue, tripling
the number of extension calls during the critical login path.
2. List loading was gated on both relaysReadyPromise AND permissionPromise in
the coordinator. Since the NIP-07 extension already grants decrypt permissions
during loginWithExtension(), the redundant pre-grant was adding 2-5 seconds
of unnecessary latency before any list fetching could begin.
3. The DECRYPT_RETRY_DELAY_MS was 10 seconds across all services. When a decrypt
attempt failed transiently (extension still initializing), the retry waited
far too long before trying again.
Changes:
- Create shared decryptionSchemeCache module used by all three list services
- Add fast-path: when shared cache knows the correct scheme, try ONLY that
scheme before falling back to parallel Promise.any() probe
- Remove permissionPromise from the list-loading gate in authSessionCoordinator
- Reduce DECRYPT_RETRY_DELAY_MS from 10s to 3s in all three services
- Clear shared cache on logout
https://claude.ai/code/session_013QrGMnxtMTsGTPqaP35jMj
When hashtag preferences decryption fails with "permission-required" during
background loading (allowPermissionPrompt=false), the service silently returned
without setting loaded state, scheduling a retry, or emitting a change event.
This left the UI stuck showing empty hashtag lists. The subscriptions module
already handled this case correctly.
Also fix error scheme mapping in Promise.any() catch blocks for both hashtag
preferences and subscriptions — the index into the original order array was
wrong after filtering out deduplicated scheme families.
https://claude.ai/code/session_016xYxLQ1U2XVVY5rFuCRKfc
Login with NIP-07 extensions was extremely slow because list loading
(blocks, subscriptions, hashtag preferences) happened sequentially and
each list's decryption tried encryption schemes one at a time.
Changes:
- Load blocks, subscriptions, and hashtags in parallel instead of
sequentially (blocks no longer blocks other lists from starting)
- Use Promise.any() to try all decryption schemes simultaneously
instead of trying nip44_v2 → nip44 → nip04 one-by-one
- Deduplicate same-family schemes (nip44/nip44_v2) to avoid redundant
extension calls
- Increase NIP-07 request queue concurrency from 3 to 5 to support
parallel decrypt operations during login
- Raise no-prompt decrypt timeout from 3s/5s to 8s since extensions
that have already granted permission still need time to decrypt
- Increase feed sync timeout from 8s to 12s to give lists more time
to settle before rendering unfiltered content
https://claude.ai/code/session_01V6m1gTecsppPrBHQnUyPnF
The subscription list was failing to load correctly during login because:
1. Permission-required decryption errors silently returned without setting
loaded=true or scheduling a retry, leaving subscriptions permanently
stuck in a loading state.
2. Generic decryption failures (all schemes exhausted) had no retry path,
unlike timeout errors which already scheduled retries.
3. The ensureLoaded timeout (30s) was too tight for the worst-case
pipeline of relay fetch (12s) + decryption (20s) = 32s, causing
premature timeouts that left loaded=false.
4. The loadingPromise was cleared in a finally block even when the loader
was still in-flight after a timeout, allowing duplicate concurrent
loads that could race against each other.
Changes:
- Handle permission-required errors like timeout errors: set loaded=true,
schedule a retry (with allowPermissionPrompt: true), emit change event
- Schedule decrypt retries for all decrypt failure paths
- Increase ensureLoaded timeout from 30s to 45s
- Mark loaded=true on ensureLoaded timeout so the UI can proceed
- Clean up loadingPromise via loader.then() instead of finally block,
preventing duplicate loads when the first is still in-flight
https://claude.ai/code/session_015MhPiSWNa7dm4UaRuq5x4U
Increases NIP-07 concurrency to 3 and lowers priority for non-critical decryption tasks to prevent blocklist timeouts. Increases relay max listeners to 200 to mitigate warnings.
Co-authored-by: PR0M3TH3AN <[email protected]>
The login-to-feed pipeline had several issues causing lists (blocks,
subscriptions, hashtag preferences) to load too slowly or not reflect
in the feed:
1. All list loading was chained after profileStatePromise, adding
2.5-6s of unnecessary delay. Lists only need relay URLs, not profile
data. Now they chain off relaysReadyPromise instead, loading in
parallel with profile fetching.
2. The video grid refreshed immediately without waiting for any lists,
so the feed briefly displayed unfiltered content. Now the grid refresh
waits (up to 8s) for critical feed-filtering lists to settle.
3. Hashtag preferences weren't cached into the snapshot before the feed
rendered, so interests/disinterests were empty on first paint.
4. Subscription ensureLoaded had a 6s timeout that was too aggressive
for relay fetch (12s) + decryption (3-12s), causing silent failures.
Increased to 30s.
5. Subscription relay fetches for user and session actor ran sequentially.
Now they run in parallel via Promise.all.
https://claude.ai/code/session_01UKmTWb83AnQPRVQo3bKZqm
- Updates `js/userBlocks.js` to write block entries to a standard Kind 10000 Mute List event.
- Implements a hybrid storage model: unencrypted public mutes are stored in `p` tags, while private blocks are stored in the encrypted event content.
- Maintains read compatibility with legacy `d=user-blocks` lists (both Kind 10000 and Kind 30002) by merging and prioritizing the newest event timestamp.
- Updates local caching to persist both public mutes and private blocks, preventing data loss or "zombie" entries on reload.
- Updates `js/subscriptions.js` to correctly detect NIP-44 capabilities on `Nip07Adapter` signers, fixing a regression where NIP-04 fallback was skipped.
- Updates `js/nostrEventSchemas.js` to mark `USER_BLOCK_LIST` as legacy and define `MUTE_LIST`.
- Updates unit tests to verify the new multi-kind fetching and merged publishing logic.
Co-authored-by: PR0M3TH3AN <[email protected]>
- Reduced decryption timeout for user lists from 90s to 15s to fail faster.
- Optimized NIP-07 decryption calls to avoid retries and long timeouts during critical loading paths.
- Fixed a bug where `loginWithExtension` triggered `getPublicKey` twice, doubling the user interaction time.
- Added a 10s grace period for relay health warnings to prevent false positives on startup.
- Verified `deferBlocks` logic in `authService` prevents blocking the login UI on slow list decryption.
Co-authored-by: PR0M3TH3AN <[email protected]>
- Fixes issue where user lists (subscriptions, blocks) fail to load if local cache is stale but metadata store is fresh.
- Updates `js/userBlocks.js` and `js/subscriptions.js` to persist `createdAt` timestamp and use it for incremental fetch `since` parameter.
- Optimizes `js/nostr/client.js` `ensureActiveSignerForPubkey` to reduce redundant `getPublicKey` calls to NIP-07 extension by 50%.
- Adds `preloadedPubkey` support to `createNip07Adapter`.
- Adds reproduction test for NIP-07 concurrency issue.
Co-authored-by: PR0M3TH3AN <[email protected]>
- Implements a priority queue for NIP-07 (browser extension) requests to allow critical tasks (blocklist/prefs decryption) to jump the line.
- Updates NIP-46 client and adapters to support priority overrides for encryption/decryption.
- Updates UserBlockListManager, SubscriptionsManager, and HashtagPreferencesService to request HIGH priority when decrypting data.
- Ensures strict serialization for NIP-07 to prevent extension errors while optimizing latency for critical data.
Co-authored-by: PR0M3TH3AN <[email protected]>
- In `js/subscriptions.js`, updated `decryptSubscriptionEvent` to register decryptors from `window.nostr` (NIP-04 and NIP-44 v2) alongside the active signer.
- Refactored permission request logic to only trigger `requestDefaultExtensionPermissions` if neither the active signer nor `window.nostr` supports the required encryption schemes.
- Implemented "fail-fast" behavior to return an error immediately if no suitable decryptor is available, avoiding unnecessary timeouts.
- Removed redundant permission checks from `updateFromRelays` as the logic is now centralized in `decryptSubscriptionEvent`.
Co-authored-by: PR0M3TH3AN <[email protected]>