Reviewed against main at commit cd5ae9d on June 14, 2026.
Fixed in code (verified via wrangler dev + npm run check):
worker.js).rowsToText preserves positions (js/admin.js).worker.js).r.ok (js/admin.js).ACCESS_* config is absent (worker.js).worker.js).snapshots/<date>/<stamp>.json (worker.js).lastName → stripped from the lookup response (worker.js).worker.js).'unsafe-eval' scoped to /privacy + /cookies only; Termly renders (worker.js).worker.js).workers.dev → lookup restricted to the portal host + workers_dev: false (worker.js, wrangler.jsonc).js/trip.js).js/trip.js).css/styles.css).css/styles.css).worker.js).permissions: contents: read; added Dependabot for npm + Actions (.github/)..html URLs → site now uses
extensionless canonical URLs everywhere (links, canonicals, sitemap).crypto.getRandomValues, 8 chars/~40 bits; demo
reference changed off the short FH-DEMO.POST /api/admin/purge + admin "Erase permanently"
removes live + trash; bookings carry createdAt/updatedAt; retention policy
documented (README).RUNBOOK.md (outage, backup failure, data loss,
unauthorized access, contact-form + third-party outages, recovery targets).updatedAt token (save returns 409 on a stale base).GET /api/health?ready=1 probes
KV + R2 and returns 503 if degraded.DATA.md (records of processing +
vendor map + access-review schedule).scripts/decrypt-backup.mjs recovery tool.observability.enabled in wrangler.jsonc./privacy + /cookies if Termly can't load.test/ worker.test.mjs loads the real worker.js module and integration-tests
lookup, admin auth + JWT, CRUD/collision/concurrency, delete/purge,
snapshot/export, readiness, and headers (15 tests, npm test). check.mjs
reduced to syntax-only; the fragile regex extraction is gone.npm run verify (used as the Cloudflare build
command) runs check + test + build, so a failure blocks the deploy; CI runs
the same on every push/PR.Needs a Cloudflare dashboard / account action (not code):
Decisions (resolved):
.html → extensionless: it was Workers Assets' default
html_handling; aligned canonicals/sitemap/links to extensionless. Done.crypto-random 8-char codes; demo
reference regenerated (no live bookings to migrate). Done.Done — Cloudflare dashboard / account (2026-06-14):
merle-michael-landing Pages project deleted (Worker serves it).www → farholm.com redirect rule verified; Termly consent confirmed working.ci.yml: checkout v4.3.1, setup-node v4.4.0);
Dependabot reads the version comments and proposes SHA bumps as PRs.Still open — Cloudflare dashboard / account (not code):
/api/health.Still open — larger / ongoing (partially addressed or beyond current scope):
RUNBOOK.md; not yet automated/rehearsed.'unsafe-inline' via nonces — the documented final hardening step.workers_dev,
observability, routes already in config).esbuild dev-only advisory (no fix available yet; Dependabot will catch it).worker.js:162
Saving a renamed booking writes the new reference without checking whether it
already exists, then deletes the original. Accidentally renaming FH-A to
FH-B permanently replaces FH-B without creating a trash backup. Reject
reference collisions unless explicitly confirmed.
js/admin.js:45
rowsToText removes empty fields before joining them. Because parseRows
later restores fields by position, an empty arrival time causes the departure
time and every following value to shift into the wrong fields after saving and
reopening.
worker.js:160
Unlike snapshotAll, this KV list call does not follow pagination cursors.
Once the namespace exceeds one page, bookings disappear from the admin list
even though they remain stored and accessible directly.
js/admin.js:80
The delete handler ignores response status and always clears the form and reports success. An expired admin session or Worker failure can leave the booking intact while telling the administrator it was deleted.
worker.js:26, wrangler.jsonc
The Worker accepts /api/lookup before applying any hostname restriction.
wrangler.jsonc does not set workers_dev = false and does not declare routes;
Wrangler therefore enables the workers.dev endpoint by default. Requests to
that endpoint can reach the lookup API outside a WAF rate-limiting rule
configured for the farholm.com zone.
This matters because a successful lookup exposes itinerary details including
traveler names, confirmation numbers, ticket numbers, loyalty numbers, dates,
and locations. Restrict lookup to trip.farholm.com, disable workers.dev,
and enforce throttling in the Worker or with a Rate Limit binding.
worker.js:104-118
When either ACCESS_TEAM_DOMAIN or ACCESS_AUD is missing, accessEmail
accepts the plaintext cf-access-authenticated-user-email header or decodes an
unsigned JWT payload. A deployment/configuration regression therefore removes
the Worker's independent authentication check and makes authorization depend
entirely on an external Access policy being perfect.
Production should fail closed unless a verified Access JWT is present. Keep the existing explicit localhost bypass as the only development exception.
js/admin.js:74
Generated references contain five characters selected with Math.random,
providing only about 25 bits of reference entropy. The second lookup factor is
a surname, which is commonly known or guessable. This is weak protection for
the sensitive itinerary data returned by the public lookup route, especially
if rate limiting is bypassed or misconfigured.
Generate substantially longer references with crypto.getRandomValues, or
use expiring signed links or a stronger client authentication flow.
worker.js:131, worker.js:156
The public endpoint accepts arbitrary-length and arbitrary-format references and surnames. Oversized references can exceed KV key limits and turn a normal lookup miss into an unhandled Worker error; repeated malformed requests can consume Worker resources and produce noisy failures.
Enforce a strict reference format and small request-field limits before
accessing KV, and return a controlled 400 or generic 404.
worker.js:43-56: the CSP still allows inline scripts and styles. No stored
XSS path was found in the reviewed renderers, which consistently escape
booking values, but removing unsafe-inline would reduce future impact.worker.js:61-72: consider adding HSTS and a restrictive
Permissions-Policy.js/admin.js:80-82: state-changing admin requests do not include CSRF
tokens or enforce Origin. Cloudflare Access cookie behavior may currently
limit exploitability, but explicit origin checks would make the boundary
robust.npm audit --omit=dev --audit-level=low: no production vulnerabilities.npm audit --audit-level=low: could not complete because registry
network access was unavailable.npm ls --all: dependency tree resolved; only expected platform-specific
optional dependencies were absent.worker.js:135-150, worker.js:162, worker.js:165
Live bookings have no expiration or retention metadata. Deleting a booking copies it to trash for 90 days, and daily snapshots retain additional copies for up to 90 days. There is no operation that purges a client's data from the live record, trash, and snapshots together. Define a retention policy and add a documented, auditable erasure workflow.
worker.js:156, worker.js:173
bookingForClient returns a clone of the entire booking, including lastName.
The lookup surname is used only for authentication and is never rendered by the
client page. Return an explicit allowlist of client-visible fields instead of a
nearly complete stored record.
privacy.njk:20-28, cookies.njk:20-28
Both policies are rendered entirely by Termly at runtime. Without JavaScript, users receive only an email address, and reviewers cannot confirm that the published policy covers itinerary KV storage, R2 backups, retention periods, Google Analytics, Web3Forms, or client erasure requests. Keep a reviewable policy copy or automated disclosure checklist in the repository.
worker.js:141-144
All automatic and manual snapshots use snapshots/YYYY-MM-DD.json. A manual
snapshot after accidental corruption can overwrite the healthy snapshot from
earlier that day, and the scheduled snapshot can overwrite a manual recovery
point. Include a timestamp or unique identifier in every snapshot key.
README.md:350-364, scripts/check.mjs:89-127
The application creates and prunes snapshots, but it has no restore command, admin action, validation step, or restore test. The documented process requires manually downloading and re-saving records during an incident. Add a dry-run-capable restore tool and regularly test recovery into a separate namespace.
worker.js:25
/api/health always returns success without checking KV, R2, or Access
configuration. Monitoring can report a healthy portal while lookup, admin, or
backups are broken. Add dependency-aware readiness checks or separate monitors.
worker.js:162
Bookings contain no version or update timestamp, and saves perform unconditional KV writes. Two admin tabs can edit the same booking and the last save silently destroys the other changes. Add optimistic concurrency with a version token.
js/trip.js:236-238
The client UI maps every non-success response and network failure to the same
not-found message. A Worker, KV, or network outage tells clients to recheck
their details rather than reporting a temporary service problem. Distinguish
404 from service and network failures.
js/trip.js:189-207
Event UIDs are based on array position, so reordering itinerary items changes
their identity and can create duplicate calendar events. DTSTAMP is also
hard-coded, and long content lines are not folded as required by ICS clients.
Use stable segment IDs, a current timestamp, and standards-compliant line
folding.
FH-DEMO client lookup rendered the itinerary without browser
errors.500.css/styles.css:591-600, css/styles.css:868-869
The general form rule gives every input width: 100%. The pill-group radios
are then positioned absolutely but retain that width, extending past their
container. Browser checks measured the contact page at 423px wide in a 375px
viewport and 1444px wide in a 1280px viewport. Use a standard visually-hidden
control pattern with fixed dimensions or override the radio width.
h1, one main, labeled form controls, image
alt text, and no unsafe blank-target links.css/styles.css, _includes/nav.njk
At 1025px wide, the site switches from the hamburger menu to the full desktop
navigation before there is enough room for it. The logo touches the Home link,
and Points & Miles, Why Us, and View My Booking wrap onto two lines. At
1100px the navigation displays cleanly. Raise the hamburger breakpoint or
reduce desktop navigation spacing.
_includes/base.njk:57
The Termly banner occupies about 303px of an 844px-tall phone viewport and
345px of a 700px-tall small-phone viewport. When the mobile menu is open, the
banner partially covers its Start Planning CTA. Review Termly's compact
mobile configuration or ensure the menu and consent layer do not overlap.
_includes/base.njk:57
The Termly resource-blocker script is synchronous in the document head. A slow or unavailable third party can delay parsing and rendering across the public site. Confirm whether its blocking mode is required and measure its effect on Core Web Vitals; otherwise load it with a less disruptive integration.
noindex or are excluded
from the sitemap.wrangler.jsonc, README.md:408-427
Cloudflare Access applications, WAF rate limits, custom domains, redirects, and health alerts are described as dashboard setup steps but are not defined or verified in CI. Configuration drift can silently remove authentication, throttling, routing, or monitoring. Manage these controls as infrastructure as code where possible and add deployment smoke tests for the live hostnames.
npm outdated could not run because the local npm cache has a permissions
problem; dependency freshness was not verified.Reviewed live production on June 14, 2026 without submitting forms or changing booking data.
Live: http://farholm.com, http://trip.farholm.com,
http://cruise.farholm.com
All three hosts returned 200 OK over plain HTTP instead of redirecting to
HTTPS. HTTPS responses also did not include Strict-Transport-Security.
Visitors can therefore receive pages over an unauthenticated, modifiable
connection. Enable Cloudflare Always Use HTTPS and HSTS after confirming all
required hosts support HTTPS.
Live: https://farholm.com/privacy, https://farholm.com/cookies
The Termly policy embed throws a CSP error because it evaluates JavaScript
strings while the site's script-src does not allow unsafe-eval. Both policy
containers remained empty in a live browser. Resolve this without broadly
weakening CSP if possible, or publish reviewable first-party policy content.
Live: https://farholm.com/sitemap.xml
Every .html sitemap URL except the homepage returned a 307 redirect to an
extensionless URL. Page canonical metadata also uses the redirecting .html
addresses. Update internal links, canonicals, and sitemap output to the final
extensionless URLs, or stop redirecting the .html versions.
Live: /api/lookup, /api/health
The lookup and health APIs respond on farholm.com, trip.farholm.com, and
cruise.farholm.com, rather than only on their intended portal hostname.
Production rate limiting did apply across the tested custom hosts, but the
unnecessarily broad route exposure increases configuration complexity and
attack surface.
www.farholm.com and farholmtravel.com redirect to
https://farholm.com/./admin and
/api/admin/* on trip.farholm.com./admin on the
cruise hostname returned not found.404 response.noindex.robots.txt, sitemap, branded 404 handling, custom domains, and health route
were live.500 with Cloudflare
Worker error code 1101.Reviewed the committed dependency tree, install behavior, CI workflow, and third-party build inputs on June 14, 2026.
npm audit reports a high-severity advisory in esbuild@0.27.3, pulled in by
wrangler@4.100.0. The advisory concerns missing binary integrity verification
when an attacker controls NPM_CONFIG_REGISTRY; a separate low-severity
advisory affects the development server on Windows.
The repository is already using the latest Wrangler release, and npm currently
reports no fix as available. This does not affect the deployed site's runtime:
npm audit --omit=dev reports zero vulnerabilities. Continue using the official
npm registry in trusted build environments, avoid exposing the local
development server, and update Wrangler as soon as it moves to
esbuild@0.28.1 or later.
.github/workflows/ci.yml:16-17 references actions/checkout@v4 and
actions/setup-node@v4 using moving tags. If either tag or its publishing
account were compromised, CI would execute changed code on the next run.
The workflow also does not declare a permissions block, so its token
permissions depend on repository-level defaults.
Pin both actions to reviewed full commit SHAs and declare the minimum required
workflow permissions, which for this read-only build should be
contents: read.
The repository has no Dependabot configuration, and
.github/workflows/ci.yml:21-25 installs, builds, and runs project checks
without an audit or dependency-review step. The current high-severity Wrangler
advisory can therefore remain unnoticed until someone manually runs an audit.
Enable Dependabot or Renovate for npm and GitHub Actions. Add a scheduled audit or GitHub dependency-review check with an explicit policy for development-only advisories that have no available fix, so an unavoidable warning does not silently normalize future actionable vulnerabilities.
npm ci --ignore-scripts --dry-run completes without lockfile drift.npm outdated reports no outdated direct dependencies.npm audit --omit=dev reports zero production vulnerabilities.esbuild, fsevents, sharp,
and workerd; all resolve from registry.npmjs.org..dev.vars, node_modules, generated output, and Wrangler state are
gitignored, and no secret-like files are tracked.Reviewed client-facing copy, contact destinations, internal links, metadata, and trust claims on June 14, 2026.
about.njk:87 and contact.njk:158 say that IATA and CLIA credentials prove
strict standards for financial accountability and client protection, while the
About page also states that Farholm carries professional liability insurance.
The repository contains badge images and membership numbers, but no review
date, evidence link, or process ensuring these claims remain current and use
credential-provider-approved wording.
Confirm the wording against current IATAN/CLIA rules and the active insurance policy. Prefer narrower factual wording, link to independently verifiable credentials where possible, and schedule an annual review before publishing a new dated CLIA badge.
The site promises a response “within 24 hours,” says alternative contact options “always work,” invites visitors to “reach out anytime,” and describes booking transfers as having “no downside.” These phrases create expectations that may not hold during weekends, outages, supplier restrictions, or after a transfer changes who controls a booking.
Use specific, sustainable commitments such as “within two business days,” and qualify transfer and support claims with supplier eligibility and availability.
555 WhatsApp number is the correct Meta-issued WhatsApp Business
contact number.404.html page is the only page without a meta description;
this is low impact because it should not be indexed.Reviewed repository-defined analytics, health checks, Worker diagnostics, scheduled jobs, and operational documentation on June 14, 2026. Dashboard-only Cloudflare alerts and analytics settings could not be verified from the repository.
worker.js:19-20, worker.js:135-150
The daily scheduled handler passes snapshotAll(env) to ctx.waitUntil() but
does not record success or failure, and there is no alert when KV reads, the R2
write, or pruning fails. A broken backup job can therefore remain unnoticed
until a restore is needed.
Emit a privacy-safe structured result for every scheduled run and alert when a run fails, writes zero bookings unexpectedly, or no fresh snapshot exists within the expected window. Monitor the backup artifact itself rather than only whether the cron was invoked.
worker.js:13-16, worker.js:94-100, worker.js:156-164, wrangler.jsonc
The Worker has no structured application logging, error correlation, or repository-defined observability configuration. Several dependency, parsing, and storage errors are intentionally caught and discarded, making Access, exchange-rate, corrupt-record, and external-service failures difficult to distinguish after an incident.
Enable privacy-safe Worker logs and traces with an explicit sampling and retention policy. Log route, outcome, status, duration, dependency, and a request or Cloudflare Ray identifier, but never surnames, booking references, tokens, itinerary contents, or third-party API keys.
No runbook or playbook defines incident ownership, severity levels, escalation contacts, service priorities, communication templates, recovery objectives, or the steps for security and privacy incidents. The README documents individual backup and health-check mechanics, but not how to coordinate a real outage or data event.
Create a short operational runbook covering portal outage, failed backups, unauthorized access, lost or corrupted booking data, contact-form failure, and third-party outages. Include decision owners, evidence-preservation steps, client-notification criteria, rollback and restore procedures, and a periodic exercise schedule.
_includes/base.njk:43-65, js/site.js:78-111, _data/site.json:14
Google Analytics is configured for pageviews, but the site emits no explicit events for successful or failed inquiry submissions, contact-channel clicks, cruise referral clicks, or progression to the booking portal. Cloudflare Web Analytics is not enabled through the committed token, though dashboard-side automatic injection may still be active.
Define a small consent-aware measurement plan for inquiry success and failure, contact clicks, and cruise referrals. Avoid sending names, email addresses, booking references, surnames, free-text messages, or itinerary details to analytics providers.
wrangler.jsonc
declares none of them.console logging, external error-reporting service,
incident runbook, alert configuration, or response ownership was found.Reviewed automated checks, CI triggers, production deployment flow, release documentation, and rollback evidence on June 14, 2026. GitHub branch-protection and Cloudflare Workers Builds dashboard settings could not be verified from the repository.
README.md:9-22, .github/workflows/ci.yml:6-25
Every push to main triggers a Cloudflare production deployment, while GitHub
CI independently runs on the same push. Nothing in the repository makes the
deployment wait for CI success, and the documented Cloudflare build command
does not include npm run check. A syntactically valid site with failing
security or behavior checks can therefore begin deploying before GitHub CI
reports failure.
Deploy only after required checks pass, or make the Cloudflare production build
run the same complete verification command before deployment. Protect main
from direct pushes and require the build-and-check job on pull requests.
scripts/check.mjs, worker.js:156-167, js/admin.js:72-82,
js/trip.js:180-239, js/site.js:78-111
Automated behavior checks cover Access-token verification and the happy path of snapshot creation and pruning. They do not cover public lookup, admin authorization routing, booking create/edit/rename/delete behavior, pagination, client-data filtering, dependency failures, contact submission, itinerary rendering, calendar export, or browser interactions. Several defects already identified in this review would have been caught by focused tests.
Add Worker integration tests with fake KV/R2 bindings and a small browser suite covering the contact form, demo lookup, itinerary rendering, and admin CRUD. Prioritize regression tests for every accepted code-review finding.
scripts/check.mjs:48-58, scripts/check.mjs:93-101
The checks extract functions from worker.js using regular expressions and
execute them through new Function. This can break when functions are
reformatted or begin depending on module-level helpers, and it does not prove
that the deployed Worker module, routing, bindings, or response headers behave
correctly together.
Make Worker logic importable and test it through the same module boundary used in production, ideally using Cloudflare's Worker testing support. Keep a small end-to-end smoke test for deployed-host behavior.
The repository has no staging environment, release checklist, rollback procedure, deployment marker, or automated post-deploy smoke workflow. Production smoke testing was performed manually during this review, but there is no repeatable process ensuring each deployment serves the expected pages, protects admin routes, and preserves lookup behavior.
Document how to identify and roll back to a known-good Worker version, then rehearse it. Add a post-deploy read-only smoke check for the main, trip, and cruise hosts, including HTTPS behavior, key static pages, health/readiness, generic invalid lookup, and protected admin routes.
npm run build and npm run check pass locally.main.Reviewed data collection, storage copies, retention behavior, deletion paths, third-party transfers, and access governance on June 14, 2026. This review builds on the incomplete-erasure finding in the earlier Privacy and Data-Protection Review.
worker.js:162, wrangler.jsonc:15-24
Live booking: records are stored in KV without expiration metadata and remain
until manually deleted. The system does not record trip completion, record
creation, last update, retention category, or a disposal date, so there is no
reliable way to identify records that should be archived or deleted.
Define retention periods based on business and legal needs, add lifecycle metadata to every booking, and automate a review or purge queue. Separate active-trip operational records from records retained for accounting, dispute, or legal purposes.
js/itinerary-schema.js, worker.js:162, worker.js:169-174
Booking records can contain traveler and guest names, loyalty numbers, ticket numbers, confirmation codes, travel dates, locations, costs, contact details, and unrestricted notes. These fields are copied into KV, soft-delete records, and full R2 snapshots, but there is no documented classification, prohibited data list, or field-specific retention rule.
Create a data-classification and minimization standard for the itinerary editor. Explicitly prohibit passports, payment-card data, account passwords, medical details, and other unnecessary sensitive information; label fields that require shorter retention or redaction.
contact.njk:26-117, js/site.js:78-111
Contact-form submissions send names, contact details, state, budget, travel timing, party type, and free-text messages directly to Web3Forms. The repository defines no retention period, deletion workflow, export procedure, or reconciliation process for those submissions and any resulting email copies.
Document where inquiry copies are stored after submission and configure retention in Web3Forms and email systems. Include those systems in access, export, correction, and erasure procedures.
The README describes individual systems, but there is no single inventory mapping data categories to purpose, owner, storage location, recipients, retention period, deletion mechanism, and authorized roles. There is also no documented periodic review of Cloudflare account access, Access policy members, Web3Forms, Termly, Google Analytics, email, or API-provider accounts.
Maintain a lightweight records-of-processing/data inventory and review access at a defined interval and after personnel or vendor changes. Record completed reviews and removal of obsolete accounts or credentials.
Reviewed critical providers, service dependencies, fallback behavior, account recovery evidence, and vendor continuity documentation on June 14, 2026. Provider contracts, account security settings, billing contacts, and recovery methods could not be verified from the repository.
wrangler.jsonc:15-28, worker.js:133-150
Cloudflare provides the live Worker, static assets, DNS/routing controls, Access authentication, primary KV booking records, scheduled jobs, and R2 backups. R2 protects against loss of the KV namespace, but it does not protect against Cloudflare account loss, account compromise, billing suspension, provider-wide failure, or an erroneous deletion affecting both stores.
Maintain a periodically verified encrypted export of booking data outside the Cloudflare account and document how to rebuild the essential client itinerary service elsewhere. Protect Cloudflare account recovery materials and use independent break-glass ownership with strong multifactor authentication.
privacy.njk:19-28, cookies.njk:19-28, _includes/base.njk:45-57
Privacy and cookie disclosures are loaded entirely from Termly at runtime. When Termly is unavailable, blocked, or incompatible with the site's security policy, the pages contain no substantive policy text. This failure is already present in the live-production review.
Keep a current first-party copy or static fallback of each published policy and define how policy changes are reviewed and synchronized. Legal disclosures should remain available during a Termly outage or account loss.
The repository identifies Cloudflare, GitHub, Web3Forms, Termly, Google, RapidAPI/AeroDataBox, Frankfurter, and Virgin Voyages integrations, but it does not document business owners, account owners, billing contacts, renewal dates, recovery methods, service criticality, data handled, contractual commitments, or exit procedures.
Create a vendor register and keep recovery codes, account-transfer procedures, and emergency contacts in an appropriately protected system. Review it periodically and whenever a provider, credential, or responsible person changes.
js/site.js:93-110, worker.js:25-35, worker.js:156-164
Web3Forms failure falls back to an email link, and external place, flight, and exchange-rate services generally fail without corrupting records. However, there is no documented plan for operating during a booking-portal outage, Cloudflare Access failure, email outage, Web3Forms failure, or loss of a critical vendor account.
Define minimum service during each outage: how clients receive itineraries, where new inquiries are captured, how urgent-trip support is handled, and when the site should display a service notice. Exercise the highest-impact scenarios using non-production data.
Reviewed likely attacker goals, abuse paths, privilege boundaries, destructive actions, injection defenses, spam controls, and the ability to detect and investigate abuse.
The admin API can list, read, create, overwrite, and delete every booking, trigger snapshots, and use external lookup APIs (worker.js:160-167). There are no scoped roles, independent approval for high-impact actions, security event audit trail, or alerts for unusual admin activity. Soft deletion and snapshots help with mistakes, but provide limited protection if the admin identity or the shared Cloudflare account is maliciously controlled.
Action: Enforce strong MFA and tightly controlled recovery for Cloudflare Access and the Cloudflare account. Add an immutable audit trail for admin reads and changes, alerts for bulk or unusual activity, reauthentication or confirmation for destructive actions, and separate least-privilege roles where practical.
Successful public lookup reveals sensitive itinerary and identity-related details. The route is exposed before the admin-host restriction (worker.js:26, worker.js:156), while references are short and generated with Math.random (js/admin.js:74). The documented WAF rate limit helps on the intended hostname, but alternate Worker/custom-host access can bypass that perimeter control. This consolidates the related security-review findings into the primary external attacker scenario.
Action: Restrict lookup to the intended hostname inside the Worker, enforce rate limiting in application code or across every reachable hostname, replace references with longer cryptographically random values, and alert on repeated failed or distributed lookup attempts.
The public contact form submits directly to Web3Forms (contact.njk:26-31, js/site.js:78-111). Its page-level honeypot can be bypassed by calling the provider directly with the intentionally public access key. This creates a practical path for spam, inbox flooding, phishing content, and operational distraction.
Action: Configure Web3Forms domain restrictions, provider-side spam controls, and rate limits. Add Turnstile or equivalent challenge controls if abuse occurs, and monitor/filter the receiving mailbox for bursts and malicious content.
There is no security-focused audit trail or anomaly detection for repeated lookup failures, admin record access and changes, deletions, snapshot activity, or external API quota use. This makes it difficult to identify an attack early or determine its scope afterward.
Action: Record privacy-conscious security events with timestamps, actor identifiers, action types, outcomes, and request correlation IDs. Alert on suspicious lookup patterns, destructive admin activity, authentication anomalies, and unexpected API-volume increases. Define retention and access controls for these logs.