Skip to content

security: constant-time auth comparisons, pinned JWT alg, 600k PBKDF2 iterations#39

Open
guirguispierre wants to merge 47 commits into
masterfrom
claude/memoryvault-production-hardening-x7rmlz
Open

security: constant-time auth comparisons, pinned JWT alg, 600k PBKDF2 iterations#39
guirguispierre wants to merge 47 commits into
masterfrom
claude/memoryvault-production-hardening-x7rmlz

Conversation

@guirguispierre

@guirguispierre guirguispierre commented Jun 12, 2026

Copy link
Copy Markdown
Owner
  • 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


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-density attribute and clamps the preview to one line.

Settings (including the custom palette) now persist server-side, brain-scoped, behind GET/PUT /api/viewer-settings through the same auth context as every other /api/* route — localStorage remains 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 with npm test, a live test:isolation run (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 /mcp guide, 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 button name/value is 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/signature probe (count + newest updated_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.js rather 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 auto with 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 and color-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 /view keeps the full theme system (light/dark/auto, the other palettes, the custom builder). Verified with npm 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 cover no 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-/view page 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. /view keeps its own theme system; OAuth forms and behaviour are byte-identical. Verified with npx 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.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. 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). /view theming is untouched. Perf-capped (DPR, density, offscreen/hidden pause) and reduced-motion-safe. Verified with npx 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.

guirguispierre and others added 9 commits March 30, 2026 21:30
…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
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 12, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

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

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 12, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

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

claude and others added 10 commits June 12, 2026 00:18
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/).
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 12, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

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.
guirguispierre and others added 20 commits June 14, 2026 21:44
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants