Skip to content

Consolidate local main: CMS v2, opaque sessions, auth hardening, docs - #9

Merged
davis9001 merged 43 commits into
starspacegroup:mainfrom
donaldfilimon:consolidate/local-main-20260810
Aug 18, 2026
Merged

davis9001 merged 43 commits into
starspacegroup:mainfrom
donaldfilimon:consolidate/local-main-20260810

Conversation

@donaldfilimon

@donaldfilimon donaldfilimon commented Aug 10, 2026 •

Copy link
Copy Markdown
Contributor

What this is

The consolidated local main — 39 commits (253 files, +13,534/−8,273) that accumulated across the CMS v2, security, and quality-pass work streams, delivered as one reviewable branch since direct pushes to main are permission-blocked.

What changed

Security

  • Opaque server-side sessions: closes the forgeable-cookie owner bypass — the browser now holds only a random token; expiry and the trusted payload live in D1 (db30a29, merged and hardened in c7040ab)
  • isSuperAdmin restored in the consolidated admin guards after the auth-guard consolidation dropped it (e0e509c)
  • Security-boundary consolidation (93bf6aa) and OAuth write-path hardening (924a5ac)

CMS v2

  • WYSIWYG Svelte embeds with typed props and write-path hardening (3a793c7, merged in e24f2ce)
  • Full-page create/edit editor replacing item modals (d9519a5)
  • TipTap SvelteEmbed node-view test coverage (9058278, def34dd)

Migrations

  • Removed the 0010_ numbering collision via renumber while still branch-local (5139a14) — per migrations/README.md, only safe because nothing had applied them remotely

