security: constant-time auth comparisons, pinned JWT alg, 600k PBKDF2 iterations#39
Open
guirguispierre wants to merge 47 commits into
Open
security: constant-time auth comparisons, pinned JWT alg, 600k PBKDF2 iterations#39guirguispierre wants to merge 47 commits into
guirguispierre wants to merge 47 commits into
Conversation
…tion - memory_merge: Merge overlapping memories into one richer memory, combining content, tags, and graph links while archiving duplicates - memory_temporal_cluster: Retrieve memories grouped by time windows (hour/day/week) with surrounding graph context for episodic recall - memory_spaced_repetition: Surface important but fading memories due for review, scored by urgency (importance, confidence gap, staleness, isolation) Bumps version to 1.11.0.
Shows a dismissable banner after login when the server version has new changelog entries. Fetches tool_changelog via MCP, displays change items with type badges, and persists dismissed version in localStorage.
- Cap memory_merge memory_ids at 20 to bound query/loop size - Use parseTagSet for normalized, deduplicated, sorted tag merging - Delete old links from archived memories after transfer to prevent link count inflation - Align temporal clustering to UTC boundaries (day/hour/week) instead of anchoring to startTs - Use exclusive end bound (created_at < endTs) for correct [start,end) range semantics - Batch link query for all memories once instead of N+1 per cluster - Fix CHANGELOG ordering: [Unreleased] above released versions
… iterations - Add constantTimeEqual and use it for JWT signature and password hash comparison (closes a timing side-channel on both paths). - Validate the JWT header before signature work: reject anything that is not alg=HS256 / typ=JWT (alg-confusion hardening). - Raise PBKDF2_ITERATIONS to 600,000 per OWASP guidance; stored hashes embed their iteration count so existing credentials keep verifying. - Add tests/crypto.test.mjs (node --test, no new deps) covering password round-trip/rejection, signature tamper, alg=none, expiry, and missing claims, plus a loader shim so tests import the real TS sources. https://claude.ai/code/session_01TzX3Qb6rNYDfsRkmJV6hnd
- tests/isolation.mjs: Alice-vs-Bob cross-tenant cases for memory_get, get_fact, search, update, delete, reinforce, forget (soft+hard), consolidate, merge, entity_resolve, conflict_resolve, link/unlink, snapshot restore, and the REST endpoints /api/export, /api/import (id-collision attack), /api/purge, /api/links/:id, /api/graph, plus unauthenticated-access checks. No mocks: hits the real auth + D1 path. - tests/wrangler.isolation.toml: local-only worker config without AI / Vectorize bindings so the suite runs with zero Cloudflare credentials. - npm scripts test:isolation / dev:isolation / db:isolation; CI workflow .github/workflows/isolation.yml boots the worker, applies schema.sql, waits for readiness, runs the suite, tears down. - ci.yml now also runs the crypto unit tests. - tests/README.md documents the suite and the rule that every new memory-touching tool needs a cross-tenant case in the same PR. https://claude.ai/code/session_01TzX3Qb6rNYDfsRkmJV6hnd
SECURITY.md states what is defended (cross-tenant IDOR, token forgery, SQL injection, timing attacks, PKCE downgrade, brute force) and what is explicitly not (no E2E encryption at rest, self-hosted infra, stored prompt-injection content, async vector deletion residue), documents how brain_id scoping works, and sets a disclosure process with a 72h ack / 30d fix window. README gains CI badges plus Testing and Security sections. https://claude.ai/code/session_01TzX3Qb6rNYDfsRkmJV6hnd
Quality sweep otherwise found nothing to fix: zero any-casts, no TODO/FIXME/dead code, all multi-line memory SQL verified brain-scoped, tool errors stay inside the JSON-RPC envelope. https://claude.ai/code/session_01TzX3Qb6rNYDfsRkmJV6hnd
Mechanical move of the callTool switch cases into memory / graph / knowledge / trust-policy / snapshots / objectives / observability modules plus tools/shared.ts for common helpers; tools/index.ts keeps the exact callTool signature and unknown-tool error. Case bodies are verbatim; verified by calling all 44 tools before and after (responses identical modulo generated UUIDs), 18/18 isolation tests, and clean tsc. Every memory query keeps its brain_id = ? scope. https://claude.ai/code/session_01TzX3Qb6rNYDfsRkmJV6hnd
The two template literals are cut at safe line boundaries into markup, themes, styles (base/components/overlays), and four client-script modules, reassembled by viewer/index.ts in original order. Output is byte-identical: sha256 of viewerHtml() and viewerScript() match the pre-split build, verified both via direct import and against the live /view and /view.js endpoints. No file exceeds ~800 lines. https://claude.ai/code/session_01TzX3Qb6rNYDfsRkmJV6hnd
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
ai-memory-mcp-dev | 4f2424f | Commit Preview URL Branch Preview URL |
Jun 12 2026, 01:55 AM |
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
ai-memory-mcp | 411e94f | Commit Preview URL Branch Preview URL |
Jun 15 2026, 07:09 PM |
The crypto suite imports src/crypto.ts directly via Node's built-in type stripping, which requires Node 22.18+; CI was pinned to Node 20 and failed with ERR_UNKNOWN_FILE_EXTENSION. The smoke workflow stays on Node 20 since it is bash/curl-based and unaffected. https://claude.ai/code/session_01TzX3Qb6rNYDfsRkmJV6hnd
… failure - npm test now runs type-check + crypto suite (zero-setup); test:all chains the live isolation suite on top. - ci.yml runs npm test so new unit suites are picked up automatically. - crypto.test.mjs prints a pointer to the npm script when imported without the TS loader (ERR_MODULE_NOT_FOUND / UNKNOWN_FILE_EXTENSION), and tests/README.md documents the npm-script-only invocation and the Node 22.18+ requirement. https://claude.ai/code/session_01TzX3Qb6rNYDfsRkmJV6hnd
- Extract buildSemanticQueryOptions and filterSemanticMatches from querySemanticMemoryCandidates (behavior identical) so the namespace and metadata-brain-check isolation layers are unit-testable. - tests/vectorize.test.mjs (10 tests): vector-id prefix/length/brain-salt invariants, namespace always equals the caller brainId, foreign-brain metadata matches dropped, malformed matches tolerated. - tests/vectorize.integration.mjs: live cross-tenant semantic/hybrid search test for a deployed worker with real bindings; skips cleanly (exit 0) without VECTORIZE_TEST_URL. - npm test now chains the vectorize suite; tests/README.md documents lexical (local suite) vs semantic (integration) coverage. Code inspection confirmed the D1 re-fetch (loadMemoryRowsByIds) is brain_id-scoped; no fix was needed there. https://claude.ai/code/session_01TzX3Qb6rNYDfsRkmJV6hnd
Verification-only pass over auth: refresh rotation revokes the prior session immediately (replaced_by lineage), authenticateRequest enforces exp + revocation + the brain_memberships join, and session cookies are HttpOnly/Secure/SameSite=Lax with no path that drops Secure. No gaps found, so the only change is a 19th isolation case: 10 wrong-password logins return 401, the 11th returns 429, keyed per-run so the 15-minute KV window cannot bleed between runs. https://claude.ai/code/session_01TzX3Qb6rNYDfsRkmJV6hnd
States the three semantic-search isolation layers (namespace, metadata filter, brain-scoped re-fetch), points each mitigation at its enforcing test suite, and says plainly that the semantic path is verified by unit tests + inspection rather than a continuous live CI run. Notes the rate limiter is per-IP, not per-account. https://claude.ai/code/session_01TzX3Qb6rNYDfsRkmJV6hnd
…ules Mechanical move of the case blocks: conflicts.ts gets memory_conflicts + memory_conflict_resolve, entities.ts gets memory_entity_resolve, review.ts gets the temporal-cluster and spaced-repetition tools, and knowledge.ts keeps consolidate/forget/activate/reinforce/decay/merge, landing at 807 lines. Verified by probing all 44 tools before and after (responses identical beyond generated UUIDs), 19/19 isolation tests, clean tsc, and every moved memory query keeping its brain_id scope. https://claude.ai/code/session_01TzX3Qb6rNYDfsRkmJV6hnd
tests/wrangler.vec-iso.example.toml carries the verified binding names (DB, RATE_LIMIT_KV, AI, MEMORY_INDEX) and the 768-dim cosine index spec matching EMBEDDING_MODEL, with the full provision/deploy/run/teardown sequence in its header. The filled-in copy is gitignored since it holds account-specific resource ids. tests/README.md links the flow. https://claude.ai/code/session_01TzX3Qb6rNYDfsRkmJV6hnd
…rdening-x7rmlz' into feat/cognitive-memory-tools # Conflicts: # src/tools.ts # src/viewer.ts
Swap the amber terminal look for an espresso/cream palette. The list is now grouped by memory strength (Active / Settling / Resting) with a per-row strength meter and bead, and a 24h activity strip under the header replaces the stat pills. Login copy rewritten; bearer-token sign-in moves behind a disclosure but works as before. No behavior changes: every id, data-action, shortcut, overlay and setting is preserved, the other themes are ported onto the new tokens, and stored theme settings migrate to the new default. Facts now render their key once (title) with just the value below. Adds mockup/verify-ui.mjs, which boots the worker on the isolation config, seeds an account with aged memories via /api/import, and screenshots the login, desktop and mobile views (see mockup/shots/).
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
ai-memory-mcp-dev | 411e94f | Jun 15 2026, 07:09 PM |
Selecting "Custom" in either theme picker opens a builder for the six tokens the whole UI keys off (grounds, primary/secondary text, accent, rule) plus a font from a vetted, already-loaded list. The remaining tokens are derived from those six with documented formulas, so one palette reskins everything rather than leaving stray hardcoded colours. Tokens inject as a single style block scoped to [data-theme="custom"] and apply through the existing applyTheme path. A WCAG AA contrast check warns (without blocking) when text-on-background drops below 4.5:1. The compact-cards toggle previously only tightened padding via a body class and never touched the row body, so it read as half-wired. Drive density off a data-density attribute on the list container instead and clamp the memory preview to one line. Preferences also now write through to the server in the background, with localStorage as the offline fallback.
Settings lived only in localStorage, so a user's look didn't follow them across devices. Add a viewer_settings table (brain_id keyed, mirrors brain_policies) and GET/PUT /api/viewer-settings scoped through the existing auth context exactly like the other /api/* routes. The PUT payload is whitelisted server-side: unknown keys are dropped, numbers clamped to the UI's ranges, and the custom palette reduced to six hex colours plus a vetted font key. Adds an isolation case proving one brain can neither read nor overwrite another's settings, and extends the unauthenticated-rejection checks to cover the new route.
The landing, /mcp, and endpoint-guide pages still rendered in the old dark-blue terminal palette and read like a different product. Move them onto the viewer's dark-vanilla system, sharing its tokens and font setup from a new src/viewer/tokens.ts so the hex values live in one place instead of five. The pages keep their structure and content; only the chrome changes, and the old Share Tech Mono / Syne fonts are dropped.
The OAuth authorize page hardcoded its own blue palette. Reuse the viewer's login-card look (recall-tick mark, Fraunces wordmark, butter primary) via the shared tokens. Behavior is unchanged: the form action, method, hidden state/PKCE inputs, field names, and button name/value attributes are all preserved -- only the wrapper markup and CSS differ.
The live poll keyed off a per-type count signature, so edits, reinforcement, decay, and link changes left the list, recall ribbon, and tiers stale until a manual refresh. Add a cheap /api/memories/signature probe (count + newest updated_at + link total) and poll that instead. Also re-render the recall ribbon on a one-minute wall-clock tick so its 24h window keeps breathing as memories age out even when no data changes.
Only /view applied the saved theme; the landing, /mcp, endpoint-guide, and OAuth pages were hardcoded vanilla. Factor the theme resolver and custom-token derivation into a shared module (out of client-core) and ship a small /theme-bootstrap.js that sets data-theme before paint, plus every theme-variant token block, on each page. It loads from same-origin because the page CSP forbids inline scripts. OAuth is presentation only — forms, fields, hidden inputs, and submit targets are unchanged.
The recall ribbon and header summary were fed the type-filtered memory list, so filtering to Notes/Facts/Journal scoped the ambient activity readout to that type. Track the whole brain (corpusMemories) separately from the filtered grid (displayedMemories): the ribbon, link count, and the once-a-minute breathe tick read the corpus; the grid still honours the filter. When a filter or search is active the corpus is refreshed from an unfiltered request. Server stats were already full-corpus, so the per-tab counts were already correct.
The update banner was a translucent normal-flow sibling under a sticky header with little top spacing, so on scroll the opaque header collided with it and they sat cramped together. Give the banner clear separation, an opaque surface background, and its own stacking context below the header so it scrolls away cleanly with no overlap at any scroll position.
None of the existing themes is a neutral clean dark — vanilla is warm, the rest are tinted. Add slate (and slate-light): true grays with one muted slate-blue accent, registered everywhere the other themes are (client and server whitelists, the bootstrap resolver, the pickers). Default theme and light_theme to slate so new users and the server- rendered pages open clean-dark. Existing saved themes are preserved; only the absence of a setting falls through to the new default.
The live poll refreshed the recall ribbon but not the open graph, so it went stale until a manual action. On a detected change while the graph is visible, re-fetch and re-render it quietly — carrying over current node positions and the zoom/pan transform so new data settles in place instead of relaying out from scratch.
The banner and changelog still advertised the old cognitive-tool release and said nothing about the recent work. Add a 1.12.0 entry covering the viewer redesign and clean-dark default, custom and site-wide theming, the live full-corpus recall view, and the auth hardening, and bump the version.
Add a paper theme — a warm near-white reading surface with Spectral for content, Inter for chrome, and JetBrains Mono for data, with one restrained indigo accent — plus a dark counterpart, registered in every place themes are. Make it the default light theme so auto pairs paper by day with the clean-dark slate at night; every other theme stays selectable and saved choices are kept, with only the absence of a setting falling through. Centre the reading column for the document feel while the graph view stays full-width.
Replace the bare endpoint list at / with a real landing page in the paper design: a sticky nav with a light/dark toggle, a hero, a viewer facsimile, the graph-and-open and self-hosted features, how-it-works, pricing, an FAQ, and a footer. The lead order keeps graph-with-nothing-paywalled first, self-hosted second, and isolation as the closing FAQ point. The developer endpoint reference moves to /endpoints, linked from Docs and the footer. The hosted tier is marked coming soon since there's no billing yet.
Convert the hero photo to WebP (a full and a mobile variant) and the paper viewer screenshot, embed them as base64, and add a small helper to decode and serve them. Keeping the bytes in the Worker means real images on the landing page with no build step and no new dependency.
Replace the bare / with a full marketing page: a photographic hero with a per-letter blur-up headline, glass nav, and CTAs that melt into the paper page, then the product shot, features, pricing, and an FAQ accordion below. The animations are plain CSS keyframes plus one IntersectionObserver served from /landing.js (the page CSP forbids inline scripts); a reduced-motion block and html.js gating keep everything static and visible without motion or JS. The developer endpoint reference stays at /endpoints.
Give the hero its own contrast independent of the photo: a radial text scrim that travels with the copy, the near-white fade-blobs pushed low and out of the text zone, and the mono sub-line rendered as glass pills above the CTAs so it reads over the bright water too. Verified at 1280 and 390. Lock every public page (landing, /mcp, guides, /endpoints, OAuth) to the light paper theme: they hard-set data-theme and color-scheme and no longer ship the theme bootstrap, so a visitor in OS dark still sees the light design and no global dark state leaks in. Only /view keeps the full theme system. Removing the bootstrap is why the reveal and headline fallbacks switch from an html.js gate to a noscript one.
Replace the embedded WebP hero background (full + mobile variants) on the / landing page with the new photograph. Dimensions and asset paths are unchanged (1672x940 full, 836x470 mobile).
The hero WebP is served immutable at a fixed path, so browsers and the CDN kept serving the old cached image after the swap. Version the asset URLs (?v=2) so the new hero is fetched. Route matching keys off pathname and is unaffected by the query string.
- Brand in the hero nav now reads 'memoryvault' (removed the .md suffix and its now-unused styles). - Fix the headline overflowing narrow screens: the per-letter animation built spaces as non-breaking inline-block spans, so 'The memory layer your' could not wrap. Spaces are now real collapsible whitespace, so the headline wraps between words on phones. - Add overflow-x:hidden and a phone (<=480px) breakpoint; scale hero type, eyebrow, section headings and CTAs down, give the product shot a side gutter, full-width stacked CTAs, and reduce hero bottom padding so the fold isn't mostly empty on small screens.
Re-export the hero at the source's native 1672x940 at WebP q90 with a light unsharp pass, so the sky and water gradients no longer show the softness/blockiness the old ~221KB export had. The source art is only 1672px wide, so it still upscales on 2x/4K screens — a >=3000px source is needed for true retina sharpness; do not fake it by upscaling. On phones the landscape image was forced to cover a portrait viewport, so cover zoomed into a magnified centre slice. Ship a portrait crop (545x940) via the existing mobile <source>, shorten the mobile hero, and pin object-position so the crop barely has to zoom. Also stop the animated headline breaking mid-word: each word is now its own inline-block wrapper, so it only breaks at spaces at any width.
The shot used margin -9vh (and -6vh on mobile), pulling the viewer card
up over the bottom of the hero photo so the two images collided. Drop
the negative margin: the hero resolves fully to paper, then the shot
sits centred below it with its shadow reading against the page. A quiet
Spectral caption ('Your memory, legible') introduces it so it reads as
a deliberate product moment rather than a floating screenshot.
Shared deep-space tokens, the background gradient, and a hand-written canvas starfield (glow gradient + white core, sparkle spikes, additive blending, twinkle, drift, cursor parallax, linking lines). Animated for the landing hero; a sparse static mode for calm pages. No new deps.
The landing now opens on the living memory-graph hero (the animated starfield over the deep-space gradient) with the tier legend tying star colors to memory states; the content sections sit calm below it. Every other public page — /mcp, the endpoint guides, /endpoints, and the OAuth screens — takes the same dark identity with a sparse static field so forms and docs stay legible. Drops the photographic hero and its embedded images. /view keeps its own theme system; OAuth behavior is unchanged. Hero and site theming land together because all four pages share one token swap (vanilla -> constellation); the shared module went first.
Aligns the shared tokens to the final mockup (bg2/surface/rule, semantic names with component aliases). Reworks the starfield: smaller, crisper glow with a white core and no sparkle spikes, fewer hero stars, and a text dead-zone that fades stars and links to nothing behind the hero copy. Keeps the perf caps (DPR, density, offscreen/hidden pause) and the reduced-motion static frame.
Replaces the landing with the constellation-final design: the living-graph hero over a text dead-zone, blunt copy with no em-dashes, monospace only on the ledger keys and the wrangler.toml block, sections that flow without divider lines, a final-CTA glow that bleeds past its section, and a nav that collapses to a dropdown sheet on mobile. Drops the product shot (the only raster) and its embedded asset.
The shared page chrome and the OAuth screen used monospace for decorative labels (pills, sub-headers, button text, form labels), a flagged AI tell. Switches those to Inter, keeping monospace for genuine code, endpoint URLs, and identifiers. Also drops an em-dash from an OAuth label. Form fields, names, and behavior are unchanged.
… seam The stars stopped and the background tone changed on the same row, stacking two soft edges into one visible line. The canvas now runs to 135vh and its lower edge is masked, so the stars thin out below the boundary instead of on it, and a main::after gradient eases the background tone across a different, taller band. Because the dead-zone is a fraction of canvas height, its constants are rescaled so the protected ellipse stays over the same viewport region with the taller canvas (the seam-fix reference mock does the same). Pixel probe at x=60..120 across the boundary: max background luminance step 1.5 (well under 6), smooth from hero into the first section.
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.
comparison (closes a timing side-channel on both paths).
not alg=HS256 / typ=JWT (alg-confusion hardening).
embed their iteration count so existing credentials keep verifying.
round-trip/rejection, signature tamper, alg=none, expiry, and missing
claims, plus a loader shim so tests import the real TS sources.
https://claude.ai/code/session_01TzX3Qb6rNYDfsRkmJV6hnd
Viewer customization
On top of the preset themes, you can now build and save your own. Picking "Custom" in either theme picker opens a small builder: six colours (the grounds, primary/secondary text, the accent, and the rule) plus a font from a short vetted list. Everything else — the dimmer surfaces, the muted accent, the borders — is derived from those six with fixed formulas, so a single palette reskins the whole interface rather than leaving stray hardcoded colours behind. Colours inject as one style block scoped to
[data-theme="custom"], applied through the same path as the presets.The trade-off with free colour choice is that someone can make the app unreadable. Rather than constrain the palette, there's a quiet contrast check: if the text-on-background ratio falls below WCAG AA (4.5:1) the builder warns you, but it never blocks saving — your call.
A couple of things I deliberately left out: no arbitrary font URLs (the font options are families the page already loads, so no surprise layout breakage or extra requests), and no per-element colour overrides (everything goes through the existing CSS variables). I also kept density as a single global control instead of duplicating it inside the builder, since density isn't theme-specific. While here I fixed that compact-density toggle, which previously only tightened padding and never affected the row body — it now drives a
data-densityattribute and clamps the preview to one line.Settings (including the custom palette) now persist server-side, brain-scoped, behind
GET/PUT /api/viewer-settingsthrough the same auth context as every other/api/*route —localStorageremains the offline fallback and instant-paint source. That new endpoint is the only place this work touches the security surface, and it's covered by an isolation test (one brain can neither read nor overwrite another's row). Verified withnpm test, a livetest:isolationrun (20/20), and screenshots of the picker, builder, compact list, and a fully custom-themed list.Site-wide vanilla consistency
The redesign now covers every page the worker serves, not just
/view. The landing page, the/mcpguide, the per-endpoint guides, and the OAuth authorize/login screen all render in the same dark-vanilla system, sharing the viewer's design tokens from a single module (src/viewer/tokens.ts) rather than copying hex into each page. The OAuth screen reuses the login-card look but its behavior is untouched — every form field, hidden input, submit target, and buttonname/valueis preserved; only the markup wrapper and CSS changed. Verified with the screenshots,npm test, and the live isolation run (still 20/20).Reactive view and site-wide appearance
The live view now refreshes on any content change — edits, reinforcement, decay, link changes — not just when the memory count changes. The 10s poll keyed off a per-type count signature and missed everything that altered a memory's strength without changing the count, so recall/strength/tiers went stale until a manual refresh. It now polls a cheap
/api/memories/signatureprobe (count + newestupdated_at+ link total). The recall ribbon also re-renders on a one-minute wall-clock tick so its 24h window keeps breathing as memories age out. The recall ribbon and the entry/link summary are whole-brain readouts: they reflect the full corpus regardless of the active Notes/Facts/Journal tab or an in-progress search, while the list below honours the filter.Appearance now themes every page the worker serves, not only
/view. The landing,/mcp, endpoint guides, and the OAuth screens were restyled onto the vanilla tokens earlier but rendered the default theme; they now read the saved settings through a shared theme bootstrap and resolve the chosen preset or custom palette before paint. The resolver and custom-token derivation were factored into one module so the viewer and the pages run the same code — no duplication. The bootstrap loads from a same-origin/theme-bootstrap.jsrather than an inline script, because the page CSP forbids inline scripts; set the theme to midnight or a custom palette and/,/mcp, a guide, and the authorize screen all follow. OAuth behaviour is unchanged — only its presentation is themed. Verified with before/after reactivity screenshots, the per-page theme screenshots,npm test, and the live isolation run (still 20/20).Clean-dark default and polish
The default appearance is now a neutral clean-dark theme (slate) — the warm vanilla look and the other presets are still selectable, and anyone with a saved theme keeps it; only the absence of a setting falls through to the new default, so the server-rendered pages open clean-dark too. Alongside that: the update banner no longer collides with the sticky header on scroll, the in-app changelog now describes what actually shipped in this PR (bumped to 1.12.0), and the open graph view refreshes live on data changes — preserving zoom/pan and node positions — instead of only the recall ribbon. Verified with
npm test, the live isolation run (20/20), and screenshots of the default theme, the banner at rest and scrolled (desktop and mobile), the changelog overlay, and the graph updating without a manual refresh.Paper (Spectral) default and a marketing landing
The default appearance is now the paper light theme — Spectral for content, a calm near-white surface, one indigo accent — paired by
autowith the clean-dark slate at night; every other theme is still selectable and saved choices are kept, with only the absence of a setting falling through. And/is now a real marketing landing page in that design, with a working light/dark toggle, explaining the product (open and graph-included, self-hosted on your own Cloudflare account, works with any MCP agent) and linking to the app, GitHub, and docs. The hosted tier is marked "coming soon" since there's no billing yet. The old developer endpoint reference moved to/endpoints, linked from Docs and the footer. OAuth and the rest of the site theme to the same default. Verified with screenshots (landing in light, dark, and mobile reflow; the viewer and OAuth in paper),npm test, and the live isolation run (still 20/20).Photographic landing hero
/now opens with a full-bleed photographic hero — a per-letter blur-up headline, a glass nav, and CTAs over the photo that melt into the paper page via a gradient and glow fade — with the product shot, features, pricing, and an FAQ accordion flowing underneath on the same page. It's all hand-written HTML/CSS plus a little vanilla JS (CSS keyframes and one IntersectionObserver), no framework and no build step; the hero photo ships as an embedded WebP asset with a full and a mobile variant. Reduced-motion and no-JS both render everything static and visible. The page is light/dark aware on the paper default, and the developer endpoint reference stays at/endpoints. Verified with screenshots (hero at 1280 and 390, the product-shot overlap, a revealed section, the FAQ open, and dark mode),npm test, and the isolation run (still 20/20).Hero readability and a site-wide light lock
I made the hero text carry its own contrast instead of depending on the photo: a radial scrim sits with the copy, the near-white fade-blobs are pushed low and out of the text zone, and the mono sub-line is now three glass pills above the CTAs — so everything stays legible even over the bright water (checked at 1280 and 390). And every public page (the landing,
/mcp, the guides,/endpoints, and the OAuth screens) is now locked to the light paper design: they hard-set the theme andcolor-scheme, drop the theme bootstrap entirely, and no longer read saved or OS theme — a visitor in dark mode still gets the light site. Only/viewkeeps the full theme system (light/dark/auto, the other palettes, the custom builder). Verified withnpm test, the isolation run (20/20), and screenshots of the settled hero, the product-shot overlap, a revealed section, the FAQ open, and a public page rendering light under OS-dark.Hero sharpness and product-shot transition
Re-exported the hero background at higher quality (native 1672x940, WebP q90 + a light unsharp pass) and shipped a portrait crop for phones so
coverno longer zooms into a magnified centre slice; the source art is only 1672px wide, so a >=3000px source is still needed for true 2x/4K sharpness. Fixed the product shot crashing into the bottom of the hero photo — it now sits cleanly below on paper with a quiet caption. Verified at 1440/1280/390 with screenshots,npm test,npx tsc --noEmit, and the live isolation run (20/20).A dark "constellation" identity for the public site
This supersedes the photographic light-paper direction above. Every non-
/viewpage now shares one dark deep-space theme (shared tokens, Spectral/Inter/JetBrains Mono). The landing opens on a living memory-graph hero: a hand-written<canvas>starfield of glowing, drifting, twinkling, parallaxing stars — each a radial glow with a white core, the larger ones with sparkle spikes, linked by faint lines as the graph — whose colours map to memory tiers (active/settling/fading/reinforced), with a legend tying the visual to the product. No dependencies, no build step. It's performance-capped (DPR<=2, density scaled to width, the loop pauses when the hero scrolls away or the tab is hidden, throttled framerate) and reduced-motion-safe (a single static frame). The content sections sit calm on the deep-space gradient below the hero, and the utility pages —/mcp, the endpoint guides,/endpoints, and the OAuth authorize/login screens — take the same identity with a sparse static field so forms and docs stay perfectly legible. The photographic hero and its embedded images are gone./viewkeeps its own theme system; OAuth forms and behaviour are byte-identical. Verified withnpx tsc --noEmit,npm test, the live isolation run (20/20), and screenshots of the hero, a calm content section, the OAuth screen, and the mobile hero.Final "Constellation" pass on the public site
The landing is rebuilt to the constellation-final design: a living memory-graph hero with a text dead-zone so no star sits behind the copy, crisp glints instead of soft bloom, blunt copy ("Your agents forget everything. Fix that."), monospace used only for real code (the ledger keys and a
wrangler.tomlblock), sections that flow without divider lines, a final-CTA glow that bleeds past its section, and a nav that collapses to a dropdown sheet on mobile. No em-dashes anywhere in rendered copy. The product shot (the only raster) is dropped. Utility pages and the OAuth screen share the same dark theme with a calm static starfield and now use monospace only for code; OAuth form and behaviour are unchanged. The hosted tier links to a GitHub waitlist (no billing yet)./viewtheming is untouched. Perf-capped (DPR, density, offscreen/hidden pause) and reduced-motion-safe. Verified withnpx tsc --noEmit,npm test, the live isolation run (20/20), and screenshots of the hero, a feature section, how-it-works, the final CTA, the OAuth screen, and the mobile hero plus open menu.Hero/section seam fix
The hero/section seam is resolved by extending and masking the canvas so the stars thin out below the boundary, and easing the background tone across a different, taller band. Verified on the live worker with a pixel probe (max background luminance step 1.5, well under 6) and boundary screenshots at 1280 and 390.