feat(forms): validate submitter npub on submission forms (#22)

The embed submission forms accepted any free-text identifier and published it,
so they were an easy spam vector. Add a shared, testable NIP-19 validator and
require a well-formed npub before a submission is sent.

- js/utils/validateNpub.js: validateNpubHex() decodes via nostr-tools nip19 and
  returns the 64-char hex pubkey, or null for anything that isn't a real npub
  (free text, wrong prefix, bad checksum, wrong NIP-19 entity). Also exposed as
  window.validateNpubHex for the classic iframe form scripts.
- Application form (required npub): submit is blocked unless valid; the decoded
  hex is written into the application body for canonical storage/matching.
- Feature-request / bug-fix / feedback (optional npub): stays optional, but a
  non-empty value must be a valid npub or submit is blocked with an inline error.
- Tests: tests/validate-npub.test.mjs (real nostr-tools, no implementation mirror).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Opus 4.8 committed 2026-06-25 08:31:45 -04:00
1 parent fdcf034464
commit e4e5c552c7
7 files changed
+203 -5

No files matched your search

@@ -9,6 +9,8 @@
<link rel="stylesheet" href="../../css/tailwind.generated.css" />
<!-- Load nostr‑tools v2.17.0 -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/nostr.bundle.min.js"></script>
<!-- Shared anti-spam npub validation (sets window.validateNpubHex) -->
<script type="module" src="../../js/utils/validateNpub.js"></script>
</head>
<body class="iframe-form" data-ds="new">
<div class="container">
@@ -295,6 +297,21 @@
const applicantNpub = document
.getElementById("applicantNpub")
.value.trim();
// Anti-spam: the application identifier must be a valid npub.
const applicantPubHex =
typeof window.validateNpubHex === "function"
? window.validateNpubHex(applicantNpub)
: null;
if (!applicantPubHex) {
log(
"Please enter a valid Nostr public key (npub1…) before submitting. Your application was not sent.",
"error"
);
document.getElementById("applicantNpub")?.focus();
return;
}
const contactMethod = document
.getElementById("contactMethod")
.value.trim();
@@ -353,6 +370,7 @@
**1. Applicant Information**
- **Nostr Public Key (npub):** ${applicantNpub}
- **Pubkey (hex):** ${applicantPubHex}
- **Preferred Contact Method:** ${contactMethod || "N/A"}
- **Username or Alias:** ${username || "N/A"}
@@ -8,6 +8,8 @@
<link rel="stylesheet" href="../../css/tailwind.generated.css" />
<!-- Load nostr‑tools v2.17.0 -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/nostr.bundle.min.js"></script>
<!-- Shared anti-spam npub validation (sets window.validateNpubHex) -->
<script type="module" src="../../js/utils/validateNpub.js"></script>
</head>
<body class="iframe-form" data-ds="new">
<div class="container">
@@ -275,6 +277,22 @@
try {
// Section 1: User Information
const userNpub = document.getElementById("userNpub").value.trim();
// Anti-spam: npub is optional here, but if provided it must be a
// valid npub so the submission can't carry free-text garbage.
if (
userNpub &&
(typeof window.validateNpubHex !== "function" ||
!window.validateNpubHex(userNpub))
) {
log(
"That Nostr public key isn't valid. Enter a valid npub1… or leave it blank. Your submission was not sent.",
"error"
);
document.getElementById("userNpub")?.focus();
return;
}
const roleNodes = document.querySelectorAll(
'input[name="userRole"]:checked'
);
@@ -8,6 +8,8 @@
<link rel="stylesheet" href="../../css/tailwind.generated.css" />
<!-- Load nostr‑tools v2.17.0 -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/nostr.bundle.min.js"></script>
<!-- Shared anti-spam npub validation (sets window.validateNpubHex) -->
<script type="module" src="../../js/utils/validateNpub.js"></script>
</head>
<body class="iframe-form" data-ds="new">
<div class="container">
@@ -212,6 +214,22 @@
try {
// Section 1: User Information
const userNpub = document.getElementById("userNpub").value.trim();
// Anti-spam: npub is optional here, but if provided it must be a
// valid npub so the submission can't carry free-text garbage.
if (
userNpub &&
(typeof window.validateNpubHex !== "function" ||
!window.validateNpubHex(userNpub))
) {
log(
"That Nostr public key isn't valid. Enter a valid npub1… or leave it blank. Your submission was not sent.",
"error"
);
document.getElementById("userNpub")?.focus();
return;
}
const roleNodes = document.querySelectorAll(
'input[name="userRole"]:checked'
);
@@ -8,6 +8,8 @@
<link rel="stylesheet" href="../../css/tailwind.generated.css" />
<!-- Load nostr‑tools v2.17.0 -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/nostr.bundle.min.js"></script>
<!-- Shared anti-spam npub validation (sets window.validateNpubHex) -->
<script type="module" src="../../js/utils/validateNpub.js"></script>
</head>
<body class="iframe-form" data-ds="new">
<div class="container">
@@ -235,6 +237,21 @@
try {
// Section 1: User Information
const userNpub = document.getElementById("userNpub").value.trim();
// Anti-spam: npub is optional here, but if provided it must be a
// valid npub so the submission can't carry free-text garbage.
if (
userNpub &&
(typeof window.validateNpubHex !== "function" ||
!window.validateNpubHex(userNpub))
) {
log(
"That Nostr public key isn't valid. Enter a valid npub1… or leave it blank. Your submission was not sent.",
"error"
);
document.getElementById("userNpub")?.focus();
return;
}
const roleNodes = document.querySelectorAll(
'input[name="userRole"]:checked'
);