You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This commit was created on GitHub.com and signed with GitHub’s verified signature.
Added
Phase 24.3 — rotation warnings + key recovery UX. Switching,
generating, or importing a primary identity now confirms with the
previously-silent consequences (entity-sync blobs encrypted to the
old key become unreadable; publish stamps stay per-identity; derived
entity keys can't be re-derived from the new primary). Options gains
a Restore entity keys action (the restoreDerivedKeys surface —
keystore loss recovers derived-era entities' original pubkeys), and
published entity profiles open with an honest self-description line
("An X-Ray subject record maintained by — a research dossier
…, not the subject posting") so generic NOSTR clients render an
honestly-labeled record. Completes Phase 24.
Phase 24.2 — creator binding on the wire. Entity identities are
now provably the creator's: a kind-30069 OwnedKeys manifest
(primary-signed, replaceable — revocation is republish-without-the-
key) lists every owned entity pubkey and publishes automatically with
entity batches (fingerprint-gated); entity-signed events (kind 0 /
30067) gain a ['p', creator, '', 'creator'] backlink and a NIP-26-format delegation tag minted by the primary (Local mode)
— the strongest self-contained proof, verified by X-Ray itself with
fail-closed condition checking (kind whitelist + created_at window).
The portal badges entities ✓ creator-bound (manifest + token) or ◐ partially bound (one of the two). Wire: new kind 30069 +
two additive tags on entity events — existing consumers unaffected. docs/NIP_DRAFT.md §"Kind 30069" + §"creator binding".
Phase 24.1 — durable entity keys (deterministic derivation). New
entity keypairs are now derived from the primary identity
(HKDF-SHA256, domain xray-entity-v1, info = the entity id, mod n) —
same primary + same entity ⇒ the same pubkey, forever. A lost or
reset keystore is recoverable: EntityModel.restoreDerivedKeys()
re-derives every missing owned key (derived-era entities get their
original pubkey back; legacy random keys re-derive to a new one,
reported honestly). Existing random keys stay valid — the stored key
always wins, no forced migration. The full NIP-26 tradeoff analysis
and the layered design (derivation + owned-keys manifest + NIP-26
token + honest kind-0) live in docs/ENTITY_IDENTITY_DESIGN.md; the
binding wire (kind 30069 + delegation tags) is the next slice. No
wire change in this slice.
Phase 23.2 — publish the corpus analysis. The Phase-20 corpus
synthesis (summary / opposing positions / cruxes / load-bearing claims
/ gaps) can now be published to NOSTR from the case dashboard's
"Publish brief…" button, so a stranger with only a keypair can read
the insights. Two artifacts, user-signed and cross-linked: a readable kind-30023 long-form article (readable in any NOSTR
client — the "article that shows the insights") and a structured
kind-30068 CaseBrief event (rendered richly in X-Ray). Both are
prose/data only — no fused score, no verdict (the Phase-20 firewall
carried onto the wire, guard-tested), and the reviewer-facing proposals never publish. The portal reads both back into a new Briefs tab with an inspector summary. Wire: new addressable
kind 30068 (additive; existing consumers unaffected). docs/NIP_DRAFT.md §"Kind 30068 — CaseBrief".
Phase 23.1b — per-link evidence role (citation intent). Each
outbound link in a capture can be tagged with WHY the article cites it
— evidence (the primary source relied on), mention, supports, disputes, or reviews — from the reader's Media & source dialog. A
lay-relabelled CiTO subset. This is what marks a debater's article as disputing the primary paper vs. citing it as evidence; combined
with the target's source-type, it's a visible secondary→primary
derivation edge. The portal inspector lists a capture's roled "cited
sources." Wire (additive, kind 30023): an optional 4th positional
on the existing link tag, whitelisted both directions — no new kind. docs/NIP_DRAFT.md §"link tag".
Phase 23.1 — source-type provenance (evidence classification). Any
capture can now be declared a primary-record / primary-research / reporting / analysis / reference source from the reader's 🎙
Media & source dialog — auto-suggested from scholarly identifiers
and schema.org type, confirmed by the user. Distinguishes an
originating primary source (e.g. the Proximal Origins paper) from a
write-up that cites it. Grounded in the library primary/secondary/
tertiary distinction + the epistemic-audit source taxonomy. The portal
shows a primary-source badge. Wire (additive, kind 30023): one new ['source-type', <value>] tag, whitelisted both directions, distinct
from content_format/media and from the verdict tier ladder — no
new kind. docs/NIP_DRAFT.md §"source-type".
Phase 22 — URL-first media metadata + reader transcript attach
(22.1–22.2). The URL is the episode's identity: capture a podcast
episode's page (Spotify, Apple, Substack, YouTube, a custom site) like
any page, then the reader's 🎙 Media… button declares what the URL
contains (podcast / video — user-declared, never inferred), sets the
universal podcast IDs, and attaches a pasted/uploaded transcript as a ## Transcript section upserted into the captured body (bounded
replace on re-attach; YouTube's own suffixed transcript sections are
never touched). The body change re-hashes honestly (prior version
snapshotted); metadata-only saves never touch the hash; the
speaker→claim prefill now works on any article carrying transcript_meta. Wire (additive, kind 30023): one new ['media', 'podcast'|'video'] tag, whitelisted both directions —
no new kind, existing consumers unaffected. docs/NIP_DRAFT.md
§"media tag". The Phase 21 portal import remains as the no-URL
fallback.
Phase 21 — podcast transcript import (21.1–21.3). Paste or upload a
podcast transcript (SRT, WebVTT with <v> voice tags, or Speaker:
lines) from the portal — library header or a case view — and it becomes
an ordinary archive record: a speaker-labeled markdown body, a contentType:'transcript' / platform:'podcast' article, and (when the
user supplies them) universal podcast identifiers. It joins cases via
the 20.2 picker and feeds 20.4 corpus synthesis untouched; selecting a
turn prefills "who said it" with that turn's speaker. Wire (additive,
kind 30023): new show / podcast_guid / podcast_episode_guid / feed_url / itunes_id tags with NIP-73 i co-emits
(podcast:guid:…, podcast:item:guid:…), a feed_url second-r, and
a transcript_meta (<format>:<turns>:<speakers>) manifest — bodies
live in content, and all round-trip through reconstructArticleFromEvent.
No new kind. docs/NIP_DRAFT.md §"podcast identity tags".
Phase 20 — case-first: making a case corpus usable (20.1–20.4).
Driven from the live COVID/Rootclaim corpus. Union membership — a
case includes an article that is tagged with the case entity OR
referenced by a claim; tag-only articles are now first-class sources
everywhere (the portal case view stopped reading "0 sources"), with a
processed/unprocessed distinction and an "extract claims" nudge. Add-to-case outside the reader (case-membership.js) — an "Add
sources…" picker on the portal case view and the side-panel case
detail tags archived articles into a case (local-only, republish hint
on published records) with a "✕ remove" affordance. Local case
entity graph (case-graph.js + case-graph-view.js) — the case at
center, its member articles, tagged/claimed entities, co-tag
adjacency, and the contradiction edges the dossier's knots compute;
the spokes-graph empty state routes to the case dashboard. LLM
corpus synthesis behind the new caseSynthesis flag (+ llmAssist
key, default off) — a map/reduce over the member articles that
drafts a grounded case brief (summary, positions, cruxes of
disagreement side by side, load-bearing claims, coverage gaps) plus
reviewable cross-article proposals; every quote is machine-grounded,
there is no fused score or verdict, and the brief is stored in the
precious xray-audits DB (v2 case-briefs store, export-included)
and carried in the case export. No new wire kind — proposals
materialize as ordinary 30040/30055 on accept. docs/CASE_SYNTHESIS_DESIGN.md.
Phase 19 — entity dossiers, end to end (19.2–19.8). The
provenance-pinned knowledge base: facts ride claims (additive kind-30040 fact/valid_from/valid_to/observed_at tags, band-precision ISO
dates, parseClaimEvent read-back); the computed-on-read entity-dossier.js assembler over the alias family (unknown-by-default
field table, contested-never-resolves, judgments routed to the
integrity record — no score anywhere, string-guard tested); the portal
dossier view + side-panel compact table with evidence click-through;
the reader "📇 Add fact" flow with honest-precision date inputs and an
inform-never-block conflict pre-flight; the case scope editor
(authored framing — side panel, case export, portal case header, and
per-member "dossier →" links); LLM fact extraction (kind:'fact',
default OFF, quote-grounded, "never supply a value from your own
knowledge"); and — behind the new entityCorpusPublishing flag
(default off, full disclosure) — enriched kind-0 profiles
(published-claim facts only, contested fields omitted, "per "
attribution) plus the new kind-30067 entity fact sheet (contested
fields both sides, every fact a-referencing its published claim),
with hash-gated automatic republish and a manual portal republish.
Phase 17 Part A — entity health + canonical sweep (E1+E3). The
...
This commit was created on GitHub.com and signed with GitHub’s verified signature.
Fixed
Inline person/place names no longer vanish from article captures on
sites that wrap them in popup-classed elements (e.g.
josephsmithpapers.org). Readability's unlikelyCandidates blocklist
matches the substring popup and was deleting the whole wrapper —
visible name included — leaving dangling punctuation. The extractor now
unwraps aside.popup-wrapper to its reference text before Readability
runs.
Publishing no longer fails with invalid: tag val was not a string
on pages whose JSON-LD carries a non-string articleSection (array) or inLanguage (object). The kind-30023 builder now sanitizes every tag
value to a string — flattening arrays, extracting name/@value from
schema.org objects, and dropping anything unstringifiable — so a single
odd metadata field can't make relays reject the whole event.
This commit was created on GitHub.com and signed with GitHub’s verified signature.
This release consolidates three feature lines onto main: default-to-local
signing + the consolidated Settings hub, the Phase 9a crowdsourced
URL-metadata data model + NIP draft, and the Phase 9 social-media identity
layer.
Added
Signing method is now an explicit user preference
(preferences.signing_method, values 'local' | 'nip07' | 'nsecbunker')
with local signing as the default. Replaces the auto-detect-NIP-07-
then-NSecBunker probe at content-script init.
Signer façade (src/shared/signer.js) — single sign call site that
dispatches to Crypto.signEvent (local), the injected NIP-07 client, or NSecBunkerClient.signEvent. The whole publish path goes through it.
Local primary identity in a dedicated local_primary_identity
storage key with its own Storage.primaryIdentity namespace (Generate /
Import nsec / Show nsec / Reset). Kept distinct from keypair_registry
so an entity-key export never leaks the user's own nsec.
Idempotent signing-method migration — existing profiles land on signing_method=local, signing_method_configured=false so a one-time
setup banner fires on the next Settings open.
Signing tab redesign in Options — radios for Local / NIP-07 /
NSecBunker, per-method panels, NSecBunker Test connection button,
and an always-visible Active method: … line at the top of the tab.
Per-relay flags surfaced in Options → Relays — URL / read / write
/ enabled rows replace the textarea. The structured shape persists as preferences.relays; preferences.default_relays (URL list) is
auto-synced to the enabled+writable subset for back-compat with every
reader (nostr-client.js, background, reader, sidepanel).
CONFIG override controls in Options → Advanced — article cache
enabled/budget, min content length, max claim length. Applied via a
new applyConfigOverrides() at content-script init.
FAB panel header gained a settings cog and entity-browser icon.
Clicking either messages the SW (xray:openSettings, xray:openEntities), so the FAB itself is a settings entry point.
FAB relay picker now reads from Storage.relays.get(), filtered
to enabled+writable relays. Settings → Relays drives what the FAB
offers; static CONFIG.relays no longer leaks through.
Quick-actions bar at the top of the Options page (Toggle Capture
/ Entity Browser / Capture tips), so Options is a complete settings
hub on its own.
Storage.relays.set() — structured-shape writer; round-trips
through Storage.relays.get() and keeps default_relays in sync.
12 new tests in tests/signer.test.mjs covering all three
signing branches, primaryIdentity round-trip, the signing_method
migration, and Storage.relays.set().
Phase 9a — crowdsourced URL-metadata data model (src/shared/metadata/).
The wire-format foundation for annotations, fact-checks, ratings,
topic-scoped trust, and helpfulness votes, conforming to docs/NIP_DRAFT.md:
NIP-73-conformant URL normalizer (url-normalizer.js); Utils.normalizeUrl is now a thin wrapper over it.
W3C Web Annotation selector capture + a confidence-scored
resolution cascade (anchor-capture.js / anchor-resolver.js).
Builders for kinds 30050 (Annotation), 30051 (FactCheck), 30052
(Rating), 30053 (TopicTrust), 9803 (HelpfulnessVote) + the kind
30023 responds-to tag extension (builders.js).
First-order trust graph (kind 3 + kind 30053) and a v1
binary-trust ranker (trust-graph.js / ranker.js).
Feature flags gating the not-yet-surfaced kinds; IDB v2 metadata
stores in archive-cache.js. Data model only — no UI yet.
docs/NIP_DRAFT.md — the authoritative wire-format spec
("Web Content Annotations, Fact-Checks, and Topic Trust").
Phase 9 — social-media identity layer (src/shared/identity/).
Turns a captured author (commenter or post author) into a stable,
cross-capture identity and lets the user collapse cross-platform
accounts into one canonical person:
Deterministic accountPubkey per <platform>:<stableId>
(platform-account.js) — an identifier-only key that never signs;
a Storage.platformAccounts registry in chrome.storage.
Published comments and post authors now carry a stable p-tag
identity (kind 30041 / 30023), via recordAccount + the existing buildCommentEvent socket and a new optional authorAccountPubkey
on buildArticleEvent.
YouTube comment capture — parses the InnerTube /youtubei/v1/next responses (reusing the Phase 8a api-interceptor;
both legacy commentThreadRenderer and modern commentEntityPayload
shapes), keyed on the commenter's channelId. Scroll the comments
into view before capturing (see docs/CAPTURE_GUIDE.md).
Manual account↔entity linking in the sidepanel Entity Browser,
with alias-chain-aware resolveAccountToEntity. v1 is
local-index-only (no kind 32126 relay publish) and manual-link-only.
Combined test suite: 519 passing (up from 223 at v0.4.0), adding tests/metadata-*, tests/identity-*, tests/youtube-comments, and tests/platform-account-event.
Changed
Toolbar-icon click no longer opens a popup. The icon now toggles
the FAB capture panel on the active tab via chrome.action.onClicked,
mirroring the keyboard shortcut. On non-injectable pages
(chrome://, file://, extension pages) it falls back to opening
the Options page so the click is never a silent no-op.
Right-click menu on the toolbar icon expanded to host the
jump-actions the popup used to provide: Toggle Capture / Entity
Browser / Settings… / View Keypair Registry / Export Keypair
Registry / Capture tips.
content/ui.js publish flow simplified — Local + NIP-07 collapse
into a single Signer.signEvent path; NSecBunker keeps its
per-publication-keypair flow.
Background xray:sign / xray:getPubkey route through Signer
instead of NIP07Client directly. The reader-page publish flow
respects the user's chosen method.
README, ROADMAP, SMOKE_TEST, CONTRIBUTING updated to reflect the
current signing model, the popup removal, and the consolidated
Settings hub. Test counts and bundle counts brought current.
Removed
Toolbar popup (src/popup/) — files deleted, popup bundle
dropped from esbuild.config.mjs, action.default_popup removed
from manifest.json. The popup duplicated state already shown in
the FAB header and split jump-actions across multiple surfaces.
Forward-looking design plans under docs/plans/ — the five
tentative documents (NIP-COURT-OF-PUBLIC-OPINION, evidentiary-standards, protocol-adoption-guide, trust-reputation-system, ui-ux-design) are out of scope for
X-Ray's current phase and have been removed. The Phase 9 row that
referenced them is gone from the roadmap.
Phase 8d — Facebook handler (src/shared/platforms/facebook.js).
Third and final hard-tier platform; Phase 8 now complete.
Recognizes all eleven FB post URL shapes: /<user>/posts/<id>, /<user>/videos/<id>, /<user>/photos/<set>/<id>, /watch/?v=, /reel/<id>, /permalink.php, /story.php, /share/p|v|r/<code>/, /photo/?fbid=, and /groups/<g>/posts|permalink/<id>/. Four
parallel extraction paths with explicit provenance:
GraphQL response interception via the Phase 8a api-hook
buffer — scored recursive walk picks the focal story by
longest message.text + bonuses for feedback/attachments.
Recursive findCreationTime fishes the timestamp out of comet_sections.timestamp.story.creation_time and similar
nestings; owner accepted alongside actors[0] for the author.
Open Graph + Twitter Card meta tags — parser handles the "<Author>: \"<body>\"" and "<Author> wrote on Facebook: <body>"
shapes plus optional leading engagement-count prefixes.
DOM scrape scoped to [role="dialog"] (post-detail modal)
/ [role="article"][aria-posinset] (feed unit) via pickFocalScope — every scraper (author, body, verified flag,
post date, images) bounded so sibling posts visible behind the
modal never leak into the capture.
HTML snapshot + screenshot — always-on evidence layer.
Post date extracted from absolute aria-label dates
("Monday, April 21, 2026 at 9:30 PM"), <time datetime>,
or short-text relative-time tokens ("12h", "3d", "45m"). Event
builder emits post_id, post_kind, author_handle, and platform_account: facebook:<handle> tags. Reader renders a
Facebook-specific header with author block, engagement chips, an extractedFrom provenance chip, and inline screenshot evidence.
Image gallery scraped from the modal-scoped <img> tags
(fbcdn.net host filter, 200px size floor, signing-token-preserving
dedup) and embedded in the markdown body. 35 new tests (URL
grammar × 11 shapes, og:description variants, GraphQL walker
across nested envelopes, image extractor with srcset/data-src/
avatar-filter coverage, relative-time + absolute-date parsers, findCreationTime against nested-story shapes).
Capture quality hints + user documentation.
docs/CAPTURE_GUIDE.md — user-facing walkthrough covering the
correct way to capture from Instagram, Facebook, TikTok, plus
brief mentions of the easy-tier platforms. Includes a
symptom → cause → fix table for common bad-capture cases.
In-reader hint banner — amber dashed-border <details> above
the article metadata when extractedFrom === 'none', body text
is short, media extraction missed on a photo post, or the
author handle is empty. Platform-specific retry instructions,
linked back to the capture guide.
Platform-aware FAB tooltip — hovering the FAB on Instagram,
Facebook, or TikTok shows a one-line reminder of the right
URL shape to capture from, before the user clicks.
Popup adds a "Capture tips (Instagram, Facebook, …)" button
that opens the guide on GitHub.
Instagram post-item inline user fallback — when og-description,
URL path, and description-pattern author extraction all come up
empty (post-detail pages loaded via /p/<shortcode>/ without a
username prefix), extractMediaFromGraphQL now also surfaces the
post item's embedded user object. Used as the fourth
handle-resolution fallback; also feeds profile enrichment when
no dedicated data.user response was in the buffer.
Fixed
Instagram captures were rejected by relays with "invalid: tag val was not a string" because user.pk from the
REST /api/v1/media/…/info/ response is a number. Fixed at two
layers: normalizeUserShape now stringifies at the normalization
boundary, and the event-builder author_id emission is
defensively String()-wrapped. Regression test asserts every
tag value in a built article event is typeof 'string'.
Facebook captures produced "null (@handle)" bylines when the
author name came from a path other than og-meta/GraphQL.
Defensive guard: author ? "author (@handle)" : "@handle".
Facebook capture misattribution chips — the author/extraction
source was re-inferred at the end instead of being tracked at
assignment, defaulting to og-meta even when og-meta contributed
nothing. Now recorded as the extraction runs.
Facebook picked the wrong story from multi-story GraphQL
responses (first-quack walker grabbed a sibling or comment node).
Replaced with a candidate-scoring pass: longest message.text
wins, with bonuses for feedback metadata and attachments.
Facebook image scraper pulled feed posts from behind the modal.
Scoped to the focal post via pickFocalScope (dialog → aria-posinset
article → article → document).
Facebook DOM body scraper swallowed an adjacent profile-feed
post when the focal post's GraphQL story had no message.text
and the scraper walked the whole document for longest <div dir="auto">. Same pickFocalScope scoping applied.
Facebook screenshot captured a 680×80 sliver because pickScreenshotTarget walked up from a thumbnail-strip image
that passed the 200px floor. Floor raised to 400px; falls back
to the whole post container when no large media qualifies.
Facebook title with embedded newlines split into two link-lines
in the reader because the 80-char truncation landed mid-paragraph. truncate now collapses whitespace and cuts at word boundaries.
Phase 8c — Instagram handler (src/shared/platforms/instagram.js).
Recognizes instagram.com/p/<id>/, /reel/<id>/, /tv/<id>/,
and the /<user>/p/<id>/ and /<user>/reel/<id>/ variants.
Capture path: Open Graph + Twitter Card meta tags as the
load-bearing data source (server-rendered, stable contract),
defensive DOM scrape for fields meta doesn't cover (post date
via <time datetime>, verified-account flag via aria-label),
and the Phase 8a evidence layer (HTML snapshot + screenshot).
Reader gets an Instagram header with author handle (verified ✓
when applicable), engagement counts, post-kind chip
(post/reel/igtv), and an extractedFrom provenance chip.
Phase 8b — TikTok handler (src/shared/platforms/tiktok.js).
First hard-tier platform shipped. Recognizes tiktok.com/@<user>/video/<id> URLs. Three-layer capture model
in production: structured extraction from __UNIVERSAL_DATA_FOR_REHYDRATION__ / SIGI_STATE / __NEXT_DATA__ (defensive across all three SSR shapes) +
bounded HTML snapshot of the video container + element-cropped
screenshot. Reader gets a video-shaped header with author chip
(verified ✓ when applicable), play/like/comment/share counts,
music attribution, an sourceShape provenance chip, and an
inline collapsible "📸 Screenshot evidence" panel.
Phase 8a — Anti-obfuscation infrastructure for upcoming
hard-tier platform handlers (FB/IG/TikTok). Three standalone
modules, all tested, none wired to a platform yet:
src/shared/html-snapshot.js — bounded sanitized outerHTML
extractor (strips <script>, on* handlers, data: URLs,
iframes; truncates to a byte cap with a marker; SHA-256 helper).
src/shared/screenshot.js + background-side handler — element-
cropped screenshots via chrome.tabs.captureVisibleTab + an
OffscreenCanvas crop. Pure crop-math helper covered by unit
tests across DPR 1/1.5/2 + viewport-clamp edges.
src/page/api-interceptor.js — MAIN-world fetch + XHR hook
that captures responses to URL/header-pattern matches and posts
them back to the content script. Bundled to dist/api-interceptor.bundle.js for on-demand injection via chrome.scripting.executeScript. Pattern matcher extracted to src/shared/api-pattern.js for unit-testability.
Article-shape evidence layer — new optional article.evidence
field carries { screenshot, screenshotHash, screenshotUrl, htmlSnapshot, htmlSnapshotHash }. Event builder surfaces the
hashes/URL as event tags (screenshot_sha256, screenshot_url, html_snapshot_sha256); archive-reader inverse rehydrates them.
Bodies stay in event content; tags carry verifiable refs.
30 new tests (html-snapshot, screenshot crop math, api-pattern) bringing total to 126 (up from 96).
Real toolbar icons — purple-on-purple X-Ray scan-lens
treatment replaces the plain placeholder X. Source SVG at icons/source.svg; rasterize with npm run icons.
Release pipeline — CHANGELOG.md, npm run version:set for
lockstep package.json/manifest.json bumps, .github/workflows/release.yml
fires on v* tag push and creates a GitHub Release with the
packaged .zip attached.
Test coverage for the wire-protocol surface — new tests for Utils.normalizeUrl, EventBuilder.buildRelayListEvent / buildEntitySyncEvent, normalizeRelayUrl, deserializeEntityFromSync's schema tolerance, and the full
userscript migration round-trip (96 tests total, up from 67).
YouTube Shorts capture — FAB now recognizes youtube.com/shorts/<id>
URLs. Captures produce metadata-rich artifacts (thumbnail, channel,
duration, view count, video id) with a SHORT chip in the reader
header. Outbound events get an is_short tag.
Userscript migration importer — Options page gains a "Migrate"
tab that ingests a JSON blob exported from the nostr-article-capture userscript's storage. Handles user_identity, entity_registry, relay_config, article_claims, evidence_links with schema normalization
(privkey/privateKey, 16-/64-char entity ids).
Native OS notifications for publish — long-running publish
flows now fire chrome.notifications so completion is visible
even when the user has tabbed away from the reader.
NIP-65 relay-list sync — Push publishes a kind-10002 event with
the device's relay list; Pull discovers it on other devices and
offers a one-click "Add to my list" prompt.
Archive banner sensitivity setting — Options → Advanced exposes
a three-way preference (Always / Only when richer / Never) for the
Phase 7 archive-reader banner. Default is Always (any non-identical
archived copy surfaces).
NIP-04 read fallback in entity sync — pull path now decrypts
legacy NIP-04 events from the userscript era alongside NIP-44 v2.
Per-relay + per-format pull diagnostics — sync log shows
received counts per relay and a Format split: N NIP-44, M NIP-04
line.
Firefox sidebar support — sidebar_action manifest entry plus
feature-tested opener that prefers browser.sidebarAction.toggle()
on Firefox, then chrome.sidePanel.open() on Chrome/Edge.
Engineering journal — docs/JOURNAL.md is now the chronological
log of bugs, design decisions, and external platform changes.
Smoke test checklist — docs/SMOKE_TEST.md codifies the
~20-min manual pre-release sweep across Phases 0–7, with an
agent-runnable subset called out.
Changed
Entity-sync deserializer accepts both userscript and X-Ray
payload shapes, normalizing on input. Previously rejected
userscript-pushed entities with Malformed.
Sync log layout has a max-height with internal scroll so verbose
pulls don't push the rest of the side panel out of view.
NIP-78 sync filter now uses raw URL normalization (lowercase
scheme/host, trailing-slash strip) when comparing remote-vs-local
relay lists, eliminating spurious "missing" matches.
Fixed
Popup signing-state badge now reflects reality — content script
writes xr_signing_state to chrome.storage.local after NIP-07
detection. Previously always showed "not detected".
Twitter focal-tweet detection broadened past data-testid="tweet" to handle the tweetDetail container; URL/id
backfill from <a href*="/status/"> anchors.
YouTube transcript dedup — visible cues that YouTube renders
3× in the DOM no longer triple in the published markdown.
Archive banner over-firing on short-form captures (single
tweets) suppressed via threshold check.
Sidepanel CSS view-toggle — [hidden] attribute now honored,
so the inactive view actually disappears instead of overlapping
the active one.
Removed
Forward-looking references to the (separate) Keystone browser
project from the README. X-Ray and Keystone are independent.