Commit Graph

3332 Commits

Author SHA1 Message Date
filemon 7fa856224e Fix Blobbi looking at sidebar during route transitions
Keep previous attention alive during the 250ms route-reaction delay
instead of clearing it immediately. This prevents the gaze system from
falling to random mode (which could point toward the sidebar) while
waiting for the new page's DOM to mount.

- Split cancelReaction into cancelPendingTimeouts (timeouts only) and
  full cancel (timeouts + attention); route changes use the former
- Add bypassCooldown option to triggerAttention so the delayed reaction
  can override the kept-alive attention without being blocked by cooldown
- Stabilize triggerAttention via uiAttentionRef instead of stale closure
2026-04-18 01:06:15 -03:00
filemon 379c21c458 Reset attention cooldown on clearAttention to fix rapid route changes
clearAttention() now resets lastAttentionTimeRef to 0 so that a
triggerAttention call immediately after a forced clear is not silently
rejected by the 1500ms cooldown guard.
2026-04-18 01:06:15 -03:00
filemon 3b576685b7 Simplify route reaction to single center-look with random duration
Remove right-sidebar detection and multi-target chaining. The generic
reaction now fires a single attention target at the top-center of the
main content area for a random 2-6 seconds.

- Remove findRightSidebarPosition() and all sidebar scan logic
- Replace fixed 1200ms duration with random 2000-6000ms
- Compute target position at reaction start via live DOM query
  and window.innerWidth/Height fallback (no stale closure)
- Cancel both pending timeouts and active attention on drag
- Remove viewport prop from hook options (no longer needed)
- Update docstrings to describe the simplified behavior
2026-04-18 01:06:15 -03:00
filemon c95287e5a4 Add generic route-transition reaction for Blobbi companion
On page navigation the companion now briefly pauses and scans the
layout areas that changed. Center content is always scanned first,
followed by the right sidebar if a non-placeholder sidebar is
detected in the DOM.

Implementation:
- useRouteReaction.ts: thin orchestration hook that watches pathname,
  determines changed areas, and chains triggerAttention calls via
  setTimeout. Cancels on new route change, drag, or unmount.
- useBlobbiCompanion.ts: wires the new hook with existing
  triggerAttention/clearAttention from useBlobbiAttention.

No changes to the attention system, state machine, gaze hook, motion
hook, or entry animation. The existing attending state and attend-ui
gaze mode handle all the visual behavior.

Includes an empty ROUTE_REACTIONS map for future per-route overrides.
2026-04-18 01:06:15 -03:00
Alex Gleason 54a49f1ece release: v2.10.1 2026-04-17 22:43:30 -05:00
Alex Gleason a2f2d9ff89 Add configurable shareOrigin to AppConfig
window.location.origin resolves to capacitor://localhost on iOS and
https://localhost on Android, which produces broken QR codes, broken
copy-link actions, and a broken remote-login callback URL on native
builds.

Add an optional shareOrigin field to AppConfig and a useShareOrigin
hook that falls back to window.location.origin when unset. Replace
all 13 call sites that build shareable URLs.

The origin can be configured three ways, in order of precedence:
user localStorage > ditto.json > VITE_SHARE_ORIGIN env var. Native
deployments can set VITE_SHARE_ORIGIN=https://ditto.pub at build time
so that shared URLs resolve correctly when opened on another device
(and get caught by DeepLinkHandler when opened on the same app via
Universal/App Links).

Regression-of: a12d5db5
2026-04-17 22:39:44 -05:00
Alex Gleason cb26238729 release: v2.10.0 2026-04-17 19:34:53 -05:00
Alex Gleason f4ae344b30 Unify follow list, follow set, and follow pack rendering
Kind 3 (NIP-02 follow list), kind 30000 (NIP-51 follow set), and kind 39089
(follow pack) are all the same semantic thing — an event containing a list of
p-tagged pubkeys — but were being rendered three different ways, with kind 3
having no rendering at all, kind 30000 routing to a bespoke ListDetailPage, and
kind 39089 using its own FollowPackDetailContent.

