` block with JUST the inner JSON text, stripping the wrapping script tags. This silently corrupts the registry HTML.", "current_code": " } catch {\n return _\n }", "fix_spec": "Change the catch block to return the full original match. The replace callback signature is (fullMatch, openTag, content, closeTag). The variable `_` was used as a placeholder name but it refers to the first captured group (openTag), not the full match. Fix: rename the callback params to use proper names and return openTag + content + closeTag (i.e., reconstruct the original) or capture the full match. Cleanest fix: `return regHtml.replace(metaPattern, (fullMatch, openTag, content, closeTag) => { try { ... } catch { return fullMatch } })`", "rollback": "git checkout -- lib/publish/core.ts", "acceptance_criteria": [ "If hc-metadata contains invalid JSON, the registry HTML is returned unchanged", "The

PRD: NowPage Infrastructure Hardening

17 issues • 4 sprints • Machine-readable for Claude Code autonomous execution
4
SPRINT 1: Critical
6
SPRINT 2: High
7
SPRINT 3: Medium
7
SPRINT 4: Low
Safety & Rollback Protocol

Before each sprint: git stash push -m "pre-sprint-S{n}"

After each file edit: npx tsc --noEmit

After each sprint: Run full test suite (57 tests)

Rollback single file: git checkout -- path/to/file.ts

Rollback full sprint: git revert HEAD (if committed) or git stash pop (if stashed)

Registry safety: All mutations auto-save page_versions before changes

How to Use This PRD with Claude Code

# Option 1: Give Claude Code the URL
"Read https://ideas.asapai.net/prd-nowpage-issues-v1 and execute Sprint 1"

# Option 2: Fetch raw and parse
curl -s "https://ideas.asapai.net/prd-nowpage-issues-v1?format=raw" | ...

# Option 3: Ralph Loop (autonomous)
"Start a Ralph loop on Sprint S1 of the PRD at ideas.asapai.net/prd-nowpage-issues-v1.
For each issue: read file, apply fix, type-check. After all fixes: run test suite and commit."

Sprint 1: Critical Security & Data Integrity

S1 — Fix These First ~1-2 hours
IDSeverityFileIssueFix
C1 CRITICAL lib/auth/api-key.ts:61,68 Empty domain_ids/permissions = full access Add logging + warning response. Document intent. Backwards compatible.
C2 CRITICAL lib/publish/core.ts:384 JSON parse catch returns wrong variable → strips script tags Fix callback: return fullMatch instead of return _
C3 CRITICAL update-registry/route.ts:~61 Null domain → 500 crash Add null check: return 404 if domain missing
C4 CRITICAL update-registry/route.ts:~933 URL in regex not escaped → false matches Add escapeRegex() utility, apply to all URL patterns

Sprint 2: High-Priority Reliability

S2 — Reliability Fixes ~2-3 hours
IDSeverityFileIssueFix
H1HIGHcore.ts:99-100CSS class order breaks stat countsMatch both class orderings in regex
H2HIGHcore.ts:490-505Concurrent publish skips versionAdd retry on version number conflict
H3HIGHpublish/route.ts:49.single() throws on not-foundUse .maybeSingle() + null check
H4HIGHhc-publish.js:~446Tag retry loop — no maxAdd MAX_RETRIES=3 guard
H5HIGHupdate-registry:732-748Day streak breaks at year boundaryUse Date math instead of day-of-year
H6HIGHpublish/route.ts:51Domain check short-circuitsResolved by H3

Sprint 3: Medium Quality & Safety

S3 — Quality Polish ~2-3 hours
IDSeverityFileIssueFix
M1MEDcore.ts:~230Dashboard: blockers OR actions, not bothInject both independently
M2MEDcore.ts:353-376Metadata dedup missingDeduplicate by (text + url) in aggregation
M3MEDcore.ts:564-587Registry type fallback mismatchSkip registration if no matching type found
M4MEDhc-publish.js:~43.env parser breaks on = and quotesHandle export prefix, strip quotes
M5MEDwebhooks/fire.ts:58No SSRF protectionBlock localhost/private IPs, require HTTPS
M6MEDpublish/route.ts:64Empty tags from comma splitAdd .filter(Boolean)
M7MEDapi-key.ts:41null expires_at fragile comparisonAdd explicit comment + null guard

Sprint 4: Low-Priority Hardening

S4 — Future Hardening ~3-4 hours
IDSeverityFileIssueFix
L1LOWskill-injector.tsNo registry fetch cachingAdd sessionStorage cache (5min)
L2LOWupdate-registry/route.tsNo meta-meta recursion guardAdd visited set to cascade
L3LOWserve/route.tsJSX false-positive detectionUse hc-metadata render_mode flag
L4LOWpages/route.tsNo slug uniqueness per folderQuery before insert, return 409
L5LOWwebhooks/route.tsNo webhook retryAdd webhook_deliveries table + retry cron
L6LOWAll publish pathspage_versions unboundedCleanup old versions (keep 20, >60 days)
L7LOWkeys/route.tsNo rate limit on key creationMax 20 active keys per user

Execution Checklist

Sprint S1 (Critical):
  [ ] Commit snapshot: git add -A && git stash push -m "pre-S1"
  [ ] C1: lib/auth/api-key.ts — add logging + scope_warning
  [ ] C2: lib/publish/core.ts:384 — fix catch return to fullMatch
  [ ] C3: update-registry/route.ts:~61 — add null domain check
  [ ] C4: update-registry/route.ts — add escapeRegex utility
  [ ] Type check: npx tsc --noEmit
  [ ] Test: node scripts/test-publish-api.js (31/31)
  [ ] Commit: "Fix critical issues C1-C4: auth logging, JSON catch, null guard, regex escape"

Sprint S2 (High):
  [ ] H1: core.ts:99-100 — match both class orderings
  [ ] H2: core.ts:490-505 — version insert retry on conflict
  [ ] H3: publish/route.ts:49 — .maybeSingle() + null check
  [ ] H4: hc-publish.js:~446 — MAX_RETRIES=3
  [ ] H5: update-registry:732-748 — Date math for streak
  [ ] H6: resolved by H3
  [ ] Type check + test + commit

Sprint S3 (Medium):
  [ ] M1-M7: apply all fixes
  [ ] Type check + test + commit

Sprint S4 (Low):
  [ ] L1-L7: apply all fixes
  [ ] Type check + test + commit

Post-Execution:
  [ ] Publish updated issue tracker with status
  [ ] Run cleanup: node scripts/cleanup-test-pages.js --delete
  [ ] Push to remote: git push origin main

Related Pages

PageURL
Issue Tracker (live status)ideas.asapai.net/nowpage-issue-tracker-2026-02-20
API Manualideas.asapai.net/nowpage-api-manual
Neural Registryideas.asapai.net
Meta Registryideas.asapai.net/meta-registry