security(persist): never eval persisted code + hardened CSP (TODO 10) - #102
Merged
lopugit merged 48 commits intoJul 29, 2026
Merged
Conversation
Replaces the resolver from PR #81 (which granted the model unrestricted Bash(git:*)/Bash(gh:*)) with a defense-in-depth version: the model only edits+stages, enforcement is re-derived from git's object store (merge-tree) rather than tamperable files, git-grep pager breakout removed, global/system git config pinned to /dev/null, persist-credentials off with env-injected push auth, hook-proof git throughout, deterministic graphify-out one-side reset with subtree-OID verification, pre-AI-spend guard for workflow-file merges (optional CONFLICT_RESOLVER_PAT lifts it), repository_dispatch stacked-PR cascade with depth cap, and phased failure comments. Exactly one push-triggered resolver may exist; two race each other's pushes. Adversarially reviewed across 4 multi-agent panel rounds (~100 agents); every sustained finding fixed and re-verified. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ion cleanup
Every logical collection now lives in a versioned physical collection
(things -> things_v2, rosters -> rosters_v1), with the version coming from
COLLECTION_SCHEMA_VERSIONS - now the complete registry of all 20 collections.
Once a migration moves the database forward, every generation below the
current version can safely be deleted from the MongoDB backend.
- collectionNames.ts: pure single-source resolver (physicalCollectionName,
classifyPhysicalCollections) + colocated node --test unit tests
- collections.ts: getCollection(logical) choke point, all named getters and
ensureIndexes resolve through it; boot-time adoption renames unversioned
legacy collections in place (instant, index-preserving) before any caller
gets a handle, recording issues instead of throwing when rename is
unavailable (Atlas M0) or both names exist
- migrations.ts: all migrations resolve through getCollection; new
merge-legacy-collections copies leftover legacy docs forward by _id
(insert-if-absent, unique-key conflicts skip + note, source never deleted);
new drop-stale-collection-generations (destructive: true) is the only place
old generations are removed - refuses legacy collections with unmerged docs
and generations a pending migration still reads (sourcePhysicals), never
touches unknown collections or generations above current
- getMigrationStatus: census gains physical names, a storage-generations
listing, and live-derived adoption issues; run endpoint requires
confirm: true for destructive migrations
- raw query API keeps logical names client-side; queryRunner resolves the
collection, collStats, and every $lookup/$graphLookup/$unionWith target
(resolvePipelineCollections) to physical names at execution
- MigrationsPanel: physical column, storage-generations table, adoption
banner, destructive badge, confirm passthrough
- removed the dead legacy Mongo path (getUser, userCreateSession,
userCheckExists, userValidatePassword, mongodb/{client,connection,db,
collection}.ts) - the last unversioned-access bypass
- docs: FUNDAMENTALS section 3 physical-versioning contract, CLAUDE/AGENTS
pointers, TESTING checklist, apiDocs migration endpoint updates
Verified live against local Mongo: adoption renamed all 20 collections in
place (sessions survived, unknown thingTypes untouched), merge copied 15
docs + skipped 1 unique-key conflict, cleanup 400s without confirm, dry-run
listed 20 candidates, confirmed run dropped 19 and kept the collection with
unmerged docs; census/generations/confirm flow exercised in the browser at
desktop + mobile widths.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…DO 10)
The localforage reviver eval'd {ttype:'function'} payloads on every load,
so anything able to write same-origin storage (XSS, extension, another
tab) gained persistent arbitrary code execution. Persisted functions now
revive as inert warn stubs and functions are dropped from persist
entirely (load falls back to code-defined defaults).
A build-only Vite plugin injects a CSP meta tag into the static shell
(meta so it applies wherever Vercel serves index.html, bypassing Nitro):
script-src 'self' + sha256 hashes for the shell's two inline scripts --
no 'unsafe-inline' for scripts, so injected inline/remote scripts are
blocked. 'unsafe-eval' is retained deliberately: smarts and the
Commander evaluate user-typed JS by design; dropping it is blocked on
migrating them off eval. No frame-ancestors -- embedding is a feature.
Verified: 26-check node suite (attack payload never executes at parse or
stub-call; functions dropped; TODO 9 date semantics unchanged) and live
against the built shell via vite preview -- app boots and renders under
the CSP, zero violations from the shell's own scripts (hash equality
confirmed in-browser), an injected inline script IS blocked, and eval
still works for the Commander.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…/:id permalinks - ReactionControl: controlled reaction picker opener — touch-and-hold opens the picker on mobile (Chakra trigger="hover" never fires on touch), hover still opens it on desktop, plain tap quick-reacts. Shared by post cards and comment rows. - Comments share the post schema: rich comments are ["post","comment"] things (photos, marketplace listings, thingtime things) validated by the post crystal rules; the comment payload now carries the post vocabulary + reactions + reply counts; comments are reactable and replyable; comments always inherit the thread root's audience; feeds/profiles exclude them. - Simple comment UI unchanged, with a context icon opening the full PostComposer in comment mode. - Every post and comment gets a shareable /post/:id permalink page (comments include parent/root thread navigation) and every timestamp is a link to it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Chakra closeOnBlur treats a keyboard dismissal as leaving the popover, so searching for a reaction and closing the keyboard nuked the picker mid-use. New useOutsideTapClose hook closes on a real outside tap (capture-phase pointerdown) instead of focus loss — applied to the reaction picker, the share composer (same caption-loss bug), and the schemas-page picker. Escape still closes via closeOnEsc. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…env leak - dev.mjs bootstraps missing dependencies (pnpm install --prefer-offline with CI=true so the modules-purge prompt can never abort without a TTY) before booting — a fresh worktree previously died with 'sh: vite: command not found'. - ensure-bcrypt + pre-dev.sh now run inside dev.mjs (after the bootstrap, with a scrubbed env) instead of as nested npm runs, and dev.mjs deletes the npm_config_verify_deps_before_run / npm_config__jsr_registry vars pnpm 11 leaks into the environment — killing the repeated 'npm warn Unknown env config … will stop working' noise. - pnpm-workspace.yaml sets confirmModulesPurge: false for every non-interactive install path (PM2, agent shells, worktree bootstraps). Verified in a fresh worktree from origin/main: first run auto-installs in ~4s and boots vite+nitro on the worktree port trio; second run skips the install; nested npm warnings are gone even with a polluted shell env. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…leanup
- Comment reactions repaint immediately (optimistic toggle + server
reconcile + revert on error) — no more waiting a network roundtrip for
the reaction to show as added/removed.
- Reply/replies on a comment open INLINE: a reply input plus the loaded
thread render right on the current page (recursive CommentRow); nothing
navigates away. The /post/:id permalink stays on the timestamp.
- Composer thingtime tab is a clean slate: the embedded editor drops its
window toolbar (traffic lights + tmp.<session>.New Thing path) via a new
EditorSplit chromeless mode, the tree hides its root key (hideRootPath —
the hover/hold context trigger still renders), and the draft seeds as
undefined so the editor opens on the bare "Imagine.." slate instead of
an {} object. Popped-out floating windows keep their chrome.
- Converting an empty object/array value to an Editor.js document starts a
BLANK doc (placeholder showing) instead of a paragraph containing the
literal {} / [].
- ThingContextMenu's modal and context presentations portal to body:
position:fixed inside the composer's transformed editor frame was
trapped and clipped (the cut-off Convert-to/style panel + locked scroll
on mobile). The popover presentation already portalled; now all three do.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
When a text field inside a popover is focused (the on-screen keyboard is up), an outside TOUCH tap now just blurs it — the picker/composer stays open; the next outside tap closes it. Mouse clicks (desktop) still close immediately. Applies to every useOutsideTapClose surface: the reaction emoji picker, the share composer, and the schemas-page picker. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…c sends, full picker on comments - viewport meta gains maximum-scale=1: stops iOS Safari's input-focus auto-zoom (user pinch-zoom still works on iOS 10+). - Only one EMPTY reply input is open at a time per card (ReplyFocusContext); rows with typed drafts always keep their input open. - Comment/reply text autosaves as a per-user draft (useCommentDraft, localforage-backed like the thingtime editor drafts, debounced) — leave, reload, continue where you left off; a stored draft reopens its thread on mount. Device-local for now: server-side profile sync is its own follow-up. - Comment and reply sends are optimistic: the comment renders instantly (pending row at 60% opacity), the server copy swaps in on success, and a failure removes it and gives the text back — no more send spinners. - Comment rows get the + → full EmojiPicker (recents recorded), same as posts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…replies, DevKit reset - The "N comments" counter now counts the WHOLE thread — every descendant via one $graphLookup per page (plus legacy-era entries the graph can't see) — so a post with replies-to-replies reports the real total. - The counter is clickable and toggles the comments section. - Threads render two levels deep by default: resolveRelated ships the latest 5 replies per comment (with their own reactions + reply counts), and every level pages 5 at a time — "Show more comments/replies" reveals 5 more per click (fetching the full reply list when the preload is short). - Reply and thread controls split: "Reply 💬" toggles the input (still one empty input at a time), "N replies 🧵" toggles the thread. - Reply inputs get the ⤢ full composer (photos/listing/thing rich replies), same as the comment input. - DevKit panel gains a 🎯 reset-position button: clears the dragged spot, closes the panel, and the trigger returns to its bottom-right corner. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…cross reopen Clicking "N comments" opens the section on its first page (5 comments, Show-more paging beyond). The revealed count is component state, so closing and reopening keeps everything you'd already expanded — it resets only when the page is left or refreshed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the placeholder gradient AppIcon with a pixel-crisp 1024x1024 render of the Thingtime tree t (remix/public/favicon-tree.ico), drawn as exact colour blocks (#A8E61D canopy, #9C5A3C trunk) on an opaque white background per App Store icon rules. Bump CURRENT_PROJECT_VERSION to 10 for the TestFlight upload. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
npm run dev: self-bootstrap fresh worktrees + silence pnpm-11 env-leak warnings
Audit of every brand raster in the repo found the legacy favicon-generator sets were smooth-scaled (blended edges, 11-88 unique colors instead of 2-3): favicon.ico, favicon-16/32/96, all apple-icon-*, android-icon-*, ms-icon-* in remix/public, and the favicon_io (2) set in resources/favicon. Regenerate every one as an exact nearest-neighbour cell grid via the new remix/scripts/gen-pixel-icon.sh (symmetric cell split, transparent bg, k=2 colors). favicon-tree.ico moves 388->384px for uniform 128px cells and adopts the Logo.tsx canonical green #A8E61D (was off-by-one #A8E61C). favicon.ico is now a real 16+32+48 multi-size ICO. iOS icon-1024 redrawn with power-of-2 geometry (256px cells, 128px margins) so cell edges land on integer pixels at common rendered sizes; bump build to 11 and re-upload to TestFlight. Already crisp, untouched: the six 63px favicon variants (exact 21px cells) and branding/thingtime-horizontal.svg (vector). Not the logo, untouched: favicon_rainbow.ico, raycast/assets/command-icon.png. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The batch-6 refactor left thread fetching only on 'Show more replies', so opening an UNLOADED thread (a level-2 comment's replies, which don't ship in the payload) showed just the button and no replies. A shared openThread() now fetches on every open path — the replies toggle, the reply input, and the draft auto-open — so the first page appears immediately; optimistic sends that race the fetch are kept and deduped. Loaded replies + reveal depth persist across close/reopen and reset on refresh. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… skeleton - Every rendered comment row auto-fetches its missing reply depth on mount, so revealing a level is instant — and freshly revealed rows prefetch the NEXT depth themselves, cascading as you go deeper. - Opening a thread / clicking Show more reveals cached replies immediately and still fires a background refetch, reconciling any live comments added in the meantime (optimistic sends that race the fetch are kept + deduped). - The only loading state left is a left-to-right shimmer skeleton shaped like comment rows, shown solely on a cold open with nothing cached — and never rendered alongside the show-more button. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A comment's things?id= read fetches its parent as a potential share original, so the page comment surfaced BOTH as a page doc (pass 1 collected its replies) AND as its parent's child (pass 2 re-fetched them) — doubling every reply in commentsByTarget. The client's prefetch/reopen cycle then painted the duplicates (the reported show → hide → show doubling). resolveRelated now skips page ids in the pass-2 reply fetch (their children and reactions were already collected against the page ids), pushComment refuses same-id duplicates outright, and the client's fetchThread merge dedupes by id as a last line of defense. Feed preloads verified unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The skeleton was appearing because only two levels shipped: a level-2 row's replies (level 3) had to be fetched after mount, and a quick tap raced that prefetch — on a cold serverless hop the skeleton lingered. resolveRelated's per-level passes are now one loop shipping THREE levels of replies (REPLIES_PER_LEVEL each, with reactions + direct counts per level; the last processed level fetches counts only, and seenIds guards cycles). Opening anything visible renders straight from the payload; level 4+ still prefetches lazily on row mount — verified a 4-deep thread: level 3 renders with zero interaction and level 4 reveals in one frame from its prefetch. Client: the mount prefetch fires exactly once per row (the api identity is render-unstable and permanently-short rows would have refetched forever), and the skeleton shows 3 rows with roomier spacing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
SHIPPED_REPLY_LEVELS returns to 1 (direct comments + one nested level per payload — the default two-level view), and CommentRow gains a depth prop: only depth-1 rows auto-open their preloaded thread, so each deeper tap reveals exactly ONE more level. Every reveal stays instant — a row's mount prefetch pulls its own two-level payload, putting the next depth in hand before it can be tapped. All the dedupe/prefetch-once/skeleton fixes stay. Verified on a 4-deep thread: default shows levels 1-2 only; tapping level 2 reveals level 3 alone; tapping level 3 reveals level 4 within 80ms. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ed type tabs - threadCache.ts: loaded reply threads live OUTSIDE component state (module map + localStorage tt-thread-cache, 5-day TTL, newest-400 cap, in-flight dedupe) — collapsing a parent no longer forgets fetched children, so re-expanding paints instantly with no skeleton, across reloads too. Reveals still background-refresh so live comments reconcile in. - Always one depth ahead: the post card prefetches the first HIDDEN depth the moment it mounts (short level-1 threads + every shipped level-2 with replies), and every thread fetch prefetches the level below what arrived — the first taps can never skeleton. Avatar URLs warm into the image cache at every step so revealed rows never pop in placeholder avatars. - The thingtime editor now opens in a bottom-sheet modal from a tappable preview (ThingView of the draft once it has content): flush left/right/ bottom with a padded, rounded top on mobile; centered 680px sheet on desktop. Nested comment composers get a real editor instead of a cramped inline embed; Done returns to the composer with the draft preview. - Post type tabs wrap so labels never bunch or overlap on narrow screens. Verified at mobile viewport: collapse→re-expand→tap repro shows zero skeletons, first level-3 tap instant from the post-mount prefetch, cache survives full reloads, sheet geometry 0/0/0 with typing working inside, tabs wrap cleanly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…eeded Restructure the resolve job into tiers: 1. Conflicts confined to graphify-out/ trees resolve MECHANICALLY: take the base branch's side wholesale (repo rule for generated graph files), pipx-install graphifyy in CI and re-run `graphify update` (text/AST-only, no LLM), push the merge, and comment on the PR with a note that semantic AI extraction still needs a local run. Runs even when an AI credential exists — deterministic beats tokens for generated files. 2. Real code conflicts go to the Claude resolver (unchanged prompt). 3. Code conflicts with no ANTHROPIC_API_KEY / CLAUDE_CODE_OAUTH_TOKEN secret: abort the merge, leave one deduped PR comment explaining what to configure, and fail the job visibly. Also handles the mergeability race: if GitHub reported CONFLICTING but the merge applies cleanly, push it and note that on the PR. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…y-fallback ci: graphify-only conflict fallback — mechanical resolve + text-only graph refresh, no AI key needed 🔀🪄
Add Thingtime/Resources/AppIcon.icon — a hand-authored Icon Composer package (white fill, canopy + trunk SVG layers with exact srgb brand fills, glass/specular disabled) so iOS 26 renders the tree flat and crisp instead of applying its auto-glass emboss to the legacy bitmap. The appiconset remains as the pre-iOS-26 fallback. Both flat and glass-chip variants were previewed on an iOS 26.5 simulator home screen; the flat look was chosen. Add a fastlane upload lane for uploading an already-built IPA, bump CURRENT_PROJECT_VERSION to 12, and document the .icon manifest gotchas plus the beta-actool hybrid build workaround in iOS/AGENTS.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… repost/share split - Action row is icon + count only (X-style): comment, repost, share left; the merged react button pinned to the card's right edge so every react control — post and all comment depths — shares one right edge (IG-style). - Merged react button shows everyone's reactions (top token emojis + total, heart outline at zero, accent tint when the viewer reacted); click, hover, and touch-and-hold all open the quick-react popup (tapOpens mode on ReactionControl, with preventDefault so Chakra's trigger toggle can't close a hover-opened popup on click). - Comment rows: icon-only reply toggle under the bubble, "View N replies / Hide replies" link below the comment (FB/IG), show-previous reveals below the lists, reply avatars (20px) smaller than parent comments (28px), indent capped past depth 4 so deep chains can't squeeze off 375px. - Share split: repost menu (instant optimistic repost + quote composer with caption/circle, both inheriting the original post's visibility and an outward share icon (native sheet / copy link, link handed over in a toast when clipboard is unavailable). - Guards: reacting on a pending (in-flight) comment no-ops; zero-width-space join keeps adjacent reaction glyphs from shaping into one emoji; shared AnchoredEmojiPicker replaces the duplicated popover blocks. - TESTING.md: new checklist section for the engagement row + comment threads. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> EOF )
- No max depth, no flattening: opening replies (or the reply input) at visual depth 4 REFOCUSES the comments panel on that comment — it slides in from the right as the new top-level row (28px avatar), a back arrow slides you back out one level per tap, and its replies restart at depth 1 relative to it. Repeatable indefinitely; closing comments exits the drill-down back to the conversation root. - Focus is a stack on PostCard (ThreadFocusContext hands focusThread to rows at the cap); slide-left/right keyframes pick direction from push vs pop; the focused root opens its replies on mount (defaultOpen), served instantly from the thread cache in the common case. - Server: canViewInherited and the /post parent-chain walk no longer cap legitimate comment chains at 4 hops (deep replies 404ed as "Post not found") — cycle safety now comes from a visited set, with a 64 rail for stack safety only, matching the crystal depth-rail convention. - TESTING.md: drill-down checklist replaces the indent-cap expectation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tion-versioning-7cbc86 # Conflicts: # TESTING.md
…ing-7cbc86 Version physical MongoDB collections as <name>_v<N> with safe generation cleanup
…chains
- schemas/registry.ts: sanitizeDataValue and checkExtendedKeys are now
ITERATIVE (explicit work stack) with identity WeakSets — circular or
repeated object references 400 loudly, nesting never touches the JS call
stack, and the 64-level rails (MAX_DATA_CRYSTAL_DEPTH, MAX_EXTENDED_DEPTH)
are gone entirely. The one depth-shaped bound left is the database's own:
mongod physically refuses BSON nesting past 180 levels per doc, probed
live on mongod 8.0.1 through the real create API (179 levels inside
crystal/extended store; 180 is refused with "BSONObj exceeds maximum
nested object depth"). MAX_STORABLE_NESTING=179 reports that as a precise
400 naming MongoDB instead of a raw driver 500. crystal.thing payloads
carry their one-level-deeper offset (base {path:'thing', depth:2}) —
verified exact: 178-deep thing saves, 179 400s with the prefixed path.
- api/utils/things/things.ts: canViewInherited and the permalink
parent-chain walk drop their 64-hop stack rails too. The visited set alone
terminates both walks (finite db + no revisits), canViewInherited keys on
shareId with an _id fallback so termination holds unconditionally, and
each hop awaits a db read so chain length never grows the call stack.
Verified by seeding an 85-deep reply chain through the real API
(multi-user round-robin past the comment rate limit) and resolving the
deepest comment's parent + root through GET /api/v1/things?id=.
- components/Feed/PostCard.tsx: the post's merged react button moves to sit
DIRECTLY beside the comments icon (per feedback); comment rows keep their
IG-style right-aligned react columns. Live-verified desktop + 375px
mobile: popup placement (no clipping at the left position), optimistic
toggle both directions, and the drill-down focus panel on the fresh
85-deep chain (root -> depth 4 panel with back arrow).
- TESTING.md: engagement-row, thread-depth, and crystal checklists updated
to the no-rail semantics.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ight-0a4b22 iOS: use the Thingtime tree t icon as the app icon (TestFlight build 10)
…solver-action # Conflicts: # .github/workflows/pr-conflict-resolver.yml # graphify-out/GRAPH_REPORT.md # graphify-out/manifest.json
…d pills - CommentRow: the merged react control leaves the right-edge column and sits INLINE beside the reply icon under the bubble, mirroring the post's comments-then-react row. Restyled horizontal (emoji + count side by side, heart outline at zero) for the inline position. - Single tap on a comment's react control now HEARTS the comment (default ReactionControl mode, onQuickTap ❤️ — optimistic, tap again to unheart); hover and touch-and-hold still open the quick-react popup, and the post's merged button keeps tapOpens (click opens the popup) per feedback. - "Write a comment…" and "Reply to…" pills are subdued: house border grey, soft ink text, muted placeholder, muted hover/focus border — replacing Chakra's default near-black outline. - Live-verified desktop + 375px mobile: inline controls render at every depth without wrap/overflow, single-tap heart applies instantly and no popup opens on click, hover still opens the popup, and the composer pill reads grey. TESTING.md checklist updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ports the graphify-only capability from PR #144 into the hardened resolver so superseding pr-conflict-resolver.yml does not regress graph freshness: after the resolution is verified and committed, graphify update runs AST/text-only (no LLM, no API key) on a pristine tree and is committed separately, before the push. Ordering is load-bearing: the verify step asserts the staged graphify-out subtree still equals the base side, so refreshing any earlier would fail that check by design. The tree is reset --hard + cleaned first so stray model edits can never be indexed, and a guard refuses the refresh if graphify touches anything outside graphify-out/. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
View/Hide replies now starts flush with the comment reply icon's glyph (same paddingX ladder) instead of hanging off a 20px divider line; the line denoting the linked parent is gone. Verified live at 375px. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ents-deeplinks-v2
Adversarial review of the #144 port found four defects, all fixed and verified locally: 1. SECURITY: 'git clean -qfd' skips IGNORED files, so a model-planted graphify-out/.graphify_labels.json (read verbatim into the tracked GRAPH_REPORT.md) or a graphify-out/cache/ast/** entry (injecting nodes/edges into graph.json) survived the pristine-tree reset and would have been committed — smuggling model-authored content past the verify step. Now 'clean -qffdx' plus an outright rm+checkout of graphify-out. Verified: both planted files survive -qfd and are removed by -qffdx. 2. Any unguarded failure was fatal, which SKIPS the push step and discards the verified resolution after full AI spend (reachable via 'git add' on a graphify-out deleted by the base side: fatal rc=128). Now continue-on-error + a step timeout + an existence-guarded staging loop. 3. 'git add -A -- graphify-out/' swept in graphify's own leftovers (.graph.tmp.json, needs_update). Now only the tracked portable outputs are staged. 4. Unpinned graphifyy could rewrite the whole graph on version drift, manufacturing the very conflict the step exists to resolve. Pinned to 0.9.4. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
) Replaces pr-conflict-resolver.yml with the hardened resolve-pr-conflicts.yml. The model gets no shell and no git access; all git is deterministic and enforced from git's object store. Ports #144's in-CI graphify refresh (AST-only, pinned) so retiring the incumbent loses no capability.
…evalidation fix - reactionDisplayEmojis replaces topReactionEmojis: every token the viewer reacted with always shows (their full set, first), then the crowd's top remaining tokens by count (2 on comments, 3 on the post button). Verified live: a second user's 😮 renders AFTER the viewer's 😂🔥❤️ set despite equal counts. - Stale-thread fix this surfaced: rows that mount with their thread OPEN (two-level ship, drill-panel roots) now force the background refetch — a cache-complete thread previously skipped it entirely (loaded >= commentCount bail), freezing reply reactions/edits at the cached snapshot until a manual close/reopen. Server-confirmed hearts now reconcile in after reload. - TESTING.md: both behaviors added to the engagement checklist. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes the disappearing/reappearing/uncounted reactions on mobile (screen recording): background fetches (thread revalidation, feed and profile stale-while-revalidate, permalink loads) snapshot the db BEFORE the viewer's tap but land AFTER its optimistic paint — ingesting those copies wholesale wiped the reaction, which flickered back on ack or stayed wrong when the fetch landed after the ack. Real network latency (Vercel preview) made the windows wide and constant. New merge layer, components/Feed/reactionOverlay.ts: - every local reaction mutation (optimistic paint, server ack, failure revert) notes its resulting counts per thing id, timestamped; - every ingestion of server copies merges through the overlay stamped with the fetch START time: fetches started before the last note keep the overlay's reaction fields (entry retained), fetches started after it carry the mutation server-side and retire the entry; - recursive over shipped reply trees, referentially stable when nothing matches. Wired at every ingestion point: fetchThreadInto (single choke point for all thread paths, stamps + merges + caches merged copies), getCachedThread (merges with the entry's write time), CommentRow payload seeds (epoch 0), Feed + ProfilePage post pages (search and feed paths), and the /post permalink load (post + parent + root). Both react handlers note optimistic, ack, and revert states. Deterministically verified in-browser by delaying thread GET responses 3s and tapping inside the window: the stale response lands and the tap survives; a clean reload converges on server truth and retires the overlay; re-hearting sticks. TESTING.md gains the race checklist line. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- UserAvatarCircle (drawer footer, feed/comment composers) now renders the user's avatarUrl image when set — the rainbow initial circle is only the no-avatar fallback, matching AccountSwitcher's RowAvatar. - Mobile drawer: clicking a NAVIGATING menu item (top-level or sub-item) now dismisses the drawer after navigation; items without a destination only select their submenu and keep it open. Desktop panel behavior is unchanged (DrawerSystem's onNavigate closes on mobile only). - New per-item 'Close after click' setting (settings.drawer.closeOnClick, default ON) in Settings -> Drawer: every auth-visible menu item, children nested, each with a switch — off restores the old explore-the-submenu behavior for that item. - Live-verified at 375px: Things click navigates + closes; toggling Feed off persists and keeps the drawer open on Feed clicks; avatar photo renders in the composer, drawer footer, and account switcher. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…it chains canViewInherited capped tt:inherit chain resolution at depth 4, so a comment nested 5+ levels below its root post became invisible to every single-thing path — react, comment, save, share, and the /post/:id permalink all returned 404 "Post not found" — while the feed (query-side match + owner shortcut) still rendered it. Confirmed on live thingtime.com: depths 0-4 of a real reply chain resolved, exactly the 5-deep replies 404ed. The depth cap only ever existed to stop pathological cycles, so replace it with real cycle detection: extract the walk into aclChainCore.ts (pure, DB- free, quotaCore-style) bounded by a visited set plus a far-out-of-band MAX_INHERIT_CHAIN=256 ceiling. Broken chains (deleted/missing targets) and cycles still fail closed. - node --test unit suite (npm run test:acl): deep-chain regression, ceiling edge, missing target, dangling inherit, cycle/self-cycle - E2E via real API against the dev stack: 8-deep comment chain, react + GET succeed at every depth (previously failed from depth 5) - TESTING.md: new checklist section for inherit-chain interactions Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
dismissAfterNavigate in DrawerContent runs onNavigate (popup dismiss + mobile panel close) AND setOpen(false), so navigating top/sub items, the brand link, and editor-section items now dismiss the pinned desktop panel as well. The per-item 'Close after click' switches gate both viewports; search is untouched (openSearch still honours the separate 'Search closes drawer' setting, and mobile search still force-closes so the commander is never buried). Live-verified at 1280px: Docs/Account clicks navigate + close the panel; Feed with its switch off navigates and the drawer stays open. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ved conflicts) Conflicted paths: remix/app/api/utils/things/things.ts Resolved by the resolve-pr-conflicts workflow: https://github.com/lopugit/thingtime/actions/runs/30194952822 Co-Authored-By: Claude <noreply@anthropic.com>
…ound-0630bb Fix live "Post not found" on deep comment reactions (inherit-chain depth cap)
Owner
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes TODO 10 — 🔒 SECURITY: persisted-state
eval= arbitrary code execution on load; add a CSP (spec:TODO/claude-todo/09-security-hardening.md§C).The vulnerability
The localforage persist reviver ran
eval(value.code)(plus a second scopedeval) for any parsed{ttype:'function'}value, and the whole thingtime tree revives on every load. Anything able to write same-origin storage — an XSS, a browser extension, another tab — could plant a payload giving persistent arbitrary code execution on every subsequent load. No CSP existed anywhere.The fix
1. Persisted code never executes.
{ttype:'function'}values now revive as inert warning stubs (noeval, no scoped re-eval), and the replacer drops functions from persist entirely — load falls back to code-defined defaults for those slots. Legacy blobs keep working; their function entries just stop being live code.2. Hardened CSP on the built shell. A build-only Vite plugin (
cspMetaPlugininremix/vite.config.ts) injects a CSP<meta>tag intodist/index.html:script-src 'self' 'unsafe-eval' 'sha256-…' 'sha256-…'— the two hashes are computed at build time from the shell's own inline scripts (theme pre-paint + title prefix). No'unsafe-inline'for scripts, so injected inline/remote scripts are blocked.object-src 'none';base-uri 'self'.index.htmlvia filesystem routing (bypassing Nitro), so a meta tag applies wherever the static shell is hosted. Dev is untouched (apply: 'build').frame-ancestorson purpose — embedding Thingtime is a feature (embed SDK, default-open SSO origins, PR Thingtime SSO: default-open origins — any URL (incl. preview deploys) can use Login with Thingtime #87).Why
'unsafe-eval'stays (deviation from the TODO's ideal)smarts/index.tsxand both Commanders evaluate user-typed JS by design (≈20evalsites — command execution,$contextscoping, property access). A CSP without'unsafe-eval'bricks the Commander outright. This PR eliminates the actual persistence-RCE vector (storage-planted code) and blocks script injection; dropping'unsafe-eval'is a separate migration (sandboxed evaluator for smarts), noted inTODO/TODO.mdand the spec checklist.Verification
Node suite (26 checks, reviver/replacer extracted verbatim from the shipped file, real
flatted): an attack payload{ttype:'function', code:'(globalThis.__pwned = true, …)'}does not execute at parse time nor when the stub is called; functions are dropped from persist while sibling values survive; all TODO 9 date/string semantics unchanged.Live against the built shell (
build:client+vite preview, Browser pane):crypto.subtlehash of the served inline scripts equals the meta hashes<script>is blocked (script-src-elem:inlineviolation — the only violation recorded)evalstill works in-page (Commander/smarts safe)[LC]tab-title prefix disappearing under preview is pre-existingroot.tsx:92document.title = routeTitlebehavior with no server loader data — unrelated to the CSP; hash-allowed scripts run.)Also updates
TODO/TODO.mditem 10 and the §C/“Done when” status inclaude-todo/09-security-hardening.md(A/B/D were already shipped).Session 3 of 10 parallel todo sessions; claim branch pushed before work started.
🤖 Generated with Claude Code