Merge the three into a single PeopleListContent (feed card) and
PeopleListDetailContent (full detail). The detail component hosts every
feature from the predecessors: Follow All with existing p-tag preservation,
Save-as-copy for non-owners, owner-mode member removal for kind 30000, the
Feed/Members/Comments tabs, sidebar integration, and the share/copy-link
menu. For kind 3 the event has no title of its own, so we fall back to the
author's display name.

Additional refinements bundled in:

- Register kinds 3 and 30000 at every previously-missing rendering point:
  KIND_HEADER_MAP, shellTitleForKind, CommentContext KIND_LABELS/ICONS,
  extraKinds specific labels and icons, and ExternalContentHeader fallbacks.
  Kind 3 and 30000 now share the packs feed toggle via extraFeedKinds.
- Add infinite scroll to the people-list Feed tab via useTabFeed +
  IntersectionObserver sentinel, replacing the useStreamPosts 40-post cap.
- Add Comments tab alongside Feed and Members, powered by useComments
  (NIP-22 kind 1111). Drop the redundant variant badge. Allow kind 39089
  packs to be pinned to the sidebar.
- Trim redundant chrome: drop the member-count pill in the feed card,
  drop the member-count line in the detail header, and stop pulling the
  author's 'about' and 'banner' into kind 3 follow list views.
- Add a dedicated FollowListCommentContext branch so comments on kind 3
  show '@Name's follow list' instead of 'a follow list'.
- Replace the three-dots DropdownMenu on the detail view with the shared
  PostActionBar (reply/repost/react/zap/share/more), matching other
  detail views.
- Promote EmbeddedPost from ReplyComposeModal into a shared component
  that dispatches to EmbeddedNote / EmbeddedNaddr, with a new
  EmbeddedPeopleListCard for kinds 3/30000/39089 so quote posts, reply
  indicators, hover cards, and the More menu all render follow lists
  correctly.
- Link the 'N following' count on profile pages to a naddr of the kind 3
  event (routing to the new detail view) instead of a bespoke modal.
  Delete FollowingListModal. Using naddr rather than nevent ensures the
  link always resolves to the latest replaceable event.

Delete ListDetailPage, FollowPackDetailContent, and FollowingListModal
entirely. All three kinds now route through AddrPostDetailPage →
PeopleListDetailContent.
2026-04-17 18:52:00 -05:00
Alex Gleason 8f6361f6fc Bump dompurify to 3.4.0 for security fixes
DOMPurify 3.4.0 is a security release that fixes multiple issues
including mXSS via re-contextualization and closing tags, prototype
pollution via CUSTOM_ELEMENT_HANDLING and USE_PROFILES, ADD_ATTR
predicates skipping URI validation, and ADD_TAGS/FORBID_TAGS
precedence bugs.

The project uses DOMPurify to sanitize user-supplied SVGs in
sanitizeSvg.ts and sanitizeBlobbiSvg.ts, so pulling in these fixes
hardens our SVG rendering path against hostile inputs.
2026-04-17 16:53:16 -05:00
Alex Gleason 85894b98f5 Add stronger language to commit rule in AGENTS.md 2026-04-17 16:50:14 -05:00
Alex Gleason 3b052d3eb6 Fix naddr lookup for legacy replaceable kinds (0, 3, etc.)
useAddrEvent only treated kinds in 10000-19999 as replaceable, so any
naddr with a kind outside that range got a '#d' filter applied. For
legacy replaceable kinds like 0 and 3, real events don't carry a 'd'
tag, so the query matched nothing even when the relay had the event.

Invert the check to only apply the '#d' filter for true addressable
events (30000-39999). Legacy replaceable kinds and 10000-19999 are
now queried by kind+author alone.

Regression-of: 9b5df28b
2026-04-17 16:47:09 -05:00
Alex Gleason 5810c86e07 Reword Blobbi state action header to 'cared for their Blobbi'
The previous 'updated their Blobbi' wording felt mechanical for what is
really a care interaction (feeding, cleaning, playing, etc.). 'Cared for'
better reflects the user's intent.
2026-04-17 16:30:05 -05:00
Alex Gleason 650a45729e Disable global pinch-to-zoom via viewport meta
On Capacitor iOS, leaving user-scalable unrestricted let WKWebView's
scroll view pinch recognizer engage intermittently, then get stuck
disabled once it fired. Adding maximum-scale=1 and user-scalable=no
disables browser-driven pinch zoom consistently across web, iOS, and
Android.

