Conversation
…rruption
TODO/claude-todo/09-security-hardening.md sections C and D.
Persisted-state eval (RCE): ThingtimeProvider revived any persisted
{ttype:'function'} value via eval(code), and the replacer serialized every
function that way into localforage/IndexedDB on each state change. Anything able
to write same-origin storage (an XSS, an extension, another tab) could plant a
payload that executed on every subsequent load. Functions are now dropped on
both write and read; ThingtimeDefaults re-supplies the real ones on hydrate.
CSP: no Content-Security-Policy existed anywhere, so 'unsafe-eval' was
effectively allowed. Added a single-source policy (scripts/csp.mjs) with NO
'unsafe-eval', stamped as a header on the Vercel output (patch-vercel-output)
and the Vite dev server, and asserted by verify-vercel-output. The two inline
boot scripts in index.html moved to an external /tt-boot.js so the policy can
stay script-src 'self' without per-edit inline hashes.
Date.parse corruption: the reviver turned ANY string that passed the lenient
Date.parse ("Post 1", "2024", "March 2024") into a Date on reload, then the
replacer rewrote it as an ISO string, permanently corrupting user data after
one save/reload cycle. Revival is now restricted to strict ISO-8601 timestamps.
The codec moved to app/Providers/thingtimePersistCodec.ts (pure, React-free)
with regression tests in thingtimePersistCodec.test.ts (npm run test:persist,
6 cases: strings survive round-trips, real Dates revive, functions/hostile
payloads are dropped).
Verified: full build + verify:vercel-output pass; test:persist 6/6; app
hydrates and renders correctly under the CSP in a live browser with fonts and
Emotion styles loading unblocked. Pre-existing hydrateRoot(document) DOM errors
(TODO #1) are unchanged and reproduce identically without this change.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
TODO/claude-todo/09-security-hardening.md §B. Login and resend-verification already enforce the shared config-driven limiter; register was the last auth endpoint with neither a rate limit nor a body cap. Each signup creates a user, burns a bcrypt hash, and queues a verification email/outbox row, so an unbounded register is a resource-amplification + mailbox-spam vector. - Add an `auth.register` rule to RATE_LIMIT_DEFAULTS (10 / 10 min per IP, admin-tunable; flows through RATE_LIMIT_ENDPOINTS + the admin panel automatically). - Enforce it in the register action before any work (fail-open, like the other auth routes) and switch the raw `request.json()` to `readJsonBody(request, 16 KB)` so an oversized payload is rejected 413 before it is buffered. The `meta` mass-assignment concern in §B is already handled — the route whitelists username/password/email/displayName and never passes `meta`. Tests (app/tests/api/apiTests.ts): auth-register-validation now also accepts a shape-checked 429; new auth-register-body-cap asserts an oversized body is rejected 413. Verified live against local Nitro: a fresh per-IP bucket returns 10x 400 then 429, and an oversized body returns 413. Full build + verify:vercel-output pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…15 follow-up) useThingtime.tsx carried its own copy of ThingtimeTypes/EverythingTypes, drifted from the canonical definitions in ThingtimeProvider.tsx (an unused optional Provider field). The hook now imports and re-exports the provider's types — one source of truth, no consumer changes (the only external importer, ThingtimeURL.tsx, already imports from the provider; nothing reads the dropped Provider field). The remaining item-15 Commander V1→V2 swap is deliberately sequenced AFTER PR #130 lands: CommanderV2 lacks the pathPrefix/placeholder props the Thingtime.tsx inline call site depends on, and #130 is actively rewriting CommanderV2 — porting those props now would guarantee conflicts. Verified: tsc --noEmit reports no errors in the touched files; /feed and /things render live with zero console errors. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…98/#106) Upgrade the persist codec from strict-ISO string revival to the tagged scheme the duplicate TODO-9 PRs used, closing the remaining false-positive: - Real Dates persist as {ttype:'date', iso}. The replacer reads the original off the holder (this[key]) because Date.toJSON converts Dates to strings before the replacer sees the value. - A USER string that merely looks like a full ISO timestamp is escaped as {ttype:'iso-string', s} so the legacy fallback can never capture it — previously such a string became a Date on the next load. - The legacy bare-ISO fallback remains only to migrate pre-tagging persists. - Both replacer and reviver skip their own wrappers' inner keys (without the guard the escape rule re-wraps its own output unboundedly, and the legacy fallback hands the wrapper branches a Date instead of the original string — caught by the new tests). Tests: 9/9 in test:persist, including Date + identical-looking string coexisting, two-cycle string stability, and hostile-payload drops. TESTING.md gains the register rate-limit checklist (from #106) and a persisted-codec checklist. Live-verified: app hydrates under the CSP, persists and reloads cleanly, zero console errors. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
# Conflicts: # TESTING.md
Conflicted paths: TESTING.md, remix/app/api/utils/rateLimit/config.ts Resolved by the resolve-pr-conflicts workflow: https://github.com/lopugit/thingtime/actions/runs/30546808318 Co-Authored-By: Claude <noreply@anthropic.com>
Conflicted paths: TESTING.md Resolved by the resolve-pr-conflicts workflow: https://github.com/lopugit/thingtime/actions/runs/30547496533 Co-Authored-By: Claude <noreply@anthropic.com>
POST /api/v1/auth/register was the one unauthenticated mutating auth route with no limiter: each attempt burns a bcrypt hash, a success emails an arbitrary address, and since PR #162 a broken-index state re-runs the ensureIndexes battery per attempt. Throttle by IP (default 10 per 15 min, admin-tunable, fail-open like the other auth rules) before any work runs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The MongoDB connection inputs on /mongodb-status rendered as plain text — a URL with embedded credentials (or even just an infra hostname) was readable by anyone watching a shared screen. - SecretInput: password-masked input with a per-field 👁/🙈 show/hide toggle, hidden by default; autoComplete="new-password" keeps password managers from offering to save or autofill; values live only in component state and the POST body, never in localCache. - The connection-URL input is now a SecretInput. - New "Individual fields" mode beside "Connection URL": scheme select (mongodb:// / mongodb+srv://) + user/password/host/port/database, each its own hidden-by-default SecretInput. The fields live-compose into the same `url` state, so the existing activate/save buttons work unchanged in both modes; user/password/database are URI-encoded, srv disables + clears the port, and switching modes round-trips (a pasted URL parses into the fields, the fields compose back). - Inputs clear after a successful activate/save. Verified live (worktree stack, logged-out flow): all six inputs render type=password with individual toggles (toggle flips password↔text and relabels Show↔Hide); fields for user "test user" / pass "p@ss:word" / localhost:27017/tt_override_test composed byte-exact to mongodb://test%20user:p%40ss%3Aword@localhost:27017/tt_override_test and parsed back decoded; activation from fields mode succeeded (Custom badge, active label, fields cleared); srv disables the port with an explanatory placeholder; localStorage carries only host/db summaries (no mongodb:// anywhere); mobile viewport wraps with zero horizontal overflow. TESTING.md gains three checklist lines for the new bug class. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
refactor: unify ThingtimeTypes to one source of truth (TODO 15 follow-up)
Rate-limit public registration (auth.register)
…nputs-4dcd51 Endpoint config: screen-share-safe secret inputs + individual-fields mode
Conflicted paths: remix/app/api/utils/rateLimit/config.ts, remix/app/routes/api/v1/auth/register/_register.tsx Resolved by the resolve-pr-conflicts workflow: https://github.com/lopugit/thingtime/actions/runs/31152830723 Co-Authored-By: Claude <noreply@anthropic.com>
Conflicted paths: remix/package.json Resolved by the resolve-pr-conflicts workflow: https://github.com/lopugit/thingtime/actions/runs/31153996937 Co-Authored-By: Claude <noreply@anthropic.com>
docs: record develop Vercel domain
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
docs: all PRs target develop, not main (AI_ALL.md publishing rule)
Preserve the canonical register IP limiter while consolidating PR #99 hardening and make sensitive conflict paths reviewer annotations instead of resolver stop conditions.
…rdening # Conflicts: # remix/CHANGELOG.md
…e3a72d feat(attachments): play every browser-playable video container inline + label sniffed types
|
📋 Promotion changelog updated — now carrying 8 PRs (100 commits). Added:
The full changelog lives in the PR description. |
The app CSP never allowed the direct-to-S3 multipart PUT the composer performs, so every attachment upload died in the browser with 'The file could not reach storage' on any surface serving the header (pr-*.previews.dev.thingtime.com, dev, staging - and production once the policy ships there). Bucket CORS was already correct; only the page policy blocked the connection. connect-src now carries the exact bucket origin derived from THINGTIME_PRIVATE_S3_BUCKET/_REGION at build time, with a regional wildcard fallback when the env is absent. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fix(csp): allow the private S3 bucket origin in connect-src (uploads died on CSP-serving domains)
|
📋 Promotion changelog updated — now carrying 9 PRs (102 commits). Added:
The full changelog lives in the PR description. |
Conflicted paths: remix/CHANGELOG.md Resolved by the resolve-pr-conflicts workflow: https://github.com/lopugit/thingtime/actions/runs/32222420675 Co-Authored-By: Claude <noreply@anthropic.com>
|
📋 Promotion changelog updated — now carrying 10 PRs (118 commits). Added:
The full changelog lives in the PR description. |
|
|
|
|
|
🤝 Merged Conflicted files:
Please review the merge commit before relying on it. |
Conflicted paths: graphify-out/GRAPH_REPORT.md, graphify-out/cache/semantic/4adb3e86d21894e6120d862d66150515a163e73e44a114f5c8ede282bf674d2e.json, graphify-out/cache/semantic/706ec8159e5c848da475c5b5e0b6f346cd304c49d50fbcb792aab4487e4357eb.json, graphify-out/cache/semantic/84230e475b26161bdcd7e75a245026998588279eeb9bcd2699b13cde500034fa.json, graphify-out/cache/semantic/94e4a16bf1f2effc5c42f92dd9b745b96be99ff5590fbf365bdc4792df23e96e.json, graphify-out/cache/semantic/e396f00630e821b189d4e1e3e751efd075116d18db92023c07a9b28dce0ca920.json, graphify-out/graph.json, graphify-out/manifest.json, remix/CHANGELOG.md, remix/scripts/verify-vercel-output.mjs Resolved by the resolve-pr-conflicts workflow: https://github.com/lopugit/thingtime/actions/runs/32386080538 Co-Authored-By: Claude <noreply@anthropic.com>
`graphify extract` with LLM semantic extraction (graphify 0.9.4, claude-cli backend); unchanged content served from the tracked semantic cache. Refreshed by the resolve-pr-conflicts workflow: https://github.com/lopugit/thingtime/actions/runs/32386080538
…-scopes Reconcile canonical public/private upload scope UX
|
📋 Promotion changelog updated — now carrying 11 PRs (120 commits). Added:
The full changelog lives in the PR description. |
|
📋 Promotion changelog updated — now carrying 12 PRs (125 commits). Added:
The full changelog lives in the PR description. |
|
🤝 Merged Conflicted files:
Please review the merge commit before relying on it. |
Conflicted paths: graphify-out/GRAPH_REPORT.md, graphify-out/graph.json, graphify-out/manifest.json, remix/app/components/Attachments/AttachmentComposer.tsx, remix/app/components/Attachments/attachmentUiCore.test.ts Resolved by the resolve-pr-conflicts workflow: https://github.com/lopugit/thingtime/actions/runs/32392536898 Co-Authored-By: Claude <noreply@anthropic.com>
`graphify extract` with LLM semantic extraction (graphify 0.9.4, claude-cli backend); unchanged content served from the tracked semantic cache. Refreshed by the resolve-pr-conflicts workflow: https://github.com/lopugit/thingtime/actions/runs/32392536898
|
📋 Promotion changelog updated — now carrying 13 PRs (127 commits). Added:
The full changelog lives in the PR description. |
Standing promotion PR opened by the Promote develop to main workflow. Its head is
develop, so every new push or merge todevelopshows up here on its own. Merge it whenever main should catch up — the workflow opens the next one after the following push todevelop. The Sync main into develop workflow levels develop with main again after each promotion.📋 What this promotion carries
13 pull requests merged into
develop(127 commits) will land inmainwhen this PR merges — newest first:codex/ios-drawer-camera-crashmaincodex/reconcile-canonical-upload-scopesmainclaude/csp-allow-s3-uploadsclaude/screenshot-video-display-e3a72dclaude/all-branch-listener-concurrency-hotfixclaude/wildcard-all-branch-auto-merge-49d963claude/media-files-sort-drag-drop-1f75a0mainclaude/eval-csp-hardeningclaude/fix-appdata-index-search-null-guardcodex/fix-vercel-custom-env-ignoreDirect commits on `develop` without a merged PR (2)
07e242f5Merge remote-tracking branch 'origin/main' into develop2322f192Merge remote-tracking branch 'origin/main' into developAuto-maintained by the Promote develop to main workflow — refreshed from
develop@2d6dfd4b(2026-08-20). Delta comments below track when entries enter or leave the promotion window.