Skip to content

Console: NIP-46 bunker fix · Studio: finish Phase 2 receipts/policy · Admin: Phase-3 storage draft#53

Merged
MartinMontero merged 7 commits into
mainfrom
claude/wcjbt-credential-health-check-64ymr3
Jul 11, 2026
Merged

Console: NIP-46 bunker fix · Studio: finish Phase 2 receipts/policy · Admin: Phase-3 storage draft#53
MartinMontero merged 7 commits into
mainfrom
claude/wcjbt-credential-health-check-64ymr3

Conversation

@MartinMontero

@MartinMontero MartinMontero commented Jul 11, 2026

Copy link
Copy Markdown
Owner

What does this PR do?

Three sectioned workstreams, one branch (7 commits, one logical change each):

1. NIP-46 bunker sign-in fix (/console/) — closes the silent-failure class behind the failed Amber/test-bunker logins:

  • 0cf7ddf — pure bunker-login.ts helpers, 15 new tests: timeout bounding, http(s)-only auth_url sanitizer, static-strings-only error mapping (signer text is matched, never echoed), key-refusal gate now test-pinned.
  • caf60e3 — both bunker round-trips bounded at 60s; the bare catch un-swallowed: real cause → browser console (error object only — never the pasted string, never key material), UI explains via mapped static messages incl. the single-use bunker:// secret rule.
  • 972cafb — web-bunker auth_url approval handled: sanitized, opened noopener,noreferrer, clickable fallback for popup blockers. No behavior change for push-approval signers (Amber).

2. Connective-spine Phase 2, finished (/build/) — the receipts/policy plumbing that was computed but never wired:

  • 9867745 — license-at-commit receipts now travel into the downloaded README.md (linked) and AGENT_PROMPT.txt (plain) via a pure, tested receiptLine helper; unpinned entries say "verification pending", never a fabricated claim.
  • a6a8533 — the in-browser policy re-check is surfaced (✓ badge / conflict alert, en/es/ar) and gates the handoff step.
  • 439456b — adversarial-review fix: the gate now also covers the copy-prompt button and starter-file viewers (a clipboard is as much a handoff channel as a zip), and the ✓ badge requires a non-empty stack.

3. Admin Phase-3 storage draft (review-first, nothing provisioned):

  • 0235195migrations-admin/0001_admin_storage.sql as a reviewable spec: staged catalog/content edits (expiring), moderation cases + NIP-32/NIP-69-shaped labels, insert-only action audit. No binding added, no database created, no code path reads it; the file header carries the exact operator steps that bring it live after review.

Type of change

  • New catalog entry
  • Fix/update to an existing entry
  • New or updated recipe
  • Build flow / docs
  • Enforcement engine / tooling
  • Other

Required checks

  • npm run check passes (schema validation)
  • npm run enforce passes (three-layer exclusion policy + recipe contract)
  • npm test passes — 335/335 (28 net-new tests)
  • npm run build succeeds (full verify:all green, twice — before and after the review fix)