The in-app lightbox (LightboxImage in ImageGallery.tsx) already
implements its own pinch-to-zoom with custom touch handlers and CSS
transforms, so it continues to work. Future components that want
pinch-zoom can follow the same pattern.
2026-04-17 16:13:40 -05:00
Alex Gleason ab2f574ff3 Document Regression-of trailer convention for commit messages
When a commit fixes a bug introduced by an identifiable prior commit,
the fix should record the offending short SHA in a Regression-of:
trailer at the bottom of the commit message body.

This is a standard Git trailer (parseable by git interpret-trailers)
that makes intra-release regression detection trivial: the release
skill can now read the trailer directly instead of hunting through
git log and git blame to figure out whether a 'Fixed' entry actually
describes a bug a shipped user ever saw.

- AGENTS.md: new 'Attributing Regressions' subsection under Using Git
  with the convention, when-to-add/skip rules, and tracing tips.
- .agents/skills/release/SKILL.md: Step 5.2 now has a fast path that
  reads Regression-of trailers via 'git log --format=%(trailers:...)',
  with the existing manual git log/blame approach as fallback.
- CONTRIBUTING.md: brief mention in the Bug fixes section and a new
  self-review checklist item pointing at AGENTS.md.
2026-04-17 16:03:51 -05:00
Alex Gleason 6bca0922f1 Improve release skill and remove intra-release fix from v2.9.0 notes
Adds a Changelog Quality Checklist to the release skill covering:
- Diffing code between tags (not just reading commit messages)
- Tracing every 'Fixed' entry to its origin commit
- The 'would a user on the previous version notice this?' test
- A worked example of the intra-release bug pattern

Removes the 'expanded emoji picker background' fix from the v2.9.0
changelog -- that bug was both introduced and fixed within the 2.9.0
release window, so no shipped user ever saw it.
2026-04-17 15:57:14 -05:00
Alex Gleason 482c99281c release: v2.9.0 2026-04-17 15:43:30 -05:00
Mary Kate Fain baf77c95aa Fix missing background on expanded emoji picker in feeds
The full emoji picker in QuickReactMenu had no background because
EmojiPicker sets its shadow DOM background to transparent, and the
wrapper div only had rounded-xl/shadow-xl without a background class.
Added bg-popover and border-border so the picker matches the quick-react
pill bar styling.

Closes #235
2026-04-17 14:55:57 -05:00
Alex Gleason d224035d28 Merge branch 'main' of gitlab.com:soapbox-pub/ditto 2026-04-17 13:30:22 -05:00
Alex Gleason e914109b4b Show full interactions on reaction/repost/zap/poll-vote detail views
These activity-style detail pages previously rendered only a slim action
row, missing the stats summary (Reposts / Quotes / Likes / Zaps), the
client + full-date row, and the InteractionsModal affordance. Users had
no way to see or browse the interactions the event itself had received.

Extract the stats + date row into a shared JSX block and replace the
four inline action-button grids with PostActionBar, bringing these
detail views in line with the standard post layout while keeping their
compact emoji/icon headers.
2026-04-17 13:29:27 -05:00
Chad Curtis 1e694a6cf8 Remove blobbi post requirement from evolution hatch missions
Add migration logic so users with stale persisted evolution missions
(e.g. containing the removed create_post mission) get their mission
list rebuilt to match current definitions while preserving progress.
2026-04-17 12:30:57 -05:00
Alex Gleason 8e6bd29be0 Include kind 8 badge awards in home, profile, and Badges feeds
Declares kind 8 as a third sub-kind under the existing Badges
ExtraKindDef with its own 'showBadgeAwards' / 'feedIncludeBadgeAwards'
toggles. The home feed and profile feed both derive their kinds list
from getEnabledFeedKinds, so both pick up badge awards automatically.
The Badges page's follows feed is a hardcoded list, so kind 8 is added
there explicitly.