Docs & hygiene

  • CLAUDE.md/AGENTS.md realignment with the opaque-sessions design, content-proof + CI topology documentation, validate:all trap (2f3a4b0, 46a0247)
  • ROADMAP.md independent-product conversion, identifier/PII scrub, Prettier-clean tree (7cd85e6, 620f6e2, 106c3ce)
  • Task ledger (tasks/*.md) records for each delivery and its verification evidence

Reviewer notes

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

davis9001 and others added 30 commits July 12, 2026 07:13
…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
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
donaldfilimon and others added 9 commits August 8, 2026 05:52
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>
…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
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>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for security reviews. Please try again later.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 08ab6f9046

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/lib/components/CmsItemEditor.svelte
Comment thread src/app.d.ts
Comment thread src/routes/auth.md/+server.ts Outdated
Comment thread src/lib/cms/embeds/callout/Callout.svelte Outdated
Comment thread src/routes/api/setup/+server.ts Outdated
Comment thread src/routes/api/admin/users/+server.ts
Comment thread src/lib/utils/oauth-state.ts
Comment thread src/routes/api/chat/stream/+server.ts
Comment thread src/routes/reset/+page.server.ts
donaldfilimon and others added 4 commits August 10, 2026 18:28
CI was failing at install in under 15s: `bun.lock` is lockfileVersion 2,
which the pinned Bun 1.3.14 cannot parse, so `--frozen-lockfile` aborted
before any test ran. Pin CI to 1.4.0 to match the Bun that wrote the
lockfile, and record the coupling in CLAUDE.md.

Then the Codex review findings:

- setup (P1): three sequential KV writes could tear, leaving setup locked
  with no owner able to authenticate — and the reset endpoint that clears
  the lock is itself owner-only, so the deployment was unrecoverable
  without manual KV surgery. Roll back what was written so a failed
  attempt leaves no state and can simply be retried.
- admin users: while PII is masked the row ids are masks, so PATCH/DELETE
  targeted nonexistent rows (404) and the "not yourself" guard compared a
  mask against a real id and never matched. Gate the row actions and the
  invite flow on reveal rather than letting them fire. Masking itself is
  unchanged — `admin-users-pii-security.test.ts` still holds.
- superadmin: `isSuperAdmin` passed every admin API but was refused by the
  admin layout and hidden from the command palette and nav. Route the 12
  duplicated owner/admin predicates through `requireAdmin`, and extend the
  three checks that cannot call it (a redirect, two UI gates).
- oauth: every anonymous init inserted a row that consumption only stamped,
  with no cleanup for consumed or expired rows. Prune on the path that
  creates them — best-effort, so housekeeping never fails a sign-in. The
  replay boundary is untouched.
- chat: a model-less request took the first enabled key even when it was
  voice-only, returning 503 while /api/chat/models listed models from later
  keys. Prefer a chat-capable key; fall back so a genuinely chat-less
  configuration still 503s.
- tasklist fields rendered as a plain text input in the full-page editor,
  which stringified the `{ text, done }[]` value on save. Restore the
  `TaskListField` branch.
- `--color-danger` was referenced with a hardcoded `#ef4444` fallback and
  never defined. Add paired light/dark tokens; contrast validation passes.
- docs: the session cookie is random and hashed, not signed — `/auth.md`
  and the documentation page said "signed". The reset page still warned it
  was reachable without authentication after it became owner-only.

Gates: check clean, 2151 tests pass, branches 95.01% (floor 95),
validate:contrast passes. Six admin-users tests encoded the pre-fix
behavior (actions enabled while ids were masked) and were updated to
exercise the revealed state, with a new test covering the gated one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The previous attempt raised the CI pin to 1.4.0 to match the local Bun.
That was wrong twice over: 1.4.0 is not a released version — this machine
runs 1.4.0-canary.1 — so `setup-bun` 404s trying to download it, and the
newest release is the 1.3.14 CI was already pinned to.

The actual regression is the lockfile. `origin/main` carries
lockfileVersion 1; somewhere on this branch a canary Bun rewrote it as
version 2, which no released Bun can parse, so `--frozen-lockfile` aborted
before any test ran. Restore the 1.3.14 pin and regenerate bun.lock with a
downloaded 1.3.14 build — restoring main's lockfile was not an option,
since this branch legitimately upgrades Svelte, Vitest and Wrangler.

Verified with the exact command CI runs: `bun install --frozen-lockfile`
under 1.3.14 reports "Checked 239 installs across 317 packages (no
changes)". Gates re-run on the regenerated tree: check clean, 2151 tests
pass, branches 95.01%.

CLAUDE.md now says to check `bun --revision` before `bun install`, since
the failure mode is invisible locally and raising the pin cannot fix it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
With the install failure fixed, E2E ran in CI for the first time and both
command-palette tests proved flaky there — one failed, one passed only on
retry — while passing locally every time.

Neither is a product bug. Both waited on `networkidle` and then acted, but
`networkidle` says nothing about hydration: the button and page paint
server-side, so a click or Ctrl+K that lands before Svelte attaches its
handler is silently dropped, with nothing left to retry against. It only
shows on a loaded CI runner where hydration lags the network going quiet.

Wrap each open in `expect(...).toPass()` so the action is retried until the
dialog actually appears. The assertions themselves are unchanged.

Full suite green locally: 8 passed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CI has now executed on starspacegroup/NebulaKit: both jobs green at PR #9's
head 8443b8b (run 31439300377), so the 'workflow has never run' claims in
CLAUDE.md and tasks/goals.md were stale and are corrected with run ids.
PR #6/#7/#8 heads are all ancestors of #9's head (merge-base verified);
PR #6 now fails install on the canary bun.lock that e3b87b9 fixed on #9.
Push blocker re-verified 2026-08-10: origin main still 403.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@davis9001
davis9001 merged commit 80c2f4f into starspacegroup:main Aug 18, 2026
3 checks passed
davis9001 added a commit that referenced this pull request Aug 25, 2026
Local main had been carrying four commits off an older base (CMS fixes, the
Svelte-browser-build Vitest fix, the drag-and-drop widget board, an AI-keys test
fix) plus three more from blog-card-image-link, while origin/main moved 44
commits ahead with PR #9. This merges the two, keeping origin/main's direction.

NebulaKit is a product, not a starter template (924a5ac), so the customization
workflow stays retired: CUSTOMIZE.md, INITIAL_CUSTOMIZATION_STATUS.md,
docs/INITIAL_CUSTOMIZATION.md and scripts/customize.mjs are gone, and the
onboarding half of the credential-field change went with them. The rule itself
survives as a Load-Bearing Invariant in AGENTS.md: credential and secret inputs
take their id/name from fieldName(), which prefixes site.slug, so two
deployments never share field identifiers. The new /setup bootstrap-secret field
is prefixed too.

Conflict resolutions worth naming:

- The blog card keeps its link-wrapped image AND origin/main's sanitizeCmsUrl on
  the image URL. Losing the sanitizer to keep the link would have reopened the
  write-path hole.
- The CMS empty state keeps the filtered/unfiltered distinction, but creates
  through /admin/cms/<type>/new — origin/main replaced the item modals with a
  full-page editor, so the modal state went with them.
- vite.config.ts keeps svelteTesting() and drops the hand-rolled browser
  condition; the plugin does the same job.
- The AI-keys test keeps both the mount-time model response and origin/main's
  fetch stub.
- README and FEATURES are origin/main's, with the widget board written into
  them — it is a shipped feature now and their versions predate it.

WidgetBoard.test.ts needed a real migration: under origin/main's dependency set
Svelte 5 rejects component.$on and $set outright, so it listens through mount's
events option and updates props through rerender().

bun run check is clean, 2281 tests pass, coverage is 97.84% lines against the
95% floor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Sw6X7FaR4pwJwH7ANVd11
davis9001 added a commit that referenced this pull request Aug 25, 2026
PR #9 converted this repository into "an independent product, not a starter
template", deleted the customization workflow, and wrote a test to enforce it.
That is wrong: nebulakit-site is the marketing site for this template, and it
sells it as one — anyone following the site to the repo was told the opposite
thing. I framed that badly last time, as a merge-conflict question rather than
what it actually was, so this puts it back.

Restored from ab460a2^1: CUSTOMIZE.md, INITIAL_CUSTOMIZATION_STATUS.md,
docs/INITIAL_CUSTOMIZATION.md, docs/ZERO_ENV_SETUP.md,
customize.config.example.json, scripts/customize.mjs, the `customize` package
script, and the gitignore entry for its input file. That also brings back the
credential-field gate for new sites — the fields are only unique once the slug
belongs to the downstream app, which `bun run customize` sets.

SETUP_COMPLETE.md stays deleted; it was a stale "Copilot setup complete 🎉"
artifact, not part of the workflow.

product-identity.test.ts is inverted rather than removed. It now asserts the
workflow exists and is wired — files present, script in package.json, the input
file ignored, the entry-point docs naming the command — and that the public
surfaces call NebulaKit a starter template rather than a platform. Its icon,
manifest, and app.html assertions are untouched. The manifest description is
now checked against site.tagline instead of a hardcoded string, so the two
cannot drift.

Reframed: README, FEATURES, site.config, site.webmanifest, /documentation's
Start Here, the home page hero and features subtitles, AGENTS.md's fifth
Release Rule (kept in its slot — fourteen files cite AGENTS.md by number), and
CLAUDE.md.

Two defects found by actually running the restored script:

- It read site.config.ts with a single-quote-only regex, so the moment Prettier
  switched the tagline to double quotes — which it does as soon as the value
  contains an apostrophe, and "Cloudflare's full stack" does — the value came
  back undefined and was written straight back as the literal string
  "undefined". The parser now reads either quote style and handles escapes, and
  validate() refuses to write an unreadable value.
- The parser lived at module scope in a script that prompts on import, so it
  could not be tested. It is now scripts/site-config-parse.mjs, imported by
  customize.mjs and covered by tests/unit/customize-config-parse.test.ts.

bun run check is clean, 2287 tests pass, coverage 97.84% lines.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Sw6X7FaR4pwJwH7ANVd11
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