Repository navigation
Conversation
…rdening
Complete the davis9001.dev CMS upstreaming into the kit and improve on it.
- Write-path hardening: add cms/sanitize.ts (xss) and run sanitizeRichtextFields
on POST and PUT before storage.
- Public rendering: add CmsContent.svelte and wire it into the public content
route so embeds mount on live pages.
- R2 image field: add ImageField.svelte (upload-to-R2) and use it for image
fields in the admin editor.
- Embed system redesign (roadmap "make it better"):
- Typed props schema (props-schema.ts) with an auto-generated editor form,
replacing raw-JSON prop editing (JSON kept as a fallback).
- One-step registration: manifest/index auto-discover embeds via
import.meta.glob — drop a folder under embeds/<name>/ (definition.ts + a
.svelte), no central list.
- SSR embeds via an eager component registry.
- Ship a brand-neutral Callout reference embed (Dirac physics embeds
intentionally left downstream). Docs: docs/CMS_EMBEDS.md.
Tests: +71 passing (sanitize, embed codec, upload, typed props, registry,
write-path). New CMS .ts code at 100% coverage. Typecheck and Cloudflare build
clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UMGQ6kfKSvErz11TUfhKiX
Add server.allowedHosts: ['.starspace.group'] so Cloudflare dev tunnels (dev-nebulakit-<hash>.starspace.group) aren't blocked by Vite's host check. Dev-server only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UMGQ6kfKSvErz11TUfhKiX
Bring davis9001's dedicated CMS editor into the kit and extend it so both creating and editing an item happen on their own full page (no modals). - Add CmsItemEditor.svelte: a shared two-column editor (content fields + sticky sidebar for status, command-palette visibility, SEO, save). Handles create (POST) and edit (PUT). Stripped of davis9001 app-specifics (predictions, field locking, timestamp-proof, tasklist). - Add routes: /admin/cms/[type]/new (create) and /admin/cms/[type]/[id] (edit), each with a clean server loader. `new` beats `[id]` in routing. - Slim the list page to list-only: New button and row Edit are now links to the dedicated pages; removed the create/edit modal and its handlers. Keeps filters, pagination, tag manager, and delete confirmation. - GET /api/cms/[type]/[id] now attaches the item's tags (best-effort) so the editor round-trips tag selections. - Fix the item-count bug: the list read `totalItems` but the API returns `total`, so the count always showed 0. Tests: add loader tests for both new routes and the GET tag-attach paths; fix the list-loader test mock to use the real `total` key. Full page flow verified end to end (list → new → create → edit) via a dev tunnel. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UMGQ6kfKSvErz11TUfhKiX
Decided behavior: logout keeps the user on public pages, goes to /auth/login with a return-to reference on protected ones, and re-login returns to the original page. Marked not-yet-implemented (logout currently always redirects to /auth/login); return-to must be validated as a same-origin relative path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Design doc + ROADMAP wiring for bringing Stripe into the kit with purchasing-power-parity region pricing on by default. Learned from the AgapeVerse rewrite: PPP rides on Stripe currency_options of existing prices (no new price IDs, no migration, entitlement/webhook logic untouched). ROADMAP's Stripe entry had omitted PPP entirely; now points at the design doc and lists region-pricing.ts / setup-region-prices.ts / fx.ts to lift. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018yhkXsEDzWYBgfSHwQazu7
… cached Cloudflare-proxied dev tunnels stamp a browser-cache TTL on query-less dev module URLs (.svelte-kit/generated/client/nodes/*.js). After routes change those nodes renumber, so a browser holding stale copies renders the wrong page. Set Cache-Control: no-store on the dev server to prevent it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018yhkXsEDzWYBgfSHwQazu7
Both branches had independently implemented CMS embeds, so this was a re-integration rather than a mechanical merge: 11 conflicted files, ~1,900 conflict lines. Donald chose to favor cms-v2 where the two designs collide. Resolved in an isolated worktree so conflict markers never entered the shared working tree, which another session is actively using. Favored cms-v2 (design conflicts): - admin/cms/[type]/+page.svelte — the full-page create/edit editor replaces the item modals (786 conflict lines). Main's modal-side publish-lock and contentTypeName changes to that file are superseded. - CmsContent.svelte, docs/CMS_EMBEDS.md, cms-embed.test.ts, cms-upload.test.ts — add/add conflicts, took the v2 implementation. - The embed registry now ships the `callout` embed. Main asserted the registry ships empty "so a new project inherits no embeds it did not ask for"; that assertion is replaced. Its real guarantees (unregistered names resolve to undefined, and to null rather than undefined for components) are preserved, retargeted at a genuinely unregistered name. Took the union where the sides were complementary, not competing: - api/cms/[type]/[id]/+server.ts — main's requireAdmin guard and session-derived provenance actor, AND v2's write-time richtext sanitization. Taking either side wholesale would have dropped real hardening. Same for both files' import sets. - vite.config.ts — main's config is a superset (staleDepsFix, preview port, HMR toggle, optimizeDeps); v2's only unique contribution was the .starspace.group tunnel host, which is now unioned into allowedHosts. The 95% coverage thresholds block was outside the conflict and is unchanged. - [contentType]/[slug]/+page.svelte — same design both sides; kept main's `?? ''` null-guards, which stop a literal "undefined" rendering. One deliberate deviation from "favor cms-v2", called out rather than silent: PUT with an unknown content type. The v2 branch let that write through with its richtext UNSANITIZED; the merged route 404s first. Rejecting the write is strictly safer than storing dirty HTML, and main's timestamp-proof path needs the content type to exist anyway. The v2 test asserting the tolerant behavior now asserts the 404. Also fixed while merging: the resolution initially issued a second, shadowing getContentTypeBySlug query in PUT — the handler already resolves the content type above, so it now reuses it. CmsContent's `html` prop is typed `string | null | undefined` to match its own `html || ''` runtime handling and the nullable CMS field values callers pass. Gate: svelte-check 0 errors, 1980 tests pass (0 fail), coverage 98.2% stmts / 95.1% branches / 98.14% funcs / 98.2% lines — above the 95 floor on all four.
NebulaKit is a product, not a starter template, and the repository still carried the onboarding scaffolding from when it was one. That scaffolding is now gone: CUSTOMIZE.md, INITIAL_CUSTOMIZATION_STATUS.md, SETUP_COMPLETE.md, docs/INITIAL_CUSTOMIZATION.md, docs/ZERO_ENV_SETUP.md, customize.config.example.json and scripts/customize.mjs are deleted, and the docs that described a rebranding step no longer do. src/lib/site.config.ts stays the single source of identity, and tests/unit/product-identity.test.ts reads README, FEATURES, site.config, /documentation, app.html and the manifest off disk so the surfaces that cannot import it fail loudly when they drift instead of rotting silently. Auth and OAuth hardening is the substantive half: - Secrets are split by purpose. AUTH_SECRET (a leftover from @auth/sveltekit, which nothing imported) is replaced by SESSION_SECRET and SETUP_SECRET, so session signing and the one-time setup gate no longer share a key. - Ownership is keyed on GITHUB_OWNER_ID, a numeric user id, instead of GITHUB_ADMIN_USERNAME. GitHub usernames are re-assignable; ids are not, so the old form let a renamed-and-reclaimed handle inherit owner rights. - The unused Google provider is dropped rather than left half-wired. - OAuth flow logic moves out of the route handlers into src/lib/utils/oauth-state.ts, src/lib/server/oauth-account.ts and oauth-finalization.ts, with src/lib/server/auth-guards.ts holding the authorization checks. Migrations 0010 and 0011 add the one-time OAuth transaction table and stop persisting provider access tokens we never read. - src/lib/cms/sanitize.ts sanitizes richtext on the write path. New suites cover the boundaries this touches rather than only the happy paths: auth-hook-security, auth-security-routes, oauth-state-security, oauth-account, oauth-finalization, logout-security, admin-users-pii-security, cms-sanitization-security, security-boundaries, public-cms-discovery and usage-agent-hooks. Also adds LICENSE, the PWA icon set and site.webmanifest, and switches the Copilot instructions from npm to bun to match the sole lockfile. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The cms-v2 merge brought in richtext-embed-extension.ts, but only its pure helper (embedNodeToHtml) was exercised. The extension's stateful surface — attribute parse/render round-trips, the insertSvelteEmbed command, and the node view that wires the edit-props and remove buttons back into TipTap's command chain — was untested, which is the part most likely to break silently when the editor is refactored. Adds coverage for that surface, including the fallback paths: an unknown embed name renders the raw name with no description, and a node view with no resolvable position must not dispatch a chain at all. 13 tests pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The SvelteEmbed node-view test destructured the result of
Array.from(dom.querySelectorAll('button')). `dom` comes back from an untyped
TipTap node view, so both elements widened to `unknown` and svelte-check
failed on the .click() calls. Vitest does not typecheck, so the suite passed
while `bun run check` did not.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Formatting only — no assertions, mocks or behavior changed. These three files landed in the template-retirement commit without having been run through Prettier; `bunx prettier --check` now passes on them. 8 tests still pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Logs what the stash-apply resolution actually verified rather than asserting it was fine: 34 conflicts resolved with no markers left, check clean, test:coverage green at 2,149 passing (97.88% statements, 95.18% branches, 98.00% functions, 98.44% lines), no pending local migrations, 8 Chromium e2e tests passing, contrast passing on both themes, and build:ci completing with the expected placeholder-binding warning. Also records the honest gaps and boundaries: HawkScan could not run (no hawk runtime, no Docker, no API key), so DAST remains missing evidence rather than a pass; and the note that another process advanced local main to 924a5ac and 9058278 during verification. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CLAUDE.md carried one factual drift and three gaps:
- Test topology described `poolOptions.threads.singleThread: true`; the
config actually uses `pool: 'threads'` with `fileParallelism: false`.
Same serial execution, different key — the `unstubGlobals` leak
narrative was correct and is unchanged.
- Record that AGENTS.md requires reading tasks/goals.md and tasks/todo.md
before editing, and that uncommitted changes are user-owned.
- Add a docs/ pointer table. hooks.server.ts cites ADMIN_STATS.md and
AGENT_READINESS.md by path in source comments; the map was nowhere.
- Name src/lib/server/auth-guards.ts in the auth section, which AGENTS.md
mandates reusing.
Also make the `§N` convention explicit: it indexes AGENTS.md's Release
Rules bullets, a convention vite.config.ts shares in a comment.
migrations/README.md's inventory omitted 0010_content_item_timestamp_proof
.sql, so it under-reported what is on disk. Two files share the `0010_`
prefix, meaning filename sort — not sequence number — decides which runs
first; CLAUDE.md now records that.
README.md kept one starter-template line ("additions to the kit ...
upstream from downstream projects") that the template retirement missed,
contradicting AGENTS.md §5.
Verified: bun run check 0 errors/0 warnings; product-identity and
agent-readiness suites 53 passing; Prettier clean on all touched files.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XYSrNbAxjFZwDood16sx6a
The remaining ledger item is partially done and partially blocked on access this account does not hold. Recording both precisely, per the ledger's own rule about not treating a blocker as a passing gate. Done: diff reviewed for publication safety, doc corrections pushed, draft PR #6 head reconciled with local HEAD, remote state verified. Blocked: the PR's workflow run concluded action_required with zero jobs (fork-PR approval gate), and donaldfilimon holds pull-only permission on starspacegroup/NebulaKit, so neither approving the run nor squash-merging is possible without a maintainer. Also noted: the squash needs a Co-Authored-By trailer for the cms-v2-embeds lineage, and ROADMAP.md/PAYMENTS_AND_PPP.md carry private paths and third-party project names that are already public via PR #6. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XYSrNbAxjFZwDood16sx6a
The prior entry asserted the content was readable via PR #6 and that scrubbing was 'damage limitation, not prevention'. What was actually verified is that both repos report private: false, so the content is readable at the pushed fork branch. The stronger claim also pre-empted a judgement about third parties' project names that belongs to the owner. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XYSrNbAxjFZwDood16sx6a
Both repos are public. ROADMAP.md and PAYMENTS_AND_PPP.md disclosed absolute local filesystem paths, the names of private and third-party downstream projects, three downstream commit shas, and one third party's subscription pricing floor. Downstream sources are now identified by what they are rather than who owns them. Nabu keeps its name, being a first-party sibling already cross-linked from AGENTS.md; only its path is gone. The technical substance is unchanged: every upstream candidate keeps its rationale, design constraints, and security notes. The PPP worked example keeps its figures. With the owner removed, a Premium/price_ABC product at 999 usd with per-currency options is an anonymous illustration, and the arithmetic carries the doc's central point that PPP pricing is a discount rather than an FX conversion. The one genuine third-party business figure, its subscription floor, is gone. Verified: Prettier clean, bun run check 0 errors/0 warnings, product-identity and agent-readiness 53 passing, and a case-insensitive sweep for the scrubbed identifiers returns nothing in either file. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XYSrNbAxjFZwDood16sx6a
The entry still said the scrub was pending after 620f6e2 had already applied it, and it was the last file in the tree carrying the four project names that commit removed. Both are now fixed: the decision and its outcome are recorded, and the sources are described rather than named. Also lists what the decision did not cover — attribution comments, a setup-doc row, and test fixtures that pre-exist on public origin/main. The migration comment is called out separately because migrations are immutable, so it needs a deliberate remedy rather than an edit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XYSrNbAxjFZwDood16sx6a
Extends 620f6e2 to the attribution comments and setup-doc table that the docs scrub did not reach. Two source comments now credit 'a downstream app' rather than naming it, and the shared-database incident table identifies the six affected projects by role. The forensics are unchanged: same six rows, same collision, same conclusion. First-party names stay — NebulaKit and Guides are already cross-linked from AGENTS.md. migrations/0006_contact_form_submissions.sql keeps its attribution comment. Migration files are immutable under the Release Rules, and that outranks a cosmetic scrub; it needs a deliberate remedy from the owner instead. .remember/ was untracked but not ignored, so a git add -A could have committed session transcripts — including the identifiers just removed. vite.config.ts already excluded it from coverage; .gitignore now excludes it from commits. Verified: bun run check 0 errors/0 warnings; product-identity, agent-readiness and pii-mask suites 68 passing; touched-file Prettier clean. The unrelated type-union reformat Prettier wanted in contact-validation.ts was reverted — that drift pre-exists at HEAD and is not this change's business. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XYSrNbAxjFZwDood16sx6a
The goal cannot be done: its remaining acceptance criterion is a squash-merge into an org repo where this account holds pull only, and that repo's CI has never run for the PR because the fork approval gate parked it at action_required with zero jobs. Blocked on access, not on work. Records the full coverage gate (97.86/95.13/97.96/98.42 against a 95 floor) and check across 1,634 files, and names what is still not claimed as passing: DAST, production build, live bindings, and the sibling audit the ledger gates on this merge. Also records two residuals left deliberately: the immutable migration's attribution comment, and 33 files of pre-existing Prettier drift that a repo-wide sweep would have dumped into a 223-file PR awaiting merge. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XYSrNbAxjFZwDood16sx6a
The previous entry listed both as resting on prior evidence. Both were re-run against the current tree: contrast passed both themes, and E2E passed 8/8 Chromium tests in 41.6s with no pending local migrations. Only build:ci still rests on the earlier entry. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XYSrNbAxjFZwDood16sx6a
Seven findings, all verified against the source before acting. CLAUDE.md's migrations section documented the 0010_ collision as a permanent fact. It is not: only 0010_content_item_timestamp_proof.sql is on origin/main, so the other two are branch-only, have never been applied to a remote D1, and can still be renamed before merge. The section now says so, and flags that merging as-is bakes a duplicate number into main. It also claimed db-migrate.mjs refuses to run against a placeholder, without qualification. Line 63 exempts --local, which is what lets db:migrate:local and therefore test:e2e work on a fresh clone. Documented as load-bearing rather than as a bug to fix. The section further told the next reader to add a migrations/README.md row that the same commit had already added, and the header said renumbering AGENTS.md's Release Rules desyncs three files when git grep finds 14, four of them shipped source. Both corrected. ROADMAP.md promised every entry gives paths to lift from, which stopped being true when the credits entry was generalized during the identifier scrub; the intro now matches what the entries deliver. Its first line also still said 'the kit', the template framing retired elsewhere. The ledger claimed the touched-file Prettier gate still held. It did not: contact-validation.ts is a file this branch edits, so the 'unrelated drift' rationale never covered it. Prettier is applied, which collapses a type union. It also asserted davis9001 was the owner's own handle while ROADMAP.md anonymized it as third-party; both cannot be true, and the ledger should not have picked one without evidence. Recorded as one open decision. Verified: check 0 errors/0 warnings across 1,634 files; product-identity, agent-readiness, pii-mask 68 passing; contact-validation and contact 19 passing; Prettier clean on every file this session touched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XYSrNbAxjFZwDood16sx6a
The template retirement missed this file. ROADMAP.md called the product 'the kit' 17 times — the framing AGENTS.md §5 forbids and README was already corrected for. The P1 ledger item claiming the conversion was finished across Markdown was therefore not accurate for this file. Also removes a dangling reference to docs/ZERO_ENV_SETUP.md, which commit 924a5ac deleted when it retired the customization workflow. A code review flagged it as pre-existing context; it is a broken reference in a retired-workflow doc, which is the same class of defect the P1 item covers. Mechanical note: BSD sed does not support \b, so the first pass silently no-oped on most occurrences and the survivors were replaced with perl. Two artifacts that wrapped across lines escaped a line-based grep and were caught by a whitespace-normalized scan. Verified: Prettier clean, product-identity and agent-readiness 53 passing, no bare 'kit' and no dangling docs/ reference remain. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XYSrNbAxjFZwDood16sx6a
The independent-product conversion item claimed Markdown was done. README carried a starter-template line and ROADMAP.md called the product 'the kit' 17 times with a dangling reference to a doc the retirement commit deleted. Recording that the item is accurate as of 7cd85e6 rather than when it was first checked off, and noting the seven review findings fixed in 8d14eeb. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XYSrNbAxjFZwDood16sx6a
Code reviewFound 1 issue:
NebulaKit/migrations/0010_oauth_transactions.sql Lines 1 to 4 in 6cb967c 🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
A full review of PR #6 scored this the only finding above the reporting threshold. 0010_content_item_timestamp_proof.sql is already on origin/main, so numbering the new pair 0010_/0011_ produced two migrations sharing a prefix, where order falls to the filename string comparison rather than the sequence number. Renamed via git mv so history follows: 0010_oauth_transactions.sql -> 0011_oauth_transactions.sql, 0011_minimize_oauth_tokens.sql -> 0012_minimize_oauth_tokens.sql. Both were branch-only and had never been applied to a remote D1, so AGENTS.md §2 immutability did not bind them; merging as-is would have made the duplicate permanent on main and weakened the next-sequential rule for the siblings that share this file. Verified by replaying against a local D1 that had already applied both under their old names: db:migrate:local re-applied them cleanly, as the IF NOT EXISTS guard and the idempotent UPDATE predicted. check 0 errors/0 warnings; coverage 97.86/95.13/97.96/98.42 against a 95 floor; e2e 8/8. Doc references updated in migrations/README.md, CLAUDE.md, and tasks/todo.md. The CLAUDE.md section that described the collision is replaced with the rule that prevents it: take the next number from origin/main, not from your branch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XYSrNbAxjFZwDood16sx6a
93bf6aa consolidated src/lib/server/auth-guard.ts into auth-guards.ts and dropped the isSuperAdmin branch, deleting the covering test in the same commit with no rationale recorded. stats-guard.ts still honoured the flag, so the two disagreed: a superadmin passed canViewStats and canManageStatsConnection but was refused by every admin API. Restores the predicate in requireAdmin and requireOwner, along with the docstring explaining why the two files must agree, and declares isSuperAdmin on App.Locals so the extension point is typed rather than only structural in stats-guard. NebulaKit never sets the flag; the contract exists for downstream apps that add a tier above owner, which is what the deleted docstring said. The restored test now asserts the two guards agree with the stats guards for the same user, so the files cannot silently diverge again. Found by a review of PR #6. It scored 75 and so fell below that workflow's reporting threshold — correctly, since in-repo impact is nil — but the inconsistency is real and inherited by the siblings that share this code. Verified: check 0 errors/0 warnings; coverage 97.86/95.13/97.96/98.42 with the three new branches covered; auth-guard and stats-guard suites 10 passing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XYSrNbAxjFZwDood16sx6a
32 files carried formatting drift, including .prettierrc itself. Earlier sessions deferred this because a repo-wide sweep would add unrelated files to a large PR, and the project's stated gate is touched-file Prettier rather than repo-wide. Sweeping it now on request. Formatting only: prettier --write across the repo, no hand edits. 12 .svelte files are included, so the change was verified beyond the formatter. Verified: check 0 errors/0 warnings across 1,634 files; coverage 97.86/95.13/97.96/98.42 against a 95 floor; e2e 8/8; contrast both themes; build:ci compiles with the expected placeholder-binding warning. prettier --check now passes across the whole repo. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XYSrNbAxjFZwDood16sx6a
…duals The branch is merged into local main and pushed to the fork, which is PR #6's head. Publishing to origin/main is impossible from this account: git push --dry-run origin main returns 'Permission to starspacegroup/NebulaKit.git denied to donaldfilimon', HTTP 403. Recorded as measured evidence rather than inference from the permissions API. Two residuals closed since the last entry: the repository-wide Prettier drift (106c3ce) and the isSuperAdmin guard regression (e0e509c). build:ci has now been run, so no gate rests on earlier evidence. No direct push to origin/main was attempted beyond the dry run — it would bypass the PR this ledger specifies and publish 29 commits to a public default branch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XYSrNbAxjFZwDood16sx6a
ROADMAP.md anonymized davis9001 as a third-party downstream project while the ledger called it the owner's own handle and left it in test fixtures. Both could not be true, and waiting on that answer was the wrong gate: neutral fixtures are correct whichever way it resolves, costing nothing if the handle is the owner's and helping if it is not. Fixtures now use example-user and example.com across ContentProof.test.ts, branch-coverage-boost.test.ts, branch-final-push.test.ts, and wayback.test.ts. The URL-encoded form in wayback.test.ts was kept consistent with its plain counterpart. Nothing asserted the literal string — all 56 tests in those files pass unchanged. Verified: prettier --check clean repo-wide; check 0 errors/0 warnings; coverage 97.86/95.13/97.96/98.42. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XYSrNbAxjFZwDood16sx6a
Runs the read-only half of the deferred sibling audit, which does not depend on the PR #6 merge gate, across Guides, nabu, and sortalizer for 4 of the 7 P0 items. No sibling-repository file was staged or edited. - isSuperAdmin does not apply: absent from all three siblings. - Admin-users PII masking already present in Guides and sortalizer; nabu is not exposed, gating all three endpoints on requireOwner instead. - Destructive reset and auth-key administration are owner-gated everywhere. - One real gap: Guides alone guards admin/users/[id] PATCH and DELETE with an inline isOwner || isAdmin, so any admin can promote or delete users, where NebulaKit (924a5ac), nabu, and sortalizer all require owner. Also re-verifies the 403 rather than carrying the earlier entry forward, and records that 3 of the 7 P0 items remain unaudited. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YWw88WCoHmDnMLdiHUB8pn
…all trap Three gaps in CLAUDE.md, all verified against enforcement rather than doc comments before being written down. The RFC 3161 timestamp-proof subsystem had no mention anywhere despite being real: src/lib/content-proof/, src/lib/timestamp/, migration 0010_, three API routes, and seven test files. It has no docs/ note either, so the source and tests were its only specification. Records that settings.enableTimestampProof gates it, that dispatch is first-publish-only through waitUntil, and that the third-party reproducibility of the hash rests entirely on lockedAfterPublish and lockTitleAndSlugAfterPublish actually freezing the hashed fields — weaken either and every existing proof stops verifying with nothing failing at write time. CI topology was absent. Adds the two jobs, the absent deploy job and why, and the trap behind it: validate:all runs test, not test:coverage, so the 95% floor never fires under it, and it skips build:ci and E2E entirely. Framed as the workflow's intended gates, with the note that it has never executed on starspacegroup/NebulaKit — reporting it as passing would violate AGENTS.md's Verification rule. Also maps the chat/AI files behind the two chat docs already in the table. AGENTS.md is untouched: its section numbering is referenced from 14 tracked files, four of them shipped source. Verified: prettier --check CLAUDE.md clean; product-identity.test.ts 6/6, which reads CLAUDE.md off disk and asserts on its content. No full gate run — this is a Markdown-only change and AGENTS.md scopes that gate to source. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Thanks for this — the auth and OAuth work here is genuinely stronger than what's on Recommendation: split before merging. Details below. Blocking — CI cannot install this branch
This is why the PR shows no checks — nothing has ever run against it, so the verification in the description is local-only. It also breaks downstream: the Cloudflare Pages build image ships bun 1.2.15, older still, and derived projects install with Fix is either bumping Blocking — the Svelte 5 migration should be its own PRThe description doesn't mention it, but this upgrades the whole toolchain by major versions:
Credit where it's due: it typechecks clean (0 errors across 1621 files) and I reproduced your test numbers exactly — 2128 passed, 22 skipped. But a framework major belongs in a PR where that's the headline, so it can be reverted independently if something surfaces in a downstream app. Bundled with a security fix, neither can be rolled back without the other. Needs a product decision, not a review
That contradicts the README, This might be the right direction! But it's a positioning call for the maintainer to make deliberately and record, not something to land inside a security PR. Could it come out of this one? Two defects1. Session expiry is up to 24 hours late. sqlite> SELECT '2026-08-08T00:00:00.000Z' > '2026-08-08 07:00:00'; -- 1 (expired 7h ago, still valid)
sqlite> SELECT '2026-08-08T06:59:00.000Z' > '2026-08-08 07:00:00'; -- 1 (expired 1m ago, still valid)
sqlite> SELECT '2026-08-01T07:00:00.000Z' > '2026-08-08 07:00:00'; -- 0 (correct once the date rolls)Any session expiring on the current calendar day survives until midnight UTC. Same pattern in This is pre-existing on 2. What I verified and liked
Suggested split
Happy to re-review any of them individually. |
…ner bypass
The session cookie is unsigned base64(JSON(user)) that hooks.server.ts trusts
verbatim, including isOwner/isAdmin, and only ever refreshes isAdmin/canViewStats
from the DB — never isOwner. So anyone can hand-craft
session=<base64 of {"id":"x","isOwner":true}>
and pass requireOwner on every admin/owner endpoint. No secret, no account. Every
project derived from this template inherits the bypass; verified live against one
deployed downstream (no cookie → 401, forged owner cookie → 200).
The cookie becomes an opaque session id. The trusted payload lives server-side in
sessions.data (migration 0011, additive + nullable) and is read back every
request via getAuthSession; a forged or unknown cookie names no row and resolves
to nobody, so hooks leaves the request unauthenticated — fail closed, including on
a DB error. All session issue points (login, signup, github ×3, discord ×3,
dev-simulate, connections) create a server session instead of encoding one, and a
login that cannot reach the store redirects rather than issuing a cookie the hooks
reject. logout and reset delete the row so a copied cookie cannot be replayed.
encodeSession/decodeSessionCookie are removed with the old scheme.
Also fixes, found alongside:
- getAuthSession/findValidSession/cleanupExpiredSessions compared expires_at with
datetime('now') against a stored ISO string — a raw string compare where 'T'
sorts after ' ', so a same-day-expired session read as valid for up to a day.
Normalized with datetime(expires_at).
- The dev-simulate bypass short-circuited on DEV_AUTH_BYPASS before the localhost
check, so a stray env var could re-enable the owner simulator on a deployed
host. Both paths now require a local host; the simulator also mints a real
server session, so it needs the DB too.
NOTE: this overlaps PR #6, which rewrites sessions to its own opaque-D1 scheme as
part of a much larger change. This is the security fix on its own so main stops
minting vulnerable derived projects now; #6 can rebase its session work on top.
Derived projects need migration 0011 applied before/with the deploy.
Tests: dedicated forgery/round-trip/expiry (auth-session-server), logout/reset
revocation (auth-session-teardown), and every auth suite updated to read the
server-side payload and assert fail-closed. Suite green (1979), svelte-check
clean, coverage 97.69% lines / 95.02% branches.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJpGb3i6KG2o941FwEuurW
Resolves the parallel-implementation conflict between origin/security/ opaque-sessions (db30a29, David Monaghan) and main's later HMAC-signed session scheme — both independently fixed the same forgeable-cookie owner bypass with incompatible designs. Per Donald's decision the OPAQUE design wins: the cookie carries an opaque random token, the trusted payload lives server-side in sessions.data, and a forged cookie names no session and fails closed. Main's competing signValue/verify cookie scheme is removed. Resolution record, per surface: - session.ts: David's, wholesale — every main-side addition was HMAC infra. - db.ts: union. David's createAuthSession/getAuthSession IMPROVED with two hardenings grafted from main's scheme (deliberate, not drift): tokens are 256-bit createSessionToken() output rather than UUIDs, and rows are keyed by hashSessionToken(token) so a leaked sessions table exposes no value a cookie could present. findValidSession keeps main's pre-lookup hashing AND David's datetime() expiry normalization. New replaceAuthSession revokes a previous token in the same D1 batch (link-flow re-auth, no replay). - hooks.server.ts: David's cookie mechanism + main's per-request privilege refresh — identity comes from the stored session, but is_admin / can_view_stats / owner status are re-read from the users table on every request so revocation needs no re-login. Pretend identities are honored only under isDevAuthSimulationEnabled, which keeps David's new local-host-only gate (a stray DEV_AUTH_BYPASS on a deployed host can no longer mint owner access). - OAuth callbacks (github/discord): MAIN's, wholesale — its transactions rewrite postdates and supersedes the legacy flow David's branch edited (his side still parsed the old forgeable base64 cookie for link mode). Session issuance swapped to the opaque scheme in oauth-finalization.ts; initiation + callback routes validate the raw cookie against the sessions table before binding transactions to it. - login/signup/logout/reset/connections/dev-simulate/dev-auth: David's shapes, with main's re-ports (auth-guards import path, reset's DB checks) and one behavior kept from main: logout logs a failed server-side revocation instead of swallowing it (a failed delete leaves a replayable row - worth a log line). - oauth-state.ts: main's signValue/verifySignedValue helpers RELOCATED here. The OAuth state cookie legitimately stays HMAC-signed; only the session cookie scheme was removed. - Migration renumbered 0011 -> 0013 (main had already shipped 0011_oauth_transactions and 0012_minimize_oauth_tokens; migrations are immutable). The ALTER is orthogonal to both. - Tests: adapted to the merged contract — hash-at-rest assertions (stored id == digest of cookie token, raw token never in the DB), auth-hook-security rewritten for the opaque hook incl. a NEW case pinning the non-local-host pretend rejection, Set-Cookie-reading suites opt into the undici Response helper, reset-calling mocks gained cookies.get. Gate: svelte-check 0 errors / 0 warnings across 1623 files; vitest 2138 passed / 0 failed (22 skipped); coverage 97.69% stmts / 95.02% branches / 97.95% funcs / 98.27% lines — all four above the 95 floor, thresholds untouched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011FBz9gfnaATmjb1C2kBiyc
|
Two things from a diagnostic pass — one is a CI root cause, the other is a fix you haven't seen yet because it landed after your review. 1. CI root cause: the lockfile, not the pinBoth jobs die in 5–7s at The cause is not a stale
It was regenerated by a newer bun during the CMS v2 / Tiptap work ( I deliberately have not pushed a fix, because every version of it is a decision rather than a repair, and it belongs in whichever PR the split lands in:
So the bun/lockfile question is the Svelte 5 / toolchain PR you asked to split out — it can't be settled independently of it. One thing for whoever does fix it: there is a parked stash ( 2. Session expiry is already fixed — your review predates itYou flagged expiry being off by up to 24h. That was fixed in the Still genuinely open from your review: 3. On the splitNo objection here — #7 being merged first and #6 rebasing its session work on top reads as the right order. Flagging only that this PR's current head has not been reviewed by anyone since 🤖 Generated with Claude Code |
CLAUDE.md was last written at 2f3a4b0; db30a29 and c7040ab landed after it and falsified four statements. Corrected, with two previously undocumented invariants recorded alongside them. - Migration inventory was one file behind in three places -- CLAUDE.md, the Current Migrations table in migrations/README.md (which omitted the row entirely), and the 0010_-collision entry in tasks/todo.md. 0013_session_payload.sql has landed; the next number is 0014_. The CLAUDE.md numbering paragraph now carries 0013_ as its second worked example: authored as 0011_ on the opaque-sessions branch, renumbered at merge because main had meanwhile shipped 0011_ and 0012_. - The auth paragraph still described the retired cookie scheme -- "a signed opaque session token ... its digest and expiry live in D1". Nothing is signed. Rewritten to the actual scheme: an opaque random token, the row keyed by its SHA-256, expiry and the trusted payload in sessions.data, and getAuthSession failing closed on both a forged cookie and a pre-0013_ row with no payload -- plus an explicit instruction not to reintroduce a cookie the hooks trust without a server-side lookup. - Newly documented: the datetime(expires_at) normalization (a raw string compare sorts 'T' after ' ' and reads a same-day expiry as valid for up to a day), and the two coexisting session APIs in db.ts. Checked and found still true, so recorded as a verification rather than changed: authHandler takes identity from the stored payload but re-reads is_admin, can_view_stats, and owner status from users on every request, so the AGENTS.md Security Boundaries rule holds and revocation still needs no re-login. The isPretend bypass, gated on isDevAuthSimulationEnabled, is now stated explicitly rather than left implicit. Everything else in CLAUDE.md was re-verified against source and still holds: the 95 thresholds in vite.config.ts, devPort 4277, the 14-file `git grep -ln "AGENTS.md §"` count, the CI job topology and BUN_VERSION pin, the docs/ table, and the absence of a lint/format script. Docs only, no source touched. prettier --check clean on all four files, git diff --check clean, and product-identity + agent-readiness pass 53/53 -- those are the suites that read these files off disk. No coverage or e2e claim is made, because nothing under coverage changed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
46a0247 went to PR #6's head branch rather than a new PR, because the corrections only parse on top of the opaque-sessions work that is not on origin/main -- a fresh PR would have shown 36 commits instead of four doc files. PR #6 reports MERGEABLE / CLEAN at that head. Also records, without acting on it, that PR #7 (security/opaque-sessions) overlaps PR #6, which already contains the merge of that branch at c7040ab. Merge order is a maintainer decision. The org-repo blocker is unchanged and re-verified rather than carried over: git push --dry-run origin main still returns 403. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…lose fork/dev fast-forwarded c7040ab -> 952438c, so it now contains both open PR heads. PR #7 needed no separate merge: its head db30a29 is already an ancestor of PR #6's head, since the opaque-sessions merge landed at c7040ab. Recorded explicitly because the merge is easy to over-read: both PRs target starspacegroup:main and remain OPEN, the org repo has no dev branch and one cannot be created from this account (403), and ci.yml triggers on main/develop so a dev branch would not run CI regardless. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…tems)
The three P0 items deferred in the fourth-session pass are now compared across
Guides, nabu, and sortalizer. Read-only throughout: no sibling-repository file
was staged, edited, or committed, and all three sibling trees are clean.
- Sanitize-before-{@html}: no gap. Verified across every {@html} site in each
repo rather than sampled. sortalizer has no CMS and no {@html} at all. nabu
covers both halves of the rule -- write path in cms.ts and again at the
render boundary in [contentType]/[slug]/+page.server.ts. Guides has no HTML
sanitizer and no xss dependency, which is not the gap it looks like: its
three {@html} sites are two escape-first renderMarkdownToHtml calls and one
hardcoded SVG literal. Guides stores Markdown where NebulaKit stores
rich-text HTML, so the sanitizer does not apply to it.
- One-time OAuth state: no gap. All four repositories consume the transaction
with a single atomic UPDATE ... consumed_at IS NULL AND
datetime(expires_at) > CURRENT_TIMESTAMP RETURNING, then enforce intent match
and session binding for link mode. Only packaging differs. A first pass
called sortalizer's expiry check missing; that was a truncated-grep false
positive and is recorded as one.
- Open finding, nabu only: vite.config.ts excludes src/hooks.server.ts from
coverage. nabu's handle is sequence(authHandler), so that file is its entire
authorization boundary. A unit test exists but supplies DB in all four cases,
so the fail-closed !db branch is exercised by nothing and the 95% floor
cannot notice, because the file is not measured. Below the Guides escalation
in severity -- no live privilege gap -- but the gate that would catch one is
off. Fix is one deleted line and belongs in a nabu branch.
No sibling test suite or coverage run was executed, so the nabu conclusion is a
source-reading result, not a measured percentage. Propagation of both findings
still waits on PR #6, so the ledger item stays [~] rather than [x].
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Superseded by #9 ( #9 is Note on this PR's red CI: it fails at 🤖 Generated with Claude Code |
|
Superseded by #9, now merged as Closing because every one of these 39 commits is already in #9 — verified by comparing commit sets, not titles. The nesting is complete: #8 (6) ⊂ #6 (39) ⊂ #9 (43), and #7 (1) ⊂ #9. #9 also carried three commits these branches lacked — Nothing here is lost. It is all on |
Summary
Verification
bun run check: 0 errors/warningsbun run test:coverage: 2,128 passed, 22 skipped; 97.86% statements, 95.13% branches, 97.96% functions, 98.42% linesbun run db:migrate:local: no pending migrationsbun run test:e2e: 8/8 passedbun run validate:contrast: passedbun run build:ci: passedgit diff --check: passedExternal blockers
Direct push to upstream
mainwas denied because the authenticated account has read-only repository permission; this fork PR contains the exact verified local merge.