Defaults match existing badge settings: enabled in hardcodedConfig (new
users see them), conservative in InitialSyncGate and TestApp. The
ContentSettings UI auto-generates a new Badge Awards toggle row.

Removes the now-redundant KIND_SPECIFIC_LABELS/ICONS entries for kind 8
since the sub-kind carries that metadata.
2026-04-17 11:39:54 -05:00
Alex Gleason ab1f95f2df Render NIP-58 badge award events (kind 8) in feeds
Badge awards previously only appeared as notifications when you were the
recipient. Now they render as full feed cards — showcase image, badge
metadata, recipient row, and an Accept button for logged-in recipients —
so issuers can share awards and feeds can surface community recognition.

Extracts parseBadgeATag, unslugify, and AcceptBadgeButton out of
NotificationsPage.tsx into shared modules, adds a compact embedded card
for kind 8 nevent references, and wires the kind through NoteCard,
PostDetailPage, CommentContext, and extraKinds registries.
2026-04-17 11:31:57 -05:00
Alex Gleason fe11513a6f Merge branch 'main' of gitlab.com:soapbox-pub/ditto 2026-04-17 11:06:33 -05:00
Alex Gleason 52e42fcd6e Replace hardcoded 'Ditto' with appConfig.appName and appConfig.appId
User-facing display strings now read from config.appName so forks can
rebrand without code changes, and localStorage keys are namespaced by
config.appId so forks running on the same origin don't clobber each
other's preferences. Module-level cache-key constants that previously
hardcoded 'ditto:' have been refactored into hook-scoped reads from
config.appId (via a new getStorageKey() helper). The helpContent FAQ
template now uses {appName} placeholders substituted at read-time
through getFAQCategories(appName)/getFAQItem(appName, id).
2026-04-17 11:01:04 -05:00
Chad Curtis 3aa08ba93e Overhaul compose box UX: inline picker, draft autosave, prevent accidental dismiss
- Move emoji/GIF/sticker picker from popover to inline panel in all contexts
- Add pill-style tab highlights for inline picker (emoji, GIF, stickers)
- Auto-save compose drafts to localStorage with debounce, keyed by context
- Prevent accidental modal dismissal (backdrop click blocked)
- Theme emoji picker to match Ditto design system (search, nav, scrollbar)
- Fix emoji picker width (dynamicWidth + shadow DOM overrides)
- Create StickerPicker component with search bar
- Restructure modal layout: scrollable content, sticky toolbar, fixed picker
- Mobile keyboard handling (blur/refocus textarea around picker)
- Move Tenor attribution into GIF search bar as inline hint
- Bump picker height to 280px
- Auto-focus textarea on modal open (iOS keyboard fix)
- Remove rounded corners from feed compose box
2026-04-17 00:01:38 -05:00
Alex Gleason 9837c23a96 Always add NIP-89 client tag, including on localhost
The HTTPS check was a leftover from when the client name was derived
from the hostname. Now that it comes from appConfig, the tag should
be added unconditionally.
2026-04-16 23:48:07 -05:00
Alex Gleason 71918f8381 release: v2.8.0 v2.8.0 2026-04-16 18:05:50 -05:00
Alex Gleason 99fefdda67 Merge branch 'main' of gitlab.com:soapbox-pub/ditto 2026-04-16 17:17:47 -05:00
Alex Gleason dabe3c1687 Fix avatar shape not saving during signup
The signup and onboarding profile steps rendered ProfileCard without
passing onAvatarShape, so emoji shape selections were silent no-ops and
never made it into the published kind 0 event.
2026-04-16 17:06:27 -05:00
Chad Curtis 1caf911f53 Merge branch 'ai-chat-429' into 'main'
Overhaul AI chat: handle 429 rate limiting, require Shakespeare credits

Closes #230

See merge request soapbox-pub/ditto!187
2026-04-16 22:03:57 +00:00
Chad Curtis c3f0e9d3fa Merge branch 'main' of gitlab.com:soapbox-pub/ditto into ai-chat-429
# Conflicts:
#	package-lock.json
2026-04-16 16:59:54 -05:00
Chad Curtis bc39c99d07 Merge branch 'feat/evolution-missions-to-kind-11125' into 'main'
Move hatch/evolve task progress from kind 31124 tags to kind 11125 evolution[]