Security posture (unchanged or strengthened)

  • nsec/bunker://-secret refusal preserved and now pinned by tests; nothing pasted is ever logged or rendered back.
  • auth_url treated as signer-provided untrusted input: http(s) only, length-capped, noopener,noreferrer.
  • Zero new dependencies; CSP and the /console/* wss: delta untouched; Path A intact.
  • Reviewed by a 15-agent adversarial pass: 2 real defects found and fixed in 439456b; 6 candidate findings refuted.

(Correction on record: an earlier revision of this description cited the Phase-3 draft commit as d59a4a9 — an unverified hash. The correct commit is 0235195.)

🤖 Generated with Claude Code

https://claude.ai/code/session_01LVPyHAtQ1cdK76RpFasafP

claude added 7 commits July 11, 2026 05:32
The NIP-46 sign-in's security-relevant logic (timeout bounding, signer-error →
static-message mapping, auth_url sanitization, the key-material refusal gate)
moves to a plain module the node test runner can import — the Svelte island
cannot be. 15 tests pin the contracts, including: TimeoutError on a bunker
that never answers, late-rejection swallowing, http(s)-only approval URLs
(javascript:/data:/blob: dropped), and that bunkerReason never echoes
attacker-influenceable signer text.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVPyHAtQ1cdK76RpFasafP
…l failure

The two silent-failure defects behind 'bunker sign-in just spins':
- connect() and the remote signEvent() had NO timeout — an unanswered on-phone
  approval or a dead relay hung the UI forever. Both awaits are now bounded
  (60s each, generous for a human approval tap but never infinite).
- the bare catch discarded the actual error. It now goes to console.error as
  the operator's diagnostic (error object only — never the pasted input, never
  key material), and the UI message is mapped via bunkerReason() from static
  strings: timeout → approve-and-retry + the single-use-secret rule; bunker
  rejection → fresh bunker:// string hint; verify → allowlist refusal.

signer.close() is now fire-and-forget so cleanup can never hang the UI either.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVPyHAtQ1cdK76RpFasafP
Web bunkers (nsec.app-style) reply to connect with an auth_url the user must
open and approve; without an onauth handler nostr-tools only warns and the
connect promise never settles — the third silent-hang path. Amber and other
push-approval signers never send one, so this changes nothing for them.

The URL is signer-provided input and is treated as such: sanitized via
safeApprovalUrl (http/https only, length-capped — javascript:/data:/blob:
dropped), opened with noopener,noreferrer, and rendered as a clickable
fallback link for the common case where the popup is blocked (the open runs
outside the click gesture). The link lives only while the attempt is in
flight and is cleared once the signer closes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVPyHAtQ1cdK76RpFasafP
The 'receipts travel' half of the connective-spine Phase 2 stopped at the
screen edge: the license-at-commit receipt rendered in the blueprint UI and
persisted into session.stack, but the files the builder actually keeps —
README.md and AGENT_PROMPT.txt — omitted it. A new pure, tested helper
(receiptLine) renders the same facts into both: linked in the README, plain
in the agent prompt. Entries with no pinned commit get an honest
'verification pending' note, never a fabricated claim.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVPyHAtQ1cdK76RpFasafP
policyClean/policyMatches were computed since the Phase-2 merge but had zero
consumers — the 'live guarantee that the handoff is policy-clean' produced no
observable effect. Now:
- blueprint step: a ✓ receipt line when the re-check is clean; a conflict
  alert listing name → org when it is not (silent when /policy.json didn't
  load — no claim either way, the build-time CI engine stays the authority).
- handoff step: an excluded-vendor match PAUSES the handoff — no zip, no
  GitHub push, no Goose launch — until the stack is clean. Unreachable in
  practice (the catalog is enforced at build); this is the belt-and-suspenders
  the receipts promise. Localized en/es/ar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVPyHAtQ1cdK76RpFasafP
The adversarial review caught the gate leaking: the 'Copy the instructions
for your AI agent' button and the constitution/spec/package.json copy viewers
sat OUTSIDE the {#if policyMatches.length} gate, so with an excluded-vendor
match present the UI said 'handoff paused' while one click still copied an
agent prompt asserting the flagged tool was policy-clean and a package.json
pinning it. A clipboard is as much a handoff channel as a zip — the gate now
wraps all of them.

Also: the ✓ 'policy re-checked' badge is only claimable over a NON-EMPTY
stack — a failed catalog load or an emptied blueprint no longer renders a
vacuous clean claim.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVPyHAtQ1cdK76RpFasafP
…rovisioned

The ADMIN_DB schema (staged catalog/content edits, moderation cases + NIP-32
labels, insert-only action audit) lands as code FIRST so it is reviewed before
any resource exists: no wrangler.jsonc binding, no database created, no code
path reads it. The file header carries the exact three operator steps that
bring it live after review.

Shape encodes the project's values: public identifiers only (never session
tokens or key material), minimal references instead of content copies,
insert-only audit (an editable audit trail is theater), staged drafts that
expire rather than accumulate, labels stored 1:1 as NIP-32/NIP-69 events so
portability stays a switchable mirror, and CSAM evidence explicitly excluded
(Phase 6's locked R2 bucket, never this database).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVPyHAtQ1cdK76RpFasafP
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

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

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
wecanjustbuildthings 0235195 Jul 11 2026, 06:31 AM

@MartinMontero
MartinMontero merged commit 007f232 into main Jul 11, 2026
14 checks passed
@MartinMontero
MartinMontero deleted the claude/wcjbt-credential-health-check-64ymr3 branch July 11, 2026 06:42
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