Closes #234

See merge request soapbox-pub/ditto!186
2026-04-16 21:41:51 +00:00
Chad Curtis 377b536456 Merge remote-tracking branch 'origin/main' into feat/evolution-missions-to-kind-11125
# Conflicts:
#	package-lock.json
2026-04-16 16:35:38 -05:00
Chad Curtis bf0fde9d06 Fix interaction tally not incrementing: ensure evolution missions exist in session store
The interactions tally mission was silently dropped because
trackEvolutionTally maps over the evolution[] array — if it's empty,
nothing gets incremented. This happened when evolution missions
weren't persisted to kind 11125 or weren't hydrated on page load.

Both useHatchTasks and useEvolveTasks now have a safety-net effect:
if the companion is in an active task process (incubating/evolving)
but evolution[] is empty, they re-populate from the static mission
definitions. This ensures tally tracking works immediately regardless
of hydration timing.
2026-04-16 16:33:45 -05:00
Alex Gleason fb5278b891 Add nsec backup to Profile settings
Lets users with a local-nsec login reveal, copy, and back up their secret
key from /settings/profile. Uses saveNsec() so iOS gets iCloud Keychain,
Android gets Credential Manager with a file fallback, and web gets a
.nsec.txt download plus an opportunistic PasswordCredential save.

Renders an explanatory message for NIP-07 extension and NIP-46 bunker
logins, where the key is not accessible from the app.
2026-04-16 16:24:07 -05:00
Chad Curtis a27ee3af86 Fix self-review findings: remove dead code, fix task progress display, fix hydration race
- Remove dead code: useSyncTaskCompletions, incrementInteractionTaskTags,
  getInteractionCount, getEvolveInteractionCount, unused lookup maps
- Fix task progress showing 0/N on load: compute event-based task counts
  directly from Nostr query results (authoritative) instead of relying
  solely on the evolution mission store which may not be hydrated yet.
  Use max(queryCount, missionCount) so progress displays immediately.
- Fix hydration race: useDailyMissions raw memo now waits for hydration
  before creating fresh missions, preventing overwrite of persisted
  evolution[] with empty array. Also preserve evolution missions across
  daily resets during hydration.
- Fix session store miss: use ensureSessionStore in incubation/evolution
  start so evolution missions are always populated even if the store
  hasn't been hydrated yet.
- Extract duplicate findMission to shared findEvolutionMission in
  evolution-missions.ts
- Document evolution[] field on kind 11125 in NIP.md
2026-04-16 16:17:57 -05:00
Alex Gleason 7073cadb43 release: v2.7.1 v2.7.1 2026-04-16 16:09:12 -05:00
Alex Gleason 2dfb880566 Improve signup save-key step
Addresses confusion on the key-save step during signup:

- Rename the primary button from 'Continue' to 'Save Key' with a
  Download icon, so the label matches the action it performs.
- Change saveNsec() to return 'saved' | 'saved-to-file' | 'dismissed'
  instead of throwing on native dismissal. Dismissing the iCloud
  Keychain prompt is a legitimate user choice so the handler now
  proceeds silently rather than blocking with a 'Save failed' toast.
- Add an in-flight guard on the Save Key button with a spinner and
  'Saving…' label. The finally block guarantees the disabled state is
  cleared, so users can never get stuck on an unresponsive button —
  fixing the 'button became disabled after I dismissed the prompt'
  complaint by construction.
- On de-Googled Android builds (GrapheneOS, /e/OS, etc.) the AndroidX
  Credential Manager has no provider to delegate to, so the keychain
  save fails immediately. Fall back to writing the key to the app's
  Documents directory so the user always has a persistent backup, and
  surface a toast telling them where the file is.
- iOS keeps its original behaviour: dismissing the iCloud Keychain
  sheet is a deliberate user choice, no automatic fallback. The
  Documents folder on iOS is accessible via the Files app without
  authentication, so silently dropping a plaintext nsec there would
  violate user intent.
- Use the app name (from config.appName) as the filename slug for any
  .nsec.txt file written to disk. On Capacitor location.hostname is
  always 'localhost', so passing the app name is the only way to get
  a meaningful filename. Drop the redundant 'nostr-' prefix since the
  '.nsec.txt' extension already identifies the file.
- Rewrite the description and title on the save step: 'Your secret
  key' + a single paragraph explaining what the key is and why it
  matters.
- When the user reveals the key via the eye toggle, show an amber
  callout with sharing/screenshotting warnings and a 'Learn more' link
  to the Managing Nostr keys blog post. The warning appears at the
  moment risk is highest.
- Auto-select the full nsec on focus/click so users copying into a
  password manager don't have to fight mobile selection handles.
- Use openUrl() for the external 'Learn more' link so it works
  correctly inside Capacitor's WKWebView.
- Singularise the keygen step copy ('cryptographic key' / 'Generate
  my key') to stay consistent with the save step which presents a
  single secret key.
2026-04-16 15:50:59 -05:00
Chad Curtis 13d4f667b6 Restore AI chat widget and extract shared credits hook
- Restore full interactive chat widget with ScrollArea, streaming messages,
  input area, and conversation cache that was regressed in ec9b6c43
- Extract useShakespeareCredits hook so credits gating is DRY between the
  widget and the full AI chat page
- Show Dork ASCII mascot consistently across all empty/logged-out states
  instead of the generic Bot icon
2026-04-16 15:47:33 -05:00
Chad Curtis d73460a617 Fix self-review findings: invalid HTML nesting, credits error handling, swallowed 400 errors 2026-04-16 15:36:09 -05:00
Chad Curtis ec9b6c43be Overhaul AI chat: handle 429 rate limiting, require Shakespeare credits
- Add RateLimitError class with Retry-After header parsing
- Distinguish insufficient_quota 429 from rate-limit 429
- Friendly Dork-themed error banners for rate limiting and out-of-credits
- Clean no-credits empty state with directive CTA and Get Credits button
- Hide model selector, trash, and input when user has no credits
- Hide page title on mobile, align model selector right
- Simplify sidebar widget to Shakespeare CTA
2026-04-16 15:28:09 -05:00
Alex Gleason 0d3b8ed23d Harden CSS/URL handling, NWC storage, and Android backup
- Sanitize event-sourced URLs before CSS url() interpolation in
  ProfileCard banner and letter stationery background (closes H-1, H-2)
- Sanitize event-sourced font families at the parse layer and in letter
  card/detail consumers that bypass resolveStationery (closes M-6)
- Export sanitizeCssString for broader reuse
- Route NWC wallet connection URIs and active pointer through a new
  useSecureLocalStorage hook, storing in iOS Keychain / Android KeyStore
  on native (closes M-1)
- Add removeItem to secureStorage
- Add Android backup/data-extraction rules that exclude WebView storage
  and Capacitor secure-storage SharedPreferences so wallet credentials
  don't leak via Google Auto Backup (closes M-5)
- Document that GOOGLE_PLAY_SERVICE_ACCOUNT_JSON must be base64-encoded
  to match what the CI job expects (closes M-2)
2026-04-16 14:20:26 -05:00
Alex Gleason a61925b821 Merge branch 'main' of gitlab.com:soapbox-pub/ditto 2026-04-16 13:50:42 -05:00
Alex Gleason cbfbca063e Validate theme font and background URLs at the schema layer
`ThemeFontSchema.url` and `ThemeBackgroundSchema.url` previously accepted
any string, relying entirely on downstream `sanitizeUrl()` calls for
protocol enforcement. Tightening the schema to `z.url()` rejects
obviously malformed inputs up front and matches the approach already used
for the relay list (`BlossomServersEventSchema`). `sanitizeUrl()` remains
the authoritative guard for `https:` enforcement at render time.
2026-04-16 13:47:16 -05:00
Alex Gleason f3393b2cc8 Store nostr-push device key in secure storage on native builds
The per-device ephemeral key used to sign nostr-push RPC events was
previously stored unconditionally in localStorage. On Capacitor builds
this bypassed the iOS Keychain / Android KeyStore wrapper that every
other persistent key in the app already uses.

Route the key through `secureStorage`, which keeps the native path
encrypted at rest and falls back to localStorage on web (where it was
before). Because the key is now loaded asynchronously, convert the
`NostrPushClient` constructor into a private constructor plus a public
`create()` factory, and restructure `usePushNotifications` bring-up to
await the client before registering the service worker.

The key is ephemeral and per-device, so compromise only reveals which
Nostr events this device subscribes to -- not the user's identity --
but matching the existing secure-storage contract closes an obvious
inconsistency.
2026-04-16 13:47:16 -05:00
Alex Gleason 2eb643f422 Sanitize app-handler picture and banner URLs
The `picture` and `banner` fields parsed from a kind 31990 NIP-89 event's
JSON content were passed directly to `<img src>` attributes without any
scheme validation. Non-https URLs could leak the user's IP to arbitrary
hosts, and data: URIs could be used for fingerprinting.

The same event's `website` URL was already sanitized; apply the same
treatment to the image URLs for consistency. The app's CSP `img-src`
already blocks most of these at the browser level, so this is
defense-in-depth.
2026-04-16 13:47:15 -05:00
Alex Gleason e22dbbe85c Validate NIP-05 resolver returns a 64-char hex pubkey
Previously the resolver accepted any string value from a domain's
.well-known/nostr.json `names` map and persisted it to IndexedDB. A
malicious or misconfigured NIP-05 server could return arbitrary data
(non-hex, wrong length, HTML, etc.) that would then be cached and
passed to downstream consumers as a pubkey.

Exploitation impact is limited because invalid hex simply fails to
match anywhere in the Nostr filter API, but hygiene and cache
integrity warrant rejecting malformed values outright. Enforce the
standard 64-char lowercase hex shape and evict any cached entry that
fails validation.
2026-04-16 13:47:15 -05:00
Alex Gleason e01ed039fb Add restrictive sandbox attribute to web sandbox iframe
Previously the SandboxFrame iframe relied entirely on cross-origin
subdomain isolation (the HMAC-derived `<id>.sandbox.ditto.pub` origin)
for containment. That does give origin-keyed storage and postMessage
isolation, but it does not restrict top-frame navigation, pointer lock,
or other capabilities that a hostile nsite/webxdc app could abuse.

The highest-value protection here is blocking `allow-top-navigation`:
without it, a malicious nsite could do `window.top.location = evilUrl`
and redirect the entire Ditto tab to a phishing page that impersonates
the app. The user opened a preview expecting to stay inside Ditto, so
this is a realistic and impactful attack.

The policy grants the capabilities that real web apps legitimately use
(scripts, same-origin storage + Service Workers per iframe.diy's
architecture, forms, modals, popups that escape the sandbox, downloads)
while withholding the ones that are either attacks (top navigation) or
unused niche features (pointer lock, presentation API, orientation
lock).

Also Omit 'sandbox' from the spread props so consumers cannot
accidentally weaken the policy.
2026-04-16 13:47:13 -05:00
Chad Curtis 17cdb87723 Merge branch 'fix/scroll-restoration-on-back-navigation' into 'main'
Fix scroll position lost when navigating back from post detail page

Closes #217

See merge request soapbox-pub/ditto!161
2026-04-16 18:35:28 +00:00
Alex Gleason a55ff61669 Verify NIP-17 inner rumor pubkey matches seal pubkey
NIP-17 requires that clients verify `messageEvent.pubkey === sealEvent.pubkey`
before trusting a gift-wrapped direct message. Without this check, any
attacker can construct a rumor claiming to be from another user and
gift-wrap it to the victim -- the seal signature only authenticates the
seal author, not the (unsigned) inner rumor.

Ditto's primary sender display uses sealEvent.pubkey so the headline
impersonation case is mitigated in practice, but the inner event's fields
(including its pubkey) are passed whole to NoteContent for kind 15 file
attachments, which could leak into downstream zap/reply targeting. Add
the spec-mandated check to prevent any trust in the inner pubkey.
2026-04-16 13:21:15 -05:00